Skip to content

给 Agent 装一个会学习的记忆

番外隐喻:一条溪流冲刷过河床,大量碎屑泥沙随水流走,少数几粒发光的金粒沉淀在河床缝隙里——噪声海洋里的稀疏信号

这篇番外讲一个具体的系统:给 coding agent 装一个跨会话的长期记忆。

写它的原因和上一篇番外不同。那篇是一次理论探索的摊开——"我借信息论的视角重新理解 agent 工程,但不知道对不对"。这一篇是一次工程落地的复盘——"我把一个记忆系统从想法做到了能跑,过程中设计被真实数据推翻了好几次,最后留下的这套工程设计长这样"。

所以这篇的主体是工程设计,不是故事。但工程设计的每一个决策背后都有来由,我会把"一开始怎么想的、跑真实数据发现了什么、最后改成什么"穿插在对应环节里。


引言 · 一个未被开采的矿

用过一段时间 coding agent 的人,大概都有过一种隐隐的浪费感。

你和 agent 的每一次 session 里,都沉淀着东西:你纠正过它的错误、你否决过它的方案、你让它重做的理由、你最终满意的那个版本。这些东西在当下是有价值的——它们让这一次的产出变好了。但 session 一结束,它们就蒸发了。下一次开一个新会话,agent 又是一张白纸,你会把同样的纠正再讲一遍。

我和 agent 的对话记录,是一个未被开采的经验矿。

这件事让人想做点什么,但第一反应往往是错的。大多数人的第一反应是"做个记忆库"——把对话存下来,建个索引,下次检索回来。这是数据库思维。

我做完之后发现,这套东西最容易掉进去的坑,恰恰就是做成数据库——存得很漂亮,检索很快,结构很完整,但 agent 的行为没有任何改变。经验躺在库里睡大觉,下一次该犯的错一个没少。

所以这篇番外的核心判断,也是整套设计的第一性原理,先说清楚:

这不是数据库问题,是自改进问题。 一条记忆如果从不改变任何一次后续行为,就是死数据,存得再精巧也没用。唯一价值是让 agent 下一次表现得不一样。

这句话决定了后面所有的工程设计——从"记什么"到"怎么存"到"怎么取",每一个环节都要被"它能不能改变下一次行为"这把尺子量一遍。


第一节 · 先看全景:从注入端倒推

在展开每一层之前,先看这张全景图,知道整套东西最终拼成一个什么。

记忆系统四步闭环:淘金 · 炼金 · 用金 · 会学习
记忆系统四步闭环:淘金 · 炼金 · 用金 · 会学习 外圈实线是数据流(顺时针闭环),内圈虚线是倒推设计链 —— 从末端「注入」反推每一层该产出什么 经验记忆库 SQLite · 结构化经验 含置信度 + 生命周期 淘金 识别有信号的对话段 话题切分 → 规则粗筛 确定性、便宜、无幻觉 炼金 蒸馏成可复用经验 LLM 精炼 → 四判据校验 宁缺毋滥,全满足才存 用金 在对的时刻注入给 agent 三层检索 → 三种注入模式 情境优先,语义兜底 会学习 根据反馈更新置信度 reinforce ↑ / violate ↓ 不是只堆积,是会淘汰 筛出的候选段 经验入库 检索命中 用户行为反馈 更新置信度 下一轮 session ↑ 倒推设计链 从「agent 能消化什么」倒推每一层 前三步(淘金·炼金·用金)是记忆系统都该有的;第四步(会学习)是让它和「数据库」不一样的地方 一条记忆如果从不改变任何一次后续行为,就是死数据 —— 唯一验证标准是「agent 下一次表现得不一样」
图 1 | 外圈实线是数据流(顺时针闭环):对话流 → 淘金 → 炼金 → 入库 → 检索 → 注入 → 反馈 → 更新置信度。前三步是记忆系统都该有的,第四步(会学习)是它和「数据库」不一样的地方

这张图里藏着整套设计最反常识的一个决策——设计顺序是倒着来的。

大多数人设计记忆系统,正着设计:先想记什么,再想怎么存,最后想怎么取。这个顺序很自然,但它容易导向一个结果——你在"记什么"和"怎么存"上花了很多功夫,造出一个存得很漂亮的库,最后到"怎么取"的时候才发现,取出来的东西 agent 根本用不上。

我的做法是反过来,从末端倒推:

注入(agent 下一次需要什么、能消化什么)
  ← 检索(那得检索出什么形态的东西)
    ← 蒸馏(那蒸馏时要产出什么)
      ← 记什么(那原始对话里什么值得进入蒸馏)

先想清楚末端——agent 在 UserPromptSubmit 那一刻能吃下什么。是几条简洁的行动建议,还是一大段背景?能消化多少条?什么语气?想清楚这些,才知道检索要返回什么;知道检索要返回什么,才知道蒸馏要产出什么;知道蒸馏要产出什么,才知道对话里什么东西值得进入蒸馏。

倒推设计逼着你先回答"它怎么用",再回答"它存什么"。 这是对抗"数据库思维"的第一个工程手段。

四步闭环的每一步,我用四个动词来概括:

  • 淘金——从对话汪洋里识别有信号的段落(谁在纠正、谁在否决、谁在表达偏好)
  • 炼金——把信号蒸馏成可复用的经验规则("如果 X 情境,则 Y 行为")
  • 用金——在对的时刻把对的记忆注入给 agent(情境命中时)
  • 会学习——根据 agent 之后的反馈更新置信度,对的越来越强,错的被淘汰

前三步,任何记忆系统都该有。第四步是关键——它让这套东西从"堆积经验"变成"学习经验"。 一条经验被注入后,用户下一次的反应反过来更新它。符合的越来越强,违反的逐渐淘汰。库是活的,不是只进不出的仓库。

接下来的几节,按倒推设计的链条展开。先讲最核心也最难的蒸馏,再讲检索,再讲注入,最后讲让它会学习的反馈闭环,和怎么把这一切工程化落地。


第二节 · 蒸馏:规则管确定性,LLM 管模糊性

蒸馏要回答的问题是:这段对话里,有没有值得记的东西?

难点在于,原始对话是噪声的海洋,经验是稀疏的信号。一百条消息里可能只有三四条有蒸馏价值。你怎么把这几条捞出来?

借鉴 AlphaGeometry 的结构

我借鉴了 AlphaGeometry 的结构思想:规则引擎在原空间做确定性筛选,LLM 在筛选后的片段上做软理解和合成。 规则管确定性,LLM 管模糊性。

翻译到记忆系统里,蒸馏拆成两步:

原始对话流(一个话题片段)

[规则粗筛]  用固定词表匹配"有信号的对话段"
    ↓        确定性、便宜、无幻觉——只决定"值不值得进 LLM"
[LLM 精炼]  理解语境,提取"如果X情境则Y行为"的经验
    ↓        智能,处理微妙——产出结构化经验
[四判据校验] 可复现? 有信号? 可行动? 用户特有?
    ↓        全满足才存,否则 skip
经验记忆库

规则只做一件事:决定"这段对话值不值得进入 LLM 精炼"。 它不做任何理解,只做匹配。这是防幻觉的第一道关——规则不会有幻觉,它只是个模式匹配器。

规则粗筛:三级信号词表

哪些是对话里"有信号"的段落?我按信号强度分了三级:

级别匹配模式判断
强信号明确纠正("不对/错了/应该是")、否决重做("重写/算了/换方法")、偏好声明("我偏好/记住/以后都")几乎一定值得蒸馏
中信号追问不理解("没明白/为什么")、修改意图("想法变了")可能值得,进 LLM 判断
弱信号正向反馈("对了/很好/就是这样")不单独蒸馏,只作置信度信号

匹配有优先级——命中强信号就不看低级别。这套词表是从真实对话里一条条攒出来的,不是拍脑袋编的。

LLM 精炼与四判据

规则筛出的候选段,交给 LLM 去理解、提取规则、标注触发情境。但 LLM 不是自由发挥的——判据锁死在 system prompt 里,不允许模型自己决定"什么值得记"

  1. 可复现:描述的是一个会再次发生的情境,不是一次性事件
  2. 有信号:依据是用户真实的反馈动作,不是 agent 的自我猜测
  3. 可行动:能翻译成 agent 下次能执行的具体行为
  4. 用户特有:这是这个特定用户的偏好,不是任何开发者都会有的通用常识

全满足才提取,否则输出 skip。宁缺毋滥。

穿插 · 四判据不是一开始就设计对的

这里要诚实交代一个过程:判据不是一开始就长这样的。

最初只有三条(可复现 / 有信号 / 可行动)。跑了一批真实 session 之后,发现经验库被"通用常识"污染了——比如蒸馏出"用户不理解时用类比解释"这种,任何开发者都该这么做的常识,也进了库。这种东西存进来,除了占地方,还会在检索时挤掉真正有用的个人经验。

加了第四条"用户特有"——换一个用户也成立的,就不是个人经验,跳过。判据从三条收紧到四条之后,拿 5 个 session 实测:从 27 条筛到 17 条,被筛掉的都是通用常识,留下的全是"换个人就不成立"的硬核偏好。

判据不是设计出来的,是跑数据后收紧出来的。 这个过程本身也说明:防污染的防线,往往要等你真的看到了污染长什么样,才知道该在哪里加挡板。

穿插 · session ≠ 蒸馏单位

另一个被真实数据推翻的预设,是蒸馏的触发单位。

设计的时候,很自然地假设"session 结束 = 蒸馏时机"。一个会话结束了,把里面的东西捞一遍。听起来天经地义。

跑了真实数据才发现完全不是这样。我有一个 session 跨了 3 天、5 个完全不同的话题——从一篇视觉模型的论文,聊到跨域知识转移,又跳到蜂群算法,再聊到一个电子宠物的想法,最后才聊到记忆系统本身。182 条消息,前半段和后半段毫无关系。session 和任务之间根本没有对齐关系——一个 session 可能永不"结束",只是被你遗忘。

于是修正:蒸馏的触发单位不是 session,是 topic segment(话题片段)。 话题切换被检测到时,对刚结束的那个片段触发蒸馏。一个 session = N 个话题片段,每个片段独立蒸馏。

这个修正带来的连锁后果后面会讲到——它直接影响了 hook 的触发逻辑。


第三节 · 检索:三层结构,情境优先

注入依赖检索。检索的核心问题是:当 agent 遇到一个新情境,怎么把过去相关的经验找出来?

最直觉的答案是语义检索——拿当前 prompt 做 embedding,找库里语义相近的经验。但我很快发现它不够用。

一个反例:经验是"编辑 package.json 前先备份,因为 agent 可能打乱依赖顺序"。下次在另一个项目里编辑 package.json,语境和"上次出错的项目"差异很大,纯语义检索可能命中率很低。但如果有一条"情境"信息——"当前正在编辑 package.json"——就能稳稳命中。

所以检索不能只靠语义相似度,要引入情境触发条件。我做了一个三层结构:

检索依据作用
情境层当前任务类型、宿主 agent 类型优先级最高,确定性命中
语义层当前 prompt 的语义(embedding)情境命中不足时的主力
时效层衰减、最近性、置信度过滤——太老又没被复用的降权

情境层优先级最高,因为它最确定。语义层兜底,处理情境覆盖不到的情况。时效层做过滤,保证注入的不是过时的经验。

穿插 · 情境维度从四个收窄到两个

设计的时候,我给情境层设想了四个维度:宿主类型、任务类型、当前工具、当前文件。听起来很合理——知道 agent 正在用什么工具、改什么文件,检索当然更精准。

跑了真实 session 数据,发现一个根本性的时序矛盾:检索发生在 UserPromptSubmit——用户刚提交 prompt,agent 还没开始用工具。 这个时刻没有"当前工具"——上一轮的工具调用已经结束了,下一轮的还没开始。能拿到工具信息的是 PreToolUse hook,但那个时点不做检索,也没法把结果注入给已经提交的 prompt。

这不是"最后一公里没走完",是这条路本身不通。

于是修正:情境层真正能用的维度从四个收窄到两个(宿主类型 + 任务类型)。工具和文件这两个维度没有扔掉,降级为经验的"元信息"——它们标注"这条经验是在什么情境下产生的",帮助理解适用范围,但不参与检索匹配。

这个修正又一次印证了一个规律:设计阶段再合理的设想,也要用真实运行去验。 四个维度的设想在纸面上毫无破绽,是真实数据告诉你哪条路不通。


第四节 · 注入:三种模式叠加

检索到的经验,怎么送到 agent 面前?注入是记忆和 agent 行为之间的唯一接口。我做了三种模式叠加:

  • 模式 A(启动时静态注入):SessionStart 时,注入少量高置信度、跨项目通用的经验(比如用户偏好)。量要小,因为 context 有上限。
  • 模式 B(按 prompt 语义检索注入):UserPromptSubmit 时,检索和当前 prompt 相关的几条经验注入。这是主力通道,大多数经验通过这条路径抵达 agent。
  • 模式 C(agent 主动调用):agent 遇到不确定的事,主动调用一个 MCP 工具去查记忆。这条路径的优势是——agent 自己最清楚什么时候需要查记忆。

三种模式不是互斥的,是叠加的。高置信度的普适经验走 A,情境相关的走 B,需要深度的走 C。

注入内容的真实形态

这里有一个工程上很关键的细节:这三种模式最终都走同一个字段——additionalContext。理解它在 agent 内部的真实形态,是设计"注入什么内容"的前提。

它不是拼接进 user prompt,也不是 system prompt 的一部分。它被包装成一条 meta 消息,包在 <system-reminder> 标签里,和真正的用户消息并列但独立。模型把它当作"系统提醒",不是用户的真实输入,默认注意力权重低于真实 prompt。

这决定了注入内容的设计:不能太弱(会被当噪声忽略),也不能太强(会喧宾夺主,干扰 agent 对真实任务的理解)。

我当前的选择是保持克制——平铺的"请参考"格式,每条经验包含 scope 标签、类型、规则、行动建议。为什么不做得更花哨(比如用全大写标签强调)?因为现在没有反馈数据告诉我哪种格式更有效。 在没有数据的时候调格式,是拍脑袋。正确的做法是先让反馈闭环跑起来,等能区分"经验没被注入"和"经验注入了但被忽略"的时候,再做格式实验。

这条经验我在第 11 章也撞见过——鲁棒性的"默认配置"分两层:能强制的用机制锁死,只能约定的先靠纪律等数据。注入格式属于后者。


第五节 · 会学习:反馈闭环

前面四步——淘金、炼金、用金——是任何记忆系统都该有的。这一步,是让这套东西和"数据库"产生本质区别的地方:让经验会学习,而不是只堆积。

闭环的逻辑是这样的:经验被注入给 agent 之后,用户的下一次反应,反过来更新这条经验。

经验被注入 → agent 行为(可能符合经验、可能偏离)

检测用户下一轮的反应
    ├── 用户行为符合注入的经验(正向)→ confidence +0.1,reinforce
    ├── 用户行为违反注入的经验(纠正)→ confidence -0.2,violate
    └── 长期未被触发                  → decay,权重衰减

confidence < 0.2 或 violate_count > 3 → prune(淘汰)

对的,经验越来越强,在检索时排序更靠前;错的,逐渐被淘汰,不再注入。库是活的,会自我净化。

归因怎么做

这里有个绕不开的工程问题:怎么判断用户"符合了"还是"违反了"一条经验?

我的做法是:注入经验之后,拿用户下一轮的 prompt 做 embedding,和"被注入经验的 action"算语义相似度。相似度高,说明用户的行为和经验一致,是 reinforce;如果检测到纠正信号词("不对/错了/重做"),说明用户违反了经验的指引,是 violate。

这个归因不完美——它有噪声,会误判。但它有一个关键优势:信号是硬的。 它依据的是用户真实的、可观测的行为(下一轮说了什么),不是 LLM 的自我评分。

这也是我说它"是最该先成熟的闭环"的原因。上一篇番外讲过一个困境——agent 的"有效性"很难衡量,因为反事实不存在(你观测不到"没有这个建议会怎样"),信号被噪声淹没。但记忆系统的反馈闭环不一样:它的闭环很短(下一个 session 就能验证),信号很硬(用户行为是可观测的事实)。所以,如果要找一个让 agent "自改进"的切入点,这里的闭环信号是最实在的。


第六节 · 工程化落地:从 hook 到常驻 daemon

前面五节讲的是引擎设计——蒸馏怎么设计、检索怎么设计、注入怎么设计。设计想清楚了,怎么落地是另一套工程。这一节讲落地。

跨宿主架构

第一个工程决策:记忆引擎不能绑死在某个 agent 宿主上。

                    ┌─────────────────┐
                    │   核心记忆引擎   │  ← 全部核心价值
                    │  (宿主无关)    │
                    │  · 蒸馏 pipeline │
                    │  · 检索引擎      │
                    │  · 置信度更新    │
                    │  · 经验库        │
                    └────────┬────────┘

              ┌──────────────┼──────────────┐
              ↓              ↓              ↓
         CC 适配层      ZCode 适配层     未来宿主
         · 读 jsonl     · 读 sqlite      · ...
         · CC hooks     · ZC hooks
         · CC MCP       · ZC MCP

核心引擎的输入是标准化的对话段,输出是标准化的经验记忆。它不依赖任何宿主。适配层只做三件事:从宿主读 session 数据、注册宿主的 hooks、注册宿主的 MCP server。换一个宿主,只加一层薄适配,引擎不动。

daemon 化的转向

第二个工程决策,是被实测逼出来的:业务逻辑放 hook 里,还是放常驻进程里?

最初设想是放 hook 里——每次 hook 触发,现场跑蒸馏、检索、注入。听起来很直接。实测发现三个问题撞在一起:

  1. hook 有 15 秒超时限制
  2. tsx 冷启动慢
  3. 反馈归因要调 embedding,是个网络往返

三个加起来,在 hook 里跑业务逻辑会超时。

于是转向常驻 daemon。架构变成:

hook(极薄客户端,~70 行)          daemon(常驻进程,持有全部业务逻辑)
  读 stdin                            · 蒸馏 pipeline
  → POST daemon                       · 检索引擎
  → 写 stdout                         · 置信度更新
                                      · 定时维护(衰减 + 淘汰)
                                      · 持有 DB / embedding / LLM 连接

hook 变成了极薄的客户端——读 stdin、POST 给 daemon、写 stdout,三步。所有业务逻辑进 daemon。daemon 持有数据库和外部连接的复用,还跑定时维护任务。

穿插 · Stop hook 的语义陷阱

daemon 化的转向里,还牵出一个被 hook 语义坑到的故事。

设计时假设 Stop hook = session 结束,可以拿来做兜底蒸馏——session 结束了,把没处理完的片段捞一遍。这在 Claude Code 里可能是对的。但 ZCode 的 Stop hook 不是这个语义——它是"每次 agent 回复结束",一个 session 有几十轮对话,Stop 会触发几十次。

如果按原始设计,每次 Stop 都全量兜底蒸馏,等于一个 session 里重跑几十遍。纯粹是浪费。

修正:Stop hook 改做水位线增量蒸馏——只处理上次水位线(上次处理到的最后一条消息时间戳)之后新闭合的片段。没有新闭合的片段就跳过,只更新水位线。全量兜底交给定期批量任务。

原始的"Stop hook 全量兜底蒸馏"方案废弃。这个废弃的过程也挺典型——同一种机制(Stop hook),在不同宿主里语义不同。设计阶段假设的语义,要拿到目标宿主上实测才知道对不对。

穿插 · 一个诚实的回退

daemon 化前后还做过一次更大的回退。

有一版我做了"双通道注入"——除了 UserPromptSubmit 的主注入通道,还想在 PreToolUse 时做一次情境注入(当时以为"当前工具"维度能用)。做了一天,第二天复盘发现:情境层命中率低的根因不在注入端,在蒸馏端——是因为蒸馏时没有把情境维度提准,不是注入时没传进去。

根因判断错了,方向就错了。果断 revert,把双通道注入整个回退。

方向错了就回退,不硬撑。 这件事和第 3 章讲的"可回滚才有真稳定"是一回事——这里回滚的不是部署版本,是设计方向。能回退的前提是:你承认上一版可能错了,愿意拿真实数据去验。


第七节 · 元架构:规则核心 + 泛化边缘

讲到这儿,你可能发现一个贯穿全系统的设计原则一直在浮现——哪些地方用规则锁死,哪些地方交给泛化。

这个原则的判定尺度很简单:一个判断出错了,代价高不高。

代价高的(漏掉真经验、污染经验库、错误注入),用规则锁死;代价低的(话题边界略偏一点、经验措辞不完美),交给泛化。

系统层规则核心(锁死)泛化边缘(可进化)
话题切分系统事件 + 固定切分词表语义突变 + 词表扩充
蒸馏粗筛强信号的固定匹配模式弱信号的模糊匹配
LLM 精炼四判据是硬约束具体提取方式交给 LLM
检索情境层用确定性维度语义层用 embedding
置信度更新规则固定(+0.1/-0.2/淘汰线)阈值可随使用调优

这个结构的好处是:行为可预测(规则锁住骨架),同时处理长尾(泛化兜底)。 更重要的是,泛化的"进化"本身是被规则约束的——词表可以扩充,但扩充方式是结构化的;LLM 可以提取经验,但必须过四判据。

这就是"泛化进化,但不失控"。

细心的读者会发现,这和第 11 章鲁棒性撞见的"强制默认 vs 约定默认"是同一个母题——代价高的判断用机制锁死,代价低的靠纪律。 鲁棒性如此,记忆系统也如此。好的工程架构,母题是相通的。


第八节 · 诚实的边界

沿用上一篇番外的姿态,我得把这套东西的边界交代清楚。

经验库还小。 现在只有四十多条经验。置信度更新的统计有效性——哪些经验该被淘汰、阈值设多少合适——还需要更长时间的积累才能判断。当前的 +0.1/-0.2/淘汰线 0.2 这些值,都是"v0 占位",是跑起来之后才能调的。

注入格式、检索阈值、衰减参数,都是待验证的。 我在这篇文章里讲的每一个具体数值,都不是经过严格验证的最优解,而是"先用默认值跑起来、靠反馈数据驱动迭代"的起点。它们可能对,可能不对,需要时间检验。

定位还在 L2 到 L3 之间。 如果按"自改进系统的成熟度"来分——L1 是会记录,L2 是会积累经验,L3 是会根据置信度学习。这套系统目前在 L2 到 L3。再往上,L4 是"meta-memory"——系统反思自己的记忆策略本身对不对(比如"我总是蒸馏这类经验,但它其实没用")。这一层我还没碰。

这套方法能不能推广,我不知道。 它在我自己的使用习惯下跑通了,但换一个使用模式完全不同的人——比如一个几乎不怎么纠正 agent、只让它做执行的用户——这套蒸馏流水线可能根本采不到足够信号。它的适用边界在哪,还需要更多人、更多场景去检验。


最后说一句这两篇番外的关系。

上一篇,我用信息论的视角去重新理解 agent 工程——哪些工程会被模型升级吃掉、agent 的有效性为什么难衡量。它是一次理论探索的摊开,对不对我不敢打包票。

这一篇,我落地了一个具体的系统——给 agent 装一个会学习的记忆。它是一次工程落地的复盘,设计被真实数据推翻了好几次,留下的这套工程设计是验过的,但参数还要继续调。

一篇偏理论探索,一篇偏工程落地。两篇番外,从两个方向逼近同一个问题——怎么让 agent 变得更好用,以及怎么知道它真的变好了。 前者给方向感,后者给一个可以摸到的实例。

两篇都还在路上。

回到正文

结语 —— 13 章正文讲"怎么不翻车",两篇番外一篇讲"怎么重新理解翻车",一篇讲"怎么让 agent 自己少翻车"。