实验课:八条真实运行 × 四家触发规则 × 几种压缩策略
本课要做的:用真实数据给本课项目的里程碑 M5 拍板——阈值定多少、用什么尺子量、先裁什么——并确认页面上的每个移植都和原实现一致。
需要的时间:约 45 分钟。全部计算在浏览器里完成,不联网,也不需要跑模型。
数据从哪来
- 真实运行:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit(revision
{M.trace_model.revision.slice(0, 7)},mlx_lm.server,本机 Apple M5 Max)录制的 6 条运行,共 60 轮、29 次压缩:pd-v2-cap20-s1(none)、pd-v2-unlimited-s1(none)、pd-truncate-s1(truncate,预算 2400)、pd-layered-s1(layered,预算 2400)、pd-v2-truncate-s1(truncate,预算 1800)、pd-v2-layered-s1(layered,预算 1800)。 - 长任务 T3 / T3b(实验台三):项目批次 2 补录的两条运行 t3-layered-s1、t3b-truncate-s1,任务
parse_date_verbose,预算 8,000;逐轮 trace 来自项目导出src/data/project/agent-t3-longtask.json,逐轮上下文来自同一套重放(a05-context.json的 t3 块)。 - 每轮发出的消息:重放得到。按 trace 顺序追加消息,每轮之前调用项目自己的压缩函数(摘要交回录制的原文),重放出的压缩事件与录制逐条一致;每轮的 prompt token 用录制模型的 chat template 重算,与服务端 usage 全部相等(6 条运行全部核对通过;T3 / T3b:7 次压缩、21 轮,全部核对通过)。
- 移植的算法与自检:CoreCoder v0.4.0 / v0.7.0 的估算与阈值、本课项目的截断与分层,期望值由 Python 加载原函数跑出;Pi 的 calculateContextTokens / shouldCompact / findCutPoint 与 OpenCode 的 usable / isOverflow,由 bun 在 pin 源码的逐字切片上跑出;Codex 的 auto_compact_token_limit 与 approx_token_count,由 rustc 编译 pin 源码切片跑出(
{M.script},2026-09-26)。
实验台一:回放 × 触发规则
上面是逐轮 trace 回放(A1 定型的共享组件),下面是本章的上下文视图,两者联动:在回放里换轮,下面的柱子跟着选中;在下面点柱子,回放跳到那一轮。
数据:本课项目 miniagent 的真实运行(src/data/project/agent-traces.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
把本机 Claude Code 的会话记录(~/.claude/projects/…/*.jsonl)拖到这里,或点下面的按钮选择文件。文件只在你的浏览器里解析,不会上传到任何地方;关掉页面就没了。每条 assistant 消息的 input_tokens + cache_read_input_tokens + cache_creation_input_tokens 就是那一轮真正发给模型的上下文大小。
口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit(窗口 40,960,单次回复上限 4,096)本机录制,2026-09-26。柱高 = 服务端返回的 usage.prompt_tokens;分块的 token 数用录制模型的 tokenizer 与 chat template 重算,每一轮都与服务端的数逐条一致;「模板」是聊天模板加的角色标记等特殊 token。显示的文本已脱敏(本机路径换成占位符)。触发规则:CoreCoder、Pi、OpenCode、Codex 从 pin 源码移植,期望值由原实现在 pin 副本上跑出(gen_a05_context.py / .ts,2026-09-26)。
实验台二:同一段历史,换压缩策略
口径:token 按本课项目 miniagent 的计数方式(录制模型 tokenizer 数消息内容,每条另加 4,再加工具定义 609),与它录制时的压缩事件逐条一致;显示的文本已脱敏。朴素的两种删法是本页写的对照算法;截断 / L1 / 分层移植自项目源码 miniagent/context.py(项目自己的实现,不是 pin 仓库);CoreCoder Layer 1 移植自 he-yufeng/CoreCoder@v0.4.0 corecoder/context.py:69-95(MIT)。
实验台三:长任务 T3 / T3b
前两个实验台的运行,预算都是 1,800 / 2,400:预算 1,800 时,第 1 轮请求之前的估算就占了预算的 48%(工具定义、system 与任务),压缩几乎每轮都在发生,推着它走的主要是「预算太小」。设计卡计划的长任务,项目在批次 2 补录了:
- 任务
parse_date_verbose:和贯穿任务同样的两处 bug,但dateparse.py带 DEBUG 日志,-v跑一次测试的原始输出约 1 万字符;bash 工具截到 8,000 字符(保留头尾),回填进历史的结果是 8,030 字符 = 3,949 token。 - 预算 8,000:仍是人为调低的(录制模型真实窗口 40,960 的约五分之一),阈值 4,000 / 5,600 / 7,200。第 1 轮请求之前的估算 890,只占预算的 11%:这一次,压缩是被真实的长工具输出推着触发的。
- 两条运行:T3 分层(t3-layered-s1)与 T3b 朴素截断(t3b-truncate-s1),同一模型、同一 seed、同一预算,各录一次。
数据:本课项目 miniagent 的真实运行(src/data/project/agent-t3-longtask.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。
口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit(窗口 40,960,单次回复上限 4,096)本机录制,2026-09-26。柱高 = 服务端返回的 usage.prompt_tokens;分块的 token 数用录制模型的 tokenizer 与 chat template 重算,每一轮都与服务端的数逐条一致;「模板」是聊天模板加的角色标记等特殊 token。显示的文本已脱敏(本机路径换成占位符)。触发规则:CoreCoder、Pi、OpenCode、Codex 从 pin 源码移植,期望值由原实现在 pin 副本上跑出(gen_a05_context.py / .ts,2026-09-26)。
试三件事:① 回放停在第 5 轮:第 4 轮的测试输出进来后,估算 5,945 越过 70% 线(5,600)。L1 先找较早轮次里超过 1,500 字符的工具输出,一份也没有——唯一的长输出在最近 2 轮里受保护,第 2 轮读到的测试文件只有 1,479 字符。于是直接走到 L2,把最早 2 轮写成摘要(这次摘要本身花了 800 token),降到 5,618。② 走到第 6、7 轮:L1 开始裁。第 6 轮裁的是第 3 轮读到的 dateparse.py(1,910 → 227 字符,只剩头 3 行和尾 3 行,MONTHS.index 那一行不在其中);第 7 轮请求之前估算到了 9,201,已经越过预算本身,L1 把第 4 轮的测试输出裁掉,回到 5,422。③ 切到 T3b,走到第 9 轮:截断只看 8,000 这一条线,前 8 轮估算最高 6,242,一次都没动;第 8 轮(测试已经通过)的输出进来后,估算 10,283,从最早的轮次整轮丢掉 5 轮(第 1–5 轮:列目录、读测试文件、读源码、第一次修改、第一次跑测试),降到 5,137。
| T3 分层 | T3b 朴素截断 | |
|---|---|---|
| 验收(harness 自己跑测试) | 通过 | 通过 |
| 轮数 | 12 | 9 |
| 合计 token(服务端 usage) | 55,274,另有摘要 2,345 | 48,112 |
| 压缩 | L1 × 4、L2 × 2、L3 × 0 | 截断 × 1 |
| 第一次压缩 | 第 5 轮请求之前 | 第 9 轮请求之前(最后一次请求) |
| 单次请求最大(服务端 prompt_tokens) | 5,728 | 6,410 |
分层:前两层自然触发了,代价也是真的
裁掉源码之后,凭印象写 old_string。第 8 轮模型发出 edit_file,old_string 是 month = month_table.index(month.lower())——文件里根本没有这一行,工具报 old_string not found。那时它手上的 dateparse.py 只剩头尾 6 行;第 6 轮测试输出的 DEBUG 日志里倒是有一句 month table=[…],month_table 很可能是从这里来的(推断)。第 9 轮它重新读了文件,第 10 轮改对,第 11 轮测试通过。第 6 轮裁这份源码省了 505 token,换来多跑两轮。
裁剩的测试输出,让摘要认错了失败的测试。第 12 轮(测试已经通过、准备收尾)L1 与 L2 先后触发,8 轮被写成摘要。摘要说失败的是 test_many_iso_dates;实际上两次失败的测试输出里,失败的都是 test_parse_date。原因在 L1:裁过的测试输出只剩头 3 行和尾 3 行——头一行恰好是 test_many_iso_dates 开始跑时的日志,尾巴是 FAILED (failures=1);「FAIL: test_parse_date」和 AssertionError 都在被裁掉的中间(重放核对:2 份裁剪版里都没有这两行),摘要模型拿到的只有裁剪版。模型最后给用户的总结照搬了这个说法(「The initial issue with test_many_iso_dates was resolved…」)。这和第 2 课在预算 1,800 下看到的是同一件事:按行裁,关键的一行正好在中间。
L3 没有触发。越过 90% 线(7,200)的请求只有第 7 轮之前那一次(9,201),L1 先把它压到 5,422,轮不到 L3。「三层都自然触发」只做到了两层:L3 的真实样本仍只有预算 1,800 的 pd-v2-layered-s1(2 次,见第 4 课)。
朴素截断:在这个长任务上只动了最后一次
截断只看预算本身(8,000),不像分层从 50% 就开始动手。T3b 前 8 轮一次没截,源码和测试输出一直都在,第 6 轮的 +1 修改一次就对上了 old_string。第 9 轮之前截掉 5 轮时,测试已经通过,只剩写总结:截断没有伤到修复,伤到的是交代——被截掉的轮次里有第一处修改(交换日与月),总结只讲了 +1 这一处,还说原始失败是「月份下标从 0 开始」。分层那条的总结也没提交换日月,所以这不全是截断造成的;但截断之后,这件事在上下文里已经没有痕迹,只剩一句「已删 5 轮」的说明。
这组对照能读到哪一步
两条运行各录一次 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测:Qwen3-8B、同一 seed、各一次运行,预算 8,000(人为调低);只说明现象存在,不说明比例。 出处待补 。前 4 次请求一模一样(prompt_tokens 都是 971、1,014、1,499、2,095),回复从第 1 轮起就不同(生成 796 与 971 token),第 4 轮一条先跑测试、一条先改代码——之后的差别不全是压缩策略造成的。能说的是这些现象确实会发生:裁掉源码后凭印象写 old_string;裁剩的测试输出让摘要认错了失败的测试;截断让交代里少了一处修改。不能说「朴素截断更好」。和预算 1,800 那一对放在一起看:预算紧时,截断那条失败(+ 1 + 1)、分层那条 21 轮才成功;预算放宽到 8,000,两条都成功,截断那条还少用了 9,507 token——它只在超过整份预算时才动手,而这一次它只动了最后一次。
任务
任务 1 · 定阈值:在实验台一选 pd-v2-cap20-s1(不压缩的那条),把窗口设成你打算给 miniagent 的预算。四家各在哪一轮触发?哪一家在这个任务里根本不会在中途触发,为什么?把你选的规则(百分比、余量或组合)和它在这条曲线上的触发轮次写下来。
任务 2 · 找最大的一块:在同一条运行里点最后一轮,找出最大的单个工具结果;再切到 pd-v2-layered-s1 的第 6 轮,看它被 L1 裁成了什么。你的 L1 要不要为这类输出(测试报错、diff)开例外?
任务 3 · 看压缩的账:在实验台二把预算从 2,400 拖到 1,000,分别用「整轮截断」和「分层 L1 → L3」,记下每个预算下「丢了什么」清单里第一次出现 ✗ 的那一项。
任务 4 · 你自己的会话:把一份你自己的 Claude Code 会话 JSONL 拖进实验台一的虚线框。找出上下文最大的一轮,看它离你用的模型窗口还有多远;如果会话里有压缩标记,看压缩前后各是多少。
任务 5 · 读自检:两个实验台右上角各有一个「与原实现一致」的计数。它们各自对照的是什么?哪几项不是对 pin 源码,而是对本课项目自己的实现?
任务 6 · 长任务:在实验台三点 t3b-truncate-s1(它到最后一次请求之前才压缩,曲线最接近「原样重发」),窗口初始就是预算 8,000:四家各在哪一轮触发?再点「40,960 录制模型的窗口」,还有谁触发?最后回答:你在任务 2 定的 L1 例外规则,能不能保住第 6 轮被裁掉的 dateparse.py,以及第 12 轮那份摘要需要的报错行?
把结果写进项目:在 04-实践项目/DECISIONS.md 记下 ① 你为 miniagent 选的阈值规则、参数和任务 1 的依据;② 估算器的选择,以及第 3 课中文段落上的误差;③ L1 的例外规则(如果有)和任务 3 的观察。这三条就是本章里程碑的验收材料(见章首页)。
读数时注意
- 「撞墙」都撞在人为设的预算上。录制模型的真实窗口是 40,960,贯穿任务到不了;实验台一、二的预算 1,800 / 2,400 是为了让压缩真的发生,预算 1,800 时固定开销就占了 48%。实验台三的长任务预算 8,000,固定开销只占 11%,压缩由真实的长测试输出推着触发;但它仍是调低过的预算,设回 40,960 就不会压缩。三层里的最后一层(L3)至今只在预算 1,800 的那条运行里出现过。小窗口下,Pi 与 OpenCode 的固定余量会比窗口本身还大,需要按比例调参数再比。
- token 有三种口径:服务端 usage(柱高)、miniagent 自己的估算(不数模板 token,紫色圆点与压缩事件里的数)、各家移植算法自己的读数(小图)。比较时别混用。
- Codex 的「新增条目估算」是近似:它按条目序列化后的字节数 ÷ 4 估,本页用 function_call_output 的最小 JSON 形状近似序列化结果,字段与 Rust 结构体不逐字相同 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条approx_token_count 本身与 rustc 编译的原函数逐条一致;近似只在「条目长什么样」这一步。 出处待补 。
- Pi 的时机:小图里 Pi 那一格同时写了「阈值线第一次被越过的轮次」和「它实际检查的时刻(任务跑完后)」,两者不同。
- 单一模型、单一任务:这些运行只能说明现象存在,不能说明比例;换前沿模型、换更长的任务,曲线形状会变。耗时本页不作比较(本机有其他任务同时占用,只能粗看),token 与轮数可比。
- A1 的循环(所有 harness 的底层事实:模型 API 不记得上一轮)解决了实现最简单,模型每轮都看得到全部经过。卡在pd-v2-cap20-s1 从第 1 轮的 947 token 涨到第 11 轮的 2,618;第 1 轮里工具定义就占 72%,到最后一轮工具结果占 44%。只增不减,总有撞墙的一轮。
- 朴素 超了就截掉最早的本课项目 miniagent 的 truncate(整轮丢,保留 system 与任务)、learn-claude-code s08 的 snip_compact(中段存档)解决了零成本,还能保证 tool 调用与结果不被拆散(按整轮丢)。卡在pd-v2-truncate-s1 截断 2 次共丢 3 轮,读文件那一轮也在其中;第 7 轮模型凭记忆改,那一行变成
+ 1 + 1,验收测试退出码 1。 - CoreCoder Layer 1、learn-claude-code micro_compact、OpenCode prune(默认关)、Anthropic tool result clearing解决了工具结果是最占地方、也最可再生的一块;裁它不用调模型。卡在按位置裁,不懂内容:分层运行里被裁过的 0 份测试输出,0 份丢了唯一关键的 AssertionError 行。另一头,本任务最长的工具输出 1,302 字符,CoreCoder 的 1,500 字符门槛一次都没碰到。
- CoreCoder 50 / 70 / 90%、Pi 窗口 − 16,384、OpenCode 窗口 − min(20,000, 最大输出)、Codex 90%解决了阈值定在撞墙之前,给压缩本身和下一次回复留出余地。卡在本地估算在中文上偏低:一段真实中文 118 token(录制模型的 tokenizer),CoreCoder v0.4.0 估成 60,按「字符数 ÷ 4」估成 45。只靠本地估算触发,就会晚。
- 再花钱 让模型把旧轮次写成摘要CoreCoder Layer 2、本课项目 miniagent L2解决了比截断保留更多:旧轮次不是消失,而是被压成一段话。卡在pd-v2-layered-s1 的 17 份摘要里,写下确切报错的 0 份,编出不存在的「timezone」的 16 份;第 10 轮模型把改对的一处改了回去。21 轮、73,168 token(摘要另花 11,832)才完成,是同组不压缩成功运行最少的 4.3 倍。
- Codex(交接摘要 + 保留用户原话)、Pi(结构化模板 + 文件清单)、OpenCode(五节模板)解决了把「必须留下什么」写进模板:目标、约束、进度、确切报错、下一步、相关文件;完整历史留在磁盘上。卡在仍然有损。Codex 每次压缩后提醒用户:「Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.」→ 下一章:会话存在哪、明天还记得吗。
数字口径:本课项目 miniagent 用 Qwen/Qwen3-8B-MLX-4bit 本机录制的真实运行(2026-09-26),每轮发出的消息由重放得到并与录制的压缩事件、服务端 usage 逐轮核对一致;估算器的输出由 CoreCoder、Pi 的原实现在 pin 源码上跑出(a05-context.json,2026-09-26)。耗时不作比较(本机有其他任务同时占用)。
这一课的结论
同一条真实曲线上,「什么时候触发」由三件事共同决定:尺子(本地估算还是 API 回报)、线(百分比还是余量)、检查时机(每轮还是任务结束)。压缩策略则决定触发之后丢什么:整轮截断丢掉读过的文件,按行裁剪丢掉关键的报错,摘要在输入残缺时会编造。长任务 T3 / T3b 又补了一条:线画得越早,丢东西也越早——分层从 50% 起就开始裁,裁掉的源码与报错行各自在后面付了代价;截断只在整份预算用完时才动手。用你自己的 trace 把这三个选择定下来,写进 DECISIONS.md。
下一章:压缩让一个会话能接着干。明天开一个新会话,它从零开始——项目的规矩、昨天学到的东西,怎么带过去?A6 讲记忆与会话。