上下文:对话太长装不下怎么办?
本章一问:对话太长装不下怎么办?
主线定位
下面是累计数据流,本章点亮「上下文管理」。它在每一轮循环里都要经过:发请求之前先看一眼,这一轮装不装得下。
本章问题链
每一课只回答一个问题;上一课的失败,逼出下一课的问题。
| 课 | 这一课的问题 | 朴素方案在哪个真数字上失败 | 一句话 |
|---|---|---|---|
| 第 1 课 | 上下文为什么会满?每一轮到底发了多少? | 原样重发:pd-v2-cap20-s1 从 947 涨到 2,618 token,只增不减;第 1 轮里工具定义就占 72% | 上下文从第一轮起就不只是对话:工具定义先占了大半,之后涨的主要是工具结果 |
| 岔路 | 上下文越长,模型真的越差吗? | 先猜再看 NoLiMa:13 个模型里 11 个到 32K 时跌到自己短输入成绩的一半以下 | 窗口是上限,不是有效长度;能少放就少放 |
| 第 2 课 | 直接截掉最早的消息,行不行? | 按真实预算 1,800 从头删,删掉的头两条正是 system prompt 和任务说明;整轮截断丢了读文件那一轮,模型凭记忆改出 + 1 + 1 | 先裁不要钱的,再花钱摘要;永远别把 tool 调用和它的结果拆开 |
| 第 3 课 | 什么时候触发压缩?按百分比、按余量,还是按估算? | CoreCoder v0.4.0 把一段 118 token 的中文估成 60;它的计数里连 system 和工具定义都没有 | 估算只求不致命,安全边际来自预留;真实数字以 API 回报为准 |
| 岔路 | 「Pi 少送 3 倍上下文」该怎么读? | 正文说「每轮」,图表量的是「每任务累计」;我们自己的运行里这两个口径方向相反 | 先问数字的口径:每轮还是每任务、谁测的、能不能复现 |
| 第 4 课 | 压缩成摘要、整个重置,还是写交接文档? | 17 份真实摘要里写下确切报错的 0 份,编出「timezone」的 16 份;第 10 轮模型把改对的一处改了回去 | 压缩是写交接文档,不是写对话概要 |
| 实验课 | 八条真实运行 × 四家触发规则 × 几种压缩策略 | — | 用自己的数据定阈值 |
演进地图
本章的「现代做法」是被一连串真实数字逼出来的。每一课都会用这张图标出「你在这里」。
- 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)。耗时不作比较(本机有其他任务同时占用)。
贯穿任务:修好 test_parse_date
全课的贯穿任务是项目 04-实践项目/tasks/parse_date:让失败的 test_parse_date 通过,测试文件不许改。这一章给它的 trace 加上上下文轨:每一轮真正发给模型的是哪几条消息、各占多少 token,压缩在哪一轮发生、压完留下了什么。
- 本章用到的真实运行都来自本课项目 miniagent(录制模型 Qwen/Qwen3-8B-MLX-4bit,窗口 40,960,本机 2026-09-26):不压缩的 pd-v2-cap20-s1(11 轮,成功);把预算人为调到 1,800 的两条——朴素截断 pd-v2-truncate-s1(截断 2 次共丢 3 轮,失败)和分层压缩 pd-v2-layered-s1(21 轮、压缩 26 次,成功);另有预算 2,400 的一对 v1 运行。
- 设计卡计划的「打开冗长日志 + 调低窗口」长任务 T3 / T3b,项目在批次 2 补录了,放在实验课的实验台三:任务
parse_date_verbose一次测试输出约 1 万字符,预算 8,000(真实窗口的约五分之一)。分层那条(12 轮,成功)L1 触发 4 次、L2 2 次,都是被真实的长工具输出推着触发的,L3 没有触发;朴素截断那条(9 轮,成功)只在第 9 轮、也就是最后一次请求之前截了 1 次。各课用的仍是预算 1,800 / 2,400 的那几条:贯穿任务很短,在真实窗口 40,960 里谁都不会满,那里的「撞墙」都是撞在人为设的小预算上,页面上都写明了;L3 至今也只在预算 1,800 的那条运行里出现过。 - 每一轮发出的消息不是从 trace 里「猜」的,而是重放出来的:按 trace 顺序追加消息,每轮之前调用项目自己的压缩函数(摘要一步交回录制的原文),重放出的 29 次压缩事件与录制逐条一致;再用录制模型的 chat template 重算每轮的 prompt token,60 轮全部与服务端 usage 相等(生成脚本
src/data/precomputed/gen_a05_context.py)。
三层交互
| 层 | 本章里的组件 | 在哪一课 |
|---|---|---|
| 概念动画 | 演进地图(每一代卡在哪个真数字上);四份压缩提示词的要求对照 | 每一课、第 4 课 |
| 算法模拟器(真算法、小数据) | 同一段 12 条真实历史交给六种删法 / 裁法,拖预算看留下什么、请求还合不合法;六个 token 估算器在 10 段真实文本上的误差;四家触发规则在同一条曲线上的触发轮次 | 第 2、3 课 |
| 真实数据 | 六条真实运行的逐轮上下文回放(分块 token 与服务端逐轮一致);分层运行 21 轮里每份摘要还剩什么、摘要模型当时看得到什么;长任务 T3 / T3b(预算 8,000)的逐轮回放与四家触发规则;学习者把自己的 Claude Code 会话拖进来,只在浏览器里解析;NoLiMa 公开数据的先猜后揭示 | 第 1、4 课、实验课、岔路 B1 |
所有算法都在浏览器里现算,不联网。移植的部分都配了自检:CoreCoder 与本课项目用 Python 加载原函数跑出期望值,Pi 与 OpenCode 用 bun 在 pin 源码的切片上跑,Codex 用 rustc 编译 pin 源码的切片;组件右上角显示与原实现一致的条数。
项目推进:里程碑 M5 · 机械裁剪 + 触发阈值
对应 04-实践项目/MILESTONES.md 的「A5 · 上下文工程与压缩」。实现已经在 miniagent/context.py:截断(整轮丢,保留 system 与任务)与分层(L1 裁旧工具输出 → L2 模型摘要 → L3 截断,阈值为预算的 50 / 70 / 90%)。代码 AI 来写,决策你来做。
- 阈值定多少,用百分比、余量还是二者结合?必须引用真实 trace 论证。本项目照搬了 CoreCoder 的 50 / 70 / 90%;第 3 课的数据:预算 1,800 时,工具定义 685 与 system + 任务 249 token 的固定开销就占了近一半,分层运行几乎每轮都在压。
- 估算器用哪种?本项目用录制模型自己的 tokenizer,但只数消息内容和工具定义,不含聊天模板的特殊 token:六条运行里比服务端 usage 少 81–198 token(中位数 110)。换成字符估算的话,第 3 课的中文段落上会低估一半以上。
- L1 放过最近几轮、按什么裁?本项目放过最近 2 轮,超过 400 字符就留头尾各 3 行。第 2 课的真实代价:测试输出唯一关键的 AssertionError 那一行正好在中间,被裁掉了。
- L2 什么时候不该做?第 4 课的重放发现两件事:只有 1 轮可摘要时,摘要比原文还长;第 4 轮刚写好的摘要,在同一次压缩里就被 L3 当成最旧的一轮丢掉了——花了一次模型调用,什么也没留下。
验收标准(全部可测):
.venv/bin/python -m unittest tests.test_context全过:截断后 system、任务与最近一轮仍在,且 tool 调用与结果不被拆散;只超 50% 时只触发 L1 且只动旧轮次;L1 不够时触发 L2 且摘要替换旧轮次;L3 兜底(MILESTONES.md 原文)。- 真跑对照:MILESTONES.md 写的是「pd-truncate-s1 与 pd-layered-s1 在预算 2,400 下各至少出现一次压缩」。录制的数据不满足这一条:pd-truncate-s1 全程 0 次压缩(那次运行的上下文没涨到 2,400),pd-layered-s1 只有 1 次 L1。能满足它的是预算 1,800 的那一对(pd-v2-truncate-s1、pd-v2-layered-s1),建议把验收条改成这一对。
- 建议补的两条(本章重放发现的):① L3 截断时不丢刚写的摘要(或摘要的输入不足 N token 时跳过 L2);② 在
DECISIONS.md写下估算误差报告:用第 3 课的中文段落,比较你选的估算器与真实 tokenizer 的差。 - 导出供 A8 复用的上下文曲线:实验课的「运行」切换里就是六条运行的逐轮数据。
- 长任务 T3 / T3b 已录并导出(
src/data/project/agent-t3-longtask.json),实验课实验台三逐轮回放:「三层都自然触发」只做到了两层,L3 没有触发(L1 每次都先把量压回了 90% 线以下)。
坑与带走
本章拦截的误解| 误解 | 纠正 | 在哪一课 |
|---|---|---|
| 上下文里只有对话 | 第 1 轮 947 token 里,对话只有任务那 52 token;工具定义 685 | 第 1 课 |
| 命中缓存的 token 不算上下文 | 缓存省的是钱和时间,不省窗口;第 2 轮 990 token 里 989 命中缓存,照样全部占着窗口 | 第 1 课 |
| 窗口标称多大就能用多大 | NoLiMa:标称 128K 的模型,在 32K 时多数已跌到自己短输入成绩的一半以下 | 岔路 B1 |
| 50 / 70 / 90 是 Claude Code 的内部机制 | 那是 CoreCoder 自选的设计值;它注释里对 Claude Code 的描述是作者的逆向说法,没有官方证实 | 第 2 课 |
| 必须上精确的 tokenizer | OpenCode 的 pin 源码里没有任何 tokenizer,估算只有「字符 ÷ 4」;它的触发看 API 回报的 usage,安全边际来自预留 | 第 3 课 |
| Pi 每轮都检查要不要压缩 | pin 版只在一次任务跑完后检查阈值;任务中途只在真的溢出报错时压缩并重试一次 | 第 3 课 |
| Pi 每轮少送 3 倍上下文 | 原文正文说「每轮」,图表量的是「每任务累计」;Databricks 自报、不可复现 | 岔路 B2 |
| context reset 是长任务必备组件 | 提出它的文章自己写明:换到 Opus 4.5 后就拿掉了 | 第 4 课 |
| 压缩之后历史就没了 | 被压掉的只是「发给模型的那份」;完整历史仍在磁盘上(本课项目的 trace、Pi 的会话文件、Codex 的 rollout) | 第 4 课 |
| 症状 | 后果 | 怎么办 |
|---|---|---|
| 超了就从头删 | 先丢掉 system prompt 和任务说明;删在调用与结果之间,请求直接被拒 | 保住开头;按整轮删,或像 CoreCoder _safe_split 那样把切点挪到调用之前 |
| 整轮截断丢掉读文件那一轮 | 模型凭记忆改代码,改出 + 1 + 1 | 先裁工具输出、保住源码;或者在说明里告诉模型「这些内容被删了,需要就重新读」 |
| 按行数裁工具输出 | 关键的一行在中间,被裁掉 | 裁之前先按内容保留(报错行、diff);或者只裁已经「用过」的输出 |
| 按「字符 ÷ 3 或 ÷ 4」估中文 | 低估一半以上,压缩触发太晚 | 分档估算,或以 API usage 为准、只估新增部分 |
| 摘要写成对话概要 | 接手后丢掉确切报错、已完成的修改;还会编造 | 用交接文档式模板,点名要保留的东西;给摘要模型的输入别先被截断 |
| 把 50 / 70 / 90 当标准照搬 | 换个窗口大小就失效;预算小时固定开销就占一半 | 用余量或 reserve 表达阈值,用自己的 trace 验证 |
| 把 context reset 当必备组件 | 为已经不存在的问题付成本 | 先确认手上的模型是否仍有 context anxiety |
带走这些句子(各课「一句话」):
- 上下文从第一轮起就不只是对话:工具定义先占了大半,之后涨的主要是工具结果。
- 窗口是上限,不是有效长度;能少放就少放。
- 先裁不要钱的,再花钱摘要;永远别把 tool 调用和它的结果拆开。
- 估算只求不致命,安全边际来自预留;真实数字以 API 回报为准。
- 先问数字的口径:每轮还是每任务、谁测的、能不能复现。
- 压缩是写交接文档,不是写对话概要。
深入这一章
按你愿意花的时间选。每条都实际打开核对过(2026-09-26);站外链接都需要联网,离线包里只保留链接。
🎮 想更直观 10–30 分钟
🔬 想吃透原理 1–3 小时
📄 想读原文 按需
我替你排除了什么:win4r/cc-notebook 的上下文压缩三篇(给出「167K 阈值」等细节,但基于逆向或泄露代码,未经官方文档核实)· baoyu.io 的 Manus 译本(本轮不可达,改用 Manus 官方中文版)· 把 Databricks 博客的「3x」当结论的转述文章(口径问题见岔路 B2)· 把 Dex Horthy 的「40%」当阈值依据的二手文章(个人经验断言)· Chroma 报告里各模型曲线的具体数值(静态图,没有转录,只链原文)· Drew Breunig《How Long Contexts Fail》(2025-06-22)归入岔路 B1 的正文引用,不另列:它的四种失败定义本章用到了一种,并配了我们自己 trace 里的真实例子。