面试官:你的 Agent 记忆系统,到底是在-记住-还是在-堆日志-?
公众号名称:吴师兄学大模型
作者名称:吴师兄
发布时间:2026-07-09 10:00
大家好,我是吴师兄。
之前写 Memory 淘汰策略那篇的时候,评论区有人问:“所以 200 行之前的 memory 是直接丢弃吗?“再往前,拆 Claude Code 记忆机制那篇下面,问得最多的是:“project 下面每个文件夹都有 memory,它会自动加载所有的 memory 吗?”
这两个问题隔了一个月,其实是同一个焦虑:记的东西越攒越多,系统到底还管不管得住。
我见过的大部分 Agent”记忆系统”,管不住。原因不在存储选型,在第一步就走歪了:把每轮对话原样写进库里,然后管这个库叫记忆。
这不叫记忆,这叫日志。
日志和记忆的区别,我用三个判据来划:会提炼,写入的时候把对话压成结论,而不是存原文;会遗忘,有淘汰和衰减机制,旧信息会退场;会用上,检索命中率是一个可以测出来的数字,而不是”应该能搜到”。三条全占,才配叫记忆系统。缺任何一条,它就会在某个时刻用一种很难排查的方式坑你。
今天把这三个判据逐个拆开,每一条都配上我自己踩过的坑。

记忆还是日志?三个判据一验就知道
日志是怎么冒充记忆的
先讲我自己的翻车版本。
去年给 AlgoMooc 网站加答疑 Agent 的时候,第一版记忆模块我是这么写的:每轮对话结束,用户消息和回答原文一起做 embedding,写进 Chroma,元数据带上 user_id 和时间戳。下次用户提问,按语义相似度召回 top 5 拼进 prompt。
三周之后这个库涨到一万两千多条。然后出现一个很典型的现象:用户问”上次你给我讲的那个二分边界问题,再展开一下”,召回的 5 条里,3 条是”好的""明白了""谢谢”这类寒暄,1 条是三周前另一个话题的问题原文,真正那次二分讨论,排在第 11 位,没进 top 5。
为什么寒暄反而排前面?因为”上次你给我讲的那个”这句话本身没什么信息量,它和库里同样没信息量的短句相似度最高。原文入库的日志式存储,信息密度越低的内容越容易互相命中,库越大这个效应越明显。
这就是日志冒充记忆的恶性循环。写入无成本,所以什么都写。什么都写,库就膨胀。库一膨胀,命中就变差。命中变差之后,第一反应是把 top 5 加到 top 10,往 context 里塞更多,token 费用涨上去,回答质量反而往下掉。LangChain 的 ConversationBufferMemory 挨骂挨了两年,其实骂错了对象。它自己定位就是当前会话的缓冲区,是拿它当长期记忆用的人错了。

日志冒充记忆的恶性循环
判据一:会提炼,但别炼过头
记忆系统在写入侧要做的事,是把一段对话压成一条能独立成立的结论。“用户花了六轮搞明白二分的左闭右开边界”压成”该用户对边界条件类问题需要展开推导,直接给结论他会追问”。这条结论脱离原始对话也能用,这才是可以入库的形态。
但提炼有个反方向的坑:炼过头。
我踩的这个案例后来在评论区也有人讨论过同类的(那边的版本是”对花生过敏”被压成”用户讨论了饮食偏好”)。我的版本是:用户明确说过”我只会 Python,别给我 Java 的解法”,提取模型把它压成了一条”用户有编程语言偏好”。两周后用户问一道链表题,Agent 检索到这条”偏好”,但偏好内容是什么已经没了,照常给了 Java 题解。用户回了一句”我说过我不会 Java”,是带着情绪敲的。
这个 badcase 给我的教训是提炼要分档,不能一把梭:
| 信息类型 | 提炼档位 | 例子 |
|---|---|---|
| 硬约束、否定句、身份属性 | 保留原话,不压缩 | ”只会 Python,别给 Java”、“我是初中生” |
| 偏好、目标、学习状态 | 压成结构化结论 | ”边界类问题需要展开推导” |
| 过程性对话 | 压成一行摘要或直接不入库 | ”讨论了三轮快慢指针” |
| 寒暄、确认、情绪表达 | 不入库 | ”好的""懂了” |
判断规则可以写得很土:消息里含”别""不要""必须""只会""过敏""禁止”这类词的,正则直接拦下来原文入库,不经过提取模型。越是否定式的约束,越不能交给摘要模型去转述,转述丢掉一个”别”字,语义就反了。
入库的形态也有讲究。我现在每条记忆固定四个字段:type(约束/偏好/状态/事实)、content(结论本身)、source_turn(指回原始对话的轮次号)、updated_at。前两个字段是给检索用的,source_turn 是给人用的:提炼出错的时候,你得能一步跳回原文看它是从哪句话炼出来的。第一版没存这个指针,排查那个 Java 题解的问题时,我在一万多条日志里靠时间戳对了半个小时才找到原话。
Anthropic 的 memory tool(memory_20250818)在这件事上的设计很值得看:它的记忆不是向量条目,是 /memories 目录下的真实文件,模型通过 view、create、str_replace、insert、delete 五个命令直接读写。用 str_replace 更新一条记忆的时候,改的是那一行原文,不存在”转述损耗”这一层。这是文件式记忆相对向量式记忆一个经常被忽略的优点。

提炼分四档:硬约束保原文,寒暄不入库
写入什么时候发生:三种触发方式的成本账
提炼要跑模型,跑模型就有成本和延迟,所以”什么时候写”不是细节,是这个模块的成本大头。三种主流做法:
每轮同步写。每轮对话结束立刻调一次提取模型。好处是不漏;代价是每轮多一次 LLM 调用,如果放在响应链路上,用户每句话都要多等一截。我第一版就是同步写,答疑 Agent 平均响应从 2 秒出头涨到 3 秒多,涨的那一秒全是提取调用。
批量异步写。对话攒着,后台定时或者按轮数触发一次提取,一次扫一批。成本降一个量级,用户无感。风险是窗口期:还没来得及提取的信息,这一轮用不上;进程挂了这批就丢了。
工具化,让模型自己写。把记忆做成工具交给模型,模型自己判断什么值得记,任务开始先看一眼记忆。Anthropic memory tool 走的就是这条路,它配套的 system prompt 里有一句写死的话:“ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE.”读写时机全部交给模型决策。上限最高,也最省心,但每次读写都占对话轮次和 token,而且模型漏记了你没有兜底。
| 触发方式 | 额外成本 | 延迟影响 | 漏记风险 | 适合 |
|---|---|---|---|---|
| 每轮同步 | 每轮 1 次 LLM 调用 | 用户可感知 | 最低 | 轮次少、信息密度高的场景 |
| 批量异步 | 约 1/5 到 1/10 | 无感 | 窗口期内丢失 | 大部分生产场景 |
| 工具化自管理 | 占对话 token | 任务开头多一步 | 依赖模型自觉 | 长任务 Agent |
Claude Code 的选择是异步加小模型:提取这一步专门用低配调用,max_tokens 直接限死在 1024,因为提取产物只是几行结构化结论,不需要长输出。评分和提取用小模型,主对话用大模型,这个分工是把成本压住的关键。我后来把答疑 Agent 改成批量异步加小模型之后,提取成本降到原来的大概八分之一,响应回到 2 秒档。

写入什么时候发生:三种触发的成本账
判据二:会遗忘
不会遗忘的记忆系统,前面提炼做得再好,半年后还是会退化成日志,只是退化得慢一点。
遗忘要处理三件事。
第一件是衰减。一条记忆的检索权重应该随时间下降,常见做法是最终分 = importance × recency,recency 用 0.95^days_ago 这类指数衰减。这个底数别拍脑袋:0.95 意味着两周后权重掉到一半左右,适合学习进度这类时效信息;用户身份和硬约束根本不该衰减,我的做法是约束类记忆 recency 恒等于 1,只有状态类和过程类参与衰减。另外注意衰减的是权重不是数据,旧记忆下沉但还在,被再次命中的时候把 updated_at 刷新,权重就回升了。
第二件是冲突覆盖。用户三月说”目标是暑期实习”,六月说”实习没投上,准备秋招了”。这两条都在库里的话,检索”求职目标”会同时命中,模型有一半概率引用错的那条。正确做法是写入时先做一次同主题检索,把相似度过阈值的旧记忆拉出来,让提取模型顺手多判一步:新旧关系是矛盾、补充还是重复。矛盾就把旧条目标过期,补充就两条并存,重复就丢弃新条目不写。这一步在我的实现里只多花一次小模型调用,因为它和提取本来就在同一个批处理里。这也是记忆系统和 RAG 分家的地方:RAG 的知识库不需要处理”文档 A 和文档 B 打架”,版本问题由离线 ETL 管;记忆的冲突发生在线上,必须写进写入链路里。
第三件是容量上限。给记忆的总预算设硬顶,超了就按分数淘汰。Claude Code 的 MEMORY.md 索引有个写死的常量 MAX_ENTRYPOINT_LINES = 200,索引超 200 行直接截断打 warning。开头评论区那个问题”200 行之前的 memory 是直接丢弃吗”,答案是:索引条目掉出 200 行就不会被加载,但记忆文件本体还在磁盘上,这是”索引遗忘、本体保留”的两层设计。遗忘的对象是注意力,不是数据。
这三件事的共同点是都需要写维护代码,而且短期看不到收益,所以大部分项目都跳过了。跳过的代价要几个月后才显形:库里躺着四个版本的”用户目标”,谁也说不清哪个是现行的。

遗忘三件套:衰减、冲突覆盖、容量硬顶
判据三:会用上,命中率要能测出来
前两个判据管写入,这一条管读出,也是最容易自欺的一条。
大部分记忆系统上线时的验收标准是”演示的时候能召回”。演示当然能召回,库里一共二十条。真正的验收得是一个数字:构造一个回归问题集,测命中率。
做法不复杂。从真实对话日志里挑 N 个”依赖历史信息才能答对”的问题,人工标注每个问题应该命中哪条记忆,然后跑检索,数命中了几个。我给答疑 Agent 建的回归集是 40 个问题,按三类挑的:约束类(“我说过不会 Java”)、进度类(“上次讲到哪了”)、偏好类(“按我习惯的方式讲”)。日志式的第一版命中 11 个;改完提炼和遗忘之后再跑,命中 31 个。
比这两个数更有用的是没命中的那 9 个。逐个看下来,6 个是检索 key 的问题:用户话里没有出现记忆条目里的词,纯语义相似度够不着,后来给约束类记忆加了”每次会话开头无条件注入”的旁路才解决,这类信息本来就不该走检索。剩下 3 个是提炼时丢了时间信息,“上次”这种指代解析不出来。也就是说,回归集不光给了一个分数,还把改进方向按大小排好了序。40 个问题的集合半天就能标完,这半天比任何架构讨论都值。
读出侧还有一个边界问题要划清楚:什么信息该走记忆,什么该走 RAG,什么留在日志里就行。
| 记忆 | RAG | 日志 | |
|---|---|---|---|
| 存什么 | 该用户的结论态信息 | 全局共享的知识 | 全量原始对话 |
| 检索 key | user_id + 当前话题 | 问题语义 | 基本不检索,用于审计回放 |
| 写入 | 提炼后写,带冲突处理 | 离线建索引 | 原样追加 |
| 遗忘 | 必须有 | 不需要 | 按合规保留期清理 |
三个都要有,别互相冒充。日志不是坏东西,排查问题、审计、回放全靠它,它只是不该被拼进 prompt。我的判断是:凡是打算拼进 context 的信息,入库前必须过提炼;只做原样存储的,一律当日志对待,别给它接检索。

记忆 / RAG / 日志:三套系统别互相冒充

命中率不是感觉,是能测出来的数字
泼盆冷水:多数应用不配上记忆系统
最后说边界。这套东西不是所有 Agent 都该上。
记忆系统的收益来自”同一个用户会带着历史回来”。如果你的场景是单次任务型,用户来一次解决一个问题就走,复访率很低,那记忆系统是纯负资产:提取调用的钱照花,维护代码照写,收益为零。一次性的代码生成工具、偶发使用的查询类 bot,都属于这类。
复访率高但会话很短的场景也要掂量。提取一条记忆的成本是固定的,用户平均聊三轮就走,能沉淀的信息本来就少,批量异步的窗口期问题反而更突出。
我的准则是先回答两个问题:用户第二次来的时候,上次的哪条信息如果丢了,他会明显感觉到?这类信息一周能产生多少条?第一个问题答不具体,或者第二个问题的答案是个位数,先别建记忆系统,把这几条信息塞进用户表的几个字段就够了。一张 user_profile 表加三个字段,能解决大部分项目对”记忆”的真实需求,成本是向量库方案的零头。
怎么把 Agent 记忆系统讲清楚?
如果要在两三分钟里把这套东西讲给别人(同事评审也好,面试也好),按三个判据走。
先立标准(30 秒):把对话原样存进向量库那是日志,记忆有三个判据,会提炼、会遗忘、会用上,逐条对照就知道一个系统是哪种。
再讲写入侧(60 秒):提炼分档,硬约束和否定句保原文,偏好压成结论,寒暄不入库;触发方式讲清楚自己选了哪种和为什么,批量异步加小模型是多数场景的答案,成本能降一个量级。
然后讲遗忘(40 秒):importance 乘 recency 的衰减、同主题冲突覆盖、容量硬顶三件套,举 MEMORY.md 索引 200 行截断做例子,说明遗忘的是注意力不是数据。
最后给数字(20 秒):回归问题集多少个、改造前后命中率从多少到多少。有这一个数字,前面所有设计就都落了地。
写在最后
带走三件事。
一,判断一个”记忆系统”的成色,看三个判据:写入时会不会提炼,长期会不会遗忘,命中率能不能测出数。三条对照完,是记忆还是日志一目了然。
二,硬约束和否定句保原文入库,别交给摘要模型转述。丢一个”别”字,语义就反了,这种错查起来最花时间。
三,上系统之前先算复访。用户不带着历史回来,记忆就没有分母,user_profile 表加几个字段是更诚实的方案。
一句话收束:日志记的是发生过什么,记忆记的是下次该怎么办。前者越全越好,后者越少越准越好,两者方向是反的,所以一套存储永远不可能同时干好这两件事。
代价也说清楚:按这套做,你要维护提取 prompt、回归问题集和淘汰逻辑三块代码,提取模型每个月的账单不会是零。值不值,就看那两个复访问题的答案。
我是吴师兄,下篇见。
内容效果不满意?点此反馈