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

压缩成摘要、整个重置,还是写交接文档?

摘要、重置还是交接
本课项目让模型「把上面的工作记录总结一下」,真实运行里写了 17 份摘要:没有一份写下确切报错,16 份编出了不存在的 timezone,第 10 轮模型把改对的一处改了回去。拆开看:摘要模型的输入早被机械裁剪截掉了关键行,它在空白处编造,编出来的又被下一份继承。现代做法把压缩当成给接手者写交接文档:Codex、Pi、OpenCode 的模板各自点名要留什么;另一条路 context reset,提出它的作者已经弃用。
❓

本课一问:第 2 课的「先便宜后贵」里,最贵的一步是让模型把旧轮次写成摘要。摘要该怎么写?或者干脆不写,整个清空、换一个新的 agent 接手?

这一课先不碰:跨会话的记忆(A6);把活分给子 agent、让它在自己的窗口里读(A7)。

从已有出发

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

前三课定下了「什么时候压」和「先裁什么」。剩下的一块要花钱:把旧轮次换成一段摘要。本课项目 miniagent 的 L2 就是这么做的,它在真实运行 pd-v2-layered-s1(预算 1,800,21 轮,最后成功)里一共写了 17 份摘要,全部录了下来。

先自己设计:让模型「总结一下上面的对话」

miniagent 给摘要模型的指令只有两句话,已经比「总结一下」讲究:要写任务、看过的文件、测试结果与确切的报错、做过的修改、还剩什么,不许编造。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案让模型把旧轮次总结成一段话(不超过 150 词)

指令原文(miniagent/context.py:176-184):

You compress the work log of an AI coding agent so it can continue with less context. Be factual and concise. Summarize the work log above in at most 150 words. Include: the task; files inspected and what they contain that matters; test results and exact error messages; edits already made; what remains to be done. Do not invent anything.

第 10 轮请求里带着的那份摘要(第 10 轮请求之前写成,原文):

The task was to fix the failing `test_parse_date` in `test_dateparse.py`. Files inspected included `dateparse.py` (core parsing logic) and `test_dateparse.py` (test cases). The test failure indicated an issue with parsing specific date formats, likely due to incorrect format strings or missing timezone handling. Previous edits focused on adjusting format patterns in `dateparse.py` to match expected input. Tests were run after each edit, showing partial success but still failing on edge cases. Remaining work involves identifying and correcting the exact format or timezone mismatch causing the final test failure. The next step is to inspect the exact error message from the latest test run to pinpoint the remaining issue.

拆开看:摘要之后还剩什么

摘要之后还剩什么:pd-v2-layered-s1 的 21 轮

口径:本课项目 miniagent 用 Qwen3-8B 本机录制(2026-09-26),预算 1,800,L1 / L2 / L3 阈值为预算的 50 / 70 / 90%。每轮发出的消息由重放得到,与录制的 26 次压缩事件逐条一致。「摘要模型看得到什么」= 在它收到的那段输入文本里查关键原文(项目的 render_for_summary 会把每条消息截到 800 字符)。清单的判定是浏览器里现跑的正则,点「展开」可以看命中处。四份提示词只比较「要求了什么」,不代表用它们写出来的摘要会更好——那需要用同一模型重跑,本章没有这份录制。

🎯

试三件事:① 默认选中第 10 轮(时间轴上的 ↺):对照左右两栏,找出 dateparse.py 的源码在不在右边;再读模型这一轮的思考原句;② 点第 4 轮:这一轮之前先做了 L2、又做了 L3——右栏里有没有摘要?③ 往后点几轮带 L2 的,看清单里的 ⚠ timezone 从哪一轮开始出现,再看「摘要模型收到的输入里」那一行:它编出来的,还是从上一份抄来的?

拆开之后,失败不是一个原因,而是三个叠在一起:

  1. 垃圾进。17 次摘要,摘要模型收到的输入里含 AssertionError 那一行的:0 次;含第二处 bug 那一行(MONTHS.index(name.lower()))的:0 次。测试输出在进入摘要之前已经被 L1 裁成了头尾各 3 行(第 2 课),源码则被项目的 render_for_summary 按每条 800 字符截断,那一行在第 800 个字符之后。指令要它写「确切的报错」,它根本没见过。
  2. 空白处编造,然后传下去。第一份提到 timezone 的摘要,输入里没有这个词;后面的摘要把上一份摘要当输入,就把它当成事实继承了(岔路 B1 讲的「上下文中毒」)。
  3. 刚写好的摘要被当成最旧的一轮丢掉。第 4 轮之前,L2 把 1 轮写成摘要(1,580 → 1,727,反而更长),紧接着 L3 从头丢了 2 轮——摘要正是被丢的那一轮。这次摘要花了 523 token,什么也没留下。

第 10 轮的上下文里有「日月对调」的 diff 和失败的测试,却没有源码,摘要只说「之前的修改集中在调整格式」。模型在思考里说得很直白:

💬
But without seeing the regex pattern, it's difficult to proceed. So, the solution is to revert the code back to the original assignment of month, day, year. —— pd-v2-layered-s1 第 10 轮,模型的思考(英文原文)

于是它把改对的一处改了回去。直到第 13 轮重新读了文件,第 15 轮才再改对。

真实运行 pd-v2-layered-s1:逐轮回放(上下文层可展开摘要全文) 请求模型回复工具调用工具结果harness 注入

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

改进一:把压缩写成交接文档

三家生产级 harness 的压缩提示词,出发点都是「接手的是另一个模型,它只看得到你写下的东西」。

Codex:全文 9 行,开宗明义是交接。

prompt.md codex@8cabf5a:codex-rs/prompts/templates/compact/prompt.md:1-9需联网 Apache-2.0
1You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
2
3Include:
4- Current progress and key decisions made
5- Important context, constraints, or user preferences
6- What remains to be done (clear next steps)
7- Any critical data, examples, or references needed to continue
8
9Be concise, structured, and focused on helping the next LLM seamlessly continue the work.
👆 带下划线的行可以点:点哪行,讲哪行。

光有提示词还不够。Codex 压缩后重建的历史里,用户说过的原话是原样放回去的(从最新往前,最多 20,000 token),摘要只替换其余部分:

compact.rs codex@8cabf5a:codex-rs/core/src/compact.rs:629-660需联网 Apache-2.0
629fn build_compacted_history_with_limit(
630 mut history: Vec<ResponseItem>,
631 user_messages: &[CompactedUserMessage],
632 summary_text: &str,
633 max_tokens: usize,
634) -> Vec<ResponseItem> {
635 let mut selected_messages: Vec<CompactedUserMessage> = Vec::new();
636 if max_tokens > 0 {
637 let mut remaining = max_tokens;
638 for message in user_messages.iter().rev() {
639 if remaining == 0 {
640 break;
641 }
642 let tokens = approx_token_count(&message.message);
643 if tokens <= remaining {
644 selected_messages.push(message.clone());
645 remaining = remaining.saturating_sub(tokens);
646 } else {
647 let truncated =
648 truncate_text(&message.message, TruncationPolicy::Tokens(remaining));
649 selected_messages.push(CompactedUserMessage {
650 message: truncated,
651 internal_chat_message_metadata_passthrough: message
652 .internal_chat_message_metadata_passthrough
653 .clone(),
654 });
655 break;
656 }
657 }
658 selected_messages.reverse();
659 }
660
👆 带下划线的行可以点:点哪行,讲哪行。
如果这条运行用 Codex 的方式压缩:新的历史由哪几块组成(compact.rs:342-379)
+ + +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
Codex 每次压缩后还会提醒用户:「Long threads and multiple compactions can cause the model to be less accurate」——压缩本身就有损,次数越多越差(compact.rs:383-385)。

Pi:结构化模板,六节固定标题,并要求「Preserve exact file paths, function names, and error messages」。另外两个机制正对着我们的失败:序列化给摘要模型时,每条工具结果保留前 2,000 字符(我们的测试输出不到 900 字符、源码 1,302 字符,都会完整进入);读过和改过的文件清单跨多次压缩累积,不靠摘要模型记。

compaction.ts pi@31b513e3:packages/coding-agent/src/core/compaction/compaction.ts:467-498需联网 MIT 最新版已变化 · 勘误表 P4
467const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
468
469Use this EXACT format:
470
471## Goal
472[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
473
474## Constraints & Preferences
475- [Any constraints, preferences, or requirements mentioned by user]
476- [Or "(none)" if none were mentioned]
477
478## Progress
479### Done
480- [x] [Completed tasks/changes]
481
482### In Progress
483- [ ] [Current work]
484
485### Blocked
486- [Issues preventing progress, if any]
487
488## Key Decisions
489- **[Decision]**: [Brief rationale]
490
491## Next Steps
4921. [Ordered list of what should happen next]
493
494## Critical Context
495- [Any data, examples, or references needed to continue]
496- [Or "(none)" if not applicable]
497
498Keep each section concise. Preserve exact file paths, function names, and error messages.`;
👆 带下划线的行可以点:点哪行,讲哪行。

Pi 的切点规则上一课已经见过:从最新往回累加到 keepRecentTokens,切点只落在 user 或 assistant 消息上。像我们这种「一句任务跑到底」的会话,整个会话只有一个 turn,任何切点都落在 turn 中间:在 pin 源码上跑 findCutPoint,keepRecentTokens 取 1,000 时切点在第 7 条、isSplitTurn = true。Pi 对这种情况单独处理:为切点之前的那半个 turn 另写一份摘要,再与历史摘要合并(docs/compaction.md:81-107)。默认的 20,000 下,我们的整段会话都在保留范围内,切点在第 0 条——什么都不用摘。

OpenCode:五节模板(Objective / Important Details / Work State / Next Move / Relevant Files),规则里写着要保留确切的文件路径、符号、命令、报错字符串;再次压缩时,把上一份摘要与新对话合并,冲突时以新对话为准。四份提示词各自要求了什么,在上面组件的最下方逐项对照。

⚠
常见误解一:「压缩之后,前面的历史就没了。」

被压掉的只是「下一轮发给模型的那一份」。本课项目的 trace 里每条消息、每份摘要都在;Pi 把压缩写成会话文件里的一条记录,Codex 的 CompactedItem 可以回溯。完整历史在磁盘上,只是不在窗口里——A6 会用它来讲会话树与「回到三轮前换条路」。

改进二:清空重来,换一个 agent 接手(context reset)

另一条路线不在原地压缩:到了某个点,把上下文整个清空,启动一个新的 agent,只交给它一份交接文档。Anthropic 在《Harness design for long-running application development》(2026-03-24)里讲过它的动机:有些模型在自以为快到上限时会提前收尾(「context anxiety」),原地压缩给不了它一个干净的开始。

同一篇文章紧接着写道:换到 Opus 4.5 之后,这种行为基本消失,作者把 context reset 从 harness 里整个拿掉了。并由此给出一条通则:harness 里的每个组件都编码了一个「模型自己做不到」的假设,这些假设值得反复检验,因为模型一进步它们就可能过期。

⚠
常见误解二:「context reset 是长任务的必备组件。」

它的教学价值恰恰在于被弃用:它解决的是某一代模型的某个行为。加这类组件之前先确认你用的模型还有这个问题;否则就是在为不存在的问题付成本。

改进三:让模型自己决定开新窗口

Codex 有一个 new_context 工具,描述只有一句:开一个新的上下文窗口,不清除、不重置环境状态。模型调用它,harness 就开新窗口,不写摘要。它挂在实验特性 token_budget 下(features/src/lib.rs:1396-1400,开发中、默认关闭;注册处 spec_plan.rs:1060-1063):

new_context_window_spec.rs codex@8cabf5a:codex-rs/core/src/tools/handlers/new_context_window_spec.rs:1-17需联网 Apache-2.0
1use codex_tools::JsonSchema;
2use codex_tools::ResponsesApiTool;
3use codex_tools::ToolSpec;
4use std::collections::BTreeMap;
5
6pub(crate) const NEW_CONTEXT_WINDOW_TOOL_NAME: &str = "new_context";
7
8pub fn create_new_context_window_tool() -> ToolSpec {
9 ToolSpec::Function(ResponsesApiTool {
10 name: NEW_CONTEXT_WINDOW_TOOL_NAME.to_string(),
11 description: "Start a new context window. Does not clear, reset, or otherwise affect environment state.".to_string(),
12 strict: false,
13 defer_loading: None,
14 parameters: JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(false.into())),
15 output_schema: None,
16 })
17}
👆 带下划线的行可以点:点哪行,讲哪行。

三种做法的共同点:压缩写的不是「发生过什么」,而是「接手的人要继续干,需要知道什么」。

证据三问

问本课的回答
① 它解决什么问题?旧轮次必须让出位置时,把「继续干活需要的东西」留下来
② 为什么交接文档式更可能有效?模板点名要留的项(目标、约束、进度、确切报错、下一步、文件),正是我们的摘要漏掉的;用户原话与文件清单不经过摘要模型
③ 实验上呢?两条路线(原地压缩、context reset + 交接)有官方一手材料,且 reset 已被作者弃用 ;本课项目的 17 份摘要是单次运行的实测

演进地图:你在这里

上下文管理的演进:每一代都被上一代的某个真数字逼出来
  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)。耗时不作比较(本机有其他任务同时占用)。

一句话

📌
压缩是写交接文档,不是写对话概要。
🚦

先停在这里:记住三个失败原因(输入已被截断、空白处编造并被继承、摘要被 L3 丢掉)和三种做法(交接文档式压缩、context reset、让模型开新窗口)。
再深一层:去实验课,在六条真实运行上把本章所有组件串起来。

下一个缺口

会话压完了,明天开新会话,它还记得这个项目的规矩吗? → 下一章 A6:记忆与会话。

一个会话里,压缩让它能接着干;可「不许改测试文件」这种项目规矩,如果只活在第一轮的任务说明里,压几次就可能没了(本课的摘要只有 2 份写到它)。A6 讲把规矩放进每轮都会重新注入的地方,以及磁盘上那份完整历史怎么用。

你可能会问