从0打造编码Agent 实验 实验 ≈ 45 分钟 页内实验 ×4
A6 记忆与会话:明天它还记得吗? 实验课

实验课:两天会话——冻结 vs 刷新、第二天读到了什么、一次分叉

两天会话
T10 的全部数据在一页上:两组、两天、五次运行、50 个请求。磁盘与上下文的记忆泳道、每一轮的前缀缓存(实测与推算并排,可切换 Anthropic 规则算账)、两棵会话树(可以把你自己的会话文件拖进来)、77 个真实 skill 的注入方式。页面加载时,每个移植都与原实现或服务端实测逐条自检。
🧪

本课要做的:用 T10 的真实数据给本课项目的里程碑 M6 拍板——指令文件什么时候读、记忆什么时候生效、会话怎么存——并确认页面上的每个移植都与原实现一致。

需要的时间:约 45 分钟。全部计算在浏览器里完成,不联网,也不需要跑模型。

数据从哪来

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

实验台一:记忆在磁盘上,还是在上下文里

记忆注入泳道:磁盘上的版本 vs 这一轮上下文里的版本(T10 真实录制) 空有一条记忆磁盘 ≠ 上下文注入(会话开始 / system 刷新)

数据:本课项目 miniagent 的 T10 录制(Qwen3-8B-MLX-4bit,本机 mlx_lm.server,2026-09-26)。「上下文里」取自每一轮真实请求的 system 段;「磁盘上」由录制里 remember 工具写入 MEMORY.md 的时刻推出(写入在那一轮执行工具时落盘)。token 数用录制模型的 tokenizer 重算;缓存命中是服务端报告的 cached_tokens。

实验台二:每一轮的前缀缓存

前缀缓存:每一轮有多少 token 能从缓存里读 命中缓存写入缓存按原价 / 本机重算前缀断开处
构造输入 · 真算法假设第 轮调用 remember 后立即刷新 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 数会不同。耗时不作比较。

实验台三:会话树

冻结组的会话文件(可换成你自己的) systemuser调用工具回复工具结果会话起点system 刷新
换成你自己的会话文件

把本机 Claude Code(~/.claude/projects/…/*.jsonl,每条记录有 uuid / parentUuid)或 Pi(~/.pi/agent/sessions/…/*.jsonl,id / parentId)的会话文件拖到这里,或点按钮选择。只在你的浏览器里解析,不会上传;关掉页面就没了。

数据:本课项目 miniagent 的 T10 会话文件(traces/sessions/t10-frozen.jsonl,一个 JSONL,每条 entry 有 id / parent_id;2026-09-26 录制,已脱敏)。token 是这条内容本身的 token 数(录制模型 tokenizer,不含模板);带 usage 的回复另标出那一轮真实请求的 prompt 与缓存命中。

刷新组的会话文件(注意 system 刷新那个菱形) systemuser调用工具回复工具结果会话起点system 刷新

数据:本课项目 miniagent 的 T10 会话文件(traces/sessions/t10-refresh.jsonl,一个 JSONL,每条 entry 有 id / parent_id;2026-09-26 录制,已脱敏)。token 是这条内容本身的 token 数(录制模型 tokenizer,不含模板);带 usage 的回复另标出那一轮真实请求的 prompt 与缓存命中。

实验台四:77 个真实 skill

几十个技能说明怎么进 prompt:同一批真实 SKILL.md 的五种注入方式 尺子(模型窗口):
Hermes 随包的 77 个 skill:勾选「这一轮真的用到」的,它的正文加进「按需」那一条
截断试验台:把一段 description 交给三家的规则

口径:语料是 hermes-agent@56dc01d9 的 skills/ 目录(安装时由 skills_sync.py 拷进 ~/.hermes/skills/)。三种索引的原文都由 pin 源码的原函数排出:Hermes 用 build_skills_system_prompt(在临时 HERMES_HOME 里跑,macOS),Pi 用 formatSkillsForSystemPrompt(bun;location 按 Pi README 的约定目录构造)。token 用录制模型 Qwen/Qwen3-8B-MLX-4bit 的 tokenizer 数,正文按整个 SKILL.md 文件计;换成别的模型,绝对数会变,比例大体不变。规范上限据 agentskills.io 与 Claude Code skills 文档(2026-09-26 抓取)。

第二天验收了什么

第二天的验收脚本在 agent 看不到的地方,检查 format_date 存在、按 AGENTS.md 输出、测试全过、加了测试:

运行轮数记忆在第一个请求里format_date 存在按 AGENTS.md 输出测试全过验收
t10-frozen-day215是✓✓✓通过
t10-refresh-day212是✗✗✗没通过

刷新组第二天没通过,原因在代码:它写的格式串也是 %d/%m/%Y,但把 format_date 缩进写进了 parse_date 的函数体里,模块顶层没有这个函数,验收脚本判「不存在」,测试也就跟着失败。这是模型写代码的失误,和记忆何时生效无关;冻结组第二天反复发了多次一模一样的 edit(工具返回「old_string 与 new_string 相同」),也是同类问题——A9 会专门给失败分类。

任务

🎯

任务 1 · 看分叉时的记忆:实验台一默认选中 fork 的第 1 轮。为什么这一列「磁盘上」有记忆、「上下文里」却是空的?把原因写成一句话(提示:fork 沿用的是哪一刻的 system)。

任务 2 · 定生效时机:实验台二切到「反事实」,把 k 从 1 拖到 8,记下每个 k 下「比不刷新多重算」的 token 数;再换成 Anthropic 规则、Claude Opus 5.5,记下多花的输入费用百分比。写下你为 miniagent 选的生效时机(冻结、刷新,还是「只在压缩时刷新」),并引用这组数字。

任务 3 · 看布局的代价:同一个反事实下,勾上「显式断点」,读刷新那一轮读回了多少 token。对照本机规则下的断点位置,说明录制模型的 chat template 把工具定义放在 system 文本之后,对「记忆放在 system 最后」这个布局意味着什么。

任务 4 · 你自己的会话:把一份你自己的 Claude Code 会话文件拖进实验台三的虚线框。它有几个根、几个叶子?有没有分叉点?如果你用过 /rewind,分叉点在哪?

任务 5 · 读自检:四个实验台各有一行自检。哪几行对照的是服务端实测,哪几行对照的是 pin 源码的原函数,哪一行对照的是另一份独立实现?

📝

把结果写进项目:在 04-实践项目/DECISIONS.md 记下 ① 指令文件的读取时机(本项目:会话开始读一次);② 记忆的生效时机与任务 2 的数字;③ 会话存储选树的理由,以及树的代价(重建要走回根)。这三条是本章里程碑的验收材料(见章首页)。

读数时注意

记忆与会话的演进:每一代都被上一代的某个真数字逼出来
  1. 最省事的做法:任务说明里顺手写上「不许改测试文件」「日期用 DD/MM/YYYY」
    解决了不用改 harness,当场就生效。
    卡在A5 的分层压缩运行里,规矩写在任务说明中,17 份真实摘要只有 2 份还带着「不许改测试文件」;换一个新会话更彻底:T10 day2 的第一个请求只有 2 条消息、1,138 token,来自前一天对话的是 0 条。
  2. 人写的规矩 指令文件:每个请求都重新注入 你在这里
    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 自己在干活时学到的东西(比如这个仓库怎么跑测试)写到哪、什么时候生效?
  3. 本课项目 miniagent 的 refresh 模式(T10 刷新组)
    解决了本会话马上就能用上刚学到的东西。
    卡在T10 刷新组 day1 第 7 轮:上一轮命中 2,072 token,刷新后这一轮 2,300 token 只命中 294——前缀在记忆块处断开,后面的工具定义与全部历史都要重算。
  4. 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 倍,全塞进快照放不下。
  5. 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 条(原来那条成功的路径)。
  6. 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。
旁支 按相关性挑选记忆注入,或检索(RAG) 没有比较证据
learn-claude-code s09(每次用户发话先让模型从目录里挑相关记忆,最多 20,000 字符);Hermes 的外部记忆 provider 插件(默认关闭,每轮预取、只拼进当轮 user 消息)
和文件式冻结快照谁更好?档案里没有任何比较性证据——能讲清的只有设计维度:谁决定加载、什么时候注入、会不会打穿前缀缓存。

数字口径: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)。

这一课的结论

跨会话记住东西,靠的不是会话本身,而是三样放在磁盘上的东西:人写的指令文件、agent 写的记忆、按需读取的技能;它们什么时候进入 prompt,决定了前缀缓存能不能保住。会话本身存成树,换路不丢历史,分叉还能共享前缀。用你自己的数字把三个选择写进 DECISIONS.md。

下一章:到这里,一个 agent 能记住、能换路,但始终只有一个窗口。一件事大到一个窗口装不下、或者需要另一个 agent 把关时,怎么分工?A7 讲子 agent。