agent 自己学到的东西,什么时候写进 prompt?
本课一问:干活的过程中,agent 自己学到了一件以后用得上的事(比如这个仓库怎么跑测试)。它记下来之后,什么时候写进 prompt——马上,还是等一等?
这一课先不碰:几十份技能说明怎么放(第 3 课);会话本身怎么存(第 4 课);一整次任务的完整账单(A8)。这里的「代价」只看前缀缓存命中多少。
从已有出发
上一课的 system prompt 是三块:基础 prompt、AGENTS.md、# Memory from previous sessions。前两块是人写的;第三块是 agent 自己写的。miniagent 给模型一个 remember(note) 工具:把一句话追加到仓库之外的 MEMORY.md,上限 2,200 字符(照 Hermes 的默认值)。T10 第一天的任务要求「学到后面的会话用得上的东西,就用 remember 记下来」,两组各记了一条(模型原话):
- 冻结组,第 8 轮:
Date parsing uses day-first European format (DD/MM/YYYY) and 1-based month indices for datetime.date.
- 刷新组,第 6 轮:
Run tests with 'python -m unittest -v test_dateparse.py'
两组同一份代码、同一个 seed,前 4 轮的请求逐 token 相同;第 5 轮起分岔,所以记下的内容不一样。本课只比一件事:记下之后,system prompt 什么时候变。
先自己设计:写进去,马上刷新
最直接的想法:记忆一写进磁盘,下一轮就重建 system prompt,本会话马上用得上。miniagent 的 refresh 模式就是这么做的:
# miniagent/memory.py(本课项目)
def system_prompt(self):
memory = self.snapshot if self.mode == "frozen" else self.store.read()
return build_system_prompt(self.base, self.agents_md, memory)
# miniagent/loop.py:每轮调模型前调用一次 system_prompt_provider;
# 返回值与当前 system 不同就替换,并在会话树里记一条 system_refresh
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
刷新组逐轮的服务端 usage(实测):
| 轮 | prompt | 命中缓存 | 这一轮之前 |
|---|---|---|---|
| 1 | 1,152 | 293 | 会话开始 |
| 2 | 1,199 | 1,151 | |
| 3 | 1,642 | 1,198 | |
| 4 | 1,806 | 1,641 | |
| 5 | 2,073 | 1,805 | |
| 6 | 2,220 | 2,072 | 模型调用 remember,写入落盘 |
| 7 | 2,300 | 294 | 重建 system(带上刚写的记忆) |
刷新本身没错,错在时机:system prompt 在每个请求的最前面,它一变,后面所有东西都要重算。冻结快照把「写」和「生效」拆开:写入立刻落盘,不会丢;但本会话的 system 一个字节都不动,等下一个会话开始时再拍新的快照。下面把这件事拆开看。
拆开看:前缀断在哪、为什么后面全部重算
口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 在本机录制的 T10(2026-09-26)。每轮请求用录制模型的 tokenizer 与 chat template 重算成 token 序列,长度与服务端 usage 逐轮一致;「实测」是服务端返回的 cached_tokens(本机服务只报告命中数,不报告写入量)。Anthropic 规则与价目表依据官方文档实现(prompt-caching 与 pricing 页,2026-09-26 抓取);把本地模型的 token 数按云端价目表计费只是换算,真实的 Claude 请求 token 数会不同。耗时不作比较。
试三件事:① 默认选中的是刷新组第 7 轮:读下方详情,前缀断在哪一块?断点前后的原文是什么?绿色(推算)和蓝色虚线(实测)对得上吗?② 切到「反事实」,把 k 从 1 拖到 8:每次刷新多重算多少 token?为什么越晚刷新,多重算的越多?③ 把缓存规则换成 Anthropic、模型选 Claude Opus 5.5,刷新那一轮读到了多少?勾上「显式断点」再看;最后把模型换成 Claude Haiku 4.5——为什么一个 token 都没缓存?
三件事,数字都来自上面的真实请求:
- 断点在记忆块。刷新前后两个请求的前 287 个 token 完全一样(基础 prompt 加 AGENTS.md),接着是记忆块的标题,从记忆的内容开始不同:与上一轮的公共前缀是 293 个 token。实测命中 294,多出的 1 个来自本机缓存里更早的一个请求——另一组第二天的请求,它的记忆恰好也以
-开头。本机缓存跨会话共享,组件按「最近 10 个请求里的最长前缀」推算,算出的正是 294。 - 后面的全部重算,连工具定义也算。录制模型的 chat template 把 769 token 的工具定义写在 system 文本之后,记忆块一变,工具定义也落在断点后面。Anthropic 的顺序是 tools → system → messages,工具定义排在最前面;但它只在断点处写缓存条目,自动缓存的断点在最后一块,所以刷新那一轮照样一个也读不到——除非在稳定段末尾再放一个显式断点(上面的勾选框),才能读回工具定义、基础 prompt 与 AGENTS.md。
- 越晚刷新越贵。断点之后的历史越长,重算的越多;而会话越往后,缓存本来越值钱。
写入是立刻的。冻结组第 8 轮 remember 的工具结果原文:Saved to memory (104/2200 chars). It will be available in future sessions.
——磁盘上已经有了,只是本会话的 system 不改。下面的泳道里,冻结组从第 9 列开始「磁盘上」就是新内容,「上下文里」要到 day2 才追上。
不会。它的内置记忆是两个带字符上限的 Markdown 文件,在会话开始时整份注入一次,没有 embedding、没有检索——这一条在调研的对抗验证里以 0 票支持被否决,是本课程的事实红线。Hermes 另外留了外部记忆 provider 的插件接口(默认关闭),那是另一回事,见岔路。
数据:本课项目 miniagent 的 T10 录制(Qwen3-8B-MLX-4bit,本机 mlx_lm.server,2026-09-26)。「上下文里」取自每一轮真实请求的 system 段;「磁盘上」由录制里 remember 工具写入 MEMORY.md 的时刻推出(写入在那一轮执行工具时落盘)。token 数用录制模型的 tokenizer 重算;缓存命中是服务端报告的 cached_tokens。
改进:冻结快照
Hermes:写进磁盘立刻,写进 prompt 等下一个会话
上限本身在配置默认值里:
仓库的贡献准则把这件事写成了第一原则,并点名唯一的例外:
压缩时发生什么?调研档案把它列为待核实(U19),本章在 pin 源码里核到了:压缩之后使 system 失效,并从磁盘重新加载记忆;如果重新加载的记忆与缓存里那份 prompt 已经一模一样(会话中没写过),就原样保留缓存的 prompt,本地后端的 KV 前缀不丢:
Claude Code 的 auto memory 在官方文档里也是同一个形状:MEMORY.md 的前 200 行或 25KB 在每个会话开始时加载;压缩之后「Re-injected from disk」(《Explore the context window》的存活表,2026-09-26 读取)。
放在哪:会变的放最后
冻结解决「什么时候变」,布局解决「变了损失多少」。Hermes 把 system prompt 切成三段,最可能变的放最后:
miniagent 的布局「基础 → AGENTS.md → 记忆」同样把会变的放在后面;但录制模型的 chat template 又把工具定义接在 system 文本之后,于是记忆一变,工具定义也跟着重算——上面第 2 条。布局要看到最终渲染出来的 token 序列,不能只看自己拼的字符串。
冻结组的真实结果
- 会话内:冻结组 day1 的 8 个后续请求,8 个把上一轮的请求整个读回了缓存:7 个正好差 1 个 token(模板的换行被分词器合并,组件详情里有解释),剩下 1 个连这 1 个也没差(上一轮回复撞到输出上限、是一条空消息)。
- 跨会话:第二天第一个请求就带着那条记忆(录制核对:是);新快照让 system 从记忆块起变了,所以第二天第 1 轮只命中 293——和刷新组那一轮断在同一个位置,但这是每个新会话都要付一次的代价,而不是会话中途。
对照:按相关性挑选,而不是整份注入
learn-claude-code 的 s09 走另一条路:每次用户发话,先让模型从记忆目录里挑出相关的几条(最多 5 条、合计 20,000 字符)再拼进 system。它仍然不是向量检索:
挑选发生在 agent_loop 一开始(s09_memory/code.py:720-722):一次用户发话之内 system 不变,换一次用户发话就可能变。它和冻结快照的差别在「谁决定加载哪些」,这个问题留给岔路课。
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 让 agent 学到的东西跨会话留下来,又不在会话中途打穿前缀缓存 |
| ② 为什么冻结更省? | 前缀缓存要求逐 token 相同;system 在最前面,会话中途改它,后面全部失效 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:最长公共前缀在第一处不同的 token 处终止。本页的推算与服务端实测 50 轮全部一致;Anthropic 规则见官方文档「What invalidates the cache」:system 变化使 system 与 messages 两层缓存失效。 出处待补 |
| ③ 实验上呢? | Hermes 冻结快照、「不是检索系统」都是满票结论并有源码直证 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条调研档案 HP #4(冻结快照)满票、#5(自动检索)0-3 否决;压缩时重新加载记忆的行为本章在 pin 上核实(conversation_compression.py:3209-3226)。 出处Hermes pin 源码 · 满票(HP #4-#5) ↗需联网 ;T10 两组的缓存数字是单次运行实测 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:Qwen3-8B、本机 mlx_lm.server、每组一次运行。缓存规则是确定的,所以数字可复现;按云端价目表的费用是换算,不是账单。 出处待补 |
演进地图:你在这里
- 起点 把规矩写进第一条消息最省事的做法:任务说明里顺手写上「不许改测试文件」「日期用 DD/MM/YYYY」解决了不用改 harness,当场就生效。卡在A5 的分层压缩运行里,规矩写在任务说明中,17 份真实摘要只有 2 份还带着「不许改测试文件」;换一个新会话更彻底:T10 day2 的第一个请求只有 2 条消息、1,138 token,来自前一天对话的是 0 条。
- 人写的规矩 指令文件:每个请求都重新注入Claude Code 的 CLAUDE.md、Codex / OpenCode 的 AGENTS.md(Codex 从项目根到 cwd 逐级叠加、32 KiB 上限;OpenCode 同名文件 first-match)、Hermes 三段 prompt 的 context 段解决了会话开始从磁盘读一次,拼进每个请求;压缩之后从磁盘重新注入。T10 day2 的每一轮都带着 AGENTS.md 块(87 token),验收「按 AGENTS.md 的格式输出」:通过。卡在它只装人写好的规矩。agent 自己在干活时学到的东西(比如这个仓库怎么跑测试)写到哪、什么时候生效?
- 本课项目 miniagent 的 refresh 模式(T10 刷新组)解决了本会话马上就能用上刚学到的东西。卡在T10 刷新组 day1 第 7 轮:上一轮命中 2,072 token,刷新后这一轮 2,300 token 只命中 294——前缀在记忆块处断开,后面的工具定义与全部历史都要重算。
- Hermes MEMORY.md(2,200 字符)/ USER.md(1,375 字符);miniagent frozen 模式;仓库准则「Per-conversation prompt caching is sacred」解决了会话内 system 字节不变:T10 冻结组 day1 的 8 个后续请求里,8 个把上一轮的请求整个读回了缓存(最多差 1 个模板 token);day2 开新会话,第一个请求就带着前一天写的记忆。卡在流程性知识越攒越多:Hermes 随包的 77 个 SKILL.md 全文合计 215,494 token,是录制模型窗口(40,960)的 5.3 倍,全塞进快照放不下。
- Pi(name + description + location)、Hermes(描述截到 60 字符)、Claude Code(单条 1,536 字符上限)、agentskills.io 规范;分发:MCP Skills Extension(SEP-2640,2026-09-13 Final)解决了同样 77 个 skill,Hermes 实际注入的索引 2,209 token;模型觉得相关时再读正文。卡在知识的问题解决了,会话本身怎么存?想回到三轮前换条路:T10 从 e0005 分叉,线性列表只能删掉它后面的 15 条(原来那条成功的路径)。
- Pi(id / parentId,一个 JSONL,/tree /fork /clone)、Claude Code(/branch、--fork-session)、Codex rollout(append-only,压缩写成 CompactedItem)、miniagent session.py解决了两条分支并存,压缩有损、历史无损;分叉的第一个请求与原分支共享前缀:T10 fork 第 1 轮 1,236 token,实测命中 1,197。卡在一个会话、一个窗口的边界还在:一件事大到一个 agent 的上下文装不下、或需要别人把关时 → 下一章 A7 子 agent。
数字口径:A5 的摘要来自 pd-v2-layered-s1 的真实运行(a05-context.json);其余来自本课项目 miniagent 的 T10 两天会话(Qwen3-8B-MLX-4bit,本机 mlx_lm.server,2026-09-26),缓存命中是服务端报告的 cached_tokens;skill 的 token 数由录制模型的 tokenizer 在 Hermes pin 的 77 个 SKILL.md 上现算(a06-memory.json,2026-09-26)。
一句话
先停在这里:记住「写」与「生效」是两件事;前缀缓存在第一个不同的 token 处断开;会变的内容放在最终 token 序列的最后。
再深一层:走岔路:文件记忆和 RAG 谁更好?——一个还没有证据的问题。
下一个缺口
流程性知识(skills)也这样全量注入吗? → 第 3 课:几十个技能说明,全塞进 prompt 吗?
记忆是几条短句,整份注入没问题(Hermes 的上限才 2,200 字符)。可「怎么做代码评审」「怎么调试」这类流程性知识一份就是几千 token,几十份放在一起呢?
你可能会问
- 「第二天第一个请求怎么就命中了 293?」 本机服务的前缀缓存跨会话共享(最近 10 个请求),前一天的请求还在缓存里,第二天的 system 在记忆块之前和它一样。云端缓存按 TTL 过期(Anthropic 5 分钟或 1 小时,OpenAI GPT-5.6 起 30 分钟),隔夜的第一个请求要重新写入——那也是冻结快照唯一需要付的一次。
- 「刷新组第二天的第 1 轮为什么命中更多?」 因为它第一天在第 7 轮已经把带新记忆的 system 发过一次,本机缓存里有这个前缀。这是本机 LRU 缓存的巧合,不是刷新的好处:云端隔夜就过期了。
- 「为什么 Hermes 的时间戳只写到日期?」 源码注释说得很直白:精确到分钟,每次重建 system 都会让前缀缓存失效;需要精确时间,模型可以调工具去查(
system_prompt.py:545-551)。A8 会用真实的请求体现算「分钟级时间戳」让前缀在哪断开。