一、ChatClient 简介 ChatClient 是 Spring AI 提供的高级 API,比直接使用 ChatModel 更方便。它提供了链式调用的方式,代码更简洁,功能也更丰富。
ChatClient 的优势:
链式调用,代码更清晰
内置对话记忆功能
支持 Advisor 拦截器
统一的工具调用接口
更好的错误处理
什么时候用 ChatClient:
需要对话记忆的场景
需要自定义拦截器的场景
需要工具调用的场景
需要更灵活配置的场景
什么时候用 ChatModel:
简单的单次调用
不需要记忆功能
对性能要求极高的场景
二、多模型管理 2.1 为什么需要多模型管理 实际项目中,我们可能需要同时使用多个 AI 模型:
DeepSeek:适合代码生成、推理任务
阿里百炼:适合中文对话、多模态任务
OpenAI:适合英文场景
如果每个地方都直接注入 ChatModel,代码会很混乱。更好的做法是统一管理,通过参数切换。
2.2 ChatModelService 实现 创建一个服务类来管理多个 ChatModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package org.study.ai.service;import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.deepseek.DeepSeekChatModel;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import jakarta.annotation.PostConstruct;import java.util.HashMap;import java.util.Map;@Service public class ChatModelService { @Autowired(required = false) private DeepSeekChatModel deepSeekChatModel; @Autowired(required = false) private DashScopeChatModel dashScopeChatModel; private final Map<String, ChatModel> chatModelMap = new HashMap<>(); @PostConstruct public void initChatModels () { if (deepSeekChatModel != null ) { chatModelMap.put("ds" , deepSeekChatModel); chatModelMap.put("deepseek" , deepSeekChatModel); } if (dashScopeChatModel != null ) { chatModelMap.put("dashscope" , dashScopeChatModel); } } public ChatModel getChatModel (String platform) { if (platform == null ) { return null ; } return chatModelMap.get(platform.toLowerCase()); } }
关键点:
@Autowired(required = false):某个模型未配置时不会报错
@PostConstruct:应用启动时初始化映射表
支持多个别名:ds 和 deepseek 都指向同一个模型
2.3 实际使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController @RequestMapping("/api/chat") public class ChatController { @Autowired private ChatModelService chatModelService; @GetMapping(value = "/stream", produces = "text/stream;charset=utf-8") public Flux<String> streamChat ( @RequestParam("platform") String platform, @ModelAttribute ChatRequest request) { ChatModel chatModel = chatModelService.getChatModel(platform); if (chatModel == null ) { return Flux.error(new IllegalArgumentException("不支持的平台:" + platform)); } ChatClient chatClient = ChatClient.builder(chatModel).build(); return chatClient.prompt() .user(request.getMessage()) .stream() .content(); } }
调用示例:
1 2 GET /api/chat/stream?platform =ds&message=你好&model=deepseek-chatGET /api/chat/stream?platform =dashscope&message=你好&model=qwen-plus
三、流式对话接口实现 3.1 接口设计 完整的流式对话接口需要考虑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @RestController @RequestMapping("/api/chat") public class ChatController { @Autowired private ChatModelService chatModelService; @GetMapping(value = "/stream", produces = "text/stream;charset=utf-8") public Flux<String> streamChat ( @RequestParam("platform") String platform, @ModelAttribute ChatRequest request) { if (platform == null || platform.trim().isEmpty()) { return Flux.error(new IllegalArgumentException("platform参数不能为空" )); } if (request.getMessage() == null || request.getMessage().trim().isEmpty()) { return Flux.error(new IllegalArgumentException("message参数不能为空" )); } if (StrUtil.isBlank(request.getModel())) { return Flux.error(new IllegalArgumentException("model参数不能为空" )); } if (request.getTemperature() == null ) { request.setTemperature(0.7 ); } ChatModel chatModel = chatModelService.getChatModel(platform); if (chatModel == null ) { return Flux.error(new IllegalArgumentException("不支持的平台:" + platform)); } ChatClient chatClient = ChatClient.builder(chatModel).build(); return chatClient.prompt() .user(request.getMessage()) .options(ChatOptions.builder() .model(request.getModel()) .temperature(request.getTemperature()) .build()) .stream() .content(); } }
3.2 参数校验 参数校验很重要,可以避免无效请求:
1 2 3 4 5 6 7 8 public class ChatRequest { private String message; private String model; private Double temperature = 0.7 ; }
校验要点:
platform:必须指定,否则不知道用哪个模型
message:不能为空
model:必须指定,不同平台的模型名称不同
temperature:有默认值,可以不传
3.3 流式响应处理 流式响应需要注意:
编码问题:produces = "text/stream;charset=utf-8"
空值处理:.map(content -> content == null ? "" : content)
错误处理:使用 Flux.error() 返回错误
四、对话记忆功能 4.1 Redis 配置 对话记忆需要存储历史记录,Spring AI 支持 Redis 存储。
依赖配置:
1 2 3 4 <dependency > <groupId > com.alibaba.cloud.ai</groupId > <artifactId > spring-ai-alibaba-starter-memory-redis</artifactId > </dependency >
配置文件:
1 2 spring.ai.memory.redis.host =localhost spring.ai.memory.redis.port =6379
注意:
需要先启动 Redis 服务
如果没有 Redis,记忆功能不会生效,但不会报错
4.2 使用记忆功能 在 ChatClient 中配置 PromptChatMemoryAdvisor:
1 2 3 4 5 6 7 8 @Autowired private ChatMemory chatMemory; ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( PromptChatMemoryAdvisor.builder(chatMemory).build() ) .build();
完整示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @RestController @RequestMapping("/api/chat") public class ChatController { @Autowired private ChatModel chatModel; @Autowired private ChatMemory chatMemory; @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat (@RequestParam String message) { ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( PromptChatMemoryAdvisor.builder(chatMemory).build() ) .build(); return chatClient.prompt() .user(message) .stream() .content(); } }
4.3 记忆功能原理 PromptChatMemoryAdvisor 的工作流程:
请求前 :从 Redis 中读取历史对话记录
构建 Prompt :将历史记录添加到 Prompt 中
发送请求 :调用 AI 模型
请求后 :将新的对话记录保存到 Redis
实际效果:
1 2 3 4 5 用户:我叫张三 AI:你好,张三! 用户:我的名字是什么? AI:你的名字是张三。
第二次对话时,AI 能记住用户的名字,因为历史记录被自动添加到了 Prompt 中。
五、系统提示词 5.1 从文件加载 系统提示词可能很长,放在代码里不优雅,可以放到文件中:
1 2 3 4 5 6 7 Resource resource = new ClassPathResource("prompt/systemPrompt.md" ); String systemPrompt = new String(resource.getInputStream().readAllBytes()); ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem(systemPrompt) .build();
文件位置: src/main/resources/prompt/systemPrompt.md
5.2 动态参数 系统提示词中可能需要动态信息,比如当前日期:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem("" " #角色 你是一个专业的航空客服助手,名为'南方国际航空'的客服代表 你的职责是帮助客户处理航班预订相关的问题,包括查询航班信息、取消预订等。请用友好、专业的态度回复客户。 #要求 1、在涉及增删改(除了查询)function-call前,必须等用户回复" 确认"后再调用tools 2、请讲中文 今天的日期是 {currentDate} " "" ) .build();return chatClient.prompt() .user(message) .system(p -> p.param("currentDate" , LocalDate.now())) .stream() .content();
关键点:
{currentDate} 是占位符
.system(p -> p.param("currentDate", LocalDate.now())) 替换占位符
每次请求都会使用最新的日期
5.3 实际案例 航空客服助手的完整系统提示词:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ChatClient chatClient = ChatClient.builder(chatModel) .defaultSystem("" " #角色 你是一个专业的航空客服助手,名为'南方国际航空'的客服代表 你的职责是帮助客户处理航班预订相关的问题,包括查询航班信息、取消预订等。请用友好、专业的态度回复客户。 #要求 1、在涉及增删改(除了查询)function-call前,必须等用户回复" 确认"后再调用tools 2、请讲中文 3、如果用户的问题无法通过工具解决,请引导用户联系人工客服 今天的日期是 {currentDate} " "" ) .defaultAdvisors( PromptChatMemoryAdvisor.builder(chatMemory).build(), new SimpleLoggerAdvisor() ) .build();
提示词设计要点:
明确角色:告诉 AI 你是谁
明确职责:告诉 AI 你要做什么
明确规则:告诉 AI 什么能做,什么不能做
动态信息:使用占位符注入动态内容
六、Advisor 拦截器 6.1 自定义 Advisor Advisor 可以在请求前后进行处理,比如日志记录、敏感词拦截等。
实现示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class ReadReadAdvisor implements BaseAdvisor { private static final String READ_TEMPLATE = "" " {content} Read the question again: {content} " "" ; @Override public ChatClientRequest before (ChatClientRequest chatClientRequest, AdvisorChain advisorChain) { String contents = chatClientRequest.prompt().getContents(); String newContent = PromptTemplate.builder() .template(READ_TEMPLATE) .build() .render(Map.of("content" , contents)); ChatClientRequest clientRequest = chatClientRequest.mutate() .prompt(Prompt.builder().content(newContent).build()) .build(); return clientRequest; } @Override public ChatClientResponse after (ChatClientResponse chatClientResponse, AdvisorChain advisorChain) { return chatClientResponse; } @Override public int getOrder () { return 0 ; } }
使用场景:
日志记录:记录每次请求和响应
敏感词拦截:在请求前检查敏感词
翻译转换:将用户输入转换为其他语言
文档后处理:对检索到的文档进行处理
6.2 内置 Advisor SimpleLoggerAdvisor 用于记录请求和响应的日志:
1 2 3 ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors(new SimpleLoggerAdvisor()) .build();
日志输出示例:
1 2 DEBUG SimpleLoggerAdvisor - User: 你好DEBUG SimpleLoggerAdvisor - Assistant: 你好!我是.. .
PromptChatMemoryAdvisor 用于管理对话记忆:
1 2 3 4 5 ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( PromptChatMemoryAdvisor.builder(chatMemory).build() ) .build();
6.3 执行顺序 多个 Advisor 的执行顺序由 getOrder() 方法决定:
1 2 3 4 5 6 7 ChatClient chatClient = ChatClient.builder(chatModel) .defaultAdvisors( new ReadReadAdvisor(), new SimpleLoggerAdvisor(), PromptChatMemoryAdvisor.builder(chatMemory).build() ) .build();
执行流程:
before() 方法:按 order 从小到大执行
调用 AI 模型
after() 方法:按 order 从大到小执行(与 before 相反)
七、SSE 流式响应 7.1 后端实现 Spring WebFlux 会自动将 Flux<String> 转换为 SSE 格式:
1 2 3 4 5 6 7 8 @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat (@RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content() .map(content -> content == null ? "" : content); }
关键点:
produces = MediaType.TEXT_EVENT_STREAM_VALUE:指定返回 SSE 格式
每个字符串会被自动包装为:data: {content}\n\n
不需要手动处理 SSE 格式
7.2 前端接收 JavaScript 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const eventSource = new EventSource('/api/chat/stream?message=你好&platform=ds&model=deepseek-chat' ); eventSource.onmessage = function (event ) { console .log('收到数据:' , event.data); document .getElementById('output' ).innerHTML += event.data; }; eventSource.onerror = function (event ) { console .error('连接错误:' , event); eventSource.close(); };
Vue 实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <template> <div> <div id="output">{{ output }}</div> </div> </template> <script> export default { data() { return { output: '', eventSource: null } }, mounted() { this.eventSource = new EventSource('/api/chat/stream?message=你好'); this.eventSource.onmessage = (event) => { this.output += event.data; }; }, beforeUnmount() { if (this.eventSource) { this.eventSource.close(); } } } </script>
注意事项:
SSE 是单向通信,只能服务器推送到客户端
连接断开后需要重新建立连接
浏览器对 SSE 连接数有限制(通常 6 个)
八、常见问题 8.1 ChatModel 注入失败 问题: @Autowired ChatModel 注入失败,返回 null。
原因:
没有配置对应的 starter
API Key 配置错误
配置文件路径不对
解决:
检查是否添加了对应的依赖(如 spring-ai-starter-model-deepseek)
检查配置文件中的 API Key 是否正确
确认配置文件在 src/main/resources 目录下
8.2 记忆功能不生效 问题: 配置了 PromptChatMemoryAdvisor 但对话没有记忆。
原因:
Redis 未启动
Redis 配置错误
ChatMemory 未注入成功
解决:
检查 Redis 是否启动:redis-cli ping
检查 Redis 配置是否正确
确认 ChatMemory 是否成功注入(添加日志)
8.3 SSE 连接断开 问题: 前端 SSE 连接经常断开。
原因:
解决:
添加心跳机制,定期发送空消息
增加服务器超时时间
前端添加重连机制
8.4 中文乱码 问题: SSE 返回的中文乱码。
原因: 编码问题。
解决:
1 2 @GetMapping(value = "/stream", produces = "text/stream;charset=utf-8")
8.5 Advisor 执行顺序不对 问题: Advisor 的执行顺序不符合预期。
原因: getOrder() 返回值设置错误。
解决:
值越小越先执行
before() 按从小到大执行
after() 按从大到小执行(与 before 相反)
九、总结
ChatClient vs ChatModel :ChatClient 更适合复杂场景,ChatModel 适合简单场景
多模型管理 :通过 ChatModelService 统一管理,通过参数切换
对话记忆 :使用 PromptChatMemoryAdvisor + Redis 实现
系统提示词 :可以从文件加载,支持动态参数
Advisor :可以在请求前后进行处理,实现日志、拦截等功能
SSE :Spring WebFlux 自动处理 SSE 格式,前端用 EventSource 接收
下一篇文章会介绍 Tools/Function Call 功能,这是让 AI 调用外部工具的关键功能。