入口契约、ChatClient、DeepSeekChatModel、记忆工具和可观测验收组成 Spring AI 生产骨架

学 Spring AI 很容易掉进一个坑:先收集几万字笔记,从 Ollama、OpenAI、DeepSeek、流式输出一直抄到会话记忆、Function Calling、RAG、Redis 和 MySQL,最后每一段都似乎调通了,却说不清哪层负责安全、状态和故障恢复。

当前 Spring AI 2.0.0 已有原生 DeepSeek starter、统一 Chat API、记忆 repository、工具调用和可观测能力。与其从历史课程的几十个步骤开始,不如先把一条最小对话链路拆成五层,让每层都能回答:

  • 输入从哪里来;
  • 可以修改或调用什么;
  • 敏感数据在哪里;
  • 失败如何表达;
  • 用什么证据验收。

先搭五层骨架,再加 RAG 和业务功能

Spring AI 应用从 Controller 契约、ChatClient、DeepSeekChatModel、ChatMemory 工具到观测和测试的五层架构

第一层:入口契约

Controller 的职责不是把任意字符串原样送给模型。它至少要定义:

  • 谁有权调用;
  • messageconversationId 和业务字段的长度与格式;
  • 同步、SSE 或其他输出协议;
  • 限流、超时、取消和业务错误格式;
  • 哪些 provider 错误不应直接暴露给用户。

一个最小请求对象可以是:

public record ChatRequest(
        @NotBlank @Size(max = 4_000) String message,
        @NotBlank @Size(max = 128) String conversationId) {
}

这里有意没把 modeltemperature 和 API key 暴露成用户可任意修改的请求字段。provider 策略应由服务配置或受控业务规则决定。

第二层:ChatClient 编排

ChatClient 提供类似 Spring WebClient/RestClient 的 fluent API,同时支持 call()stream()Chat Client API

把公共 system 指令、记忆 Advisor、工具列表和默认行为放在统一配置处,不要散落在每个 Controller:

@Bean
ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
    return ChatClient.builder(chatModel)
            .defaultAdvisors(
                    MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
}

流式接口显式声明 SSE,并传入经身份校验后的会话 ID:

@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> stream(@Valid @RequestBody ChatRequest request) {
    return chatClient.prompt()
            .user(request.message())
            .advisors(a -> a.param(
                    ChatMemory.CONVERSATION_ID,
                    request.conversationId()))
            .stream()
            .content();
}

生产代码中,conversationId 不能只由前端随意提供后直接信任。它必须与当前用户、租户和业务资源关联,否则只需猜到他人 ID 就可能混入他人上下文。

第三层:原生 DeepSeekChatModel

当前 Spring AI 可以直接添加原生 DeepSeek starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

通过 Spring AI BOM 统一版本,不要给每个 AI 模块单独拼凑版本:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

密钥从环境或专用 secret manager 读取,不写进可提交 YAML:

spring:
  ai:
    model:
      chat: deepseek
    deepseek:
      api-key: ${DEEPSEEK_API_KEY}
      chat:
        model: ${DEEPSEEK_MODEL}

当前配置项已使用 spring.ai.model.chat=deepseek,历史文章中的 spring.ai.deepseek.chat.enabled 已被删除。原生 DeepSeek 文档

Spring AI Model 和 StreamingModel 与 ChatModel、EmbeddingModel、ImageModel 的类型层级

图:Spring AI 用统一 Model 抽象承载不同模态和 provider;业务层不应绑死某个底层客户端。

记忆与历史必须分开

原笔记已经注意到“会话记忆”和“会话历史”不同,这是很重要的判断。当前官方文档将它们定义为:

  • Chat Memory:模型为维持当前上下文而保留的相关消息;
  • Chat History:用户与模型交换的完整记录。

ChatMemory 不适合代替完整历史存储。默认 MessageWindowChatMemory 使用内存 repository 并保留最多 20 条消息,超出窗口后会淘汰旧轮次。Chat Memory

DeepSeek 多轮对话中的问题、思考内容和答案排列方式

生产设计至少要分成三个对象:

  1. 会话元数据:用户、租户、标题、时间、状态;
  2. 完整历史:用于展示、审计、导出或用户删除;
  3. 模型记忆:经窗口、摘要或检索选择后送给模型的部分。

官方已提供 JDBC、Redis、MongoDB、Neo4j 等 memory repository。但需要特别注意:当前 JDBC ChatMemoryRepository 会过滤 tool call 和 tool response 消息。如果应用依赖完整工具轨迹,不能只看“已持久化”就假设消息类型全部保留。

工具调用是业务副作,不是 prompt 技巧

Spring AI 当前支持将 DeepSeek 工具循环交给 ChatClient + ToolCallingAdvisor,也支持业务代码用 ToolCallingManager 自行控制。

不管选哪种,一个副作工具在执行前都要明确:

  • 当前用户是否有权操作目标资源;
  • 操作是只读、可回滚还是不可逆;
  • 重试是否会重复下单、发送、扣费或写入;
  • 必须在什么情况下要求人确认;
  • 工具失败、超时或返回不完整数据时如何收束。

仅在 system prompt 中写“不要操作无权资源”不够。权限必须在工具服务再验证一次,幂等必须由业务键或事务机制保证。

可观测不等于打印完整 prompt

Spring AI 基于 Spring 生态的 metrics 和 tracing,为 ChatClientAdvisorChatModel、工具调用和向量库等提供观测。可观测文档

最小观测项包括:

  • 请求次数、延迟和当前并发;
  • provider 和请求/响应模型;
  • 输入、输出和总 token 使用量;
  • 工具名称、延迟、错误与调用 ID;
  • 超时、限流、重试和最终失败的业务结果。

prompt、completion、tool arguments 和 tool result 可能很大,也可能包含隐私或密钥。Spring AI 默认不导出这些内容。只为故障诊断打开完整日志时,要限定环境、保留周期和可见人群,并先做脱敏。

重试不能靠默认值不经思考地托底

DeepSeek starter 的当前文档列出了统一 spring.ai.retry 配置,包括最大尝试、指数退避和哪些 HTTP 状态可重试。默认不对 4xx 客户端错误重试,这是合理的起点:无效 key、无权限和错误请求不会因为再发一次就变正确。

但是否重试 429、5xx 或网络超时,仍要根据请求的成本、用户等待上限、provider 限制和业务幂等设计。流式输出已经向用户发送了部分内容时,重试还要避免重复片段。

第一版只需要八个可运行检查

  1. 配置启动: 缺少 DEEPSEEK_API_KEY 时快速失败,不带假 key 启动后等首次请求爆错。
  2. 输入校验: 空消息、超长消息和无权 conversation ID 被拒绝。
  3. 流式协议: content type 正确,客户端取消后服务端停止继续消耗。
  4. 会话隔离: 不同用户即使猜到相同 conversation ID,也无法获取对方上下文。
  5. 窗口边界: 超出 memory window 后旧轮次按预期淘汰,系统消息仍保留。
  6. 工具授权: 无权用户即使模型请求了工具,服务层也不执行。
  7. 工具幂等: 相同业务键的重试不会重复写入或扣费。
  8. 观测脱敏: 常规 traces 中可见延迟、模型和 token 用量,不出现完整 prompt、API key 和工具私密结果。

这八项全部可以在未加 RAG、未做复杂前端、未接真实业务工具时先实现。它们构成后续扩展的安全地基。

再看原文中的几个典型坑

坑一:生产 CORS 允许所有来源

allowedOrigins("*") 可以让本地调试快速通,却不应作为生产默认。只允许已知前端域名,并把不同环境的 origin 放入外部配置。如果使用 Cookie 凭证,还要一起设计 credentials、CSRF 和 SameSite,不能只改一个响应头。

坑二:把数据库密码写在教程 YAML 中

即使是本地演示口令,也会被读者原样复制到项目和提交记录中。所有密钥、数据库口令和 provider token 都用环境变量或 secret manager,并在启动时校验是否存在。

坑三:把完整历史全部塞回 prompt

无限增长的上下文会增加成本、延迟和噪音。完整历史用于产品查看和审计,模型记忆使用窗口、摘要或检索只选与当前问题相关的部分。

结语

一条对话接口调通,只能证明你已经能向 provider 发请求并收到结果。它不能证明会话隔离、工具权限、重试幂等、隐私和故障可观测已经可靠。

Spring AI 真正的价值,是帮助 Spring 开发者用熟悉的分层、配置、Advisor、repository 和 observability 方式管理这些边界。

所以不要先问“十万字教程还剩多少没抄”。先问你的五层骨架是否已经做到:入口可校验,编排有一处,provider 可替换,状态与副作有边界,每次请求都能被观测和验收。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐