一个问题先于设计
你让 Agent 帮你管理一个项目,它今天记住了你的风格偏好,明天就忘了。你跑了一个两小时的长任务,中途它开始重复之前做过的操作,好像对自己做了什么没有印象。你告诉它你是开发者,喜欢简洁的回答,下一个会话它又回到了冗长的解释模式。
这些都是记忆问题,但它们不是同一种记忆问题。
把所有「Agent 忘事」的情况归结为「上下文不够长」是一种常见的误判。上下文窗口只是记忆系统里最薄的一层。真正健壮的 Agent 记忆,需要在至少四个不同的时间尺度上分别设计。
先给结论
- 「忘事」不全是 context 不够长:至少区分当前会话、跨会话事件、长期知识与稳定行为四类需求,否则会在错误层级上打补丁。
- 四类记忆对应不同衰减与调用方式:工作级信息留在 in-context;「发生过什么」进情景记忆;通用事实进语义记忆;「怎么做」进程序记忆(prompt / 代码 / Skill)。混用同一容器会像把冰块和热汤倒进同一个罐子里。
- 语义记忆与 RAG 实现重叠、目标不同:RAG 偏静态文档检索;语义记忆更强调随交互演化的用户与世界模型,工程上常并存。
- 程序记忆更新成本最高:只应放稳定、普适的行为模式;用户个性化应进语义记忆并在运行时注入,而不是写进全局 system prompt。
- 记忆质量 = 检索质量 + 维护策略:只写不读、只增不减、全盘向量化或全盘塞 context,都是典型失败路径。
为什么记忆的分层是必要的
人类记忆有一套成熟的分类框架:工作记忆处理当前正在操作的信息,情景记忆存储具体经历,语义记忆保存通用知识,程序记忆负责技能和习惯。这套框架不是偶然的,它对应的是不同类型的信息在时间维度上的衰减特征和调用方式。
Agent 面临同样的问题。一个 Agent 在执行任务时需要知道:
- 现在:当前步骤是什么,刚才做了什么,下一步打算做什么
- 这次:这个任务是怎么来的,用户说过什么约束,中间出过哪些错
- 一直以来:用户是谁,有什么偏好,这个领域有哪些背景知识
- 技能层面:遇到某类问题应该怎么处理,有哪些固定的操作模式
这四个问题对应四种不同类型的记忆,用同一种机制来处理它们,就像用同一个容器存放冰块和热汤——结果是两者都处理不好。
四种记忆形态
1. 短期记忆(In-context Memory)
定义: 存在于当前上下文窗口中的所有信息,包括对话历史、工具调用记录、模型的推理过程。
生命周期: 当前会话,上下文窗口满了之后旧内容会被截断或压缩。
存储位置: 模型的 context window,没有外部持久化。
典型内容:
- 用户本轮说了什么
- Agent 已经调用了哪些工具,返回了什么
- 当前任务的目标和进度
- 最近几步的推理轨迹
短期记忆是所有记忆形态中最直接、延迟最低的,但也是最脆弱的。它的容量有硬上限(以 token 计),而且没有任何跨会话的持久性。
工程上需要关注的点:
上下文窗口不是越长越好。一个塞满了无关历史的上下文会让模型在长尾信息中迷失,这就是「lost in the middle」问题——模型对上下文中间位置的信息注意力显著下降。因此,短期记忆需要主动管理:哪些内容应该保留、哪些可以压缩摘要、哪些应该外化存储到其他记忆层。
def compress_history(messages: list, threshold: int = 8000) -> list:
"""在对话过长时压缩消息:保留系统提示与最近若干轮,更早内容改为摘要。
参数:
messages: 对话消息列表,通常首条为 system。
threshold: token 数量阈值,低于则原样返回。
返回:
压缩后的消息列表。
"""
if token_count(messages) < threshold:
return messages
recent = messages[-6:]
older = messages[1:-6]
summary = summarize(older)
return [messages[0], {"role": "system", "content": f"[对话摘要] {summary}"}] + recent
2. 情景记忆(Episodic Memory)
定义: 跨会话持久保存的、与特定事件或交互绑定的记录。不是通用知识,而是「曾经发生过什么」。
生命周期: 跨会话,按需检索,可以设置 TTL 或容量上限。
存储位置: 外部数据库,通常是向量数据库或结构化存储(如 SQLite、PostgreSQL)。
典型内容:
- 用户上次反馈说「回答太长了」
- 上周执行某个任务时遇到了什么错误、怎么解决的
- 某个代码库的结构是上次对话时分析过的
- 历史操作的时间线记录
情景记忆的核心价值是让 Agent 能够从自身的历史经验中学习,而不必每次从零开始。一个有情景记忆的 Agent 在第二次遇到类似问题时,可以参照上次的处理路径,而不是重复同样的试错过程。
工程上需要关注的点:
情景记忆的检索方式至关重要。最简单的实现是向量相似度搜索:把历史交互嵌入为向量,查询时找最相关的几条。但纯向量检索在时间敏感的场景下表现不佳——「最近发生的事」和「最相关的事」是两个不同的维度,检索策略需要同时考虑两者。
def extract_episodes(conversation: list) -> list:
"""从一轮对话中提取值得写入情景记忆的关键事件(示意:实际应对接 LLM)。
参数:
conversation: 完整对话消息列表。
返回:
事件对象列表,元素含 summary、category、importance 等字段。
"""
prompt = f"""
从以下对话中提取值得记录的关键事件。
关注:用户偏好、任务结果、出现的问题、用户的明确反馈。
以 JSON 列表返回,每条包含 summary、category、importance(1-5)。
对话内容:{conversation}
"""
return parse_json(llm_call(prompt))
def retrieve_episodes(query: str, top_k: int = 5) -> list:
"""检索情景记忆:语义相似度与时间衰减混合排序(示意)。
参数:
query: 当前查询文本。
top_k: 返回条数上限。
返回:
排序后的相关事件列表。
"""
semantic_results = vector_search(query, top_k * 3)
scored = [
(ep, similarity + recency_bonus(ep.timestamp))
for ep, similarity in semantic_results
]
return sorted(scored, key=lambda x: x[1], reverse=True)[:top_k]
3. 语义记忆(Semantic Memory)
定义: 与具体事件无关的、通用性的知识和事实,包括关于用户、领域、世界的背景信息。
生命周期: 长期持久,可以随时间更新,但不会因为单次交互就发生根本变化。
存储位置: 向量数据库(用于非结构化知识)、结构化数据库或键值存储(用于结构化属性)。
典型内容:
- 用户的职业背景、技术水平、偏好设置(「喜欢用 Python 3.10+,讨厌冗余注释」)
- 企业内部的产品知识、术语表、流程文档
- 领域知识库(医疗、法律、金融等专业背景)
- Agent 自身的能力边界描述
语义记忆和 RAG 在实现上高度重叠,但概念层面有区别:RAG 通常指从静态知识库中检索文档来增强回答,而语义记忆更强调的是 Agent 关于「用户和世界」的动态认知模型——它会随着交互积累而更新。
语义记忆 vs RAG 的边界:
| 维度 | 语义记忆 | 传统 RAG |
|---|---|---|
| 内容来源 | 交互中积累 | 预先索引的文档 |
| 更新频率 | 动态,随时间演化 | 静态或定期批量更新 |
| 检索粒度 | 用户/任务粒度 | 文档/段落粒度 |
| 典型用途 | 用户建模、偏好管理 | 知识问答、文档检索 |
在实践中,两者经常共存:Agent 用 RAG 检索产品文档,用语义记忆保存用户偏好,用情景记忆记录历史交互。
工程上需要关注的点:
语义记忆最核心的工程挑战是更新策略。用户偏好会变,领域知识会迭代,如果语义记忆只增不改,很快会出现矛盾或过时的条目。需要在写入时做去重、在读取时做新旧冲突解决,或者为每个条目附加置信度和最后更新时间。
def update_user_memory(user_id: str, new_observation: str) -> None:
"""将新观察合并进用户语义记忆:矛盾时以新为准,可补充则合并(示意)。
参数:
user_id: 用户标识。
new_observation: 本轮交互得到的新信息描述。
"""
existing = load_user_memory(user_id)
prompt = f"""
现有用户信息:{existing}
新观察到的信息:{new_observation}
请更新用户信息。如果新信息与旧信息矛盾,以新信息为准。
如果新信息补充了旧信息,合并两者。
以 JSON 返回更新后的用户画像。
"""
updated = parse_json(llm_call(prompt))
save_user_memory(user_id, updated)
4. 程序记忆(Procedural Memory)
定义: 关于「如何做事」的知识,表现为固化的行为模式、操作规范和处理流程。
生命周期: 最稳定的记忆形态,通常不随个别交互改变,而是通过离线学习或人工维护来演化。
存储位置: 系统提示词(system prompt)、代码逻辑、工具定义、Fine-tuning 的模型权重。
典型内容:
- Agent 的角色定义和行为准则(「你是一个代码审查助手,发现 bug 时应该……」)
- 标准操作流程(SOP):「遇到数据库错误,先检查连接,再检查权限,再检查 SQL」
- 工具的使用规范和边界
- 特定任务类型的处理模板
程序记忆是唯一一种不需要「检索」的记忆——它直接内嵌在 Agent 的行为里。一个训练良好的模型或者写得好的 system prompt,就是程序记忆的载体。
工程上需要关注的点:
程序记忆的更新成本最高,因为它要么需要修改 system prompt(影响所有用户),要么需要重新 fine-tune 模型。因此,程序记忆应该只存放那些真正稳定的、普适的行为模式,而不是把个性化的用户偏好也塞进 system prompt。
Skill 机制(即动态注入 system prompt 的特定模块)是程序记忆的一种灵活实现方式,可以根据任务类型按需加载不同的操作规范,而不是在 prompt 里堆砌所有情况。
四种记忆的对比
| 记忆类型 | 时间跨度 | 存储位置 | 读取方式 | 更新频率 | 典型失败 |
|---|---|---|---|---|---|
| 短期记忆 | 当前会话 | Context window | 直接可见 | 每次交互 | 截断、lost in the middle |
| 情景记忆 | 跨会话 | 向量/关系数据库 | 语义检索 | 会话结束时 | 检索不准、相关性低 |
| 语义记忆 | 长期 | 向量/KV 存储 | 语义检索 | 知识演化时 | 过时、矛盾 |
| 程序记忆 | 永久 | System prompt / 权重 | 无需检索 | 离线维护 | 过度泛化、僵化 |
常见的设计错误
把所有记忆都塞进 context window。 这是最普遍的错误。短期内能 work,一旦上下文变长或者任务跨越多个会话,立刻崩溃。
把所有记忆都交给向量数据库。 向量检索有延迟,有精度损耗,对于当前任务中正在使用的信息(短期记忆),应该直接在上下文中保留,而不是每次检索。
记忆只写不读。 有些 Agent 实现了情景记忆的写入逻辑,但从来没有在任务开始时检索相关历史,存进去的记忆是死的。
没有记忆清理机制。 情景记忆和语义记忆如果只增不减,条目会越来越多,检索质量下降,矛盾信息累积。需要设计 TTL(生存时间)、重要性评分、去重合并等维护机制。
程序记忆个性化。 把用户 A 的特殊要求写进 system prompt,导致所有用户都受影响。个性化信息应该进语义记忆,由运行时动态注入。
一个实际的记忆架构
下面是一个中等复杂度的 Agent 记忆系统的基本结构,展示四种记忆如何在一次任务执行中协作:
任务开始时:
1. 从语义记忆中检索用户偏好和相关背景知识
2. 从情景记忆中检索历史上相关的任务经历
3. 将检索结果注入 context,构成「记忆前缀」
4. 程序记忆(system prompt)已经内嵌在 Agent 定义里
任务执行中:
5. 所有当前步骤的推理和工具调用存在短期记忆(context)里
6. 重要的中间状态可以写入外部 KV 存储,用于任务恢复
任务结束后:
7. 从对话中提取关键事件,写入情景记忆
8. 如果发现新的用户偏好或领域知识,更新语义记忆
9. 如果发现某类问题的处理方式值得固化,考虑更新程序记忆(成本最高)
这个流程里有一个隐含的取舍:检索和注入记忆会消耗 token,增加延迟。不是每个任务都值得走完整的记忆流程。简单的单轮问答不需要情景记忆;一次性任务不需要写入情景记忆;匿名用户不需要语义记忆。记忆系统的复杂度应该与任务的复杂度和用户关系的长度匹配。
与已有文章的关系
这篇文章的关注点是记忆的分类与选型:什么信息该放在哪种记忆里,为什么。
上下文与记忆:Agent 为什么会在长任务中失去方向,以及如何设计状态管理 关注的是另一个问题:在长任务执行中,为什么 Agent 会出现目标漂移和重复操作,以及如何通过状态管理来解决。两篇是互补关系,一个讲「记忆的结构」,一个讲「记忆在长任务里的失效与修复」。
什么是 RAG:让大模型用上你自己的知识库 则深入了向量检索的实现细节,本文提到的「语义记忆」在实现层面与 RAG 高度重叠,可以结合来看。
总结
记忆是 Agent 区别于普通聊天机器人的核心能力之一,但「记忆」不是一个单一的东西。把四种记忆形态混淆,或者只用一种机制来处理所有记忆需求,是大多数 Agent 记忆设计失败的根本原因。
设计记忆系统的时候,可以从这几个问题出发:
- 这条信息在当前任务结束后还有用吗?→ 有,考虑持久化;没有,放 context 就够了
- 这条信息是关于特定事件的,还是通用知识?→ 前者进情景记忆,后者进语义记忆
- 这个行为模式是稳定的、普适的吗?→ 是,考虑程序记忆;否,用情景或语义记忆动态调整
- 我的检索能找回需要的信息吗?→ 记忆系统的质量最终由检索质量决定
记忆设计没有银弹,但有了清晰的分层框架,至少不会在错误的地方解决问题。
评论
使用 GitHub 账号登录后即可发表评论,评论会同步到仓库 Discussions。