Skip to content

[Feature]: 对于compact 时机和压缩上下文组成的调研 #3829

Open

Description

@Raing5Days

这是提案,不是报 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%

明确不做的事(训练侧那套、上向量库、改服务商缓存策略)放在最后一节。

这个仓库里已经有过几次相关反馈

提之前先把已有的对上,免得重复劳动:

所以这篇不是从零提需求,而是把「压完为什么会降智、会复读」这件事,拆成几块可以逐项验收的东西。


现在的压缩是怎么走的

什么时候压

  • 先把预算算出来:可用输入 = 上下文窗口 − 预留给输出的大小 − 一万 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

  1. changed the title [-]feat(context): 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/-] [+][Feature]: 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/+] on Nov 8, 2026
  2. changed the title [-][Feature]: 摘要要说得出出处,规矩要留得住,失败别在最贵的时候反复重试[/-] [+][Feature]: 对于compact 时机和压缩上下文组成的调研[/+] on Nov 8, 2026
  3. litterbug23 commented on Nov 10, 2026

    @litterbug23
    Image 经常会出现这类问题,是不是上下文的压缩机制还存在问题?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions