总结
两阶段检索系统
- 第一阶段用元数据做粗筛,成本极低无模型调用
- 第二阶段用sonnet模型做精选成本可控,只有被选中的记忆才会读取全文注入上下文
- 每条记忆上限 4kb → 单轮对话注入上限 5条/20kb → 会话累计注入上限60kb → 压缩重置
去重机制:选择模型 在 选择 记忆文件 之前 就已经把 选择过的文件 在 候选区 中 过滤掉了
过期警告:告诉这只是某个时间点的状态,不是当前事实
后校验:硬性兜底,避免幻觉
最简单的实现
- 每条记忆的开头写一行描述
- 检索时先把所有描述拼成一个列表
- 连同用户的问题一起发给一个便宜的模型,让它选出最相关的几条,然后只读这几条的全文,注入上下文
不需要向量数据库、不需要embendding、不需要复杂的相似度计算,只需要一个结构化的摘要+模型调用
一句话总结
让模型只看需要的几条信息,先摘要后全文,用中等成本模型做选择,给旧记忆加过期警告
场景
假设存储了200条长期记忆,当用户提出一个问题,如何从长期记忆中获取信息
- 全塞进去 token 成本爆炸,大部分无关记忆干扰推理,不塞进去记忆就白存了,这就是记忆检索需要解决的问题
Active Recall
ClaudeCode中的检索机制叫 Active Recall 它不是向量检索、不是关键词匹配 而是 多阶段的筛选流程
本质上是一个两阶段检索系统
- 第一阶段用元数据做粗筛,成本极低无模型调用
- 第二阶段用sonnet模型做精选成本可控,只有被选中的记忆才会读取全文注入上下文
检索成本从 所有记忆的全文大小 → 所有记忆的摘要大小 → 5条最相关记忆的全文大小
流程
1. 扫描
- 系统递归扫描记忆目录,找到所有的记忆文件上限200个,超过阈值直接忽略
- 但是它不读取文件全文,只读取文件前30行
- 为什么是前30行
- ClaudeCode的记忆文件有一个固定的格式,开头是一个 frontmatter 的元数据
- 包含记忆的名字、一句话描述、类型
- 这一步的成本不高,200个文件一个30行,也就是6000行文本,没有任何模型调用
2. 摘要
- 扫描完成后,系统将所有记忆的元数据拼成一个清单每条一行
- 格式为: 类型、名字、最后修改时间、一句话描述
- 例如:反馈类型,测试规范,2026.05.01,集成测试使用真实数据库而不是 mock
- 两百条记忆的目录也就几千token 成本不算高
3. 选择
- 系统将用户当前的输入和记忆目录一起发送给一个模型,让它从中选出最多五条跟当前对话最相关的记忆(使用的不是主模型,是sonnet)
- 提示词思路
- 只选择确定有用的,不确定就不选,宁可少选,也不要多选,没有相关记忆返回空列表(避免误导主模型的推理)
- 工具记忆的特殊处理:如果用户当前正在使用某些工具,而且没有出现问题任务顺利,则让模型不要选这些工具的使用文档记忆,因为模型已经在正常使用了
- 但如果记忆中包含的是这些工具的已知问题或者注意事项那么还是添加上记忆
- 后校验
- 根据模型返回的文件名,系统逐一检查这些文件是否真的存在候选集中(确定性兜底,避免幻觉)
4. 注入
- 选中的记忆文件系统读取它们的完整内容,然后作为 system prompt 注入到当前对话的上下文中
- 每个文件大小上限 4kb 超过的部分截断+附上指针+提示 而不是直接丢弃
- 告诉模型记忆被截断了,如果需要完整内容根据指针去读取文件
- 每条记忆上限 4kb → 单轮注入上限 5条/20kb → 会话累计注入上限60kb → 压缩重置
- 超过 60kb的上限 active recall 会停止工作不再注入记忆,上下文压缩后计数器归零
- 为什么要有上限
- 如果长会话中每轮都注入5条,及时轮下来上下文都被记忆填满了
- 当会话触发上下文压缩,之前注入的记忆和旧的内容会被一起压缩,并且重置累计计数器
- 去重复机制
- 同一个会话中已经注入过的记忆文件不会被重复注入,选择模型在选择之前就已经把选择过的文件在候选区中过滤掉了
过期警告
打破虚假的权威感,告诉这只是某个时间点的状态,不是当前事实
- 每条被注入的记忆都会附带一个时间标签,如果记忆是最近写的不加任何警告
- 如果记忆超过两天,系统会自动附加一段提醒
- 这段记忆是多久之前的时间点快照不是实时状态,关于代码行为或文件位置的描述可能已经过时,在基于这条记忆做决策之前,请先验证代码的实际状态
- 为什么要这么做
- 源码注释中写道,根据用户反馈,旧的记忆里提到了某个函数在某个文件的某一行,模型就直接引用了这个信息,但实际上那个函数早就被重构到了其他地方,而且因为记忆中给出了具体的文件和行号模型的回答看起来非常权威,用户很难察觉
