· ARJUN · WRITING
Agent 记不住过去,通常不是模型的问题
你昨天刚交代过的事,Agent 到了新会话却像第一次听见。很多时候,不是模型忘了,而是应用没有把那条信息存下来,也没有在需要时找回来。
你对一个编程 Agent 说:「这个项目尽量使用 Go 标准库,不要随便引入框架。」它在当前对话里照做了。第二天你打开新会话,它又推荐了一套新框架。
这时很容易得出一个结论:模型的记忆不够好。
可模型可能根本没有机会“记住”。上一次对话结束后,那条偏好没有被写入持久化存储;新会话开始时,系统也没有把它取回来。问题不在模型忘了,而在应用没有完成记忆的写入与读取。
这也是 Agent Memory 最容易讲乱的地方。Context Window、聊天记录、会话状态、向量数据库和模型参数经常被统称为“记忆”,仿佛只要接上一个数据库,Agent 就拥有了长期记忆。
先把这些东西拆开,后面的设计才不会混在一起。
三个经常被混为一谈的概念
在工程系统里,可以先按作用范围区分三层:
| 层次 | 它是什么 | 能解决什么 | 不能保证什么 |
|---|---|---|---|
| Context Window | 某一次模型调用实际收到的输入 | 让模型处理眼前任务 | 不会自动跨调用保存信息 |
| Thread State | 某条会话或任务线程内持久化的状态 | 保存消息、工具结果、任务进度与中间产物 | 不会自动跨线程共享 |
| Long-Term Memory | 跨会话保存、按作用域读取的数据 | 记住用户偏好、事实、经历或规则 | 存进去不等于以后一定能正确取回 |
CoALA 论文给了一个重要提醒:语言模型本身在多次调用之间是无状态的,语言 Agent 才会通过外部数据结构维持工作记忆与长期记忆。它还特别区分了模型的 Context 和 Agent 的 Working Memory:后者可以跨模型调用保存,模型每次只接收其中的一部分。
换句话说,Context 是本次交给模型看的材料;State 是 Runtime 目前掌握的现场;Memory 是以后可能再次用到的记录。
它们有关联,但不是一回事。

Context Window 不是一个会自动保存内容的仓库
Context Window 是输入预算。系统提示词、当前问题、历史消息、工具定义、工具返回结果和检索内容,都要争夺这段空间。
窗口变长能容纳更多内容,却不代表模型一定能同样好地使用每一段内容。Lost in the Middle 的实验发现,在它测试的多文档问答与键值检索任务中,相关信息位于长上下文中间时,部分模型的表现明显下降。这个结果来自特定模型与任务,不能扩展成“所有长上下文模型都看不到中间”;但它足以说明,能装下与能用好不是同一件事。
因此,把完整聊天记录不断塞回 Prompt 不是可靠的记忆策略。它会同时带来三个问题:
- 无关历史挤占当前任务的空间;
- 推理延迟和调用成本随输入增长;
- 关键约束埋在大量旧消息里,未必会被稳定使用。
更合理的做法是先保存完整状态,再由 Context Builder 为当前步骤选择必要信息。模型看到的是工作集,而不是整个仓库。
短期记忆更接近“线程内状态”
当前 LangChain 与 LangGraph 文档把 Short-Term Memory 定义为 Thread-Scoped State:它属于一条会话线程,可以通过 Checkpointer 持久化,在每次图调用或执行步骤之间恢复。
它通常包含消息历史,但不必只有消息。一个做代码修改的 Agent 还可能保存:
{
"goal": "修复订单重复扣款",
"constraints": ["不修改公开 API", "必须可回滚"],
"current_step": "复现并定位幂等键失效路径",
"evidence": ["trace-8f21", "test_payment_retry"],
"artifacts": ["diagnosis.md", "patch.diff"]
}
这些字段比一长串自然语言更容易保存、检查和恢复。系统中断后,Runtime 可以知道任务做到哪一步,不需要让模型重新读完整聊天记录再猜。
当消息太长时,可以截断、删除或摘要早期内容。摘要能节省 Token,但它是有损压缩:一句被摘要器忽略的限制,之后可能再也回不来。像目标、权限边界、验收条件和工具执行结果这类信息,更适合放在结构化 State 中,而不是只依赖一段自动摘要。
长期记忆不是向量数据库的同义词
向量数据库解决的是一种检索问题:当用户换了一种说法,系统仍能按语义相似度找到相关文本。它不是长期记忆的唯一存储,也不负责判断什么值得记、旧信息是否失效、两条记忆冲突时相信哪一条。
从信息类型看,Agent 的长期记忆可以分成三类:
- Semantic Memory:关于用户、项目和世界的事实,例如「这个仓库使用 Go 1.24」或「用户偏好简短回答」。
- Episodic Memory:过去发生过什么,例如一次部署失败的原因、采取过的操作和最终结果。
- Procedural Memory:怎样完成一类任务,例如系统提示词、工具使用规则、代码化流程或可复用 Skill。
这个分类来自认知架构,不等于三种固定数据库。数据结构要跟查询方式匹配:
- 用户偏好和项目配置需要精确读写,JSON 文档或关系型数据库通常更直接;
- 对话片段、问题记录和经验总结适合全文、关键词、向量或混合检索;
- 实体关系确实重要时,可以使用图结构,但没有必要为了“记忆”两个字先引入知识图谱;
- 稳定的规则和流程应优先保存在版本化的 Prompt、代码或 Skill 中,而不是每次从相似文本里猜一套做法。
LangGraph 的 Long-Term Store 就展示了另一种思路:长期记忆可以是按 Namespace 和 Key 组织的 JSON 文档,再按需要增加过滤或语义搜索。先把数据的归属与更新规则设计清楚,才轮到选择检索索引。
真正难的第一步:决定什么时候写入
不是每句话都值得成为长期记忆。
「这次用 Python 写一个脚本」描述的是当前任务;「以后给我的示例默认使用 Python」才像一个跨会话偏好。如果系统把前一句也永久保存,下次处理 Go 项目时就可能拿出一条错误偏好。
一次写入至少应该回答:
- Scope:这条信息属于哪个用户、项目还是 Agent?
- Type:它是事实、经历还是规则?
- Source:来自用户明确陈述、工具结果,还是模型推断?
- Lifetime:长期有效、只在项目内有效,还是到期删除?
- Identity:它是在新增一条记录,还是替换已有记录?
可以在请求主链路中即时写入,也可以在后台整理。前者让新信息立刻可用,但增加延迟;后者不阻塞当前回复,却要处理写入时机和新旧会话之间的同步。两种方式都是真实存在的工程选择,不存在一套适合所有 Agent 的固定答案。
我更愿意把“明确陈述”和“系统推断”分开处理。用户说「以后默认简洁回答」可以直接成为候选偏好;系统从两次短对话中猜出「用户不喜欢解释」则不应该伪装成事实。记忆一旦带着错误的确定性写进去,之后每次召回都会放大这个错误。

读取记忆,不是拿 Top K 塞进 Prompt
有了存储,Agent 仍然可能“记错”。很多时候不是数据不存在,而是取回了错误的数据,或者正确记录被无关内容淹没。
一次比较完整的读取过程通常包含:
- 从当前任务生成检索意图,而不只是复制用户最后一句话;
- 先按用户、项目、记忆类型和有效状态做硬过滤;
- 根据数据选择精确查询、关键词、向量或混合召回;
- 对候选结果重排、去重,并检查相互冲突的记录;
- 只把当前步骤真正需要的内容装入 Context,同时保留来源。
Generative Agents 的研究原型把 Recency、Importance 和 Relevance 组合成检索分数。这是一个有影响力的设计实例,但不是通用公式。客服偏好、代码故障记录和虚拟角色经历,对“重要”的定义完全不同;最终权重需要结合任务和评测来确定。
还有一个容易忽略的区别:知识库 RAG 与用户记忆可以使用相似的检索技术,但它们不是同一种数据。产品文档回答「系统支持什么」,用户记忆回答「这个用户或这次经历发生过什么」。两者的权限、更新频率和可信来源都不同。

记忆必须能被纠正,也必须能被忘记
假设系统保存过一条信息:
部署方式:Docker Compose。
两个月后项目迁移到 Kubernetes。如果系统只是再追加一条新记忆,检索时就可能同时取回两个互相冲突的答案。
正确的处理不一定是删除所有历史。可以把旧记录标记为 Superseded,保留它曾经有效的时间和来源,再把新记录设为 Current。这样 Agent 回答当前部署方式时使用新值,调查历史故障时仍能知道旧环境曾经存在。
“遗忘”也不只是降低向量相似度。临时状态可以设置 TTL,过期事实可以失效,重复事件可以合并,用户要求删除的数据必须从持久化存储中真正移除。仅仅在组装 Prompt 时不再引用某条内容,并不等于它已从系统里删除。

一套够用的最小架构
不必一开始就同时部署 Redis、向量数据库、图数据库和一套记忆框架。对大多数个人 Agent,一个清楚的最小实现已经能解决不少问题:
- Checkpointer 保存每条 Thread 的消息与任务状态;
- Profile Store 保存少量明确、可覆盖的用户或项目事实;
- Episode Store 保存带来源和结果的历史事件,需要时再添加全文或向量索引;
- Memory Policy 决定写入、更新、失效和删除;
- Context Builder 根据当前步骤选择 State 与 Memory,控制最终输入预算。
伪代码反而很短:
def build_context(user_id, thread_id, request):
state = checkpoints.load(thread_id)
profile = profiles.get(user_id)
episodes = memories.search(
query=request,
filters={"user_id": user_id, "status": "current"},
)
return context_builder.pack(state, profile, episodes)
数据库负责保存,检索器负责找候选,Policy 负责判断,Context Builder 负责把材料交给模型。把职责分开之后,某一层出错时才知道应该查哪里。
什么时候不需要长期记忆
长期记忆不是 Agent 的入场券。
如果工具每次只处理一个独立请求,任务结束后没有状态需要复用,就不必为了“更像 Agent”而保存用户数据。固定业务流程需要的可能是可靠的数据库状态机,不是模型记忆。公开文档问答需要的可能是版本明确的知识检索,也不是用户画像。
先问一句:下一次任务如果拿不到这条信息,会具体损失什么?如果答案说不清楚,就先别存。
参考资料
- Sumers et al., Cognitive Architectures for Language Agents:区分 Working、Episodic、Semantic 与 Procedural Memory,并说明 Agent Working Memory 可以跨 LLM 调用持久化。
- LangChain, Memory overview:当前 Short-Term/Long-Term Memory、写入时机、Namespace 与 Store 的工程定义。
- LangChain, Short-term memory:当前 Checkpointer、消息裁剪、删除与摘要方式。
- Liu et al., Lost in the Middle:长上下文中相关信息位置对部分任务与模型表现的影响。
- Park et al., Generative Agents:Memory Stream、Reflection,以及 Recency、Importance、Relevance 检索设计。
- Packer et al., MemGPT:以分层存储和数据移动管理有限 Context 的研究架构。
- Chhikara et al., Mem0:记忆提取、整合与检索的系统实例及其评测结果。
- 原始参考文章:本次改造的知识线索来源。
Agent 看起来像在“回忆”,背后其实是一套很具体的数据流:哪些信息被保存,保存到哪里,什么时候取回,冲突时相信谁,以及什么时候彻底删除。
模型只处理眼前这次输入。让过去在下一次仍然有用,是系统的工作。