Agent + RAG 如何处理多轮对话中的上下文识别与记忆?原理、架构与实战
提问
Agent + RAG 在多轮对话中,是怎么处理上下文语义识别和记忆的?为什么「它」「那个」「接着呢」它都能懂?对话长了怎么办?
参考答案
一句话:把”整段对话历史”按”短期记忆 + 长期记忆 + 工作记忆”三层管理,再在每次调用 LLM 前做”上下文压缩 + Query 改写”,让模型始终只看到最有用的信息。
一、为什么多轮对话是难题
LLM 本身没有记忆——每次调用都是 stateless(无状态)。
1 2 3 4 5
| 用户:「Java 怎么读文件?」 Agent:讲一堆
用户:「它能读多大?」← "它" 指什么? Agent:? ← LLM 不知道"它"是 Java、文件读取、还是别的
|
LLM 必须看到完整对话历史才能理解指代。所以每个 Agent 框架都要做一件事:管理对话历史,按策略喂给模型。
二、三层记忆模型(行业标准)
| 层 |
范围 |
实现方式 |
典型大小 |
| 短期记忆 |
当前会话 |
ChatHistory 数组 |
最近 N 轮(10-50) |
| 工作记忆 |
当前任务 |
临时变量 / scratchpad |
KB 级 |
| 长期记忆 |
跨会话 |
向量库 + KV 存储 |
MB-GB 级 |
LangChain / Spring AI / LangGraph 都遵循这个模型。
三、LangChain 的实现(最经典)
3.1 5 种 ConversationBufferMemory 模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| from langchain.memory import ( ConversationBufferMemory, ConversationBufferWindowMemory, ConversationSummaryMemory, ConversationSummaryBufferMemory, ConversationTokenBufferMemory, )
memory = ConversationBufferMemory( memory_key="chat_history", return_messages=True )
memory = ConversationBufferWindowMemory( k=10, memory_key="chat_history" )
memory = ConversationSummaryMemory( llm=llm, memory_key="chat_history" )
memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=2000 )
memory = ConversationTokenBufferMemory( llm=llm, max_token_limit=2000 )
|
3.2 推荐:Summary + Buffer 混合
1 2 3 4 5 6 7 8 9
|
memory = ConversationSummaryBufferMemory( llm=ChatOpenAI(model="gpt-4o-mini"), max_token_limit=4000, memory_key="chat_history", return_messages=True )
|
四、Query 改写:解决”它”、”那个”指代问题
光保留历史不够,还得让模型理解指代。最常用是 Query Rewriting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def rewrite_query(question, chat_history): """把含糊的问题改写成独立问题""" last_turn = chat_history[-2:] prompt = f"""基于以下对话历史,把用户的新问题改写成独立的、完整的问题。
对话历史: {format(last_turn)}
用户新问题:{question}
改写后的问题(不要回答问题本身,只改写):""" return llm.invoke(prompt).content
|
变体:HyDE(让 LLM 先生成假设答案,再去检索假设答案的向量)。
五、LangGraph 的状态机做法(生产级)
LangGraph 把记忆当成「状态」来管理,支持并行 + 回退 + 人介入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated from langgraph.graph.message import add_messages
class State(TypedDict): messages: Annotated[list, add_messages] current_task: str summary: str
def chatbot(state: State): context = f"对话摘要:{state.get('summary', '')}\n\n对话历史:{state['messages']}" response = llm.invoke(context) if len(state['messages']) > 10: summary = llm.invoke(f"请摘要以下对话:{state['messages']}") return {"messages": [response], "summary": summary} return {"messages": [response]}
graph = StateGraph(State) graph.add_node("chat", chatbot) graph.add_edge("chat", END) graph.set_entry_point("chat")
app = graph.compile()
app.invoke({"messages": [("user", "Java 怎么读文件?")]}) app.invoke({"messages": [("user", "它能读多大?")]})
|
优势:
- 自动累积对话(
add_messages reducer)
- 摘要机制随时挂上
- 可以持久化(checkpointing)
- 人在回路(human-in-the-loop)天然支持
六、OpenClaw 的 Hook 机制(飞牛面试题提到过)
OpenClaw 把「记忆」做成可插拔的中间件:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @hook("before_agent_run") async def rewrite_question(context): user_msg = context.current_message history = context.chat_history if has_pronoun(user_msg): rewritten = await rewrite(user_msg, history) context.current_message = rewritten context.memory_summary = await summarize_old_messages(history)
|
实战价值:把”上下文处理”从业务代码里剥离出来,配置化。
七、Spring AI 实现(Java 生态)
Spring AI 1.0+ 的 ChatClient 提供了 Advisor 机制做记忆管理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| @Service public class RagService {
private final ChatClient chatClient;
public RagService(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) { this.chatClient = builder .defaultAdvisors( new QuestionAnswerAdvisor(vectorStore), new MessageChatMemoryAdvisor(chatMemory) ) .build(); }
public String chat(String sessionId, String userMessage) { return chatClient.prompt() .advisors(a -> a .param("conversationId", sessionId) .param("topK", 5)) .user(userMessage) .call() .content(); }
@Bean public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) { return MessageWindowChatMemory.builder() .chatMemoryRepository(new JdbcChatMemoryRepository(jdbcTemplate)) .maxMessages(20) .build(); } }
|
特点:
ChatMemory 抽象(InMemory / JDBC / Redis)
- 自动持久化到数据库(重启不丢)
MessageWindowChatMemory 滑动窗口实现
八、Claude Agent SDK 的做法(最新)
1 2 3 4 5 6 7 8 9 10 11 12
|
def handle_pronoun(context): last_messages = context.chat_history[-3:] if needs_rewrite(context.current_message): rewritten = await llm.invoke( f"重写问题使其独立:{context.current_message}\n基于:{last_messages}" ) return rewritten return context.current_message
|
把”上下文识别”做成 Skill,业务代码不感知。
九、常见坑 & 实战建议
坑 1:上下文太长导致成本爆炸
症状:10 轮对话后 token 涨到 8K,每次调用 $0.05
修法:
- 设
max_token_limit=4000 自动截断
- 用
ConversationSummaryMemory 摘要老历史
- 用 Gemini Flash 这类便宜模型做摘要
坑 2:上下文窗口溢出 → LLM 报错
症状:对话超过 32K token,API 返回 400
修法:
- 滑动窗口(最近 N 轮)
- RAG 检索时只保留 top-5 文档块
- 强制压缩老历史
坑 3:指代消解失败
症状:「它能读多大?」→ LLM 答非所问
修法:
- Query Rewriting(在 LLM 之前用另一个 LLM 改写)
- 把最近 3 轮对话和当前问题拼一起传给主 LLM
坑 4:跨会话记忆泄露
症状:A 用户问的问题,B 用户能看到
修法:
session_id 严格隔离
- 长期记忆存到
users/{user_id}/memory/
- 检索时加 user_id 过滤
坑 5:记忆系统变成性能瓶颈
症状:每次对话都查 Redis 拿历史,慢
修法:
- 用
MessageWindowChatMemory 内存缓存最近 N 轮
- 超出窗口的用 RAG 从向量库检索相关历史片段
- 不存全部,只存「摘要」和「关键事实」
十、生产级架构建议
短期对话(单次会话)
1 2 3 4 5 6 7 8
| 每轮: 1. 读 chat_history(最近 10 轮) 2. Query 改写(处理"它/那个") 3. RAG 检索(top-5) 4. 组装 Prompt = System + 摘要 + 历史 + 检索结果 + 当前问题 5. 调 LLM 6. 写回 chat_history 7. 检查是否需要触发"摘要压缩"(超过 N 轮)
|
长期记忆(跨会话)
1 2 3 4 5 6
| 用 LLM 提取对话中的「关键事实」,存向量库 + KV: - 用户偏好:"用户是 Java 后端工程师" - 用户历史问题:"之前问过 Redis 集群" - 用户身份:"叫 Sherwin.Wei"
下次对话开始时,检索相关长期记忆,加入 system prompt
|
Spring AI 推荐配置
十一、总结:多轮对话的三层心智
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ┌──────────────────────────────────┐ │ 长期记忆(跨会话) │ ← 向量库 + KV │ "用户是 Java 工程师" │ └──────────────┬───────────────────┘ │ 检索时注入 ↓ ┌──────────────────────────────────┐ │ 短期记忆(当前会话) │ ← ChatHistory │ 最近 10 轮对话 │ └──────────────┬───────────────────┘ │ 摘要压缩 ↓ ┌──────────────────────────────────┐ │ 工作记忆(当前问题) │ ← scratchpad │ Query 改写 + 检索结果 │ └──────────────────────────────────┘ ↓ LLM 生成回答
|
核心心法:
让 LLM 永远只看最相关的信息——不管历史多长、记忆多少。
相关阅读
最后更新:2026-09-04