Skip to content

长会话稳定性:上下文压缩、签名缓存与工具结果压缩

你在用 Claude Code / Cherry Studio 之类的客户端跑长会话时,最烦的不是模型不够聪明,而是对话跑着跑着突然开始报错:Prompt is too long、400 签名错误、工具调用链断了、或者工具循环越跑越卡。

这一课把 Antigravity Tools 为这些问题做的三件事讲清楚:上下文压缩(分三层逐步介入)、签名缓存(把 Thinking 的签名链续上)、工具结果压缩(避免工具输出把上下文塞爆)。

学完你能做什么

  • 说清楚三层渐进式上下文压缩分别在做什么、各自的代价是什么
  • 知道签名缓存存了哪些东西(Tool/Family/Session 三层)以及 2 小时 TTL 的影响
  • 理解工具结果压缩的规则:何时会丢掉 base64 图片、何时会把浏览器快照变成头+尾摘要
  • 需要时能通过 proxy.experimental 的阈值开关调节压缩触发时机

你现在的困境

  • 长对话后突然开始 400:看起来像签名失效,但你不知道签名从哪来、丢在哪
  • 工具调用越来越多,历史 tool_result 堆到上游直接拒绝(或变得极慢)
  • 你想用压缩救场,但又担心破坏 Prompt Cache、影响一致性或让模型丢信息

什么时候用这一招

  • 你在跑长链路工具任务(搜索/读文件/浏览器快照/多轮工具循环)
  • 你在用 Thinking 模型做复杂推理,且会话经常超过几十轮
  • 你在排查客户端能复现但你说不清为什么的稳定性问题

什么是上下文压缩

上下文压缩是代理在检测到上下文压力过高时,对历史消息做的自动降噪与瘦身:先裁掉旧的工具轮,再把旧的 Thinking 文本压成占位符但保留签名,最后在极端情况下生成 XML 摘要并 Fork 一个新会话继续对话,从而降低 Prompt is too long 和签名链断裂导致的失败。

上下文压力是怎么计算的?

Claude 处理器会用 ContextManager::estimate_token_usage() 做一个轻量估算,并用 estimation_calibrator 校准,然后用 usage_ratio = estimated_usage / context_limit 得到压力百分比(日志里会打印 raw/calibrated 值)。

🎒 开始前的准备

  • 你已经跑通本地代理,并且客户端确实在走 /v1/messages 这条链路(见启动本地反代并接入第一个客户端)
  • 你能查看代理日志(开发者调试或本地日志文件)。仓库里的测试方案给了一个示例日志路径与 grep 方式(见 docs/testing/context_compression_test_plan.md

配合 Proxy Monitor 更好定位

如果你要把压缩触发与哪类请求/哪个账号/哪轮工具调用对上号,建议同时开着 Proxy Monitor。

核心思路

这套稳定性设计不是直接把历史全删了,而是按代价从低到高逐层介入:

层级触发点(可配置)做了什么代价/副作用
Layer 1proxy.experimental.context_compression_threshold_l1(默认 0.4)识别工具轮,只保留最近 N 轮(代码里是 5),把更早的 tool_use/tool_result 对删掉不改剩余消息内容,对 Prompt Cache 更友好
Layer 2proxy.experimental.context_compression_threshold_l2(默认 0.55)把旧的 Thinking 文本压成 "...",但保留 signature,并保护最近 4 条消息不动会修改历史内容,注释里明确会 break cache,但能保住签名链
Layer 3proxy.experimental.context_compression_threshold_l3(默认 0.7)调用后台模型生成 XML 摘要,然后 Fork 一个新消息序列继续对话依赖后台模型调用;若失败会返回 400(有友好提示)

接下来按三层拆开讲,顺便把签名缓存和工具结果压缩放在一起。

Layer 1:工具轮裁剪(Trim Tool Messages)

Layer 1 的关键点是只删整轮工具交互,避免半删导致上下文不一致。

  • 一轮工具交互的识别规则在 identify_tool_rounds()assistant 里出现 tool_use 开始一轮,后续 user 里出现 tool_result 仍算这一轮,直到遇到普通 user 文本结束这一轮。
  • 真正执行裁剪的是 ContextManager::trim_tool_messages(&mut messages, 5):当历史工具轮超过 5 轮时,删掉更早的轮次里涉及的消息。

Layer 2:Thinking 压缩但保留签名

很多 400 问题并不是 Thinking 太长,而是 Thinking 的签名链断了。Layer 2 的策略是:

  • 只处理 assistant 消息里的 ContentBlock::Thinking { thinking, signature, .. }
  • 只有在 signature.is_some()thinking.len() > 10 时才压缩,把 thinking 直接改成 "..."
  • 最近 protected_last_n = 4 条消息不压缩(大致是最近 2 轮 user/assistant)

这样可以省掉大量 Token,但仍把 signature 留在历史里,避免工具链需要回填签名时无从恢复。

Layer 3:Fork + XML 摘要(极限兜底)

当压力继续升高时,Claude 处理器会尝试重开会话但不丢关键信息:

  1. 从原始消息里提取最后一个有效的 Thinking 签名(ContextManager::extract_last_valid_signature()
  2. 把整个历史 + CONTEXT_SUMMARY_PROMPT 拼成一个生成 XML 摘要的请求,模型固定为 BACKGROUND_MODEL_LITE(当前代码是 gemini-2.5-flash
  3. 摘要里要求包含 <latest_thinking_signature>,用于后续签名链延续
  4. Fork 出一个新消息序列:
    • User: Context has been compressed... + XML summary
    • Assistant: I have reviewed...
    • 再附上原请求最后一条 user 消息(如果它不是刚刚的摘要指令)

如果 Fork + 摘要失败,会直接返回 StatusCode::BAD_REQUEST,并提示你用 /compact/clear 等方式手动处理(见处理器返回的 error JSON)。

旁路 1:三层签名缓存(Tool / Family / Session)

签名缓存是上下文压缩的保险丝,尤其是客户端会裁剪/丢弃签名字段时。

  • TTL:SIGNATURE_TTL = 2 * 60 * 60(2 小时)
  • Layer 1:tool_use_id -> signature(工具链恢复)
  • Layer 2:signature -> model family(跨模型兼容性检查,避免 Claude 签名被带到 Gemini 家族模型上)
  • Layer 3:session_id -> latest signature(会话级隔离,避免不同对话污染)

这三层缓存会在 Claude SSE 流式解析与请求转换时被写入/读取:

  • 流式解析到 thinking 的 signature 会写入 Session Cache(以及缓存 family)
  • 流式解析到 tool_use 的 signature 会写入 Tool Cache + Session Cache
  • 在把 Claude 工具调用转换为 Gemini functionCall 时,会优先从 Session Cache 或 Tool Cache 把签名补回去

旁路 2:工具结果压缩(Tool Result Compressor)

工具结果往往比聊天文本更容易把上下文塞爆,所以请求转换阶段会对 tool_result 做可预期的删减。

核心规则(都在 tool_result_compressor.rs):

  • 总字符上限:MAX_TOOL_RESULT_CHARS = 200_000
  • base64 图片块直接移除(追加一段提示文本)
  • 如果检测到输出已保存到文件的提示,会提取关键信息并用 [tool_result omitted ...] 占位
  • 如果检测到浏览器快照(包含 page snapshot / ref= 等特征),会改成头 + 尾摘要,并标注省略了多少字符
  • 如果输入像 HTML,会先移除 <style>/<script>/base64 片段再做截断

跟我做

第 1 步:确认压缩阈值(以及默认值)

为什么 压缩触发点不是写死的,来自 proxy.experimental.*。你得先知道当前阈值,才能判断为什么它这么早/这么晚才介入。

默认值(Rust 侧 ExperimentalConfig::default()):

json
{
  "proxy": {
    "experimental": {
      "enable_signature_cache": true,
      "enable_tool_loop_recovery": true,
      "enable_cross_model_checks": true,
      "enable_usage_scaling": true,
      "context_compression_threshold_l1": 0.4,
      "context_compression_threshold_l2": 0.55,
      "context_compression_threshold_l3": 0.7
    }
  }
}

你应该看到:你的配置里存在 proxy.experimental(字段名与上面一致),并且阈值是 0.x 这样的比例值。

配置文件位置不在这一课重复讲

配置文件的落盘位置与修改后是否需要重启,属于配置管理范畴。按这套教程体系,优先以配置全解:AppConfig/ProxyConfig、落盘位置与热更新语义为准。

第 2 步:用日志确认 Layer 1/2/3 是否触发

为什么 这三层都是代理内部行为,最可靠的验证方式是看日志里是否出现 [Layer-1] / [Layer-2] / [Layer-3]

仓库的测试方案给了一个示例命令(按需调整为你机器上的实际日志路径):

bash
tail -f ~/Library/Application\ Support/com.antigravity.tools/logs/antigravity.log | grep -E "Layer-[123]"

你应该看到:当压力升高时,日志出现类似 Tool trimming triggeredThinking compression triggeredFork successful 的记录(具体字段以日志原文为准)。

第 3 步:理解净化和压缩的差别(不要混用预期)

为什么 有些问题(比如强制降级到不支持 Thinking 的模型)需要净化而不是压缩。净化会直接删掉 Thinking block;压缩会保留签名链。

在 Claude 处理器里,后台任务降级会走 ContextManager::purify_history(..., PurificationStrategy::Aggressive),它会把历史 Thinking block 直接移除。

你应该看到:你能区分两类行为:

  • 净化是删掉 Thinking block
  • Layer 2 压缩是把旧 Thinking 文本替换成 "...",但签名还在

第 4 步:当你遇到 400 签名错误,先看 Session Cache 是否命中

为什么 很多 400 的根因不是没有签名,而是签名没跟着消息走。请求转换时会优先从 Session Cache 补签名。

线索(请求转换阶段的日志会提示从 SESSION/TOOL cache 恢复签名):

  • [Claude-Request] Recovered signature from SESSION cache ...
  • [Claude-Request] Recovered signature from TOOL cache ...

你应该看到:当客户端丢签名但代理缓存还在时,日志里会出现 Recovered signature from ... cache 的记录。

第 5 步:理解工具结果压缩的会丢什么

为什么 如果你让工具把大段 HTML / 浏览器快照 / base64 图片塞回对话,代理会主动删减。你需要提前知道哪些内容会被替换成占位符,避免误以为模型没看见。

重点记三条:

  1. base64 图片会被移除(改成提示文本)
  2. 浏览器快照会变成 head/tail 摘要(带省略字符数)
  3. 超过 200,000 字符会被截断并追加 ...[truncated ...] 提示

你应该看到:在 tool_result_compressor.rs 里,这些规则都有明确的常量和分支,不是凭经验删。

检查点

  • 你能说清楚 L1/L2/L3 的触发点来自 proxy.experimental.context_compression_threshold_*,默认是 0.4/0.55/0.7
  • 你能解释为什么 Layer 2 会 break cache:因为它会修改历史 thinking 文本内容
  • 你能解释为什么 Layer 3 叫 Fork:它会把对话变成 XML 摘要 + 确认 + 最新 user 消息的新序列
  • 你能说明工具结果压缩会删掉 base64 图片,并把浏览器快照改成 head/tail 摘要

踩坑提醒

现象可能原因你可以怎么做
触发了 Layer 2 之后感觉上下文没那么稳了Layer 2 会修改历史内容,注释里明确它会 break cache如果你依赖 Prompt Cache 的一致性,尽量让 L1 先解决问题,或提高 L2 阈值
Layer 3 触发后直接返回 400Fork + 摘要调用后台模型失败(网络/账号/上游错误等)先按错误 JSON 里的建议用 /compact/clear;同时检查后台模型调用链路
工具输出里图片/大段内容不见了tool_result 会移除 base64 图片、截断超长输出把重要内容落到本地文件/链接里再引用;别指望把 10 万行文本直接塞回对话
明明用的是 Gemini 模型却带了 Claude 签名导致报错签名跨模型不兼容(代码里有 family 检查)确认签名来源;必要时让代理在 retry 场景下剥离历史签名(见请求转换逻辑)

本课小结

  • 三层压缩的核心是按代价分级:先删旧工具轮,再压缩旧 Thinking,最后才 Fork + XML 摘要
  • 签名缓存是让工具链不断的关键:Session/Tool/Family 三层各管一类问题,TTL 是 2 小时
  • 工具结果压缩是避免工具输出把上下文塞爆的硬限制:200,000 字符上限 + 快照/大文件提示特化

下一课预告

下一课我们聊系统能力:多语言/主题/更新/开机自启/HTTP API Server。


附录:源码参考

点击展开查看源码位置

更新时间:2026-01-23

功能文件路径行号
实验性配置:压缩阈值与开关默认值src-tauri/src/proxy/config.rs119-168
上下文估算:多语言字符估算 + 15% 余量src-tauri/src/proxy/mappers/context_manager.rs9-37
Token 用量估算:遍历 system/messages/tools/thinkingsrc-tauri/src/proxy/mappers/context_manager.rs103-198
Layer 1:识别工具轮 + 裁剪旧轮次src-tauri/src/proxy/mappers/context_manager.rs311-439
Layer 2:Thinking 压缩但保留签名(保护最近 N 条)src-tauri/src/proxy/mappers/context_manager.rs200-271
Layer 3 辅助:提取最后一个有效签名src-tauri/src/proxy/mappers/context_manager.rs73-109
后台任务降级:Aggressive 净化 Thinking blocksrc-tauri/src/proxy/handlers/claude.rs540-583
三层压缩主流程:估算、校准、按阈值触发 L1/L2/L3src-tauri/src/proxy/handlers/claude.rs379-731
Layer 3:XML 摘要 + Fork 会话实现src-tauri/src/proxy/handlers/claude.rs1560-1687
签名缓存:TTL/三层缓存结构(Tool/Family/Session)src-tauri/src/proxy/signature_cache.rs5-88
签名缓存:Session 签名写入/读取src-tauri/src/proxy/signature_cache.rs141-223
SSE 流式解析:缓存 thinking/tool 的 signature 到 Session/Tool cachesrc-tauri/src/proxy/mappers/claude/streaming.rs766-776
---------
请求转换:tool_use 优先从 Session/Tool cache 补签名src-tauri/src/proxy/mappers/claude/request.rs1045-1142
请求转换:tool_result 触发工具结果压缩src-tauri/src/proxy/mappers/claude/request.rs1159-1225
工具结果压缩:入口 compact_tool_result_text()src-tauri/src/proxy/mappers/tool_result_compressor.rs28-69
工具结果压缩:浏览器快照 head/tail 摘要src-tauri/src/proxy/mappers/tool_result_compressor.rs123-178
工具结果压缩:移除 base64 图片 + 总字符上限src-tauri/src/proxy/mappers/tool_result_compressor.rs247-320
测试方案:三层压缩触发与日志验证docs/testing/context_compression_test_plan.md1-116