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

上下文为什么会满?每一轮到底发了多少?

上下文为什么会满
把一次真实运行的每一轮请求拆开:第 1 轮 947 个 token 里,对话只有任务那 52 个,工具定义占了 685;之后每一轮都在末尾多出模型写的调用和工具的结果,只增不减,到第 11 轮 2,618,其中工具结果成了最大的一块。你也可以把自己的 Claude Code 会话拖进来看同样的曲线。
❓

本课一问:循环转起来之后,上下文为什么会满?每一轮真正发给模型的,到底是哪些东西、各占多少?

这一课先不碰:满了之后怎么删、怎么压(第 2 课);什么时候触发(第 3 课);发出去的 token 花多少钱(A8)。

从已有出发

A1 的循环里有一个容易被略过的事实:模型 API 不记得上一轮。所以 harness 每一轮都要把 system prompt、工具定义、到目前为止的全部消息整份重发一遍;A2 的岔路 B1 用真实请求验证过,第 n+1 次请求的开头原封不动就是第 n 次请求。

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

这一课用的仍是贯穿任务的真实运行。选 pd-v2-cap20-s1:本课项目 miniagent 用本机的 Qwen3-8B 修 dateparse.py,不做任何压缩,11 轮后测试通过——它是六条运行里最长的一条「原样重发」,正好用来看上下文怎么自己长大。

先自己设计:不管它,每轮原样重发

最省事的做法是什么都不做:历史有多长就发多长,模型窗口够大就行。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案不管它:每轮把整段历史原样重发

每一轮新增的是「模型上一轮写的调用 + 工具返回的结果」,旧的一样不少。逐轮增量(服务端 usage 之差):+43、+272、+443、+88、+177、+271、+174、+41、+41、+121。录制模型的真实窗口是 40,960,这个短任务撞不上;把任务拉长、换成更大的仓库,只是撞墙的轮数往后推。

撞墙时服务端会直接拒绝请求。各家的报错长什么样,Pi 在源码里收集了一份(它要靠这些字符串认出「溢出」,再压缩重试):

overflow.ts pi@31b513e3:packages/ai/src/utils/overflow.ts:36-62需联网 MIT
36 */
37const OVERFLOW_PATTERNS = [
38 /prompt is too long/i, // Anthropic token overflow
39 /request_too_large/i, // Anthropic request byte-size overflow (HTTP 413)
40 /input is too long for requested model/i, // Amazon Bedrock
41 /exceeds the context window/i, // OpenAI (Completions & Responses API)
42 /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, // OpenAI-compatible proxies (LiteLLM)
43 /input token count.*exceeds the maximum/i, // Google (Gemini)
44 /maximum prompt length is \d+/i, // xAI (Grok)
45 /reduce the length of the messages/i, // Groq
46 /maximum context length is \d+ tokens/i, // OpenRouter (most backends)
47 /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, // OpenRouter/Poolside
48 /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, // Together AI
49 /exceeds the limit of \d+/i, // GitHub Copilot
50 /exceeds the available context size/i, // llama.cpp server
51 /greater than the context length/i, // LM Studio
52 /context window exceeds limit/i, // MiniMax
53 /exceeded model token limit/i, // Kimi For Coding
54 /too large for model with \d+ maximum context length/i, // Mistral
55 /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, // DS4 server
56 /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
57 /prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error
58 /range of input length should be/i, // DashScope / Qwen Token Plan
59 /context[_ ]length[_ ]exceeded/i, // Generic fallback
60 /too many tokens/i, // Generic fallback
61 /token limit exceeded/i, // Generic fallback
62 /^4(?:00|13)\s*(?:status code)?\s*\(no body\)/i, // Cerebras: 400/413 with no body
👆 带下划线的行可以点:点哪行,讲哪行。

改进:先看清一轮请求由什么组成

下面是这次运行的逐轮回放。柱高是服务端返回的 usage.prompt_tokens;每根柱按来源分块着色,分块的 token 数用录制模型的 tokenizer 和 chat template 重算,每一轮都与服务端的数一致。红虚线是假设窗口只有 1,800。

每一轮到底发了多少:一次真实运行的上下文回放
换成你自己的会话

把本机 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。显示的文本已脱敏(本机路径换成占位符)。

🎯

试三件事:① 先看第 1 轮:整根柱 947 token,浅紫色的工具定义占了多少?用户写的任务(蓝色)又有多少?② 点最后一轮,在下方逐条列表里找出哪一个工具结果吃掉了最多上下文(带 ★ 的那条),再往回翻,看它是第几轮进来的、之后每一轮是不是都还在;③ 切到 pd-v2-truncate-s1,看柱子第一次变矮的那一轮,顶上标着什么。

回放里能读出三件事,数字都来自这次运行:

  1. 第 1 轮就不只是对话。947 token 里,工具定义 685(72%)、system prompt 197、用户写的任务只有 52,其余 13 是聊天模板加的角色标记。
  2. 之后涨的主要是工具结果。到第 11 轮,2,618 token 里工具结果合计 1,144(44%),已经超过工具定义;模型写的调用只有 268。单个最大的是第 3 轮 read_file 读回的 dateparse.py,409 token,从第 4 轮起每一轮都跟着重发。
  3. 模型「想」的不算进来。第 8 轮模型写满了 4,096 token(全是思考,一个调用都没写出来就撞到了回复上限),第 9 轮的请求却只比第 8 轮多 41——多出来的是一条空的 assistant 消息和 harness 补的一句「回复被截断了,接着干」。本课项目不把思考内容回传给模型,只回传它写下的调用与正文。

Pi 的压缩文档里有一句概括:「tool results (especially from read and bash) are typically the largest contributors to context size」(docs/compaction.md:269)。我们这条短运行印证了后半程;开头的大头则是每轮都要付的工具定义——A2 的岔路 B1 专门算过它。

⚠
常见误解一:「上下文里只有对话。」

第 1 轮模型还没说一个字,上下文里已经有 947 token,对话只有任务那 52 个。system prompt、工具定义、项目指令文件(本项目没有;Claude Code 的 CLAUDE.md、Codex 的 AGENTS.md 都是每轮注入)都在窗口里占位置,而且每一轮都在。

常见误解二:「命中缓存的 token 不算上下文。」

第 2 轮的 990 token 里,服务端报告 989 个命中了前缀缓存。缓存省的是计算和钱(A8),不省窗口:这 989 个 token 照样全部在模型眼前。

极小真数字:一轮的上下文 = 各块之和

把第 1 轮的请求拆成四块,加起来正好是服务端报告的数:

pd-v2-cap20-s1 第 1 轮:服务端 usage.prompt_tokens = 947
= + + +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
第 11 轮同样分解:685(工具定义)+ 311(system 与任务)+ 268(模型写的调用)+ 1144(工具结果)+ 210(模板)= 2618。

如果你用的是 Anthropic 的接口(Claude Code 就是),一轮的上下文要把 usage 里三个字段加起来,因为命中缓存的部分被单独计数了:

Anthropic Messages API 的一轮上下文(Claude Code 会话 JSONL 里每条 assistant 消息都带这几个字段)
= + +
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
本课项目录制用的是 OpenAI 兼容接口:prompt_tokens 已经包含命中缓存的部分(cached_tokens 是它的子集),所以不用再加。
🔍 窗口满了,只是 API 报错吗?

报错是最明显的一种。更隐蔽的代价在模型内部:每多一个 token,推理时注意力就要多看一个位置、KV cache 就要多存一份——LLM 课 C3·KV cache 用锚点模型算过每个 token 要存多少。另一个代价是「窗口没满,模型先变差了」,见本课的岔路。

证据三问

问本课的回答
① 它解决什么问题?看清一轮请求的构成,才知道满了之后该先动哪一块
② 为什么一定会满?模型 API 无状态,每轮必须自带全部上下文;循环每转一轮只追加、不删除,单调增长
③ 实验上呢?这次运行的构成:第 1 轮工具定义占 72%,第 11 轮工具结果占 44%

演进地图:你在这里

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

一句话

📌
上下文从第一轮起就不只是对话:工具定义先占了大半,之后涨的主要是工具结果。
🚦

先停在这里:记住构成(工具定义、system、任务、调用、工具结果、模板)和「只增不减」。
再深一层:把你自己的 Claude Code 会话拖进上面的回放,找出你的会话里上下文最大的那一轮;或者走岔路:上下文越长,模型真的越差吗?

下一个缺口

最省事的办法:截掉最早的消息,行不行? → 第 2 课:直接截掉最早的消息,行不行?

你可能会问