长会话稳定性:上下文压缩、签名缓存与工具结果压缩
你在用 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 1 | proxy.experimental.context_compression_threshold_l1(默认 0.4) | 识别工具轮,只保留最近 N 轮(代码里是 5),把更早的 tool_use/tool_result 对删掉 | 不改剩余消息内容,对 Prompt Cache 更友好 |
| Layer 2 | proxy.experimental.context_compression_threshold_l2(默认 0.55) | 把旧的 Thinking 文本压成 "...",但保留 signature,并保护最近 4 条消息不动 | 会修改历史内容,注释里明确会 break cache,但能保住签名链 |
| Layer 3 | proxy.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 处理器会尝试重开会话但不丢关键信息:
- 从原始消息里提取最后一个有效的 Thinking 签名(
ContextManager::extract_last_valid_signature()) - 把整个历史 +
CONTEXT_SUMMARY_PROMPT拼成一个生成 XML 摘要的请求,模型固定为BACKGROUND_MODEL_LITE(当前代码是gemini-2.5-flash) - 摘要里要求包含
<latest_thinking_signature>,用于后续签名链延续 - Fork 出一个新消息序列:
User: Context has been compressed... + XML summaryAssistant: 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()):
{
"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]。
仓库的测试方案给了一个示例命令(按需调整为你机器上的实际日志路径):
tail -f ~/Library/Application\ Support/com.antigravity.tools/logs/antigravity.log | grep -E "Layer-[123]"你应该看到:当压力升高时,日志出现类似 Tool trimming triggered、Thinking compression triggered、Fork 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 图片塞回对话,代理会主动删减。你需要提前知道哪些内容会被替换成占位符,避免误以为模型没看见。
重点记三条:
- base64 图片会被移除(改成提示文本)
- 浏览器快照会变成 head/tail 摘要(带省略字符数)
- 超过 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 触发后直接返回 400 | Fork + 摘要调用后台模型失败(网络/账号/上游错误等) | 先按错误 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.rs | 119-168 |
| 上下文估算:多语言字符估算 + 15% 余量 | src-tauri/src/proxy/mappers/context_manager.rs | 9-37 |
| Token 用量估算:遍历 system/messages/tools/thinking | src-tauri/src/proxy/mappers/context_manager.rs | 103-198 |
| Layer 1:识别工具轮 + 裁剪旧轮次 | src-tauri/src/proxy/mappers/context_manager.rs | 311-439 |
| Layer 2:Thinking 压缩但保留签名(保护最近 N 条) | src-tauri/src/proxy/mappers/context_manager.rs | 200-271 |
| Layer 3 辅助:提取最后一个有效签名 | src-tauri/src/proxy/mappers/context_manager.rs | 73-109 |
| 后台任务降级:Aggressive 净化 Thinking block | src-tauri/src/proxy/handlers/claude.rs | 540-583 |
| 三层压缩主流程:估算、校准、按阈值触发 L1/L2/L3 | src-tauri/src/proxy/handlers/claude.rs | 379-731 |
| Layer 3:XML 摘要 + Fork 会话实现 | src-tauri/src/proxy/handlers/claude.rs | 1560-1687 |
| 签名缓存:TTL/三层缓存结构(Tool/Family/Session) | src-tauri/src/proxy/signature_cache.rs | 5-88 |
| 签名缓存:Session 签名写入/读取 | src-tauri/src/proxy/signature_cache.rs | 141-223 |
| SSE 流式解析:缓存 thinking/tool 的 signature 到 Session/Tool cache | src-tauri/src/proxy/mappers/claude/streaming.rs | 766-776 |
| --- | --- | --- |
| 请求转换:tool_use 优先从 Session/Tool cache 补签名 | src-tauri/src/proxy/mappers/claude/request.rs | 1045-1142 |
| 请求转换:tool_result 触发工具结果压缩 | src-tauri/src/proxy/mappers/claude/request.rs | 1159-1225 |
工具结果压缩:入口 compact_tool_result_text() | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 28-69 |
| 工具结果压缩:浏览器快照 head/tail 摘要 | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 123-178 |
| 工具结果压缩:移除 base64 图片 + 总字符上限 | src-tauri/src/proxy/mappers/tool_result_compressor.rs | 247-320 |
| 测试方案:三层压缩触发与日志验证 | docs/testing/context_compression_test_plan.md | 1-116 |