附录 C|远程 compact v2:压缩是 provider 能力,不是第二种产品
先别把远端总结理解成「换一个模型把历史重写掉」
B4 把 compact 钉成 Task + 采样前钩子,并说本地还是远端看 RemoteCompactionSupport。没有这一附录,会把 v2 理解成再 POST 一次聊天,或把图片从摘要里随便砍掉导致 <image> 标签断裂。
判断先说清楚:v2 是 Responses 端点上的 compaction_trigger 能力(model-provider/src/provider.rs:46-50)。 Core 只消费 provider 声明的上界;配置可以再关,不能对 Unsupported 的 provider 假装有远程压缩。实现是「请服务端压」,不是本地把 SUMMARIZATION_PROMPT 当用户话再跑一轮 Regular turn。落盘和替换历史仍走同一套 replace_compacted_history——远端改变的是 谁写摘要,不是「禁止 rewrite」这条纪律。
TokenBudget feature 开着时,手动和自动 compact 都不走 v2(tasks/compact.rs:36-38,session/turn.rs:1408-1417)。那是另一条窗口重置,别和 v2 焊在一起。
1. 分叉点:provider 能力,然后才是手动 / 自动
CompactTask 和采样前 auto-compact 用同一条 match(tasks/compact.rs:41-67,turn.rs:1420-1452):
remote_compaction | 走哪 |
|---|---|
V2 | compact_remote_v2::run_remote_compact_task / run_inline_remote_auto_compact_task |
Unsupported | 本地 compact::run_compact_task,输入是总结提示 |
遥测标签 remote_v2 vs local。ModelClient 的 beta 头会 无条件带上 RemoteCompactionV2 这个 feature key(session/mod.rs:1118-1131),即使菜单里没单独打开——广告的是「客户端懂 v2」,真正打不打仍看当前 provider 的 capabilities。
手动 /compact:standalone turn,新 capture 一份 StepContext(compact_remote_v2.rs:109-112),InitialContextInjection::DoNotInject。自动:复用正在采样前的 step,并可带 fallback_step_context(换模型再试)。轮前失败仍由 run_turn 保住还没入历史的用户话(B4);非 PreTurn 才会立刻发 Error 事件(:200-210)。
Compact 不能 steer(B1)。远端再慢也是这一 Task,不是开第二条用户可见 Turn 让你插话。
2. 一次 attempt:先削工具输出,再打 compaction 流
run_remote_compact_v2_attempt(compact_remote_v2_attempt.rs:31-47)先 clone_history,再 trim_function_call_history_to_fit_context_window:工具输出太大,先截成「Output exceeded the available model context…」(compact_remote_history.rs:16-17),否则 compaction 请求自己都塞不进窗口。
请求种类是 ResponsesStreamRequest::RemoteCompactionV2。流重试 故意比普通 turn 少:MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES = 2(compact_remote_v2.rs:75-77),再和 provider 的 stream_max_retries() 取 min(:372-376)。注释:compact 可能跑很久,传输层预算要更小,避免一次压缩把整段 Session 的 HTTP fallback 耗光(对照 B1:HTTP fallback 是 Session 级)。
只收集 ResponseItem::Compaction 当产出(:431-437)。这是服务端的压缩 item,不是模型闲聊。effort 用 RequestEffortUsage::Compaction,和普通采样分开记账。
第一次 attempt 失败且 should_retry_with_current_model(InvalidRequest、超窗、额度、过载、5xx、RetryLimit,compact_model_fallback.rs:8-19),并且有 fallback_step_context,才换当前模型再打一枪(compact_remote_v2.rs:246-285)。第二次再失败,把 第一次 的 error 抛出去。TurnAborted 不走这条降级。
没有「削输出 + 小重试 + 可选换模型」会怎样?压缩请求自己 ContextWindowExceeded,然后按普通 turn 的重试把 sticky WS 打到 HTTP,后面所有对话都失去预热。v2 把压缩当成长请求、短重试。
3. 装回去:64k 保留窗,图片要整块留
服务端摘要回来之后,build_v2_compacted_history(:483-509)不是「只留 Compaction 一项」:
- 按组过滤:哪些边界消息 保留原文(含可选的 client-authored developer 消息,feature
RetainClientDeveloperMessages) truncate_retained_messages(..., RETAINED_MESSAGE_TOKEN_BUDGET = 64_000, image_budget)- 最后 追加 compaction 产出
64k 是保留消息的硬顶,从 最近往前 吃额度(:598-612)。超了丢更早的组,不是改 compaction 正文。
图片:Feature::CompactionImageBudget 开了才 RetainedImageBudget::Enabled(:309-313)。开启时,非 client-developer 消息按图+文一起计费;truncate_message_to_token_budget 把 图和两侧 harness 标签当成原子块(compact_remote_v2_images.rs:28-59),从后往前留。关预算则图不计入这条 64k(仍可能物理留在 item 里,但不和文字抢同一本账)。音频不收费、照旧保留。
没有原子块会怎样?留下半个 InputImage、丢掉 <image> 开标签,下一轮模型看见烂 XML,cache 和 view_image 全乱。v2 的图片策略是「整张图要么留下要么整组丢掉」,不是对 data URL 做 substring。
装入 live history:仍 insert_initial_context_before_last_real_user_or_summary + replace_compacted_history(:317-338)。轮前 DoNotInject,下一 Regular turn 全量再注 world state(B4)。trace 记下 input_history 和 replacement_history,resume 重放的是替换后的 JSONL,不是「再向服务端要一次压缩」。
4. 和本地 compact 什么相同、什么不同
| 本地 | 远程 v2 | |
|---|---|---|
| 谁写摘要 | 本机再采样,输入是总结 prompt | Responses compaction item |
| 能否 steer | 否 | 否 |
| 失败保用户话 | 是(PreTurn) | 是 |
| 历史替换 | CompactedItem + fragment | 同左,外加可选保留消息+图 |
| 重试 | 普通模型重试 | 流最多 2 次;可换 fallback 模型一次 |
| Provider | 任何能聊天的 | 必须声明 V2 |
抄 harness:先实现本地 compact + JSONL 替换(F3 P1)。v2 是「provider 会压」时的加速路径,不是 P0。API Key 路径有没有 V2,看该 provider 的 capabilities,不要抄 ChatGPT 后端的假能力(A1)。
5. 结语:带走一句话
远程 compact v2 是 provider 声明的 Responses 压缩:少重试、可换模型再打一枪、装回历史时 64k 保留窗且图片整块计费——它不改「采样前压、失败保住用户话、只追加不 rewrite」这三条,只是换了谁写那份摘要。