Agent 的记忆不是数据库:状态、上下文与检索的边界
把三类完全不同的问题塞进同一个向量库,是 Agent 记忆设计里最贵的错误。这篇拆开状态、上下文预算与检索治理,并给出可落地的分层。
“给 Agent 加个记忆”这句话,通常同时指三件毫不相干的事:
- 记住用户刚才说了什么;
- 记住用户三个月前的偏好;
- 从一堆文档里找出和当前问题相关的那一段。
它们的失效方式不同、存储介质不同、出错后的补救方式也不同。把它们塞进同一个向量库,短期能跑,长期一定会塌——而且塌的时候没人说得清是哪一层。
我的建议是先做一次粗暴的切割:状态归状态,上下文归上下文,检索归检索。
状态:应该被确定性地存储
状态是“系统现在处于哪一步”。它包含会话进度、已确认的事实、待办动作、权限上下文。这类东西有一个共同特征:它们必须是精确的。
订单号不能“语义相近”,退款金额不能“大概对”,用户已经确认过的地址不能被摘要改写。所以状态不应该进入模型,至少不应该只存在于模型的上下文里。
正确的位置是结构化存储:一张表、一个键值对、一段带 schema 的 JSON。模型负责读写它,但不负责持有它。
这里有一个常见的误区:把状态写进系统提示词,指望模型“记住”。这在三步任务里能工作,在三十步任务里必然漂移——因为上下文会被截断、会被摘要、会被新的指令挤出注意力。更糟的是,漂移是静默的:模型不会告诉你它忘了,它会用编造的内容把流程走完。
一个可用的约定是:每一次工具调用都携带当前状态的关键字段,由运行时注入,而不是由模型复述。模型可以选择下一步做什么,但不能自己声明“用户已经付过款了”——这个事实只能来自存储。
上下文:预算,而不是仓库
上下文窗口是一个预算问题。你有 N 个 token,要分给系统指令、工具 schema、最近对话、历史摘要、检索结果、以及留给模型思考的空间。任何一项超支,都会从别的地方扣。
大多数团队的做法是“能塞就塞”,直到撞上窗口上限,然后开始截断——通常从最早的消息开始截断,而那恰好是最重要的背景。
更稳的做法是显式分配预算,并且让分配策略可观测:
| 分区 | 典型占比 | 超支时的处理 |
|---|---|---|
| 系统指令与工具 schema | 10–15% | 精简 schema,拆分子 Agent |
| 最近若干轮原文 | 20–30% | 保留最近,其余转摘要 |
| 结构化状态 | 5–10% | 只放关键字段,细节走工具查询 |
| 检索片段 | 20–30% | 降条数、提升阈值 |
| 输出预留 | 15–25% | 不可压缩 |
这张表不是标准答案,但它强迫团队回答一个平时不会问的问题:当预算不够时,先砍哪一块? 没有答案的系统,会在最忙的时候随机砍。
摘要值得单独说。摘要是压缩,不是真理。它一定丢信息,问题在于丢的是不是关键信息。所以摘要必须保留可回溯的指针:摘要里说“用户已确认退款到原卡”,就应该能点回去看到那轮对话。否则三个月后出现争议,你手里只有一句被模型改写过的二手事实。
还有一条:不要把摘要当成状态。摘要是历史叙述,状态是当前事实。两者混用,会出现“摘要说已退款,状态显示待处理”这种无法自动消解的冲突,最后只能靠人去数据库里翻。
检索:写入时的治理比查询时的技巧更重要
检索质量差,八成不是 embedding 模型的问题,是写入时没有治理。
常见的三个坑:
去重缺失。 同一份文档被上传三次,检索时三条一样的结果占满预算。更隐蔽的是版本重复:v1 和 v2 内容 90% 相同,模型看到的是互相矛盾的两段。
时效缺失。 一条 2023 年的政策和一个 2026 年的修订版,在向量空间里可能非常接近。没有时间权重,模型只能随机选一个,然后自信地告诉你答案。
来源缺失。 检索结果没有出处,模型就无法判断该不该采信,用户也无法核验。在需要严谨的场景里,没有出处的片段等于噪声。
对应的治理动作很朴素:入库时做规范化与去重、给每条记录打上时间戳和来源、为同一主题维护“当前有效版本”标记。这些工作没有技术含量,但它们决定了检索的上限。查询侧的 rerank、混合检索、多路召回,都是在这个上限之内做优化——地基不平,装修再贵也会裂。
一个够用的分层
把上面三件事落成结构,大概是这样:
┌─────────────────────────────────────────┐
│ 运行时状态(数据库 / KV) │ 精确、可审计、可回滚
│ · 会话进度 · 已确认事实 · 权限 │
└─────────────────────────────────────────┘
↓ 按需注入关键字段
┌─────────────────────────────────────────┐
│ 上下文装配器(每次请求重新计算) │ 预算、优先级、截断策略
│ · 系统指令 · 最近对话 · 状态摘要 │
└─────────────────────────────────────────┘
↑ 召回带出处的片段
┌─────────────────────────────────────────┐
│ 知识层(向量库 + 元数据) │ 去重、时效、来源
│ · 文档片段 · 版本标记 · 访问控制 │
└─────────────────────────────────────────┘
三层之间只有单向依赖:上下文装配器读状态和知识层,状态和知识层互不认识。这个约束看起来多余,实际能省掉大量“记忆串味”的 bug——比如把某个用户的偏好写进了共享知识库,或者把检索到的文档内容当成了用户确认过的事实。
遗忘是功能,不是缺陷
最后说一件很少被写进设计文档的事:遗忘。
真实系统里,信息会过期、会被撤回、会被证明是错的。一个只会写入不会删除的记忆系统,最终会变成一个不断污染自己的噪声源。所以从第一天起就该有:
- 单条记录的删除(用户要求删除时,要真的删干净,包括派生摘要);
- 按时间的自然衰减(旧偏好权重下降,除非被再次确认);
- 冲突检测(新事实与旧事实矛盾时,标记而不是覆盖)。
“标记而不是覆盖”这一条尤其重要。覆盖会丢掉“用户曾经这么说过”这个信息,而这在客服、医疗、金融场景里往往才是关键。
记忆系统的目标不是记住更多,而是在该记住的时候准确,在该忘记的时候干净。做到这一点,靠的不是更大的向量库,而是三件无聊的事:明确的分层、显式的写入、可解释的召回。