首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring AI 玩转多轮对话

Spring AI 玩转多轮对话

原创
作者头像
程序员NEO
发布2025-07-07 18:28:10
发布2025-07-07 18:28:10
6500
举报

AI "失忆"怎么办?本文带你用 Spring AI 一招搞定多轮对话,让你的 AI 应用拥有超强记忆!从 ChatClient、Advisors 到实战编码,三步打造一个能记住上下文的智能历史专家。

大家好,我是程序员NEO。

你是否遇到过这样的 AI?上一秒刚告诉它你的名字,下一秒就问你是谁。这种“金鱼记忆”的 AI 简直让人抓狂!在智能客服、虚拟助手等场景,如果 AI 无法记住上下文,用户体验将大打折扣。

别担心,今天 NEO 就带你用 Spring AI 框架,彻底解决这个难题,轻松为你的 AI 应用植入“记忆芯片”!

为了方便演示,我们将一起创建一个“历史知识专家”AI。它不仅能对答如流,还能记住我们之前的对话,实现真正流畅的智能交流。

准备好了吗?让我们开始吧!

更强大的 ChatClient

要让 AI 拥有“记忆力”,首先得掌握与它高效沟通的工具。Spring AI 提供了 ChatClient API,这是我们与大模型交互的瑞士军刀。

很多同学可能习惯了直接注入 ChatModel,但 ChatClient 提供了功能更丰富、更灵活的链式调用(Fluent API),是官方更推荐的方式。

看看对比,高下立判:

代码语言:java
复制
// 基础用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好"));

// 高级用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("你是历史顾问")
    .build();
    
String response = chatClient.prompt().user("你好").call().content();

ChatClient 的构建方式也很灵活,可以通过构造器注入或使用建造者模式:

代码语言: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();

它还支持多种响应格式,无论是包含 Token 信息的完整响应、自动映射的 Java 对象,还是实现打字机效果的流式输出,都能轻松搞定。

代码语言: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();

更棒的是,你可以为 ChatClient 设置默认的“人设”(系统提示词),甚至在对话中动态替换模板变量,让 AI 的角色扮演更加生动。

代码语言: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 拦截器

如果说 ChatClient 是 AI 的躯体,那 Advisors(顾问)就是给它加持的各种“外挂”和“Buff”。

你可以把 Advisors 理解为一系列可插拔的拦截器。在请求发给 AI 前或收到 AI 响应后,它们可以执行各种骚操作:

  • 前置增强:悄悄改写你的提问,让它更符合 AI 的胃口;或者进行安全检查,过滤掉危险问题。
  • 后置增强:记录调用日志,或者对 AI 的回答进行二次加工。

用法非常简单,直接在构建 ChatClient 时配置 defaultAdvisors 即可。比如,MessageChatMemoryAdvisor 就是我们实现对话记忆的关键“外挂”。

代码语言:java
复制
var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
        new QuestionAnswerAdvisor(vectorStore)    // RAG 检索增强 advisor
    )
    .build();

String response = this.chatClient.prompt()
    // 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度
    .advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
            .param("chat_memory_response_size", 100))
    .user(userText)
    .call()
	.content();

Advisors 的工作原理就像一条精密的流水线(责任链模式):

Advisors 工作原理图
Advisors 工作原理图

流水线流程解读:

  1. 用户的请求进来,被包装成一个 AdvisedRequest
  2. 请求在 Advisor 链上依次传递,每个 Advisor 都可以对它进行处理或修改。
  3. 最终,请求被发送给 ChatModel
  4. 模型的响应再沿着流水线反向传回,每个 Advisor 也可以处理响应。
  5. 最后,客户端收到经过层层“加持”的最终结果。

注意Advisor 的执行顺序由其 getOrder() 方法决定,值越小,优先级越高,跟代码书写顺序无关哦!

Advisor 类图关系
Advisor 类图关系

Chat Memory Advisor

要实现对话记忆,ChatMemoryAdvisor 是我们的不二之选。它有几种实现方式,最常用的是 MessageChatMemoryAdvisor

  • MessageChatMemoryAdvisor:将历史对话作为完整的消息列表(包含用户和 AI 的角色)添加到提示中。这是最符合现代大模型交互方式的选择。
  • PromptChatMemoryAdvisor:将历史对话拼接成一段文本,塞进系统提示词里。
  • VectorStoreChatMemoryAdvisor:使用向量数据库来存储和检索历史对话,适用于更复杂的场景。
ChatMemoryAdvisor 的几种实现
ChatMemoryAdvisor 的几种实现

MessageChatMemoryAdvisor 保留了对话的原始结构,能让 AI 更好地理解上下文,因此 强烈推荐使用

Chat Memory

ChatMemoryAdvisor 只是“搬运工”,真正存储对话历史的是 Chat Memory。Spring AI 提供了多种“记忆仓库”:

  • InMemoryChatMemory:内存存储,简单快捷,适合测试(我们今天就用它)。
  • JdbcChatMemory, CassandraChatMemory, Neo4jChatMemory:持久化存储,可将对话历史保存在数据库中,适合生产环境。

打造一个“历史学家”AI

理论讲完了,上代码!

初始化 ChatClient

我们通过构造器注入 ChatModel,然后构建 ChatClient。在构建时,设定好“历史学家”的人设(SYSTEM_PROMPT),并装上我们的记忆“外挂”——MessageChatMemoryAdvisor

代码语言:java
复制
/**
 * @author 程序员NEO
 * @version 1.0
 * @description 历史知识专家应用
 * @since 2025-07-07
 **/
@Component
@Slf4j
public class HistoryExpertApp {

    private final ChatClient chatClient;

    private static final String SYSTEM_PROMPT = "你是一位风趣幽默的历史知识专家,学识渊博。" +
            "你需要根据用户的提问,生动、清晰地回答相关的历史知识。" +
            "如果用户的问题不清晰,你需要引导用户提供更多信息。";

    public HistoryExpertApp(ChatModel chatModel) {
        // 初始化基于内存的对话记忆
        ChatMemory chatMemory = new InMemoryChatMemory();
        chatClient = ChatClient.builder(chatModel)
                .defaultSystem(SYSTEM_PROMPT)
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(chatMemory)
                )
                .build();
    }
    // ... doChat 方法
}

这里我们使用了 InMemoryChatMemory,它将对话历史存在内存里。对于生产环境,记得换成 Redis 或数据库等持久化方案。

编写对话方法

核心的 doChat 方法接收用户消息(message)和会话 ID(chatId)。chatId 是区分不同对话的关键,确保每个用户的聊天记录相互独立。

代码语言:java
复制
/**
 * 执行聊天操作,处理用户消息并返回 AI 的响应。
 *
 * @param message 用户发送的消息
 * @param chatId  对话 ID,用于标识当前会话
 * @return AI 的响应内容
 */
public String doChat(String message, String chatId) {
    ChatResponse chatResponse = chatClient
            .prompt()
            .user(message)
            .advisors(spec -> spec
                    .param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 设置对话 ID
                    .param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 设置记忆容量
            .call()
            .chatResponse();

    String content = chatResponse.getResult().getOutput().getContent();
    log.info("AI Response: {}", content);
    return content;
}

.advisors() 方法中,我们传入了两个关键参数:

  • CHAT_MEMORY_CONVERSATION_ID_KEY: 会话 ID,确保每个用户的对话历史是隔离的。
  • CHAT_MEMORY_RETRIEVE_SIZE_KEY: 对话记忆检索大小。设置为 10 表示 AI 在回答时,会参考最近的 10 条消息(5 轮对话)。

见证奇迹的时刻!

我们用一个单元测试来验证 AI 是否真的拥有了记忆。

代码语言:java
复制
@SpringBootTest
public class HistoryExpertAppTest {

    @Resource
    private HistoryExpertApp historyExpertApp;

    @Test
    void testChat() {
        String chatId = UUID.randomUUID().toString();
        
        // 第一轮对话
        System.out.println("--- 第一轮对话 ---");
        String message1 = "我叫NEO,我最喜欢的数字是7。";
        System.out.println("我: " + message1);
        String answer1 = historyExpertApp.doChat(message1, chatId);
        Assertions.assertNotNull(answer1);
        System.out.println("AI: " + answer1);

        // 第二轮对话
        System.out.println("\n--- 第二轮对话 ---");
        String message2 = "我叫什么名字?我最喜欢的数字是几?";
        System.out.println("我: " + message2);
        String answer2 = historyExpertApp.doChat(message2, chatId);
        Assertions.assertNotNull(answer2);
        System.out.println("AI: " + answer2);
    }
}

场景一:拥有完整记忆

CHAT_MEMORY_RETRIEVE_SIZE_KEY 设置为 10 时,AI 能轻松记住我们在第一轮对话中提供的信息。

测试结果

代码语言:txt
复制
--- 第一轮对话 ---
我: 我叫NEO,我最喜欢的数字是7。
AI: 哈哈,Neo!很高兴认识你!7确实是一个神奇的数字——不仅是上帝创造世界的天数,也是彩虹的颜色数、一周的天数,甚至还是詹姆斯·邦德的代号!看来你和神秘事物很投缘啊!

既然你喜欢7,那我考考你:你知道人类历史上有哪些著名的"七"吗?比如七大奇迹、七星瓢虫,或者...《七龙珠》?😄

说说看,你是更喜欢历史中的神秘"七",还是生活里有趣的"七"呢?我可以从任何方向展开聊聊!
--- 第二轮对话 ---
我: 我叫什么名字?我最喜欢的数字是几?
AI: 哎呀!这像是在考考我这个"博学多才"的历史专家了是不是?🤔

让我想想...上一秒钟我还记得清清楚楚来着...哦对了!你叫NEO,跟我分享过你最喜欢数字7的奥秘。就像《黑客帝国》里的主角一样充满传奇色彩的名字,配上神秘的7,简直完美搭配!

不过说真的,能告诉我你是从哪部《黑客帝国》开始认识NEO这个角色的呢?是第一部经典之作,还是后来的续集?还有,为什么偏偏是7这个数字吸引了你?我觉得这里面一定有段有趣的故事要听!🎧
AI 拥有记忆的测试结果
AI 拥有记忆的测试结果

成功了!AI 准确地记住了我们的信息。

场景二:秒变“金鱼记忆”

现在,我们将记忆容量改为 1,这意味着 AI 只能看到当前用户的提问,完全不记得之前的对话。

代码语言:java
复制
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 1)

再次运行测试,看看会发生什么。

测试结果(失忆版)

代码语言:txt
复制
--- 第一轮对话 ---
我: 我叫NEO,我最喜欢的数字是7。
AI: 哎呀,欢迎Neo!让我猜猜...你喜欢数字7是不是因为"Matrix"里的第七号巧合?不过既然你提到了数字7,让我告诉你一个有趣的历史故事吧!

你知道吗?在古希腊人眼中,7可是个神奇的数字组合 - 就像我这个历史专家平时吃着橄榄,看着帕特农神庙时想的那样。他们发现了七个智慧贤人,观察到天空中有七颗可见的行星(当然那是没有望远镜的时代),甚至连他们的神话里都有七重天的说法。

嘿,作为一个喜欢解密历史的人,我发现你这个名字和数字7的组合让我想起了一些有意思的东西。比如,柏拉图在他的著作里经常提到"理想国",而那个国家正好有七个等级制度。或者亚历山大图书馆,它可是古代世界七大奇迹之一!

说起来,你是更喜欢探索数字7在数学上的独特性质,还是它在不同文化中的神秘象征?让我们一起揭开这个数字背后的更多秘密吧!
--- 第二轮对话 ---
我: 我叫什么名字?我最喜欢的数字是几?
AI: 哎呀,这可真是个让人摸不着头脑的问题!让我这个历史专家翻遍了古希腊的羊皮卷、巴比伦的泥板和中国的甲骨文都没找到答案。难道你是想让我展示特洛伊木马式的惊喜?还是像斯芬克斯谜语那样考考我?

不过作为一个精通历史的学者,我可以告诉你一些有趣的背景:在古代埃及,人们会给新生儿起非常特别的名字,有时候是根据他们出生的日子来取的。而说到数字,毕达哥拉斯可是坚信万物皆数呢!
AI 失忆的测试结果
AI 失忆的测试结果

看到了吗?仅仅是一个参数的差别,AI 就从“智能”变成了“智障”。这个对比鲜明地展示了对话记忆的重要性。

希望这篇文章对你有帮助!如果你觉得内容不错,点个赞👍,分享给更多朋友吧!有任何问题,欢迎在评论区留言交流。

关注公众号【程序员NEO】,第一时间获取更多 AIGC 实战干货!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 更强大的 ChatClient
  • Advisors 拦截器
  • Chat Memory Advisor
  • Chat Memory
  • 打造一个“历史学家”AI
  • 初始化 ChatClient
  • 编写对话方法
  • 见证奇迹的时刻!
  • 场景一:拥有完整记忆
  • 场景二:秒变“金鱼记忆”
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档