本文主要介绍Agent记忆 以及 如何设计Agent记忆系统。
1、Agent 的记忆和 LLM 的上下文窗口是什么关系?
这两个不是同一层的东西,上下文窗口是模型推理时的工作内存 ,Agent 记忆是模型之外的持久化状态。记忆的本质是在窗口有限的前提下,做信息的选择性进出。
上下文窗口有硬上限,不可能把全部历史塞进去,所以必须做取舍。记忆系统本质上就回答三个问题:什么值得写出去、下次怎么找回来、找回来怎么塞进上下文里 ------也就是写入、检索、注入。
2、现在上下文窗口越来越大(比如1M),还需要记忆吗?
需要,而且窗口越大越需要。记忆系统主要解决的是跨会话信息传递的问题。
- 跨会话:窗口再大,也只覆盖本次请求。用户上周说的偏好、上个月定的方案,物理上就不在窗口里。这不是成本问题,是信息根本不存在的问题。
3、 Agent 的记忆分哪几类?各自怎么实现?
- 按持久性分(工程视角)
| 记忆类型 | 存什么 | 实现方式 |
|---|---|---|
| 短期记忆(工作记忆) | 当前任务的对话上下文 | 直接放 prompt / 上下文窗口,受窗口大小限制 |
| 长期记忆 | 跨会话持久化信息 | 向量数据库(语义检索)+ 结构化存储(KV 事实表) |
- 按内容分(CoALA/认知科学视角)
| 记忆类型 | 存什么 | 实现方式 |
|---|---|---|
| 工作记忆 | 当前任务的对话上下文 | 直接放 prompt / 上下文窗口,受窗口大小限制 |
| 情景记忆(Episodic) | 具体经历/事件("上次做过 X") | 会话日志 + 向量化存储,按相似度召回 |
| 语义记忆(Semantic) | 抽象知识/事实("用户偏好 Python") | 结构化 KV 存储(如 user_facts),精确匹配或检索 |
| 程序性记忆(Procedural) | 技能/操作套路 | 沉淀为可复用的技能/脚本/模板,按场景触发加载 |
"长期记忆"是伞概念,情景/语义/程序性是长期记忆之下的三种内容类型,三类的区别在"抽象层级":
- 情景记忆:存"事件"------带时间、带上下文的具体经历。例:"2024-05-01 用户让我写了一篇 Agent 面试文章"。
- 语义记忆:存"事实"------从经历中抽象出的稳定知识。例:"用户偏好中文回复"。它往往是从多条情景记忆中提炼出来的。
- 程序性记忆:存"怎么做"------成功操作的模式。例:"写技术文章时先列大纲再逐章展开"。它是从成功轨迹中沉淀的技能。
抽象层级递进:事件 → 事实 → 技能,对应"经历 → 知识 → 能力"。
记忆分类来源知识点:
| 认知科学概念 | 原本含义 | 移植到 Agent |
|---|---|---|
| 工作记忆 | 人脑当前正在处理的信息 | 上下文窗口 |
| 情景记忆 | 对具体经历的自传式记忆 | 过往交互记录 |
| 语义记忆 | 抽象事实与概念知识 | 结构化知识/事实 |
| 程序性记忆 | 技能(如骑自行车,非陈述性) | 沉淀的操作套路 |
4、如何设计记忆系统?
记忆系统本质上就回答三个问题:写入、检索、注入,系统设计也是基于这三个问题展开。完整架构图如下:
text
┌─────────────────────────────────────────────────┐
│ 写入侧(异步) │
│ 对话 → 门控 → 抽取(带key) → 冲突消解 → 落库 │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 存储层 │
│ 结构化KV(偏好/配置) + 向量库(语义) + 图库(关系) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 读取侧 │
│ query改写 → 混合召回 → 重排+时间衰减 → top-k │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 注入侧 │
│ 预算分配 → 位置策略 → 格式化标注 → 拼入prompt │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ 维护侧(离线) │
│ 固化 / 去重 / 衰减 / 纠错 / 评测 │
└─────────────────────────────────────────────────┘
下面对这些问题进行具体说明:
4.1、写入
原则 :宁可漏记,不可滥记;写入要调 LLM 抽取,一般都采用异步方式。
记忆系统的失败模式几乎都是"记太多导致检索不准",而不是"记太少"。异步写入记忆可能出现"用户刚说完就追问,记忆可能还没写完"的情形,但这些信息仍在本轮对话的原文中,所以影响有限。
需要解决的问题:
-
问题1 :什么信息需要写入
并不是所有信息都需要写入到记忆系统中,而是有选择性的写入。三类高价值信号:
-
显式声明 :"记住..."、"以后都..."、"我的 X 是 Y"
-
纠正信息 :"不对,是..."(必须写,且要覆盖旧值)
-
重复模式 :多次提到同一偏好
实现:规则(关键词/正则)打底 + LLM 打分做门控,设阈值。
-
-
问题2 :怎么存储
原则 :必须有 key,否则无法覆盖、无法去重、无法治理。
在写入之前,必须先调 LLM 把对话抽成结构化条目(抽取),示例如下:
JSON{ "id": "m_002", // 记忆条目唯一标识,创建时生成、永不变;被 superseded_by 引用,作为外部引用锚点 "key": "project.tech_stack", // 槽位标识,取自受控词表;与 scope 共同定位"跟谁比",决定覆盖对象 "scope": "prod", // 作用域(prod/test/user/项目名);与 key 构成唯一约束,跨 scope 不构成冲突 "value": "Go", // 归一化后的值(如 "golang" → "Go");覆盖判断的比较对象,检索时返回的内容 "type": "preference", // 信息类别,决定覆盖策略(preference/decision/config/event/fact_mutable/fact_immutable),隐含 mutable "source": { // 来源追溯信息,用于校验、回溯、解释置信度 "session_id": "session_42", // 来源会话,定位上下文 "turn_id": "t_17", // 来源轮次,精确定位到具体一句话 "quote": "我们后端统一用 Go", // 用户原话片段,保真度锚点:value 被归一化时,只有原话能证明归一化没出错 "source_type": "explicit" // 来源类型 explicit | inferred | corrected;决定 confidence 是否有意义,explicit 时 confidence 恒为 1.0 }, "confidence": 1.0, // 置信度 0~1,仅 inferred 时有意义(表示模型对本次抽取的确信度,非"信息可信度");低于阈值不写入或降级为候选 "created_at": "2025-03-01T10:00:00", // 首次写入时间;用于时效性排序、判断"这条记了多久" "last_confirmed_at": "2025-03-20T14:00:00", // 最近一次被确认/重复提及的时间;用于时效性排序、判断是否陈旧 "confirm_count": 3, // 被确认的次数(含首次);用于重复模式检测、置信度加固 "expires_at": null, // 到期时间,null 表示长期有效;语义是"到期触发动作(提醒/复核)"而非"到期自动删除" "superseded_by": null, // 被哪条新记录取代(新记录 id),null 表示当前活跃;沿链可回溯变更历史 "superseded_at": null // 被取代的时间,null 表示未被取代;与 superseded_by 成对出现,用于回答"什么时候改的" }在写入之前,还有非常重要的工作:冲突消解,绝不允许两条矛盾事实同时存在于活跃记忆里。
txt相似检索 → 命中同 key → 判断类型 ├─ type对应mutable=true → update(覆盖旧值,保留 updated_at) └─ type对应mutable=false → insert 新版本(时序查询时取最新)
4.2、检索
并不是直接拿用户问题直接来检索,在检索之前必须进行Query 改写。这是因为用户查询和记忆表述有语义鸿沟,需要处理以下问题:
- 指代消解:"我上次说的那个方案" → 结合会话历史还原
- 时间归一:"上个月" → 绝对时间范围
- 意图扩展:"帮我安排会议" → 召回时区、偏好时段、常用参会人
Query改写不要只输出一个字符串,而是输出结构化检索条件,让检索层可以分路执行。示例如下:
txt
{
"original": "那个方案怎么样了",
"rewritten": "A 方案 进展",
"filters": {
"key": ["project.migration_plan"],
"scope": "prod",
"type": ["decision", "event"],
"time_range": "last_30_days"
},
"fallback_query": "方案 进展" // 改写失败时的降级检索式
}
Query改写完成后,下一步就需要去记忆系统检索。在具体检索方案中,通常会采用混合检索:
| 方式 | 擅长 | 短板 |
|---|---|---|
| 向量 | 语义相似 | 精确标识符差 |
| BM25 | 精确匹配、专有名词 | 同义表达失效 |
| 结构化过滤 | user_id、时间、类型 | 无语义能力 |
多路召回后,通常采用RRF融合选择top-50,然后用 cross-encoder 或 LLM 做重排重排选 top-5。同时,可叠加时间衰减:旧偏好降权,避免三个月前的结论压过上周的。
注意:衰减只对偏好类适用,事实类("我住北京")不该衰减。
检索流程如下:
txt
用户 query: "我们后端用的啥语言"
↓
1. Query 改写 → 补全指代、对齐槽位
↓
2. 双路检索(并行)
├─ 精确路:按 key="project.tech_stack" 查主库
└─ 语义路:embed(query) → 向量检索 top_k=10
↓
3. 合并去重(按 id)
↓
4. 过滤:superseded_by=null、scope 匹配、未过期
↓
5. 重排:精确命中 > 语义相似度 > 时效性
↓
6. 取 top_n 注入上下文
检索常见坑:
- 只做向量检索,精确查询失效
- top-k 设太大,噪声进窗口污染上下文
- 缺时间衰减,新旧信息权重相同
- 检索同步阻塞主链路,延迟高(应做预取/缓存)
4.3、注入
要解决的问题 :检索结果可能很多,窗口预算有限,且位置影响效果。注入的本质是在预算内做排序与格式化 。
在检索完成后,需要将检索到的记忆注入到上下文中,这个时候需要考虑:预算、位置和格式化。
-
预算:给记忆分配固定 token 预算(窗口的 10--20%),超出截断。固定比例,不动态挤占------保证行为可预测。
-
位置 :利用 lost-in-the-middle:
- 关键约束 → 放开头(紧跟 system)
- 次重要 → 放结尾(紧邻 query)
- 背景信息 → 放中段,且压缩
-
格式化 :不要直接拼原文,要标注来源和时间,比如:
txt[用户偏好 · 2025-03 确认] 技术栈:Go [历史事件 · 2025-01] 曾使用 Python,已迁移标注来源和时间,模型才能判断可信度和时效性;不标注,模型会把推测和事实同等对待。
注入侧常见坑:
- 无预算控制,检索多少塞多少 → 挤掉任务本身
- 关键信息放中段 → 被忽略
- 不标注来源 → 模型无法区分"用户明说"和"模型推测"
- 每次全量重拼 → 破坏 prompt 缓存,成本上升
4.4、离线维护
记忆系统上线后,还是需要定期维护的,以避免记忆膨胀、腐化等。
维护机制:
| 机制 | 作用 | 时机 |
|---|---|---|
| 固化 | N 条情景记忆 → 1 条语义记忆 | 离线、定期 |
| 去重合并 | 相似条目合并 | 写入时 + 定期 |
| 衰减归档 | 长期未命中的低权重条目降权 | 定期 |
| 纠错 | 用户说"记错了" → 定位条目删除 | 实时 |
固化是控制规模增长的核心手段------10 次"用中文"归纳成 1 条"偏好中文"。
当然,记忆系统的评测也是必不可少的,不然无法知道记忆系统好不好。
分环节评测指标:
| 环节 | 指标 |
|---|---|
| 写入 | 该记的记了吗?不该记的记了吗? |
| 检索 | 召回率、精确率 |
| 端到端 | 任务成功率 |
| 风险 | 错误记忆被引用率 |
评测集构造:多会话长程对话 + 需要跨会话引用的事实点,做 needle-in-haystack 式检验。
没有评测闭环的记忆系统,等于没有记忆系统------你无法知道改动是变好还是变坏。