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=3get() 返回:
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的能力无缝集成到对话流程中。其工作流程可以概括为:

  1. 调用前:根据chatId从ChatMemory中获取历史消息,并拼接到当前Prompt中。
  2. 调用模型:将增强后的上下文发送给大语言模型。
  3. 调用后:将本轮的用户消息和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: 今天是 212- 将这些历史消息自动添加到 Prompt3. 增强后的 Prompt 结构:
┌─────────────────────────────────┐
│ System Message                  │
│ (默认系统提示)                    │
├─────────────────────────────────┤
│ History                         │
│ User: 今天几号?                  │
│ Assistant: 今天是 212号         │
├─────────────────────────────────┤
│ 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. 原始 PromptSystem: [默认系统提示]
User: 你是谁?
5. 增强后的 PromptSystem: [默认系统提示]\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,集成 ChatMemoryMessageChatMemoryAdvisor
posted on 2026-03-20 16:22  blfbuaa  阅读(171)  评论(0)    收藏  举报