Spring AI 对话系统核心:深入解析 ChatMemory 与 Advisor 的协同设计
在构建基于大语言模型的智能对话应用时,如何让AI记住上下文,以及如何灵活地干预对话流程,是两个至关重要的挑战。Spring AI框架通过其精心设计的ChatMemory(聊天记忆)和Advisor(顾问)两大核心机制,为开发者提供了优雅的解决方案。本文将深入剖析这两者的设计理念、工作原理及协同方式,帮助你构建更智能、更可控的对话系统。
一、 ChatMemory:对话系统的记忆中枢
ChatMemory是Spring AI中负责管理对话历史的核心组件。它借鉴了传统Web开发中Session的概念,但在AI对话场景下,其职责更为复杂和关键。它的核心功能包括:
- 存储与检索:保存用户与AI的交互历史,并在新对话时将其作为上下文注入。
- 会话隔离:通过唯一的
conversationId区分不同用户或不同对话线程,确保上下文不会混淆。 - 策略管理:实现如滑动窗口等策略,以控制上下文长度,优化性能和成本。
其架构清晰分层,便于理解和扩展:
┌─────────────────────────────────────┐
│ ChatMemory 接口 │
│ - add() │
│ - get() │
│ - clear() │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ MessageWindowChatMemory │
│ (实现滑动窗口策略) │
│ - 限制消息数量 │
│ - FIFO 淘汰机制 │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ ChatMemoryRepository │
│ (底层存储实现) │
│ - InMemoryChatMemoryRepository │
│ - JdbcChatMemoryRepository │
│ - ElasticsearchChatMemoryRepository│
└─────────────────────────────────────┘
核心接口定义了其基本行为:
public interface ChatMemory {
// 向指定对话添加消息
void add(String conversationId, List<Message> messages);
// 从指定对话获取消息
List<Message> get(String conversationId, int lastN);
// 清空指定对话的消息
void clear(String conversationId);
}
public interface ChatMemoryRepository {
// 这是真正的存储实现接口
void add(String conversationId, Message message);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId);
}
二、 从内存存储到滑动窗口策略
Spring AI提供了开箱即用的实现,最基础的是InMemoryChatMemoryRepository。它将对话历史存储在应用内存的ConcurrentHashMap中,键为conversationId,值为消息列表。
// 内部结构示意
private Map<String, List<Message>> conversationMap;
// conversationId -> 对话消息列表
// "user123_chat1" -> [Message1, Message2, Message3, ...]
// "user456_chat2" -> [Message4, Message5, ...]
其添加消息的核心逻辑如下:
Timeline:
时刻 1: user123 问"今天几号?"
→ store: {"user123": [UserMessage("今天几号?")]}
时刻 2: AI 回答"今天是 2月12号"
→ store: {"user123": [UserMessage("今天几号?"), AssistantMessage("今天是 2月12号")]}
时刻 3: user123 问"天气怎么样?"
→ store: {"user123": [UserMessage("今天几号?"), AssistantMessage("今天是 2月12号"), UserMessage("天气怎么样?")]}
内存存储简单高效,适合开发和测试。但在生产环境中,我们通常需要更持久的方案,如数据库存储。配置内存存储非常简单:
@Bean
public ChatMemory chatMemory() {
// 创建内存存储库
InMemoryChatMemoryRepository inMemoryChatMemoryRepository =
new InMemoryChatMemoryRepository();
// 包装成 MessageWindowChatMemory
// 默认窗口大小为 10 条消息
return MessageWindowChatMemory.builder()
.chatMemoryRepository(inMemoryChatMemoryRepository)
.build();
}
然而,将所有历史都传给大模型是不现实的,这既受限于模型的Token长度,也会增加成本和降低响应质量。因此,MessageWindowChatMemory引入了滑动窗口策略,它只保留最近的N条消息。
对话历史(按时间排序):
Message1 (最久)
Message2
Message3
Message4
Message5
Message6
Message7 (最新)
设置 lastN=3,get() 返回:
Message5
Message6
Message7
配置窗口大小也很直观:
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.windowSize(10) // 保留最近 10 条消息
.build();
}
这种策略是自然语言处理和深度学习应用中的常见优化手段,能有效平衡上下文丰富度与计算效率。
三、 Advisor:对话流程的智能拦截器
如果说ChatMemory管理的是“数据”,那么Advisor控制的就是“流程”。它基于AOP(面向切面编程)思想,允许开发者在调用大模型的前后拦截并修改请求和响应。这是实现业务逻辑灵活注入的关键。
其核心接口CallAdvisor定义了拦截行为:
public interface CallAdvisor {
/**
* 拦截 ChatClient 的调用
* @param chatClientRequest 包含 Prompt 等信息的请求对象
* @param callAdvisorChain 调用链,用于传递给下一个 Advisor
* @return 修改或原始的响应
*/
ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest,
CallAdvisorChain callAdvisorChain
);
/**
* Advisor 的名称,用于日志和调试
*/
String getName();
/**
* 执行顺序,数字越小越先执行
* 默认值:Advisor.DEFAULT_ORDER (0)
*/
int getOrder();
}
多个Advisor可以通过CallAdvisorChain组成职责链,按getOrder()定义的顺序执行:
public interface CallAdvisorChain {
/**
* 将控制权传递给下一个 Advisor
*/
ChatClientResponse nextCall(ChatClientRequest request);
}
执行顺序示例如下:
chatClient
.prompt()
.advisors(advisorA) // getOrder() = 0
.advisors(advisorB) // getOrder() = 1
.advisors(advisorC) // getOrder() = 2
.user("问题")
.call()
执行顺序:
advisorA.adviseCall()
↓
advisorB.adviseCall()
↓
advisorC.adviseCall()
↓
chatClient.call()
↓
advisorC 返回响应
↓
advisorB 返回响应
↓
advisorA 返回响应
↓
最终响应返回给用户
这种设计使得功能模块化,例如,你可以用一个Advisor注入用户身份,用另一个进行安全过滤,再用第三个记录日志。
[AFFILIATE_SLOT_1]四、 实战:MessageChatMemoryAdvisor 与自定义 Advisor
MessageChatMemoryAdvisor是Spring AI内置的一个完美范例,它作为一个Advisor,将ChatMemory的能力无缝集成到对话流程中。其工作流程可以概括为:
- 调用前:根据
chatId从ChatMemory中获取历史消息,并拼接到当前Prompt中。 - 调用模型:将增强后的上下文发送给大语言模型。
- 调用后:将本轮的用户消息和AI回复保存回ChatMemory。
一个完整的使用示例如下:
@GetMapping("/memory")
public String memory(@RequestParam("chatId") String chatId,
@RequestParam("question") String question) {
return chatClient
.prompt()
// 添加 MessageChatMemoryAdvisor
.advisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
// 设置对话 ID,实现会话隔离
.advisors(advisorSpec -> advisorSpec.params(
Map.of(ChatMemory.CONVERSATION_ID, chatId)
))
// 用户问题
.user(question)
// 调用大模型
.call()
.content();
}
其内部执行时序清晰地展示了这一过程:
1. 用户发起请求:/memory?chatId=user123&question=天气怎么样?
2. MessageChatMemoryAdvisor 在对话前执行:
- 从 ChatMemory 中获取对话 ID 为 "user123" 的历史消息
- 假设历史是:
* User: 今天几号?
* Assistant: 今天是 2月12号
- 将这些历史消息自动添加到 Prompt 中
3. 增强后的 Prompt 结构:
┌─────────────────────────────────┐
│ System Message │
│ (默认系统提示) │
├─────────────────────────────────┤
│ History │
│ User: 今天几号? │
│ Assistant: 今天是 2月12号 │
├─────────────────────────────────┤
│ Current User Message │
│ User: 天气怎么样? │
└─────────────────────────────────┘
4. 调用大模型(Qwen3-max):
- 模型看到了完整的对话历史
- 能够理解上下文并给出准确回答
- Response: 根据历史和当前问题,给出回答
5. MessageChatMemoryAdvisor 在对话后执行:
- 将用户问题添加到 ChatMemory:
* Message(User, "天气怎么样?")
- 将 AI 回答添加到 ChatMemory:
* Message(Assistant, "根据天气预报...")
- 后续对话就能看到这些新消息了
6. 返回 AI 的回答给前端
通过chatId实现会话隔离的示例:
用户 A 的会话:
chatId = "userA_chat_001"
对话历史:["你好", "今天天气如何", ...]
用户 B 的会话:
chatId = "userB_chat_001"
对话历史:["Hi", "Tell me a joke", ...]
用户 A 的第二个会话:
chatId = "userA_chat_002"
对话历史:["新建对话", ...]
ChatMemory 内部存储:
{
"userA_chat_001": [Message1, Message2, ...],
"userB_chat_001": [Message3, Message4, ...],
"userA_chat_002": [Message5, ...]
}
除了使用内置Advisor,自定义Advisor能带来极大的灵活性。例如,创建一个为对话注入特定身份的Advisor:
static class ArtisanCallAroundAdvisor implements CallAdvisor {
@Override
public ChatClientResponse adviseCall(
ChatClientRequest chatClientRequest,
CallAdvisorChain callAdvisorChain) {
// 对话前
System.out.println("before...");
// 获取原始 Prompt
Prompt prompt = chatClientRequest.prompt();
// 增强系统提示词
Prompt enhancedPrompt = prompt.augmentSystemMessage("我是周瑜");
// 创建修改后的请求
ChatClientRequest modifiedRequest = chatClientRequest
.mutate()
.prompt(enhancedPrompt)
.build();
// 传递给下一个 Advisor 或调用大模型
ChatClientResponse advisedResponse = callAdvisorChain.nextCall(modifiedRequest);
// 对话后
System.out.println("after...");
return advisedResponse;
}
@Override
public String getName() {
return this.getClass().getSimpleName();
}
@Override
public int getOrder() {
// 数字越小,越先执行
return 0;
}
}
配置并使用这个自定义Advisor:
@GetMapping("/advisor")
public String advisor(String question) {
return this.chatClient
.prompt()
.advisors(new ArtisanCallAroundAdvisor())
.user(question)
.call()
.content();
}
其执行过程体现了环绕通知(Around Advice)的强大控制力:
1. chatClient 开始处理请求
2. artisanCallAroundAdvisor.adviseCall() 被调用
3. 打印 "before..."
4. 原始 Prompt:
System: [默认系统提示]
User: 你是谁?
5. 增强后的 Prompt:
System: [默认系统提示]\n我是周瑜
User: 你是谁?
6. 调用链继续,最终调用 Qwen3-max 大模型
- 模型看到了增强后的系统提示
- 会回答:"我是周瑜,一位 AI 助手..."
7. 返回响应
8. 打印 "after..."
9. 返回最终结果给客户端
这种模式在AI应用开发中用途广泛,例如敏感词过滤、Token消耗统计或多租户API密钥动态注入。
五、 协同作战与最佳实践
ChatMemory和Advisor在Spring AI中并非孤立存在,而是紧密协作。MessageChatMemoryAdvisor本身就是这种协作的产物。在实际复杂场景中,我们可能需要多个Advisor共同工作。
例如,一个集成了记忆、身份注入和监控的对话服务:
@GetMapping("/ragAdvisor2")
public String ragAdvisor2(@RequestParam("chatId") String chatId,
@RequestParam("question") String question) {
return chatClient
.prompt()
// Advisor 1: RAG 增强
.advisors(retrievalAugmentationAdvisor) // order: 0
// Advisor 2: 记忆管理
.advisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
// order: Integer.MIN_VALUE + 1000
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
.user(question)
.call()
.content();
}
这里,MessageChatMemoryAdvisor的order设置为1,确保它最先执行,将历史上下文准备好。身份注入Advisor(order=2)随后执行,在已有上下文的基础上添加角色信息。监控Advisor(order=3)最后执行,记录完整的请求和响应。这种顺序至关重要:
// .order(1) // 默认 MessageChatMemoryAdvisor 的 order 为
// Integer.MIN_VALUE+1000,会排在 RetrievalAugmentationAdvisor 的前面,
// 导致 chatMemory 中存的是原始的 query,而不是 RAG 增强后的 query,
// 可以调整 order 为 1,使得排在 RetrievalAugmentationAdvisor 的后面,
// RetrievalAugmentationAdvisor 的 order 默认为 0,从而使得 chatMemory
// 中存的是 RAG 增强后的 query
[AFFILIATE_SLOT_2]
六、 总结与核心洞见
Spring AI通过ChatMemory和Advisor两大抽象,为构建企业级对话应用提供了坚实而灵活的基础架构。 理解其分工与协作是掌握Spring AI的关键:
- ChatMemory是状态管理者,专注于对话历史数据的存储、检索和生命周期管理,解决了AI的“记忆”问题。
- Advisor是流程控制器,基于拦截器模式,允许在机器学习模型调用的关键节点插入业务逻辑,解决了流程“定制”问题。
两者结合,使得开发者能够轻松实现多轮对话、上下文感知、身份绑定、安全审查、性能监控等复杂功能。从简单的内存存储搭配内置Advisor开始,逐步演进到使用数据库持久化并结合多个自定义Advisor,这条学习路径能帮助你稳步构建出健壮、可维护的智能对话系统。在神经网络驱动的应用浪潮中,掌握此类框架的核心设计思想,将让你在开发中游刃有余。
| 概念 | 作用 | 核心类 |
|---|---|---|
| ChatMemory | 管理聊天历史,支持多会话隔离 | ChatMemory, ChatMemoryRepository |
| MessageWindowChatMemory | 实现滑动窗口策略,限制消息数量 | MessageWindowChatMemory |
| InMemoryChatMemoryRepository | 在内存中存储对话历史 | InMemoryChatMemoryRepository |
| Advisor | 拦截 ChatClient 调用,修改请求和响应 | CallAdvisor, CallAdvisorChain |
| MessageChatMemoryAdvisor | 内置的 Advisor,集成 ChatMemory | MessageChatMemoryAdvisor |
浙公网安备 33010602011771号