从0打造编码Agent 实验 实验 ≈ 60 分钟 页内实验 ×4
A5 上下文:对话太长装不下怎么办? 实验课

实验课:八条真实运行 × 四家触发规则 × 几种压缩策略

长任务上下文回放
把本章的组件串到一起:六条真实运行的逐轮上下文与四家触发规则同屏,和逐轮 trace 回放联动;同一段真实历史换几种压缩策略;长任务 T3 / T3b 的真实回放:预算 8,000 下,分层压缩的前两层被真实的长测试输出推着触发,朴素截断只在最后一次请求之前截了一次;最后把你自己的 Claude Code 会话拖进来,用同一套视图看。页面加载时,每个移植都与原实现逐条自检。
🧪

本课要做的:用真实数据给本课项目的里程碑 M5 拍板——阈值定多少、用什么尺子量、先裁什么——并确认页面上的每个移植都和原实现一致。

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

数据从哪来

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

实验台一:回放 × 触发规则

上面是逐轮 trace 回放(A1 定型的共享组件),下面是本章的上下文视图,两者联动:在回放里换轮,下面的柱子跟着选中;在下面点柱子,回放跳到那一轮。

逐轮回放(可切换运行) 请求模型回复工具调用工具结果harness 注入

数据:本课项目 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)。

实验台二:同一段历史,换压缩策略

同一段真实历史,换几种压缩策略 自检中…
压缩之前:12 条消息(pd-v2-truncate-s1,第 6 轮请求前的真实历史)
压缩之后

口径: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 补录了:

逐轮回放:T3 分层 / T3b 朴素截断(下拉框切换) 请求模型回复工具调用工具结果harness 注入

数据:本课项目 miniagent 的真实运行(src/data/project/agent-t3-longtask.json,2026-09-26 导出;模型 Qwen/Qwen3-8B-MLX-4bit,本机 mlx_lm.server)。token 数取自服务端 usage;思考截取约 600 字、正文与工具输出约 900 字(保留头尾)。耗时受本机同时运行的其他任务影响,只能粗看;token 与轮数才可比。

实验台三:长任务的逐轮上下文 × 四家触发规则(窗口初始设为预算 8,000) 自检中…

口径:本课项目 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 自己跑测试)通过通过
轮数129
合计 token(服务端 usage)55,274,另有摘要 2,34548,112
压缩L1 × 4、L2 × 2、L3 × 0截断 × 1
第一次压缩第 5 轮请求之前第 9 轮请求之前(最后一次请求)
单次请求最大(服务端 prompt_tokens)5,7286,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 轮」的说明。

这组对照能读到哪一步

两条运行各录一次 。前 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 的观察。这三条就是本章里程碑的验收材料(见章首页)。

读数时注意

上下文管理的演进:每一代都被上一代的某个真数字逼出来
  1. A1 的循环(所有 harness 的底层事实:模型 API 不记得上一轮)
    解决了实现最简单,模型每轮都看得到全部经过。
    卡在pd-v2-cap20-s1 从第 1 轮的 947 token 涨到第 11 轮的 2,618;第 1 轮里工具定义就占 72%,到最后一轮工具结果占 44%。只增不减,总有撞墙的一轮。
  2. 本课项目 miniagent 的 truncate(整轮丢,保留 system 与任务)、learn-claude-code s08 的 snip_compact(中段存档)
    解决了零成本,还能保证 tool 调用与结果不被拆散(按整轮丢)。
    卡在pd-v2-truncate-s1 截断 2 次共丢 3 轮,读文件那一轮也在其中;第 7 轮模型凭记忆改,那一行变成 + 1 + 1,验收测试退出码 1。
  3. 先便宜 先裁工具输出,不花一次推理 你在这里
    CoreCoder Layer 1、learn-claude-code micro_compact、OpenCode prune(默认关)、Anthropic tool result clearing
    解决了工具结果是最占地方、也最可再生的一块;裁它不用调模型。
    卡在按位置裁,不懂内容:分层运行里被裁过的 0 份测试输出,0 份丢了唯一关键的 AssertionError 行。另一头,本任务最长的工具输出 1,302 字符,CoreCoder 的 1,500 字符门槛一次都没碰到。
  4. CoreCoder 50 / 70 / 90%、Pi 窗口 − 16,384、OpenCode 窗口 − min(20,000, 最大输出)、Codex 90%
    解决了阈值定在撞墙之前,给压缩本身和下一次回复留出余地。
    卡在本地估算在中文上偏低:一段真实中文 118 token(录制模型的 tokenizer),CoreCoder v0.4.0 估成 60,按「字符数 ÷ 4」估成 45。只靠本地估算触发,就会晚。
  5. CoreCoder Layer 2、本课项目 miniagent L2
    解决了比截断保留更多:旧轮次不是消失,而是被压成一段话。
    卡在pd-v2-layered-s1 的 17 份摘要里,写下确切报错的 0 份,编出不存在的「timezone」的 16 份;第 10 轮模型把改对的一处改了回去。21 轮、73,168 token(摘要另花 11,832)才完成,是同组不压缩成功运行最少的 4.3 倍。
  6. 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.」→ 下一章:会话存在哪、明天还记得吗。
旁支 context reset:清空上下文,换一个新 agent 接手 作者已弃用
Anthropic《Harness design for long-running application development》(2026-03-24)
为治「context anxiety」(模型以为快满了就草草收尾)而设;同一篇文章写明:换到 Opus 4.5 后这种行为基本消失,作者把 reset 整个拿掉了。

数字口径:本课项目 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 讲记忆与会话。