多数 Agent 的失败不是模型不够强
做 Agent 的人常有一个惯性:效果不好就换更强的模型。但真去排查日志会发现,问题多半出在喂进去的那段上下文上——要么塞了几十轮无关历史把关键信息淹了,要么用户三天前明确说过的约束丢了,要么每轮都往向量库灌全文,检索回来的全是噪声。模型能力是给定的,记忆结构才是你能控制的变量,而且它的收益比换模型稳定得多。
把记忆拆成三层来看,大部分纠结会立刻变得可决策:哪些信息天生该待在上下文窗口里,哪些该落库,哪些该被压缩。
三层记忆:介质、写入时机与召回方式
工作记忆:就在上下文窗口里
指当前这一轮推理必须直接看到的内容:系统提示、最近若干轮原始对话、当前任务的中间状态、工具返回结果。存储介质就是进程内存或 Redis 里的一段消息数组,写 prompt 时直接拼进去。
它的特点是召回率 100%——因为根本没有检索环节,信息就在眼前。代价是成本和延迟随 token 线性上涨,而且长上下文存在"中间遗忘"现象:关键信息放在几千 token 之前,模型注意力会明显衰减。所以工作记忆的原则是"短而新鲜",只保留最近 K 轮原文,K 取 6 到 10 是常见起点。
情节记忆:带时间戳的"发生过什么"
典型形态是"用户在上周三提到生产环境跑的是 Ubuntu 22.04""上个月排障时确认过 Redis 没开持久化"。这类信息量大、时效性强、单独看价值不高,但需要时可精确取回。
存储介质用结构化数据库(Postgres / SQLite 都行)加时间戳和实体字段,需要模糊匹配时再叠一层向量索引。写入时机是每轮对话结束后异步抽取,不要让主流程等它。召回走"精确查询 + 向量检索"混合:先按 user_id、实体名做过滤,再算相似度。
情节记忆一定要有衰减策略。给每条记录一个 last_used_at,超过一定时间没被召回的降权甚至归档,否则库会越滚越大,检索精度持续下降。
语义记忆:从多次情节里归纳出的稳定结论
比如"这个用户偏好用 TypeScript""他团队的技术栈是 Next.js + Supabase"。这类信息的特点是条目少、生命周期长、质量要求高,写错一条比漏一条危害更大。
存储介质是向量库加一张关系表,关系表里保留 key、value、置信度、来源消息 ID。关键点在于写入时机不是每轮,而是定期离线归纳——每积累 N 轮对话或每天跑一次,让模型对比新旧事实、合并冲突、更新置信度。每轮都写会制造大量近似重复的条目,检索时互相挤占 top-k 名额。
摘要压缩:别把自然语言硬压,先抽结构
当对话超出窗口预算,最直接的做法是把最老的若干轮交给模型压缩成一段摘要。这里有几个容易踩的坑。
- 分层压缩,不要一次性压。每 10 轮生成一个局部摘要,每 10 个局部摘要再合成一个高层摘要,形成归约树。一次性把 100 轮压成 300 字,信息丢失是不可控的。
- 该保留的明确列出来:已做的决策、用户给的约束、未完成的待办、偏好与禁忌。该丢的也明确:寒暄、重复确认、失败尝试的过程细节(但保留失败结论)。
- 能抽成 KV 就别写成散文。"用户是张三,邮箱 xxx,项目代号 xxx"抽成三个字段,token 效率比一段摘要高一个数量级,还能直接做过滤。
摘要的本质是有损压缩且不可逆。所以更好的做法是让情节记忆和语义记忆承担主要存储,摘要只作为兜底的上下文衔接——它负责让对话读起来连贯,不负责承载事实。
容量、成本与召回率怎么取舍
三种介质的取舍关系可以用一句话概括:上下文窗口召回率最高但成本随长度线性增长,向量库成本几乎恒定但有漏召回风险,摘要 token 开销恒定但信息有损。
落到具体决策上,可以按两个维度给每条信息打分:被引用的频率,以及精确性要求。
- 高频 + 要求精确(用户偏好、系统约束)→ 常驻工作记忆或语义记忆,随每次请求注入。
- 高频 + 允许模糊(对话上下文、近期任务状态)→ 最近 K 轮原文进窗口,更早的压成摘要。
- 低频 + 要求精确(某次配置、某个报错原文)→ 落情节记忆,按需检索,不要常驻。
- 低频 + 允许模糊 → 直接不存,或归档到冷存储。
检索侧的调参空间主要在 top-k、去冗余和时间衰减。top-k 从 5 试到 20,观察召回质量在哪一档不再提升;用 MMR 类方法去重,避免五条结果说同一件事;给相似度加时间衰减,比如最终得分用相似度减去一个随天数增长的小量,让新事实更容易浮上来。
延迟预算上,检索加重排建议控制在整体响应延迟的 20% 以内。如果检索 p95 超过两三百毫秒,先调索引参数或缩小候选集,而不是急着换向量库。
一套可以照着改的最小实现
- 建 messages 表:id、session_id、role、content、token_count、created_at。
- 建 facts 表:user_id、key、value、confidence、source_msg_id、updated_at、last_used_at,value 字段上建向量索引(pgvector 够用)。
- 每轮结束后异步触发抽取器,从本轮对话里抽出事实候选,与已有 facts 比对后合并或新增。
- 组装 prompt 的顺序:系统提示 → 召回的事实(按相关度取前 8 条左右)→ 更早对话的滚动摘要 → 最近 K 轮原文 → 当前输入。把稳定的信息放前面、最新的放后面。
- 建 summaries 表,当 messages 中未压缩的轮数超过阈值(比如 20)时,把最老的 10 轮压缩后写回,并标记已压缩。
- 加一个每日离线任务,扫描 facts 表做去重、置信度调整和过期归档。
判断记忆系统好不好,有个很简单的自测:随机抽 20 轮历史对话,问系统三个必须依赖历史才能答对的问题。答不上来,问题在召回;答对了但上下文很长,问题在压缩;上下文短还答错,问题在写入时的抽取质量。
小结
Agent 的记忆不是"把历史都塞进去"或"全部丢给向量库"二选一,而是三层各司其职:工作记忆保新鲜,情节记忆保细节,语义记忆保稳定。写入要分时机——工作记忆每轮更新,情节记忆轮后异步写,语义记忆定期归纳;召回要分手段——常驻注入、混合检索、摘要衔接各用各的场景。真正拉开差距的不是向量库选型,而是"什么信息值得记住"这个判断,以及压缩时舍得丢掉什么。

