☰
Spring AI 实战:用 ChatClient 与 ChatMemory 落地 Prompt 多轮对话
2026/10/10 2:12:45 网站建设 项目流程

1. 从“断片”的客服机器人说起:Spring AI 多轮对话到底难在哪

如果你用 Java 写过一个智能客服 Demo,大概率遇到过这种尴尬:用户第一句问“你们支持退货吗”,模型答得头头是道;第二句用户追问“那运费谁出”,模型突然像失忆一样反问“请问您说的是哪件商品”。这不是模型笨,而是你的对话链路里缺了上下文管理这一环。

Spring AI 这套框架把大模型调用抽象成了 Spring 风格的 Bean 和 Advisor,核心就是两个东西:ChatClient负责“怎么问”,ChatMemory负责“记住什么”。前者是入口,后者是记忆,中间靠 ChatMemoryAdvisor 这个切面把历史消息自动拼进当前请求。听起来简单,但工程化落地时有几个坑:Prompt 模板里的变量怎么注入才不报错、多用户会话怎么隔离、历史消息无限增长怎么截断、endpoint 和 Key 怎么统一管理才能随时换模型。

这篇面向 Java 后端,聚焦智能客服和会话助手场景,给出可复制的配置片段和验证步骤。我会把 endpoint 和 Key 统一收到 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end),这样切换模型只改一个 Model ID,不用动业务代码。适合谁?已经会用 Spring Boot 写 REST 接口、想给现有系统加对话能力的后端同学;也适合正在选型、想搞清楚 ChatMemory 到底怎么落地的架构同学。

先说结论:Spring AI 的多轮对话不是“把历史消息拼成一个长字符串”那么简单,它有一套 Advisor 机制在背后做请求增强。理解这套机制,你才能写出可维护的对话链路,而不是堆一堆 if-else 拼上下文。

2. TaoToken 前置:把 endpoint 与 Key 统一收口

在写 ChatClient 之前,先把模型接入层理清楚。Spring AI 默认支持 OpenAI、Azure OpenAI、Ollama 等多种实现,但每换一个模型就要改 base-url、api-key、model 三处配置,业务代码里还散落着模型名。我的做法是:所有模型请求统一走 TaoToken 的 API 地址,Key 也用同一个,切换模型只改 Model ID。

TaoToken 的 API 地址是 https://taotoken.net/api,兼容 OpenAI 的接口规范,所以 Spring AI 的 OpenAiApi 可以直接指过去。你需要在 TaoToken 控制台创建一个 API Key,然后配置到 Spring Boot 的 application.yml 里。注意:Key 不要硬编码在代码里,用环境变量注入。

先看依赖。Spring AI 的版本迭代较快,建议用 1.0.0-M6 及以上,Maven 坐标如下:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-core</artifactId> <version>1.0.0-M6</version> </dependency>

如果你用的是 Gradle,对应改成 implementation 即可。这里有个坑:M6 版本的包名和 M5 有差异,ChatMemory 的接口从ChatMemory变成了ChatMemory加MessageWindowChatMemory实现,别照抄旧教程。

配置片段如下,路径是src/main/resources/application.yml:

spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7

base-url指向 TaoToken 的 API 地址,api-key从环境变量读,model先填一个通用模型。这样配置后,Spring AI 会自动装配一个 OpenAiChatModel Bean,你不需要手动 new。如果你要换模型,只改model这一行,比如改成claude-3-5-sonnet或deepseek-chat,业务代码零改动。

这里要提醒:TaoToken 的 API 地址不带 UTM 参数,就是干净的 https://taotoken.net/api。控制台里创建 Key 的时候,建议按环境分 Key,开发用一个、生产用一个,方便排查和轮换。Key 的权限最小化,只给 chat 权限,不要给管理权限。

配置完成后,先别急着写多轮对话,用一段最小代码验证连通性。写一个 CommandLineRunner:

@Bean CommandLineRunner testChat(OpenAiChatModel chatModel) { return args -> { String resp = chatModel.call("用一句话解释什么是 Spring AI"); System.out.println("模型返回: " + resp); }; }

启动应用,如果控制台打印出模型回答,说明 endpoint 和 Key 都通了。如果报 401,检查 Key 是否复制完整、有没有多余空格;如果报连接超时,检查 base-url 是不是写成了 https://taotoken.net/api/ 带斜杠,有些版本对尾部斜杠敏感。

3. 可复制配置:ChatClient 与 ChatMemory 的工程化装配

连通性验证通过后,进入正题。Spring AI 的多轮对话核心是三个 Bean:ChatMemory、ChatMemoryAdvisor、ChatClient。我按生产可用的方式装配,路径放在src/main/java/com/example/config/ChatConfig.java。

先配 ChatMemory。开发环境用内存版,生产环境建议换 Redis 或 JDBC。这里用MessageWindowChatMemory,它支持窗口大小限制,避免历史无限增长:

@Configuration public class ChatConfig { @Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); } }

maxMessages(20)表示最多保留 20 条消息,超出后自动丢弃最旧的。这个值要结合模型上下文窗口和业务场景调,客服场景一般 10 到 20 条够用。注意:MessageWindowChatMemory是 M6 的类名,如果你用 M5,对应的是InMemoryChatMemory,没有窗口限制,长对话会撑爆 Token。

接着配 ChatClient,把 ChatMemoryAdvisor 挂上去:

@Bean public ChatClient chatClient(OpenAiChatModel chatModel, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultSystem("你是一个电商客服助手,用简洁友好的语气回答用户问题。" + "如果用户问退货,先确认订单号;如果问运费,说明满99包邮。") .defaultAdvisors(new ChatMemoryAdvisor(chatMemory)) .build(); }

defaultSystem是系统提示词,定义角色和行为边界。defaultAdvisors挂上 ChatMemoryAdvisor,这样每次调用 ChatClient 时,Advisor 会自动从 ChatMemory 取历史消息,拼到当前请求里。你不需要手动拼上下文。

但这里有个关键问题:多用户场景下,所有用户共用一个 ChatMemory 会串话。用户 A 问“我的订单呢”,用户 B 问“运费多少”,模型会把两个人的历史混在一起。解决办法是给每个会话分配独立的 conversationId。Spring AI 的 ChatMemoryAdvisor 支持通过ChatClient.prompt().advisors(a -> a.param("chat_memory_conversation_id", "user-123"))指定会话 ID。

我封装一个 ChatService,把会话 ID 作为参数传进去:

@Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient chatClient) { this.chatClient = chatClient; } public String chat(String conversationId, String userMessage) { return chatClient.prompt() .user(userMessage) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call() .content(); } }

conversationId可以用用户 ID、会话 ID 或订单号,保证同一用户的对话落在同一个记忆空间。这样多用户隔离就做好了。

Prompt 模板的参数注入也是工程化重点。Spring AI 支持PromptTemplate,但更推荐用 ChatClient 的.user(u -> u.text("...{orderId}...").param("orderId", orderId))方式。比如:

public String queryOrder(String conversationId, String orderId) { return chatClient.prompt() .user(u -> u.text("帮我查一下订单 {orderId} 的物流状态") .param("orderId", orderId)) .advisors(a -> a.param("chat_memory_conversation_id", conversationId)) .call() .content(); }

这样参数注入是类型安全的,不会因为字符串拼接出错。注意:.param()的 key 要和模板里的{key}完全一致,大小写敏感。

如果你用 Cline MCP 或 Claude Code 做本地开发辅助,配置里同样要写全三件套:Base URL 填 https://taotoken.net/api,API Key 填你的 TaoToken Key,Model ID 填 gpt-4o-mini 或你选的模型。Codex 的 auth.json 里也是这三项,别漏了 Model ID,否则会报 model not found。

4. 验证请求:多轮上下文保持与参数注入的实测

配置写完了,怎么验证多轮对话真的生效?我设计一个三步测试:第一轮问基础问题,第二轮用代词追问,第三轮注入参数。如果模型能正确理解代词和参数,说明 ChatMemory 和 Prompt 模板都工作正常。

写一个测试类,路径src/test/java/com/example/ChatMemoryTest.java:

@SpringBootTest class ChatMemoryTest { @Autowired private ChatService chatService; @Test void testMultiTurn() { String convId = "test-user-001"; String r1 = chatService.chat(convId, "什么是 Spring AI?"); System.out.println("第一轮: " + r1); String r2 = chatService.chat(convId, "它有哪些核心组件?"); System.out.println("第二轮: " + r2); String r3 = chatService.chat(convId, "ChatMemory 怎么用?"); System.out.println("第三轮: " + r3); } }

跑起来后,观察第二轮的回答。如果模型能说出“Spring AI 的核心组件包括 ChatClient、ChatMemory、Advisor 等”,而不是反问“你指的是什么”,说明上下文保持成功。第三轮继续追问 ChatMemory,模型应该能结合前两轮的历史给出连贯回答。

实测下来,MessageWindowChatMemory在 20 条窗口内表现稳定。但如果你的对话轮次很多,比如超过 10 轮,建议在系统提示里加一句“如果历史消息过长,优先参考最近 5 轮对话”。这是因为窗口截断是从最旧的开始丢,模型可能丢失早期关键信息。

参数注入的验证更直接。调用queryOrder("test-user-001", "ORD-2024-888"),看模型返回里有没有提到这个订单号。如果返回“请提供订单号”,说明参数没注入进去,检查.param()的 key 和模板{orderId}是否匹配。

再验证一下多用户隔离。用两个不同的 conversationId 分别问“我叫什么名字”,第一轮告诉模型“我叫张三”,第二轮问“我叫什么”。如果 convId 不同,模型应该答不出来;如果相同,应该能答出“张三”。这个测试能确认 ChatMemory 的会话隔离是否生效。

成功的结果长这样:第一轮返回 Spring AI 的定义,第二轮返回组件列表,第三轮返回 ChatMemory 的使用方式,且三轮回答逻辑连贯。如果第二轮就断片,先检查 ChatMemoryAdvisor 有没有挂上,再检查 conversationId 有没有传对。

这里有个细节:Spring AI 的 ChatMemoryAdvisor 默认会把系统消息也存进历史。如果你发现历史里混入了系统提示,可以在 Advisor 配置里设置chat_memory_retrieve_size参数,控制每次取多少条历史。默认是全取,长对话建议设成 10。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

接入过程中最容易撞的几类报错,我按真实日志对照说。

401 Unauthorized。日志里通常是401 Unauthorized: Incorrect API key provided。原因就三个:Key 复制错了、Key 过期了、环境变量没读到。排查步骤:先在 TaoToken 控制台确认 Key 有效,然后用 curl 直接测:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'

如果 curl 通、Java 不通,检查 Spring 配置里api-key的占位符${TAOTOKEN_API_KEY}有没有被正确解析。IDEA 里跑测试时,环境变量要在 Run Configuration 里配,不是系统环境变量。

local proxy failed。这个报错通常出现在你本地配了 HTTP 代理,但代理不可达。日志类似Connection refused: localhost:7890。解决办法:检查application.yml里有没有proxy相关配置,或者 JVM 启动参数里有没有-Dhttp.proxyHost。如果有,去掉。Spring AI 走 TaoToken 的 API 地址是直连的,不需要额外代理配置。

reading choices 报错。完整日志可能是Error reading choices from response或Cannot deserialize value of type Choice from Object value。这通常是模型返回格式和 Spring AI 预期的 OpenAI 格式不一致。排查:确认 base-url 是 https://taotoken.net/api,且 model 是 TaoToken 支持的模型 ID。如果你填了一个不存在的模型名,返回体里没有 choices 字段,反序列化就炸了。解决:去 TaoToken 文档页确认模型 ID 列表,填对。

OAuth 相关报错。如果你用的是 Claude Code 或某些需要 OAuth 的工具,报OAuth token expired或invalid_grant,说明认证方式不对。TaoToken 的 API 走的是 Bearer Token,不是 OAuth。检查你的配置里是不是混用了 OAuth 流程。正确做法:在 API Keys 页面生成 Key,直接作为 Bearer Token 用。

ChatMemory 不生效。现象是第二轮对话模型不记得第一轮。排查顺序:第一,确认 ChatMemoryAdvisor 挂到了 ChatClient 上;第二,确认每次调用传了相同的 conversationId;第三,确认 ChatMemory Bean 是单例,不是每次 new 的。如果 ChatMemory 是 prototype 作用域,每次调用都是新实例,历史自然丢了。

Prompt 模板变量没替换。日志里能看到{orderId}原样传给了模型。原因通常是.param()的 key 拼写和模板不一致,或者用了.user(String)而不是.user(Consumer)。记住:只有.user(u -> u.text("...").param(...))这种写法才支持变量替换,直接传字符串不会解析。

对照这些报错,基本能覆盖 90% 的接入问题。如果还搞不定,去 TaoToken 的接入文档页看最新示例,或者用模型对话页直接测模型是否可用,排除是模型侧还是代码侧的问题。

6. 把对话链路收口到 TaoToken:长期维护的实用建议

写到这里,ChatClient 和 ChatMemory 的装配、验证、排障都走了一遍。最后说几个长期维护的实用点。

第一,把模型配置外置到配置中心。application.yml里的model字段不要写死,用@ConfigurationProperties绑定,这样换模型不用重新打包。配合 TaoToken 的统一 endpoint,你可以在不改代码的情况下切换 gpt-4o-mini、claude-3-5-sonnet 或 deepseek-chat,按成本和效果灵活选。

第二,ChatMemory 的生产选型。内存版只适合开发和单机测试。生产环境用 RedisChatMemory,Key 设计成chat:memory:{conversationId},设置 TTL 比如 24 小时,避免历史无限堆积。如果要做对话审计,用 JdbcChatMemory 落库,但注意脱敏。

第三,Prompt 模板版本化管理。系统提示词不要硬编码在 Java 里,放到resources/prompts/目录下,用@Value("classpath:prompts/customer-service.st")加载。这样改提示词不用重新编译,也方便 A/B 测试。

第四,监控 Token 消耗。Spring AI 的响应里带 usage 信息,你可以用 Advisor 拦截并上报。TaoToken 控制台也有用量统计,定期看哪个模型消耗大,及时调整。

如果你要做更复杂的 Agent 场景,比如多工具调用、长任务编排,建议用 Coding Plan 配合 Claude Code 做开发辅助,把重复的配置和排障工作交给工具。模型对话页可以用来快速验证新模型的效果,不用每次都跑 Java 测试。

整套链路的核心就一句话:ChatClient 管请求,ChatMemory 管上下文,Advisor 管增强,TaoToken 管接入。把这四层分清楚,你的对话系统就能从 Demo 走到生产。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询