Agent记忆系统三层架构深度测评:短期、长期、压缩优劣对比
记忆系统不只是"存聊天记录"
把对话历史一股脑儿塞进prompt——这其实是最粗糙、最原始的记忆方式。现实中,Agent对记忆的需求远比你想象的要复杂:
- 用户在第3轮提到了自己的城市,第10轮问天气时Agent应该知道去哪儿查数据。
- 用户上周告诉过系统他们用的是哪个产品套餐,本次新会话不应该再问一遍。
- 对话进行了20轮,context window快撑不住了,怎么压缩又不丢失关键信息?
这三个场景,对应三种完全不同的记忆机制。下面展开聊。
三层记忆架构
短期记忆 会话内 MemorySa ver checkpointer 多轮问答
长期记忆 跨会话 持久化 KV 存储 / 向量库 个性化
历史压缩 会话内保护 摘要替换 长对话 token 守卫
Demo 1:短期记忆——MemorySa ver
LangGraph 的 MemorySa ver 是最轻量的短期记忆实现:把对话历史绑定到一个 thread_id,下次调用时自动注入,省得你手动拼历史。
from langgraph.checkpoint.memory import MemorySa ver
from langchain_core.runnables import RunnableConfigcheckpointer = MemorySa ver()
stateful_agent = create_react_agent(
model=llm,
tools=[get_weather, calculator, get_product_info],
checkpointer=checkpointer,
)THREAD_A: RunnableConfig = {"configurable": {"thread_id": "thread-alice"}}# Turn 1: 告知名字和城市
r1 = stateful_agent.invoke(
{"messages": [HumanMessage("Hi, I'm Alice. I live in Beijing.")]},
config=THREAD_A,
)# Turn 2: 用同一个 thread_id,历史自动附带
r2 = stateful_agent.invoke(
{"messages": [HumanMessage("What's the weather like where I live today?")]},
config=THREAD_A,
)
来看看实际跑出来的效果:
Thread A Turn 1: Hello Alice! How can I assist you today?Thread A Turn 2: Sure, I can help you with that. I will need to know the
city you are in. Could you please provide me with the
name of your city?
Tools used: []Thread B (no context): I can help with that. Could you please provide
your city name?
Tools used: []
Thread A 和 Thread B 给出了同样的回答。
这个结果很有意思:MemorySa ver 的基础设施是正常的——Thread A 的第二次调用实际上带着完整的历史(两条消息),而 Thread B 只有一条。但 GLM-4-Flash 没有把"I live in Beijing"(第一轮)和"where I live"(第二轮)连起来。这其实是模型能力的问题,而不是 MemorySa ver 的问题。
同样的 prompt,如果换成 GPT-4 或 Claude,它们会直接去查北京天气;能力弱的模型可能需要更明确的表述(比如直接问"What's the weather in Beijing?")才能触发工具调用。
短期记忆有两层含义,必须分开看:
- 基础设施层:MemorySa ver 确保历史消息被传递——这件事它干得漂亮。
- 模型层:LLM 能否从历史中提取并使用上下文——这取决于模型自身的推理能力。
Demo 2:长期记忆——跨会话事实存储
跨会话记忆的核心思路很直接:用 LLM 从对话中提取关键事实,存入持久化存储,下次会话时注入系统提示词。这样模型就不用从历史中猜了,直接告诉你答案。
Session 1 — 提取并存储:
# 模拟持久化存储(生产环境替换为数据库或向量库)
LONG_TERM_STORE: dict[str, dict[str, str]] = {}def extract_facts(conversation: str) -> dict[str, str]:
resp = llm.invoke([
SystemMessage(
"Extract key facts about the user. "
'Return ONLY JSON: {"city": "...", "plan": "..."}'
),
HumanMessage(f"Conversation:n{conversation}"),
])
# 解析 JSON 响应
...
Session 1 对话内容:
User: I'm Alice. I'm based in Shanghai and my team uses WonderBot Pro.
User: We mainly use the API for data processing — about 50,000 calls a month.
提取结果并存入:
{'name': 'alice', 'city': 'shanghai', 'team': 'wonderbot pro', 'api_calls': '50000'}
Session 2 — 注入并使用:
stored = load_user_facts("user-alice")
facts_text = "; ".join(f"{k}={v}" for k, v in stored.items())personalized_prompt = (
"You are a helpful assistant. "
f"Known facts about this user: {facts_text}. "
"Use these facts to personalize your responses without asking the user to repeat themselves."
)personalized_agent = create_react_agent(model=llm, tools=TOOLS, prompt=personalized_prompt)
Session 2 运行结果:
User: What's the weather like in my city today?
Agent: The current weather in Shanghai is 22 degrees Celsius with cloudy conditions.
Tools used: ['get_weather']
Agent 直接查询了 Shanghai,没有问"你住哪"。原因很简单:city=shanghai 已经在系统提示词里了——模型不需要从对话历史中推断,而是直接读取了显式提供的事实。
这也是为什么长期记忆比短期记忆更可靠:事实以明确的 KV 格式注入,不依赖模型从历史中做推理。换句话说,你把答案直接喂到模型嘴边,它不用自己去翻聊天记录。
Demo 3:历史压缩
对话越长,token 消耗和响应延迟就线性增长。压缩策略很直接:设置一个 token 阈值,超过之后就用摘要替换掉历史消息。
COMPRESSION_THRESHOLD = 250 # tokensdef summarize_messages(messages: list) -> str:
history_text = "n".join(
f"{'User' if isinstance(m, HumanMessage) else 'Agent'}: {str(m.content)[:150]}"
for m in messages
if isinstance(m, (HumanMessage, AIMessage)) and not getattr(m, "tool_calls", None)
)
resp = llm.invoke([
SystemMessage(
"Summarize this conversation in 2-3 sentences. "
"Preserve all key facts: names, cities, numbers, product names."
),
HumanMessage(f"Conversation:n{history_text}"),
])
return str(resp.content)# 在每轮对话后检查 token 数
if total_tokens > COMPRESSION_THRESHOLD:
summary = summarize_messages(messages)
messages = [SystemMessage(f"Conversation summary so far: {summary}")]
Demo 3 的真实结果:5 轮对话(Bob 在深圳,评估 WonderBot Pro,8 位开发者,年费 299*12=3588),总 token 保持在 198,未超过 250 阈值,因此压缩未触发。
最终验证:
User: Quickly summarize: who am I, what city, and what's the annual API cost?
Agent: You are Bob, from Shenzhen, and the annual API cost for WonderBot Pro
for 8 developers is $3,588.
10 条消息历史完整保留,Agent 记住了所有关键事实。压缩机制本质上是安全阀,不是每轮对话都要触发——当对话足够短时,原始历史比摘要更精确。阈值建议设置在 4k token 左右,因为超过这个值模型推理效率会明显下降。
三种模式的实测对比
| 模式 | Demo 1 结果 | Demo 2 结果 | 关键差异 |
|---|---|---|---|
| 短期记忆 | MemorySa ver 存储正常,但 GLM-4-Flash 未能利用隐含上下文 | — | 依赖模型推断能力 |
| 长期记忆 | — | 直接调 get_weather(Shanghai),零追问 | 显式 KV 注入,不依赖推断 |
| 压缩 | — | — | 安全阀机制,按需触发 |
核心结论:
- 需要可靠的跨轮上下文 → 用长期记忆的显式注入,而不是只依赖 MemorySa ver。
- MemorySa ver 的价值在于会话隔离(不同 thread_id 互不影响)和自动历史传递,它不是用来解决"模型能否推断隐含信息"这个问题的。
- 压缩是生产环境的必备组件,但阈值不要设得太低(太低会丢失细节,得不偿失)。
设计 Checklist
短期记忆(MemorySa ver)
- 每个用户/会话分配独立
thread_id -
thread_id用用户 ID 而不是随机数,确保同一用户的多轮对话连续 - 不要依赖 MemorySa ver 解决"模型无法推断隐含信息"的问题
- 生产环境用持久化 checkpointer(
SqliteSa ver、PostgresSa ver),而不是MemorySa ver
长期记忆
- 用 LLM 提取事实,不要手写规则解析
- 事实以 KV 格式注入系统提示词,明确优于隐含
- 设置更新策略:事实过时时覆盖,不要无限追加
- 生产环境:数据库存结构化事实,向量库存语义记忆
历史压缩
- 阈值建议:2000-4000 token(过低会频繁压缩,损失精度)
- 摘要 prompt 要求"保留具体数字和名称",否则 LLM 会过度抽象
- 压缩后验证:在摘要里确认关键事实仍然存在
- 不要在工具调用中间触发压缩(会破坏上下文)
总结
五个核心结论,值得反复琢磨:
- MemorySa ver 是基础设施,不是银弹:它确保历史被传递,但模型能否利用隐含上下文取决于模型本身。
- 显式注入比隐含推断更可靠:把
city=shanghai写进系统提示词,比期望模型从 10 条消息里推断要稳定得多。 - 三层记忆要组合使用:短期(会话隔离)+ 长期(跨会话个性化)+ 压缩(token 守卫),缺一不可。
- 事实提取是长期记忆的关键步骤:LLM 提取 + JSON 解析,把非结构化对话转成结构化事实,这一步做不好后续全白搭。
- 压缩是安全阀,不是每轮都跑:对话短时原始历史更精确,超阈值才压缩,别提前触发。
参考资料
- LangGraph Persistence 文档
- LangGraph MemorySa ver
- 本系列完整 Demo 代码:agent-14-memory
