Spring AI 2.0 接 DeepSeek:别从十万字笔记开始,先搭 5 层生产骨架

学 Spring AI 很容易掉进一个坑:先收集几万字笔记,从 Ollama、OpenAI、DeepSeek、流式输出一直抄到会话记忆、Function Calling、RAG、Redis 和 MySQL,最后每一段都似乎调通了,却说不清哪层负责安全、状态和故障恢复。
当前 Spring AI 2.0.0 已有原生 DeepSeek starter、统一 Chat API、记忆 repository、工具调用和可观测能力。与其从历史课程的几十个步骤开始,不如先把一条最小对话链路拆成五层,让每层都能回答:
- 输入从哪里来;
- 可以修改或调用什么;
- 敏感数据在哪里;
- 失败如何表达;
- 用什么证据验收。
先搭五层骨架,再加 RAG 和业务功能

第一层:入口契约
Controller 的职责不是把任意字符串原样送给模型。它至少要定义:
- 谁有权调用;
message、conversationId和业务字段的长度与格式;- 同步、SSE 或其他输出协议;
- 限流、超时、取消和业务错误格式;
- 哪些 provider 错误不应直接暴露给用户。
一个最小请求对象可以是:
public record ChatRequest(
@NotBlank @Size(max = 4_000) String message,
@NotBlank @Size(max = 128) String conversationId) {
}
这里有意没把 model、temperature 和 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 抽象承载不同模态和 provider;业务层不应绑死某个底层客户端。
记忆与历史必须分开
原笔记已经注意到“会话记忆”和“会话历史”不同,这是很重要的判断。当前官方文档将它们定义为:
- Chat Memory:模型为维持当前上下文而保留的相关消息;
- Chat History:用户与模型交换的完整记录。
ChatMemory 不适合代替完整历史存储。默认 MessageWindowChatMemory 使用内存 repository 并保留最多 20 条消息,超出窗口后会淘汰旧轮次。Chat Memory

生产设计至少要分成三个对象:
- 会话元数据:用户、租户、标题、时间、状态;
- 完整历史:用于展示、审计、导出或用户删除;
- 模型记忆:经窗口、摘要或检索选择后送给模型的部分。
官方已提供 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,为 ChatClient、Advisor、ChatModel、工具调用和向量库等提供观测。可观测文档
最小观测项包括:
- 请求次数、延迟和当前并发;
- provider 和请求/响应模型;
- 输入、输出和总 token 使用量;
- 工具名称、延迟、错误与调用 ID;
- 超时、限流、重试和最终失败的业务结果。
prompt、completion、tool arguments 和 tool result 可能很大,也可能包含隐私或密钥。Spring AI 默认不导出这些内容。只为故障诊断打开完整日志时,要限定环境、保留周期和可见人群,并先做脱敏。
重试不能靠默认值不经思考地托底
DeepSeek starter 的当前文档列出了统一 spring.ai.retry 配置,包括最大尝试、指数退避和哪些 HTTP 状态可重试。默认不对 4xx 客户端错误重试,这是合理的起点:无效 key、无权限和错误请求不会因为再发一次就变正确。
但是否重试 429、5xx 或网络超时,仍要根据请求的成本、用户等待上限、provider 限制和业务幂等设计。流式输出已经向用户发送了部分内容时,重试还要避免重复片段。
第一版只需要八个可运行检查
- 配置启动: 缺少
DEEPSEEK_API_KEY时快速失败,不带假 key 启动后等首次请求爆错。 - 输入校验: 空消息、超长消息和无权 conversation ID 被拒绝。
- 流式协议: content type 正确,客户端取消后服务端停止继续消耗。
- 会话隔离: 不同用户即使猜到相同 conversation ID,也无法获取对方上下文。
- 窗口边界: 超出 memory window 后旧轮次按预期淘汰,系统消息仍保留。
- 工具授权: 无权用户即使模型请求了工具,服务层也不执行。
- 工具幂等: 相同业务键的重试不会重复写入或扣费。
- 观测脱敏: 常规 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 可替换,状态与副作有边界,每次请求都能被观测和验收。
更多推荐

所有评论(0)