多轮对话实现
背景
要实现具有 “记忆力” 的 AI 应用,让 AI 能够记住用户之前的对话内容并保持上下文连贯性,我们可以使用Spring AI 框架的 对话记忆能力。
如何使用对话记忆能力呢?参考 Spring AI 的官方文档, Spring AI 提供了 ChatClient API 来和 AI 大模型交互。
一、ChatClient
官方示例代码,ChatClient 支持更复杂灵活的链式调用(Fluent API):
@RestController
class MyController {private final ChatClient chatClient;public MyController(ChatClient.Builder chatClientBuilder) {this.chatClient = chatClientBuilder.build();}@GetMapping("/ai")String generation(String userInput) {return this.chatClient.prompt().user(userInput).call().content();}
}
Spring AI 提供了多种构建 ChatClient 的方式,比如自动注入、通过建造者模式手动构造:
// 方式1:使用构造器注入
@Service
public class ChatService {private final ChatClient chatClient;public ChatService(ChatClient.Builder builder) {this.chatClient = builder.defaultSystem("你是编程语言大师").build();}
}// 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel).defaultSystem("你是编程语言大师").build();
ChatClient 支持多种响应格式,比如返回 ChatResponse 对象、返回实体对象、流式返回:
// ChatClient支持多种响应格式
// 1. 返回 ChatResponse 对象(包含元数据如 token 使用量)
ChatResponse chatResponse = chatClient.prompt().user("Tell me a joke").call().chatResponse();// 2. 返回实体对象(自动将 AI 输出映射为 Java 对象)
// 2.1 返回单个实体
record ActorFilms(String actor, List<String> movies) {}
ActorFilms actorFilms = chatClient.prompt().user("Generate the filmography for a random actor.").call().entity(ActorFilms.class);// 2.2 返回泛型集合
List<ActorFilms> multipleActors = chatClient.prompt().user("Generate filmography for Tom Hanks and Bill Murray.").call().entity(new ParameterizedTypeReference<List<ActorFilms>>() {});// 3. 流式返回(适用于打字机效果)
Flux<String> streamResponse = chatClient.prompt().user("Tell me a story").stream().content();// 也可以流式返回ChatResponse
Flux<ChatResponse> streamWithMetadata = chatClient.prompt().user("Tell me a story").stream().chatResponse();
可以给 ChatClient 设置默认参数,比如系统提示词,还可以在对话时动态更改系统提示词的变量,类似模板的概念:
// 定义默认系统提示词
ChatClient chatClient = ChatClient.builder(chatModel).defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}").build();// 对话时动态更改系统提示词的变量
chatClient.prompt().system(sp -> sp.param("voice", voice)).user(message).call().content());
二、Advisors
Spring AI 使用 Advisors(顾问)机制来增强 AI 的能力,可以理解为一系列可插拔的拦截器,在调用 AI 前和调用 AI 后可以执行一些额外的操作,比如:
前置增强:调用 AI 前改写一下 Prompt 提示词、检查一下提示词是否安全
后置增强:调用 AI 后记录一下日志、处理一下返回的结果
直接为 ChatClient 指定默认拦截器,比如对话记忆拦截器 MessageChatMemoryAdvisor 可以帮助我们实现多轮对话能力,省去了自己维护对话列表的麻烦。
var chatClient = ChatClient.builder(chatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisorQuestionAnswerAdvisor.builder((vectorStore).builder() // RAG advisor).build();var conversationId = "678";String response = this.chatClient.prompt()// Set advisor parameters at runtime.advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId)).user(userText).call().content();
Advisors 的原理图如下👇
1、官网原文翻译:
Spring AI 框架会根据用户的 Prompt(提示词)创建一个 AdvisedRequest,同时会生成一个空的 AdvisorContext(顾问上下文)对象。
框架中的每个 advisor(顾问/拦截器) 会依次处理这个请求,它们可以:
- 修改请求内容。
- 阻止请求继续传递(如果阻止,当前 advisor 需要自己生成并填充返回结果)。
如果请求一路通过,最后会由框架自带的最终 advisor 把请求发送给 Chat Model(聊天模型)。
聊天模型生成响应后,这个响应会沿着原路通过 advisor 链,最终被封装成 AdvisedResponse,这个响应中也包含之前共享的 AdvisorContext。
每个 advisor 也可以在响应阶段处理或修改返回结果。
最终,处理完的 AdvisedResponse 会被返回给客户端,客户端会从中提取出 ChatCompletion(聊天模型生成的回答内容)。
2、翻译
你可以把 Spring AI 的这套机制 想象成:
一个多人协作的过安检流程,大家可以一起检查、修改,甚至拦下行李。
具体流程:
- 用户输入(Prompt) 就像是一个乘客的行李,交给系统处理。
- 系统会创建一个“请求包裹”(AdvisedRequest),和一个“共享便签”(AdvisorContext),这个便签可以让后面的每个人都写点东西上去(比如记录信息、加备注)。
- 这个行李会被传递给一组“安检员”(Advisor 链)。①每个安检员可以检查、调整,甚至拦住行李不让它继续走。②如果一个安检员拦下了行李,他要自己给出答案,不能再交给下一个人了。
- 如果行李一路畅通无阻,最后会被送到“AI 模型”(Chat Model),它会给出回复。
- 这时候,AI 的回答会被传回安检员们手里,他们还可以继续修改回答。
- 最后整理好的回答会被打包好(AdvisedResponse),送回给用户。
实际开发中,往往我们会用到多个拦截器,组合在一起相当于一条拦截器链条(责任链模式的设计思想)。每个拦截器是有顺序的,通过 getOrder() 方法获取到顺序,得到的值越低,越优先执行。
三、Chat Memory Advisor
前面我们提到了,想要实现对话记忆功能,可以使用 Spring AI 的 ChatMemoryAdvisor,它主要有几种内置的实现方式:
- MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中
- PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
- VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话
1、MessageChatMemoryAdvisor
将对话历史作为一系列独立的消息添加到提示中,保留原始对话的完整结构,包括每条消息的角色标识(用户、助手、系统)。
[{"role": "user", "content": "你好"},{"role": "assistant", "content": "你好!有什么我能帮助你的吗?"},{"role": "user", "content": "讲个笑话"}
]
2、PromptChatMemoryAdvisor
将对话历史添加到提示词的系统文本部分,因此可能会失去原始的消息边界。
以下是之前的对话历史:
用户: 你好
助手: 你好!有什么我能帮助你的吗?
用户: 讲个笑话现在请继续回答用户的问题。
3、总结
一般情况下,更建议使用 MessageChatMemoryAdvisor。更符合大多数现代 LLM 的对话模型设计,能更好地保持上下文连贯性
四、Chat Memory
上述 ChatMemoryAdvisor 都依赖 Chat Memory 进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。
Spring AI 内置了几种 Chat Memory,可以将对话保存到不同的数据源中,比如:
- InMemoryChatMemory:内存存储
- CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
- Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
- JdbcChatMemory:在 JDBC中没有过期时间限制的持久化存储
当然也可以通过实现 ChatMemory 接口自定义数据源的存储。
五、总结
- ChatClient 就是 Spring AI 调用大模型的客户端
- Advisors 是用于增强 AI 调用能力的拦截器
- Chat Memory Advisor 是对话记忆拦截器
- Chat Memory 是对话记忆拦截器依赖的对话存储