多轮对话实现

背景

要实现具有 "记忆力" 的 AI 应用,让 AI 能够记住用户之前的对话内容并保持上下文连贯性,我们可以使用Spring AI 框架的 对话记忆能力。

如何使用对话记忆能力呢?参考 Spring AI 的官方文档, Spring AI 提供了 ChatClient API 来和 AI 大模型交互。

一、ChatClient

官方示例代码,ChatClien‌t 支持更复杂灵活的链式调用‏(Fluent API):

java 复制代码
@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();
    }
}

Sprin‏g AI 提供了多؜种构建 ChatC​lient 的方式‌,比如自动注入、通‏过建造者模式手动构造:

java 复制代码
// 方式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();

ChatC‏lient 支持多؜种响应格式,比如返​回 ChatRes‌ponse 对象、‏返回实体对象、流式返回:

java 复制代码
// 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();

可以给 C‏hatClient ؜设置默认参数,比如系​统提示词,还可以在对‌话时动态更改系统提示‏词的变量,类似模板的概念:

java 复制代码
// 定义默认系统提示词
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 指定؜默认拦截器,比如对话记忆拦截器 Me​ssageChatMemoryAdv‌isor 可以帮助我们实现多轮对话能‏力,省去了自己维护对话列表的麻烦。

java 复制代码
var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
        QuestionAnswerAdvisor.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 的这套机制 想象成:

一个多人协作的过安检流程,大家可以一起检查、修改,甚至拦下行李。

具体流程:

  1. 用户输入(Prompt) 就像是一个乘客的行李,交给系统处理。
  2. 系统会创建一个"请求包裹"(AdvisedRequest),和一个"共享便签"(AdvisorContext),这个便签可以让后面的每个人都写点东西上去(比如记录信息、加备注)。
  3. 这个行李会被传递给一组"安检员"(Advisor 链)。①每个安检员可以检查、调整,甚至拦住行李不让它继续走。②如果一个安检员拦下了行李,他要自己给出答案,不能再交给下一个人了。
  4. 如果行李一路畅通无阻,最后会被送到"AI 模型"(Chat Model),它会给出回复。
  5. 这时候,AI 的回答会被传回安检员们手里,他们还可以继续修改回答。
  6. 最后整理好的回答会被打包好(AdvisedResponse),送回给用户。

实际开发中,往往我们会用到多个拦截器,组合在一起相当于一条拦截器链条(责任链模式的设计思想)。每个拦截器是有顺序的,通过 getOrder() 方法获取到顺序,得到的值越低,越优先执行。

三、Chat Memory Advisor

前面我们提到‏了,想要实现对话记忆功能؜,可以使用 Spring​ AI 的 ChatMe‌moryAdvisor,‏它主要有几种内置的实现方式:

  • MessageChatMemoryAdvisor:从记忆中检索历史对话,并将其作为消息集合添加到提示词中
  • PromptChatMemoryAdvisor:从记忆中检索历史对话,并将其添加到提示词的系统文本中
  • VectorStoreChatMemoryAdvisor:可以用向量数据库来存储检索历史对话

1、Messag‏eChatMemoryAdvi؜sor

将对话历史作为一系列独​立的消息添加到提示中,保留原始‌对话的完整结构,包括每条消息的‏角色标识(用户、助手、系统)。

java 复制代码
[
  {"role": "user", "content": "你好"},
  {"role": "assistant", "content": "你好!有什么我能帮助你的吗?"},
  {"role": "user", "content": "讲个笑话"}
]

2、Prom‏ptChatMemor؜yAdvisor

将对​话历史添加到提示词的系‌统文本部分,因此可能会‏失去原始的消息边界。

java 复制代码
以下是之前的对话历史:
用户: 你好
助手: 你好!有什么我能帮助你的吗?
用户: 讲个笑话

现在请继续回答用户的问题。

3、总结

一般情况下,更建议使用 MessageChatMemoryAdvisor。更符合大多数现代 LLM 的对话模型设计,能更好地保持上下文连贯性

四、Chat Memory

上述 ChatMemoryAdvisor 都依赖 Chat Memory 进行构造,Chat Memory 负责历史对话的存储,定义了保存消息、查询消息、清空消息历史的方法。

Sprin‏g AI 内置了几؜种 Chat Me​mory,可以将对‌话保存到不同的数据‏源中,比如:

  • InMemoryChatMemory:内存存储
  • CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
  • Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
  • JdbcChatMemory:在 JDBC中没有过期时间限制的持久化存储

当然也可以‏通过实现 Chat؜Memory 接口​自定义数据源的存储‌。

五、总结

  • ChatClient 就是 Spring AI 调用大模型的客户端
  • Advisors 是用于增强 AI 调用能力的拦截器
  • Chat Memory Advisor 是对话记忆拦截器
  • Chat Memory 是对话记忆拦截器依赖的对话存储
相关推荐
想用offer打牌1 分钟前
面试官拷打我线程池,我这样回答😗
java·后端·面试
真的很上进6 分钟前
2025最全TS手写题之partial/Omit/Pick/Exclude/Readonly/Required
java·前端·vue.js·python·算法·react·html5
重庆小透明12 分钟前
【从零学习JVM|第三篇】类的生命周期(高频面试题)
java·jvm·后端·学习
计算机小手20 分钟前
开源大模型网关:One API实现主流AI模型API的统一管理与分发
人工智能·语言模型·oneapi
BAStriver20 分钟前
PKIX path building failed问题小结
java·maven
welsonx41 分钟前
Android性能优化-Frida工具篇
java
圈圈编码1 小时前
LeetCode Hot100刷题——合并两个有序链表
java·数据结构·算法·leetcode·链表
小前端大牛马1 小时前
java教程笔记(十四)-线程池
java·笔记·python
魔镜魔镜_谁是世界上最漂亮的小仙女1 小时前
java-maven依赖管理
java·后端·全栈
Kim Jackson1 小时前
我的世界Java版1.21.4的Fabric模组开发教程(十三)自定义方块状态
java·游戏·fabric