Repository navigation
Description
这是提案,不是报 bug。负责「谁来压、何时压、压完谁提交」的那一层(后台预压、提交前重新对齐最新消息、同一会话只有一个提交者、能中途取消)现在是稳的,不建议动。这篇只谈一件事:压出来的那段摘要,质量到底怎么样,以及失败的时候会付几次钱。
基线:
main@cf4a5443b(2026-11-08,工作区干净)。文中行号都对这个提交。外部依据分两类:论文,和十个真实产品的源码核查,可信程度在「外部依据」那节的最后交代。
先说结论
现在的压缩,本质上是让模型自由写一段总结,再保留最近一万个 token 的原文。判断这次压缩成不成功,只有两条标准:写的东西不是空的、没有去调工具。
换句话说:它只看「模型有没有回一段文字」,不看这段文字靠不靠得住。
由此漏掉三件事:
- 摘要里那些「下一步该做什么」,很多是模型顺手编的,没人核对过原始记录里到底有没有这事;
- 用户定下的规矩(用 pnpm 别用 npm、这个接口不许改),压几次之后就只剩摘要里那句转述,甚至没了;
- 万一模型非要调工具、或者回了空的,现在的做法是把同一个请求原样再发一遍,最多十次。而压缩恰好发生在上下文最满的时候,也就是整段会话里最贵的一次调用 —— 等于在最贵的时刻反复付全价。
下面按「建议先做哪件」的顺序排开,每件都说清四件事:现在什么样、凭什么这么改、具体怎么改、怎么算改好了。
| 先做哪件 | 要改的事 | 改在哪 | 动手大小 | 依据 |
|---|---|---|---|---|
| 第一件 | 请求失败时别再原样重打十次,连续失败就停手,并把「这次到底省没省钱」记下来 | compression_request.rs、compression_job.rs |
小 | Claude Code、Qwen Code、OpenCode 都是这么分级的 |
| 第二件 | 摘要里每句话都要能追到原始记录,追不到的剔出来单放 | compressor.rs 的提示词 + 加一步核对 |
小 | SJC:失败的会话里,八成多是照着摘要里一句没有出处的条目去干的 |
| 第三件 | 用户定的规矩单独钉住、逐字保留,不进摘要正文 | CompressionContract 加字段 |
小 | Compaction Cliff:压一轮还剩 53%,压五轮只剩 10% |
| 第四件 | 摘要按固定的小标题写;压前压后各拍一张快照,逐项比对 | build_compact_prompt + 压缩收尾 |
小到中 | ACON / AutoCompact:压缩提示本身也得能改、能比 |
| 第五件 | 把「哪些话不能丢、怎么说才不跑偏」写进提示词 | build_compact_prompt + 收尾提醒 |
小 | Kimi Code、Cline、OpenCode、Claude Code 已经在用的提示词 |
| 第六件 | 已经做完的那几轮,折成一行结论,不再整段重写 | atomic_message_units、CompressionEntry::Turn |
中 | Context-Folding:用十分之一的活跃上下文打到同样效果 |
| 第七件 | 「什么时候该压」别只看 token 满没满,把已经在算的那些信号接进来 | ExecutionEngine |
中 | StateComp / CTXAgent |
| 第八件 | 想翻旧账时能按编号取回某一段原文,而不是全文搜 | 工具 + 原始记录 | 中 | ARC:按编号取回 99.40%,靠相似度检索 88.12% |
明确不做的事(训练侧那套、上向量库、改服务商缓存策略)放在最后一节。
这个仓库里已经有过几次相关反馈
提之前先把已有的对上,免得重复劳动:
- [Bug]: 上下文压缩触发过晚且同步阻塞,导致会话卡顿 #1529(已关闭):报告「压缩触发太晚、压缩的时候会话卡死、压一两次之后明显降智」。其中「卡死」那一半,后来靠「提前一万 token 在后台先压」缓解了(就是现在
compression_prefetch.rs那条路径);「降智」那一半当时没有解。 - [Bug]: 压缩之后回答的内容一直循环,怀疑压缩有问题 #1179(已关闭):报告「压缩之后回答一直循环」。处理方式是在压缩后清掉循环检测、再加一层纯文本复读检测(commit
e9c88e3)。这是把报警器修好了,不是把摘要修好了 —— 摘要本身的质量仍然一道门都没有。 - [Bug]: 无限压缩 #1558「无限压缩」、为什么老是要开始压缩上下文了?怎么控制这个压缩阈值 #701「压缩阈值」、上下文压缩问题 #842「上下文压缩问题」:同一条线上的零散反馈。
- Complete OpenAI Responses support for GPT-5.6 prompt caching and remote compaction #2235 已经在讨论另一条路:OpenAI Responses 那种「服务端压缩」(摘要由服务商生成、客户端只存一个不透明条目),并且提出要记缓存读、写分开的用量、要给请求各段做指纹。这篇不掺和那条路,只谈本地这条;但那边提的「缓存用量要分开记」,正好是下面第一件事需要的观测。
所以这篇不是从零提需求,而是把「压完为什么会降智、会复读」这件事,拆成几块可以逐项验收的东西。
现在的压缩是怎么走的
什么时候压
- 先把预算算出来:可用输入 = 上下文窗口 − 预留给输出的大小 − 一万 token 的安全余量(
execution/context_budget.rs:14-27,走模型池时还有一条等价路径在:29-41)。 - 判定只有一句:总 token 超过这个可用输入就压(
execution/execution_engine.rs:4460-4461)。 - 提前一万 token 就开始在后台先压着,真到线了直接用(
agent-runtime/compression_prefetch.rs:9-15)。 - 已经用过的 token 数优先相信服务商回报的真实用量,只重新估算那之后新增的部分(
execution_engine.rs:798-868)。
压哪一段
在 session/compression/compressor.rs:67-225:
- 一条助手消息和它后面紧接着的工具结果算一个整体,切的时候绝不从中间断开(
:295-315)。断开的后果是发出一个「助手说要调工具、却找不到结果」的非法请求。 - 最近一万 token 的原文留着不压(
:57);压完还是太满,就把这个「留多少」按一万往上加,再来一次(:58)。 - 更早的用户消息逐字保留,但总预算只有「上下文窗口的十分之一,最多两万 token」(
:195、:317-339)。 - 系统提示和工具定义根本不进摘要请求 —— 摘要请求只带系统提示 + 要被压掉的那一段(
:192-193)。
摘要怎么来
- 提示词写死在代码里(
compressor.rs:585-604),让模型写四类内容:做到哪了、有哪些约束和偏好、还剩什么没做、关键数据;另外交代两句:别粘大段文件日志,别调工具。 - 请求的形态是:同样的系统提示、同样的工具、和主请求一模一样的前缀,只在末尾追加这么一句「请总结」(
compression_job.rs:214-234)。这个形态很关键,下面会讲为什么它是现在最省钱的做法。 - 失败重试预算是十次,传输错误、回了空摘要、跑去调工具,全都算在这十次里(
compression_request.rs:36-89)。 - 拼装结果:
[逐字保留的用户消息] + [摘要] + [最近的原文] + [一句「接着干」的提醒](compressor.rs:407-438);摘要前面还有一句边界说明(:533-535),最后一条内部提醒在:60-61。 - 另外已经有两处结构性补充:
- 一份结构化的事实清单(
CompressionContract,contracts/runtime-ports/src/agent_api.rs:1247-1298),列的是改过哪些文件、要跑哪些验收命令、卡在哪个失败、几个子任务什么状态,渲染成一段「以这段为准」的说明插在摘要前面,来源是会话里累积的证据台账(session_manager.rs:2328-2334)。 - 当前这轮的任务清单快照:从被压掉的
TodoWrite结果里取最后一份,配上「以这份为准,和摘要冲突时以它为准」的措辞(compressor.rs:440-496)。
- 一份结构化的事实清单(
压完怎么收尾
- 后台预压出来的结果只是个候选,必须对得上「请求参数」和「消息编号前缀」才准用,对不上直接丢掉(
compression_job.rs:37-70)。 - 真正提交时会在会话锁里,先对齐提交那一刻的最新尾部,再核对一次参数(
compression_lifecycle.rs:199-332)。 - 压缩前会把完整对话写成一个原始记录文件,并在摘要里附上「要看历史,去读这个文件的这几行」(
compressor.rs:498-545、compression_lifecycle.rs:187-195)。 - 压完主动让提示缓存失效一次(
compression_lifecycle.rs:350-356)。 - 单个工具结果太大就落盘,只给模型一个预览加一个固定引用(
tools/tool_result_storage.rs:1-6、:45-76)。 - 最后一道兜底:压完还是超过窗口,才按最老的轮次往前丢(
execution_engine.rs:4612-4658)。
这些地方已经做对了,改的时候别碰
写这一节是为了划边界 —— 下面所有改动都不该动这些:
- 不把一次工具调用切成两半(
compressor.rs:295-315)。动了就是非法请求。 - 后台预压的结果只能当候选,对不上就丢(
compression_job.rs:37-70)。这是「可以后台加速但不引入竞争」的前提。 - 提交时重新对齐,而不是拿旧快照覆盖(
compression_lifecycle.rs:224-298)。 - 消息编号的前缀不许改:要替换内容就换一个新编号,只能往后追加(
compressor.rs:341-405)。这条其实就是「别回头改已经发出去的内容」的落地形式。 - 「把旧的工具输出直接抹掉」这个做法已经被有意删掉,原因写在模块注释里(
session/compression/mod.rs:1-11):回头改已经发出去的开头,会让服务商的提示缓存整个作废;而且模型会忘掉自己干过什么,长任务里就开始反复调同一个工具。 - 准备阶段全程可取消,一旦开始提交就不中途撒手(
compression_lifecycle.rs:117-198、:333-335)。 - 压完的东西有损,但翻得回去:原始记录文件 + 行号索引,比「有损且查不到」强一档。
- 压缩请求的形态本来就是最省钱的那种:同样的系统提示、同样的工具、和主请求一模一样的前缀,只在末尾加一句(
compression_job.rs:214-234+compressor.rs:192-193)。这一点现在没人写下来,很容易被后来的改动静默破坏 —— 所以第一件要改的事,就是先把它写成规矩。 - 接回去的顺序是对的:
[逐字保留的用户消息] → [摘要] → [最近的原文] → [接着干的提醒](compressor.rs:407-438)。最近发生的事排在最后、提醒更靠后,模型翻到最后几屏看到的正好是要接着干的活。要是学 Codex 本地压缩那样把摘要放最末,模型就得先回读一段总结再回到手头的事。
外部依据
论文怎么说
只列和下面改动直接对得上的。
能直接照做的
- SJC(arXiv:2610.20927) 讲了一件事:压缩提示要求模型写「决策和下一步」,于是摘要里混进了两种东西 —— 环境里真实发生过的事实,和压缩器自己的推测;事后模型分不出哪个是哪个。他们在一个被广泛使用的结构化压缩器上做了分析:失败的会话里,83% 追下去是照着摘要里某一条去执行的,而这些条目大多在环境输出里找不到出处。解法不改模型也不改压缩器,只是逐条问一句「这条能不能在环境输出或上一版摘要里找到出处」,找不到的删掉,可靠性提升 2.1%~8.9%,token 不增加。
→ 对这里的意义:build_compact_prompt里那句「What remains to be done(clear next steps)」正是这类条目的产地;而原始记录的行号区间已经在手,做溯源几乎是白捡。 - Compaction Cliff(arXiv:2608.22752,CIKM 2026):安全规则和流水日志被以同一个速率压缩,但只有规则必须逐字保真才能执行。他们实测 Claude Code 的
/compact(Sonnet 4.6):压一轮之后还剩 53% 的安全规则,压五轮只剩 10%。解法是按内容类型分别定保真度。
→ 外推要小心:那篇测的是「知识库型长文本」,不是代码仓库会话,数字不能直接搬到这里。但「不同性质的内容用同一个速率压」这个毛病,和这里是一模一样的(见下面「一条规矩保不保得住」)。 - ACON(arXiv:2510.00615,ICML 2026):把「怎么压」这件事本身当成可以优化的对象,在自然语言层面按失败案例反复改压缩指南(不动模型权重);峰值 token 降 26%~54%,小模型的任务成功率提升 46%。
→ 对这里的意义:压缩提示现在是写死的常量,没有版本、没有对比、没有从失败里回流。这就是「压缩质量没法迭代」的根。
要改结构才用得上的
- Context-Folding(arXiv:2510.11967):让模型主动开一个子过程去做子任务,做完把它折成一句结论,中间步骤就不占地方了;靠强化学习教会它什么时候折。活跃上下文小十倍,效果还优于「整段摘要」的做法。
- StateComp(arXiv:2609.27298):按当前进度给每段历史打「还得留着」或「可以替换了」,相邻的「可以替换」合并成一段再摘要。总 token 降 52.27%,任务指标不掉。
- CTXAgent(arXiv:2610.34425):只给模型两个工具加一段说明,让它自己决定什么时候压、留什么、交接写什么。七个前沿模型里多数能自己管好;在 Opus 5.5 上与原厂 harness 同分,成本比它的默认配置低 49%。
- ARC(arXiv:2607.25066):把工具输出放进一份只能往后追加、每段都有编号的日志,压缩时把旧输出换成一句「见编号 X」,之后按编号取回。查针测试准确率 99.40%,最好的对照方案是 88.12%。
- LCM(arXiv:2605.04050):摘要做成树,每一层都留一条指针指回原文,所以压了几层也照样能翻到底。在长上下文测试上,32K 到 1M 每一档都超过 Claude Code。
- The Complexity Trap(arXiv:2508.21433,NeurIPS'25 DL4C):在 SWE-bench Verified 上对比「把旧观测直接抹掉」和「让模型写摘要」,成本大约减半,解决率持平甚至略高。
→ 和这里「已经删掉抹观测」看似矛盾,其实不矛盾:那篇抹的是「模型看到的环境视图」,不是回头改已经发出去的历史。这里当初删掉它的两个理由(缓存作废、记忆丢失)现在依然成立,不建议靠恢复「轮内抹观测」来解决。
用不上(偏训练侧):AutoCompact(arXiv:2610.02163)、Remory(arXiv:2610.11287)。全部清单在附录。
十个真实产品是怎么做的
论文讲「什么有效」,源码讲「怎么落地」。这些产品在压缩上真正的分歧只有一个:压缩请求要不要和主请求共用同一个开头。服务商的提示缓存是「从头一个字节一个字节对齐才算命中」,而缓存命中的读价大约只有原价的十分之一;压缩又偏偏发生在上下文最满的时候,所以这个选择直接决定这一次要不要付全价。
- 复用上一次请求(把主请求原样复制一份,末尾加一句「请总结」):Claude Code、Qwen Code(满足条件时)、Codex 服务端、OpenCode 新版、Kimi Code、旧版 Cline。好处是前面全部命中缓存;代价是模型手上仍然有工具,得靠狠话加校验把它按回去。
- 另发一条小请求(换一段专门的短系统提示、不带工具、历史改写成纯文本):pi、OpenCode 旧版、Cline 的新 SDK、旧版 kimi-cli。好处是请求小、确定、能换便宜模型、好写测试;代价是每次都按原价重读全量历史。
- 居中的一个:Codex 的本地压缩,系统提示和历史都保留原样,只把「工具」这一项清空,于是缓存从工具那一段就断了。
| 工具 | 压缩请求怎么发 | 什么时候压 | 留什么 | 压完怎么接上 | 值得抄的一点 |
|---|---|---|---|---|---|
| Claude Code | 复制主请求,末尾加一句「总结」 | 快到顶前留 13k 余量;连错三次停手 | 全量总结,再回填最近 5 个文件 | 「本会话是从上一段接着来的」+ 原始记录路径 | 把开头锁死不动,连「输出上限」都不许改,改了缓存就废 |
| Codex 服务端 | 复制主请求,末尾加一个「该压缩了」的标记 | 到窗口的九成 | 用户消息共 64k | 一个加密的压缩条目 | 摘要对客户端不透明,不可审计,只适合自家模型 |
| Codex 本地 | 同系统提示、同历史,只把工具清空 | 同上 | 用户消息共 20k,超出就截断 | 「另一个模型已经做过一轮」+ 摘要 | 撑爆时从最老的一条开始删,注释写明「为了保住缓存」;压缩期间用户刚打的字单独留住 |
| Qwen Code | 满足五个条件才复用主请求,否则另发 | 到 85%,或窗口减 33k;连错三次停手 | 全量总结 + 回填文件图片 | 摘要 + 模型回一句「收到」 | 复用条件不满足就立刻换形态;旧工具输出一次清到阈值的一半,否则每轮都在改开头 |
| OpenCode 新版 | 复用主请求 | 窗口减「十分之一和 16k 里较大的那个」 | 最近 15k 写成一段「近期上下文」 | 用一个 <conversation-checkpoint> 包住摘要和近期 |
没按模板填就追一句提醒再来;规则里明写「不许提压缩这件事」 |
| OpenCode 旧版 | 另发小请求 | 输入上限减「20k 和输出上限里较小的」 | 2k~15k | 先问一句「我们干到哪了」,再给摘要 | 反面教材:复用了缓存键,但开头第一个字节就不一样,等于每次都白写 |
| Kimi Code | 复用主请求 | 到 85%,或预留 50k | 用户消息 20k(头 2k + 尾 18k) | 「这是你自己的笔记,不是证据」+ 自动附上任务清单 + 原始记录指针 | 摘要该怎么写,这几条它讲得最全(见下面「不跑偏的写法」) |
| Cline | 旧版在主循环里让模型调一个「交总结」的工具;新 SDK 改成另发小请求 | 按窗口大小分档;新版到可用的九成 | 旧版全量;新版留 20k 原文 | 「本会话从上一段接着来,不要再问用户任何问题」 | Task Evolution 一段专门记「用户中途改了什么主意」,要求附原话 |
| pi | 另发小请求 | 窗口减 16k | 约 20k 原文,工具调用和结果成对 | 用 <summary> 包起来 |
切点正好落在一个超长回合中间时,另写一段提示,专门总结被丢掉的前半截 |
| OpenHands | 另用一个模型来写摘要,指令放系统位、内容放用户位 | token 或条数,两条线都看,分「必须压」和「可以压」 | 开头几条事件永远不动 | 摘要事件插在系统提示后面 | 摘要用另一个模型写;压得太少(不到一成)就当失败不压;写不动就把每条内容截短两成再来 |
这些产品里,本项目在哪一格? 已经站在「复用主请求」这一格了(见上一节最后两条),但缺三样:失败时换形态、连错几次停手、以及「这次到底命中了多少缓存」的记录。这三样就是下面第一件事。
这些依据有多可信
- 直接读了源码:OpenHands 的摘要器(
llm_summarizing_condenser.py)、Codex 的本地压缩(codex-rs/core/src/compact.rs)。 - 二手,但附了代码快照:Qwen Code、OpenCode 新旧两版、Kimi Code、Cline、pi,来自一份 2026-10 的第三方源码核查,提示词原文和阈值都有引用,但我没有逐行对照上游仓库。
- 只当方向、不当数字:Claude Code 是闭源的,那些数字来自泄露的 source map 还原镜像,版本未知(「13k 余量」「2.79% 会去调工具」「连错三次停手」都属于这一类)。
- 论文的数字都取自各自 arXiv 摘要页。
压完之后,怎么算「跟没压过一样」
后面有两件事(请求形态、摘要写法)其实都在解决同一个问题:压完之后模型还像没压过一样接着干活。所以先把「像没压过」定义成能验收的样子 —— 压完的头一轮,下面这些都不该出现:
- 重复已经做完的事:重新读同一个文件、重跑同一条命令、反复调同一个工具;
- 向用户要已经给过的信息;
- 用「根据摘要」「之前的记录里说」这种外人视角说话;
- 方向跑了:把已经定下来的事重新拿出来讨论、把还没定的事当成定了、无视用户中途改的主意;
- 语言和格式跑了:中文对话被摘要拽成英文;
- 把「号称做完了、但从没验证过」的步骤当成事实往下推。
现在只有前两条和最后一条被对冲过 —— 收尾那句提醒里写了「不要请用户重复已保留的信息」「有没做完的就继续」。其余三条没有任何对冲手段。
可以改的地方
压缩失败的时候,别把同一个最贵的请求再打十遍
现在是什么样。 压缩请求的形态是对的:同样的系统提示、同样的工具、和主请求一模一样的前缀,只在末尾加一句「请总结」(compression_job.rs:214-234)。按缓存「从头对齐才算命中」的规则,除了最后那句话和模型的输出,前面的内容本该都是命中缓存的。问题出在失败之后:
- 模型要是坚持调工具、或者回了个空摘要,现在是把同一个请求原样再发一遍,最多十次(
compression_request.rs:36-89,中间有退避等待)。压缩发生在上下文最满的时候,也就是整段会话最贵的一次调用 —— 这等于在最贵的时刻按原价重复付费。 - 跨轮没有上限。只有「这一次压缩里的十次重试」,同一个坏掉的会话可以一轮一轮地失败下去。
- 完全没有「这次到底命中了多少缓存」的记录。压缩完成事件里只有压缩前后的 token 数和耗时(
ContextCompressionCompleted),没有「其中多少是走缓存读的」。所以「我们的形态到底算不算复用」目前只是推测,没有任何数据支撑。
凭什么这么改。 别人家的做法都是「格式不对就换形态」,而不是原样重试:Claude Code 先用一段极凶的话把工具按住(实测 Sonnet 4.6 上还有 2.79% 会去调工具,4.5 上只有 0.01%),真失败了就换成「另发一条小请求」的形态(换系统提示、关掉思考、只留一个读文件的工具);Qwen Code 的复用要同时满足五个条件,输出里有工具调用或缺了闭合的小节就切到「另发」的路径,这一次改造把缓存命中率从 0% 拉到 96.7%;OpenCode 新版则是没按模板填就追一句提醒再来一次。至于停手,Claude Code 和 Qwen Code 都是连错三次就停(Claude Code 源码注释里提到,曾有 1,279 个会话连续失败了 50 次以上)。另外,Claude Code 和 OpenCode 各自那两个真实故障(工具数 66 对 67、某个插件只往主请求的系统提示里追加规则)都是靠比对「两边系统提示和工具清单是不是一模一样、缓存读了多少」才发现的 —— 反过来证明这项记录是必须的。
怎么改。
- 把重试分成两类,别再混着算:网络那类(超时、限流、服务端 5xx)原样重试;格式那类(跑去调工具、回了空、缺了小节)换形态 —— 先追一句「上次没按要求写,不要调工具,按固定小标题直接输出」,还不行就切到「另发一条小请求」的形态(独立的短系统提示、不给工具、历史改写成纯文本)。
- 加一条跨轮的上限:同一个会话连续三次压缩失败,就停掉自动压缩,直到用户手动
/compact,同时发一条事件说明失败原因分布。 - 在压缩完成事件里补上「走缓存读的输入 token 数」和「缓存命中比例」;debug 日志里把主请求和压缩请求的系统提示指纹、工具个数并排打出来。
- 把「这个形态不许乱动」写成规矩:任何往压缩请求里塞东西的改动(改系统提示、增删工具、改思考开关或输出上限)都会让缓存整段作废,列进评审清单。
怎么算改好了。 先测一次现在的缓存命中比例,把它当基线。改完看四个数:缓存命中比例、格式类失败在「追一句提醒」和「另发小请求」之后救回来的比例、单次压缩里没走缓存的那部分输入量、以及停手之后还有没有继续偷偷自动压缩。
摘要里那些「下一步」,很多是模型自己编的
现在是什么样。 摘要是自由文本,没有一句话带出处。判断成不成功的标准只有「非空」和「没调工具」(compressor.rs:580-583、compression_request.rs:36-54)。而提示词里明确要求写「What remains to be done(clear next steps)」—— 这类条目天生就是没有环境依据的产物。另一边,原始记录的行号区间其实已经生成好了(compressor.rs:498-545),只是被拿去当「你自己去翻」的入口,没用来核对。
凭什么这么改。 就是上面 SJC 那组数据:失败的会话里 83% 追到「照着摘要里某一条去执行」,而这些条目大多在环境输出里查不到出处。
怎么改。 三步,可以分开落地:
- 摘要改成「固定小标题 + 每条后面跟一个可选的出处标记
[src:起-止]」,指向原始记录的行号;查不到出处的,标[unsourced]。 - 加一步核对:逐条判断「这条能不能在给它的那段记录或工具输出里找到」。第一版用规则就够(条目里提到的文件路径、命令、报错文本,在那段区间里到底出现过没有),判不了的才升级成一次很小的模型判断 —— 这就是 SJC 说的「一条廉价的判断请求」。
- 查不到出处的条目从正文里剔出来,单放一节「未核实的下一步(动手前先核实)」,并在收尾提醒里加一句约束。剔完只重写被剔的那部分,最多重写一次,并且和那十次重试分开算账(那十次是给网络和格式的,别混用)。
数据结构上不用大动:CompressionEntry 本来就是「合同 / 模型摘要 / 某一轮」三选一的带标签结构(core/message.rs:226-243),加一个「一句话 + 可选出处」的类型很自然,旧数据照样能读。
怎么算改好了。 拿一批历史会话回放:看摘要里 [unsourced] 的比例,以及「照着不存在的条目去干活」的次数(可以用「工具参数里引用了摘要说有、但文件或命令其实不存在的东西」做自动近似)。目标是没出处的条目一条都不进正文。
一条规矩保不保得住,看它当初写在哪儿
现在是什么样。 这条比「那份事实清单是自由文本」更准确,也更容易被忽略 —— 现在事实清单其实是结构化的、逐字渲染的、还带「以这段为准」的措辞,问题不在这。真正的问题是:
- 写在系统提示里的硬性规矩天然安全:系统提示根本不进摘要请求(
compressor.rs:192-193),压多少轮都不会动它。 - 写进用户消息的规矩,逐字保留,但总预算只有「窗口的十分之一,最多两万 token」(
compressor.rs:195、:317-339)。超了预算,就往摘要里塞了,开始有损。 - 用户在中途才提的规矩(「以后都用 pnpm,别用 npm」、某条失败的验收命令),如果落在助手的消息里,就只在摘要里;而且摘要消息本身是一条普通用户消息,下一轮压缩会把它再压一遍,越压越淡。
- 现有那份事实清单只覆盖四类能从系统状态算出来的东西:改过哪些文件、要跑哪些验收命令、卡在哪个失败、子任务什么状态。「规矩、偏好、禁令」这一类完全没有位置。
凭什么这么改。 Compaction Cliff 那组数字:安全规则压一轮剩 53%,压五轮剩 10%。注意是「知识库型长文本」上的实测,不能直接搬数字过来,但「不同性质的内容用同一个速率压」这个毛病是一样的。
怎么改。
- 在事实清单里加两个字段:「硬性约束」和「验收标准」,渲染位置跟现有四类一样(同样逐字、同样标「以这段为准」、同样不进摘要正文)。
- 内容来源分两级:先规则抽取(在被压掉的那段里找「不要 / 必须 / 只能 / 别用」这类句子、从当前这轮用户的原话里找、从规则文件的引用里找),抽不出来的再用一次模型抽取;每条都带它来自哪条消息。
- 提示词里加一句硬约束:「不许覆盖或改写事实清单里的任何一条」。
- 多轮压缩要能看见衰减:
compression_count现在只记次数(compression_lifecycle.rs:359-366),建议同时记下「这一轮钉住了几条」,掉到零或者突然少一半就告警 —— 这就是那条衰减曲线的观测点。
怎么算改好了。 造一个固定会话:「三条硬性规矩 + 二十轮工具调用」,连着压五次,看五轮之后这三条还在不在、是不是逐字还在。这个纯离线、不花模型的钱(拼装那步是个纯函数,compress_plan_with_contract)。
摘要没人检查,压完也不知道还能不能干活
现在是什么样。 摘要是自由文本;那十次重试全花在网络和格式上,对内容质量一道门都没有;压完只是重新算一遍 token 打了个日志(compression_lifecycle.rs:369-414),没有任何「压完还能不能干活」的检查;失败的轨迹也不回流去改提示词。
凭什么这么改。 ACON 的结论是压缩指南应当被当成一份可以改、可以比的资产;AutoCompact 则是拿一个裁判模型去评审压缩决策和摘要,再用评审结果去训练。
怎么改。
- 摘要按固定小标题写(做到哪了 + 证据 / 正在做什么 / 卡在哪 / 关键路径 / 还没验证的假设,再分「已经定了的」和「还没定的」两栏),标题固定才好比对、好做机器检查。
- 压前压后各拍一张快照,逐项比对(这一步不花钱,优先做):压前记下钉住的规矩、改过的文件、当前任务清单;压后在最终要发出去的那串消息上再取一次,逐项对。缺了就直接判定这次压缩失败、退回没压的样子 —— 现有代码已经有「返回空、什么都不改」的回落路径(
compression_lifecycle.rs:232-259、:299-305),不用另造。 - 再加一个可选的探针(默认关):压完的头一轮,让模型按固定格式复述一遍当前状态(任务清单 + 钉住的规矩 + 下一步),漏了就记一笔。复用现有的内部提醒机制,不用新开通道。
- 把压缩提示提成一个有版本的常量,附一段「已知失败模式」清单,让后面能按上面那些指标做对比。
怎么算改好了。 快照比对能在造出来的用例上抓到「钉住的规矩丢了」;探针的额外成本能量化(每压多少次多发一条短请求)。
摘要的写法本身就没交代清楚「哪些话不能丢」
现在这段提示词(compressor.rs:585-604)交代了「做到哪了 / 有哪些约束和偏好 / 还剩什么 / 关键数据」,也交代了「别粘大段文件日志」「别调工具」。但对着几个真实产品已经在用的提示词比,还缺几件直接影响上面那六条症状的话:
| 该说的话 | 谁在说(原话要点) | 现在的状态 |
|---|---|---|
| 别在摘要里提「压缩」「摘要」这回事 | OpenCode 的规则:Do not mention the summary process or that context was compacted | 自由写,没约束 → 可能出现「根据前文摘要」 |
| 已经定下来的事,和还没定的事,分开写 | Kimi Code:…so you neither silently reopen a closed choice nor treat an undecided point as decided | 没有 → 方向会跑 |
| 号称做完但没验证的,要标成没验证 | Kimi Code:…treat it as unverified rather than fact | 没有 → 和上面「摘要编下一步」是同一个病根 |
| 路径、命令、报错原文照抄,别改写 | OpenCode、Qwen、pi 都写了这句 | 只说「关键数据、示例、引用」,没强制 |
| 用对话原本的语言写 | Kimi Code:do not switch to English just because these instructions happen to be in English | 提示词全英文且没这句 → 中文对话被拽成英文 |
| 记下用户中途改了什么主意 | Cline 的 Task Evolution 一段,要求附原话 |
只有静态「偏好」,不记录变化 |
| 列出「下一轮必须自己去查的空白」 | Kimi Code:…Name these gaps so the next turn goes and checks them instead of assuming | 没有 |
| 接回去时提醒一句「这只是笔记,要自己验证」 | Kimi Code:Treat it as notes, not proof;Claude Code:「本会话是从上一段接着来的…」;Cline 追加「不要再问用户任何问题」 | 收尾提醒已覆盖「别重复问用户」和「没做完就继续」,缺「要自己验证」这半句 |
其中「标成没验证」和「这只是笔记」这两条,和上面「摘要编下一步」是互补的:那边是事后把没出处的剔掉,这边是事前让模型自己标出不确定 —— 两件都做才稳。
怎么改。 这些话并进提示词,做成新版本和现版本对比;收尾提醒补一句「摘要只是笔记,号称做完但没验证的步骤,自己再验一遍」。其中「已定 / 未定分栏」和上一节的固定小标题是同一处改动,建议合成一次改完,不要分三次各比一轮、互相污染。
怎么算改好了。 中文对话压完之后摘要还是不是中文(可以自动检测);「已定 / 未定」两栏是不是都有内容;「未验证」的标记出现了没有;以及上面那六条症状在回放里各出现了多少次。
压缩是一刀切,做完了的小任务没有折起来
现在是什么样。 计划阶段只保证不切断工具调用,但被压掉的整段前缀是整体丢给摘要模型、整体重写成一段自由文本的。而「按轮折叠」的结构其实早就设计好了 —— CompressionEntry::Turn { messages, todo }(core/message.rs:235-242),可是在生产代码里那个装消息的字段永远是空的,唯一往里填东西的地方在测试里(compressor.rs:1159-1162)。也就是说,早就想好了要这么折,一直没实现。
凭什么这么改。 Context-Folding 那组结果:把做过的子任务折成一句结论,活跃上下文只有原来的十分之一,效果还比整段摘要好。
怎么改。 先折再摘,两段式:
- 对已经做完的轮次(判断依据:这轮的任务清单全变成了「已完成」,或者这轮之后已经出现了新的用户消息、并且没有悬着的失败),把轮内的工具调用折成一两行「结果 + 原始记录行号」,填进上面那个一直空着的字段;
- 对还没做完的轮次原样保留,不折;
- 摘要请求只吃折完的版本,不再吃原始前缀 —— 顺带把「摘要请求自己也会撑满」的问题一起解决(现在它的输入就是整段前缀)。
怎么算改好了。 同一段会话,折完之后摘要请求的输入 token 和最终摘要长度都下降,同时「钉住的规矩 + 压前压后比对」那一套仍然通过。
什么时候该压,只看 token 满没满
现在是什么样。 唯一的触发条件是总 token 超过可用输入(execution_engine.rs:4460-4461)。而代码里其实已经在算一堆更能说明问题的信号:token 使用比例、压过几次、压缩失败过几次、最近有没有反复调同一个工具、连续几条命令失败(execution_engine.rs:381-407)—— 但这些只被拿去打了个日志(:416-429,调用点在 :4660-4672)。判断「这个会话是不是已经在原地打转」的信号全都在手,就是没接进决策。
凭什么这么改。 StateComp 讲的是「什么时候替换是安全的」,CTXAgent 讲的是「等阈值到了才压,既晚又浪费」。
怎么改(保守版,先不动阈值的意思)。 不改「超了就压」这条判定,只放宽提前预压的准入(compression_prefetch.rs:11-15):反复调同一个工具超过几次、或者连续几条命令失败时,就允许再往前一点开始预压。预压本来就有「对不上就丢」的保护,判断错了最多浪费一次候选生成。至于把信号接进真正的触发,得先回答「提前压是不是真的比压晚了更好」—— 建议先在「压前压后比对」里顺手记下「压缩发生时的真实 token 和还剩几轮」,攒够数据再定,不要现在拍一个阈值。
怎么算改好了。 看两个数:预压命中率(预压发起后真正被用上的比例)、压缩发生时的 token 分布(是不是不再总是贴着上限);同时确认「判断错了被丢掉」的比例没上升。
想翻旧账的时候,只能全文搜
现在是什么样。 能翻旧账的两块基建其实都在:原始记录文件 + 行号区间(compressor.rs:537-545)、大工具结果落盘 + 固定引用(tool_result_storage.rs)。但摘要里给出的指引是「先读索引,再读相关行区间,或者用 Grep 搜」—— 想取回某一条具体的观测,仍然是一次全文检索。
凭什么这么改。 ARC 的数字:按编号取回,查针测试 99.40%;靠相似度检索,最好的只有 88.12%。
怎么改。 开一个只读的取回工具,参数是「原始记录的行区间」或者「落盘结果的那个编号」,直接返回那一段。不用新存储,只加一层索引。
怎么算改好了。 同一批「压完需要查历史」的任务,取回它所需的轮数和 token 数都下降;不用先 Grep 再 Read 两步走。
怎么衡量到底有没有变好
先做离线回放,再谈真机对比:压缩本身只是一次消息拼装(compress_plan_with_contract 不碰任何 IO),历史会话的原始记录本来就在本地。
| 看什么 | 从哪来 | 说明什么 |
|---|---|---|
| 压完剩多少 token | 已有「压缩完成」事件里的 token 数 | 成本 |
| 压缩请求命中缓存的比例 | 新增 | 「复用主请求」这条路到底成立不成立 |
| 格式类失败救回来的比例 | 新增 | 换形态那套有没有用 |
| 摘要里没出处的条目占比 | 新增的核对结果 | 摘要可不可信 |
| 第 N 轮还剩几条钉住的规矩 | 压前压后比对 | 硬约束的衰减(对标那条 53% → 10% 曲线) |
| 比对失败次数 / 探针失败次数 | 新增 | 压缩能不能当回归项测 |
| 压缩耗时 | 已有 | 提前预压值不值 |
| 每个会话压几次 | 已有 | 触发策略的效果 |
| 压完头一轮重复调工具的比例 | 已有「反复调同一个工具」的计数,在压缩边界前后取差值 | 有没有重复做已经做完的事 |
| 摘要语言跑没跑 | 摘要里的中文字符占比 vs 原会话 | 语言有没有被拽成英文 |
| 向用户要已给信息的次数 | 压完头一轮里出现「请提供 / 再发一下」这类话 | 有没有向用户索要已经给过的东西 |
真机上再补一条「同一批任务,改前改后各跑一遍,比成功率」—— 主指标要放在任务成功率上,不是 token 数。SJC 和 Compaction Cliff 这两篇都是先证明了「token 省了」,然后跟着一句「任务掉了」。
明确不做的事
- 不做训练侧那套:把压缩决策学进模型的策略(AutoCompact)、用强化学习教它折叠(FoldGRPO)、在摘要后面接一串软记忆向量(Remory)—— 都要模型侧能力或训练数据,不在这个仓库的范围里。
- 不上向量检索、不做相似度召回:ARC 的结论恰恰是「按编号取回」赢过「按相似度找」,别走回头路。
- 不动服务商的缓存策略:压缩时重写一次开头是设计选择(现在明确让它失效一次),不做「轮内反复改开头」这种事。
- 不恢复「把旧工具输出直接抹掉」:当初删它的两个理由(缓存作废、记忆丢失)现在依然成立;要省 token 就走「按轮折叠」,不要回头改已经发出去的内容。
- 不碰「服务端压缩」那条路(就是 Complete OpenAI Responses support for GPT-5.6 prompt caching and remote compaction #2235 在讨论的 OpenAI Responses 那种):那是另一条策略,摘要由服务商生成、客户端只存一个不透明条目,跟这里的「本地压缩」是两条并列的路,不该混在一起做。这里只要求本地这条路自己能读、能审、能换模型搬走。
谁先做,坑在哪
- 先做第一件(失败别原样重打、连错就停、把缓存命中记下来)。它不动压出来的东西长什么样,风险最小,而且能给后面所有改动提供「命中缓存的比例」这条基线。但要先测一次现状 —— 现在到底有没有命中缓存,一点数据都没有。
- 第二件和第四件里「压前压后比对」可以并行,都只动
compressor.rs和新增的检查,不碰执行引擎。要注意的是摘要提示一变长,摘要请求更容易撑满 —— 用「按轮折叠」来抵。 - 第三件要改那份事实清单的结构(
agent_api.rs),得确认对外接口(ACP 那边、以及外部读这份清单的地方)会不会受影响。加的都是带默认值的追加字段,风险可控。 - 第五件纯改提示词,但和第二件、第四件共用同一段提示,建议合成一次改完。
- 第六件是唯一会改变「最终发出去的消息长什么样」的改动,必须和「压前压后比对」一起上,否则出了问题没法判断是折叠折错了还是摘要写糊了。
- 第七件和第八件独立性强,可以最后做。
再补一条:第二到第五件的产出其实都是「这次压缩的质量标签」,建议顺手一起露到压缩完成事件里(那里已经有「有没有摘要」「摘要是谁写的」两个字段,加字段成本很低),这样用户和后面做对比都能直接看见。
附录一:论文清单(14 篇,含关键数字)
精读并引用的
| 论文 | 出处 | 一句话 | 关键数字 |
|---|---|---|---|
| The Complexity Trap | arXiv:2508.21433 · NeurIPS'25 DL4C | 抹掉旧观测 vs 让模型写摘要 | 成本约降一半,解决率持平或略高 |
| ACON | arXiv:2510.00615 · ICML 2026 | 压缩指南本身也能迭代优化 | 峰值 token 降 26~54%,小模型成功率升 46% |
| Context-Folding / FoldGRPO | arXiv:2510.11967 | 开子过程做完就折成一句 | 活跃上下文小十倍 |
| LCM | arXiv:2605.04050 | 摘要做成树,每层留指针指回原文 | 32K~1M 每一档都超过 Claude Code |
| CTXAgent | arXiv:2610.34425 | 让模型自己管上下文 | 同分,成本比默认配置低 49% |
| StateComp | arXiv:2609.27298 | 按进度判断哪段可以替换 | token 降 52.27% |
| AutoCompact | arXiv:2610.02163 | 把压缩决策学进策略 | SWE-bench Verified 绝对提升 9.2 |
| SJC | arXiv:2610.20927 | 摘要逐条查出处,没出处的删掉 | 83% 的失败追到没出处的条目 |
| The Compaction Cliff | arXiv:2608.22752 · CIKM 2026 | 按内容类型分别定保真度 | 53% → 10%(五轮) |
| ARC | arXiv:2607.25066 | 每段观测有编号,可按键取回 | 99.40% vs 88.12% |
| Scroll | arXiv:2608.21690 | 把会话当成可执行的环境 | LongMemEval_S 94.8 |
| ReSum | arXiv:2509.13313 | 用摘要做重启点,继续深度搜索 | 只读了摘要,没细读全文 |
| Remory | arXiv:2610.11287 | 摘要后面接一串软记忆向量 | 只用 5.2% 的输入位置 |
| Persistent Context Graphs | arXiv:2609.40118 | 用持续生长的上下文图做压缩 | 只读了摘要,没细读全文 |
排队待读(没细读,不作为论据):Memento(2611.00244)、PAACE(2512.16970)、ACM(2607.23809)、Bud ContextCompact 2.0(2610.25516)、Self-Compacting LM Agents(2606.23525)、端到端摘要强化学习(2510.06727)。
附录二:这些实现是从哪看的
openai/codex—codex-rs/core/src/compact.rs(本地压缩主流程)、codex_prompts::SUMMARIZATION_PROMPT/SUMMARY_PREFIX。All-Hands-AI/software-agent-sdk—openhands/sdk/context/condenser/llm_summarizing_condenser.py(保留开头几条、压得太少就不压、失败时逐步截短重试)。QwenLM/qwen-code—packages/core/src/services/chatCompressionService.ts,PR #8339(缓存命中 0% → 96.7%)、#8418。anomalyco/opencode— 旧版session/(摘要模板与规则);新版packages/core/src/session/compaction.ts(追一句提醒、切分近期上下文)。MoonshotAI/kimi-code(新版,复用主请求)与kimi-cli(旧版,另发小请求)— 压缩指令原文、接回去的那段前缀。cline/cline— 旧版交给模型调工具交摘要 + 接回去的提示;新 SDK 版「到可用的九成」「保留最近 20k」。badlogic/pi-mono—packages/coding-agent/src/core/compaction/(摘要请求明确不许写缓存;切点落在超长回合中间时另写一段提示)。- Claude Code — 泄露的 source map 还原镜像(版本未知):那段「不许调工具」的狠话、正式的压缩提示词、锁死请求参数的做法、以及剥掉分析块的处理。相关 issue:#88755、#39374、#49994。
附录三:本项目代码位置
| 位置 | 是什么 |
|---|---|
assembly/core/.../execution/context_budget.rs:14-27 |
可用输入怎么算、一万 token 的安全余量 |
assembly/core/.../execution/execution_engine.rs:4460-4461 |
唯一的压缩判定 |
assembly/core/.../execution/execution_engine.rs:381-407 |
那些已经在算、但只用来打日志的健康信号 |
assembly/core/.../execution/execution_engine.rs:4612-4658 |
最后的兜底截断 |
assembly/core/.../execution/compression_lifecycle.rs:117-198 |
准备阶段(钩子、事件、摘要、原始记录) |
assembly/core/.../execution/compression_lifecycle.rs:199-332 |
提交阶段(重新对齐 + 核对参数) |
assembly/core/.../execution/compression_lifecycle.rs:350-366 |
让缓存失效 / 压缩次数计数 |
assembly/core/.../execution/compression_request.rs:36-89 |
那十次重试(不分网络类和格式类) |
assembly/core/.../execution/compression_job.rs:214-234 |
压缩请求的形态(复用主请求 + 末尾一句) |
assembly/core/.../execution/compression_job.rs:37-94 |
预压结果的领取与合并 |
assembly/core/.../session/compression/compressor.rs:585-604 |
摘要提示词 |
assembly/core/.../session/compression/compressor.rs:580-583 |
摘要输出唯一的校验 |
assembly/core/.../session/compression/compressor.rs:295-315 |
工具调用不切断的边界 |
assembly/core/.../session/compression/compressor.rs:440-496 |
当前轮任务清单快照 |
assembly/core/.../session/compression/compressor.rs:498-545 |
原始记录引用 |
assembly/core/.../session/compression/mod.rs:1-11 |
「抹掉旧观测」为什么被删 |
assembly/core/.../core/message.rs:226-243 |
压缩条目的数据结构 |
contracts/runtime-ports/src/agent_api.rs:1247-1298 |
那份结构化事实清单 |
agent-runtime/src/compression_prefetch.rs:9-15 |
提前预压的窗口 |
assembly/core/.../tools/tool_result_storage.rs:1-76 |
超大的工具结果落盘 |
Activity
[-]feat(context): 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/-][+][Feature]: 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/+][-][Feature]: 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/-][+][Feature]: 对于compact 时机和压缩上下文组成的调研[/+]litterbug23 commentedon Nov 10, 2026