摘要
Claude Code 这类 coding agent 的上下文不是普通聊天记录,而是由用户意图、模型推理、工具调用、工具结果、错误反馈和代码片段共同组成的执行日志。工具调用越多,历史越快膨胀;如果直接把所有 tool_result 长期保留在上下文里,模型会被过期输出、重复堆栈、大段文件内容和无关日志污染。反过来,如果清理过猛,又会破坏 tool_use.id 与 tool_result.tool_use_id 的配对、DeepSeek thinking mode 所需的 reasoning 回放,以及 prompt cache 的稳定前缀。
因此,工具历史清理不能只是“截断消息”。它应当被设计成一个上下文生命周期管理层:在不破坏协议状态机的前提下,把可丢弃的工具结果转化为可审计、可摘要、可复现的紧凑历史,并与缓存边界协同。
为什么工具历史会成为稳定性问题
在 Anthropic 协议中,模型返回 tool_use 后,应用必须在紧接着的 user 消息中返回对应 tool_result。这让工具结果天然进入对话历史。对于 coding agent,常见工具结果包括:
| 工具类型 | 典型体积 | 污染风险 |
|---|---|---|
| 文件读取 | 数 KB 到数百 KB | 旧代码片段长期留在上下文里,误导后续修改 |
| 搜索结果 | 数十到数百条命中 | 重复路径和片段稀释当前目标 |
| 测试输出 | 数 KB 到数 MB | 失败堆栈反复出现,模型误判问题仍存在 |
| 依赖安装 / 构建日志 | 数百 KB 以上 | 噪声极高,token 成本不可控 |
| 浏览器 / QA 结果 | 截图描述、DOM、日志 | 过期页面状态污染后续判断 |
历史膨胀会带来三类后果。
第一是成本和延迟。上下文越大,每轮请求的输入 token 越高,缓存命中失败后延迟也会明显上升。
第二是推理污染。模型在后续轮次里可能把已修复的错误、旧的文件内容或一次性工具输出当成仍然有效的事实。
第三是协议破坏。直接删除某些消息可能导致工具调用链不完整,进而触发类似“存在 tool_use id 但没有 tool_result block”的错误,或让需要 reasoning 回放的模型失去状态。
清理策略:保状态机,清大结果
一个稳健的清理策略应当区分三类内容:
1、协议必需内容:最近一轮未闭合的 tool_use、紧随其后的 tool_result、DeepSeek thinking mode 要求回传的真实 reasoning_content。这些内容不能清理或改写。
2、语义必需内容:当前任务仍依赖的文件片段、测试结论、用户约束、决策记录。这些内容可以摘要,但摘要必须保留决策依据。
3、可审计但非上下文必需内容:完整日志、完整堆栈、历史搜索结果、过期工具输出。这些内容应移出模型上下文,存入审计日志或 artifact,仅在需要复现时引用。
推荐实现一个 ToolHistoryManager:
type ToolHistoryRecord = {
toolUseId: string;
toolName: string;
callInputHash: string;
resultHash: string;
status: "ok" | "error" | "truncated" | "summarized";
rawResultRef?: string;
summary?: string;
tokenEstimate: number;
createdAt: string;
cacheBoundary: "prefix" | "mutable" | "ephemeral";
};
模型上下文中保留的是 summary 和必要的协议块;完整结果通过 rawResultRef 存入本地日志、对象存储或调试 artifact。这样既能减少 token,又能在审计时恢复原始执行轨迹。
与 Anthropic context editing 的关系
Anthropic 文档中已经提供了面向工具结果清理的 context editing / beta 能力,例如来源文件提到的 context-management-2025-06-27 和 clear_tool_uses_20250919。这些能力的价值在于:让模型服务端或 SDK 层可以自动清理旧工具结果,降低上下文压力。
但兼容层不能把它当成无条件开关,原因有两个。
第一,跨模型代理并不总是直连 Anthropic。有些请求会被转换到 OpenAI-compatible API、DeepSeek、Qwen 或其他本地推理服务。服务端 context editing 能力不一定存在。
第二,工具结果清理会影响缓存。只要历史中的工具块被删除或重写,包含这些工具历史的 prompt cache 前缀就可能失效。对于依赖长前缀缓存的 coding agent,这会把“省 token”变成“缓存击穿后的高延迟”。
因此,兼容层应当把 context editing 作为后端能力之一,而不是唯一策略:优先使用模型提供商原生清理能力;没有原生能力时,在代理层做摘要和裁剪;无论哪种方式,都要显式管理缓存边界。
缓存协同:把稳定前缀和工具历史分开
prompt cache 最怕频繁变化的前缀。工具历史恰好是高频变化内容,因此不应和稳定系统提示、工具定义、项目规则混在同一个缓存前缀里。
推荐把上下文分为四段:
| 分段 | 内容 | 缓存策略 |
|---|---|---|
| Stable Prefix | system prompt、工具定义、项目规则、编码规范 | 尽量稳定,作为主要 cache 前缀 |
| Task State | 当前用户目标、关键决策、当前文件摘要 | 中频变化,可单独缓存或短期复用 |
| Live Tool Window | 最近 N 轮未清理工具调用和结果 | 高频变化,不追求长缓存 |
| Audit References | 被清理工具结果的摘要和引用 | 小体积保留,原文外置 |
关键原则是:不要让一次大日志输出进入 stable prefix。工具定义也应保持排序稳定、schema 稳定,避免每轮重新生成导致 cache miss。
cache key 策略
兼容层需要把缓存边界显式写进上下文构造器,而不是依赖消息数组的自然顺序。一个可操作的 cache key 可以拆成四段:
| key 段 | 参与字段 | 变化频率 | 处理方式 |
|---|---|---|---|
system_key |
system prompt、工具治理策略、模型 profile | 低 | 作为稳定前缀缓存 |
tool_schema_key |
工具名、description、JSON Schema、排序版本 | 低到中 | schema 变更才失效 |
task_state_key |
当前任务摘要、关键文件 hash、用户约束 | 中 | 可短期复用 |
live_window_key |
最近工具调用、最新工具结果、错误状态 | 高 | 通常不追求长缓存 |
清理策略的目标不是让所有 key 都稳定,而是把高频变化限制在 live_window_key。如果清理工具历史时改写了 system_key 或 tool_schema_key,说明上下文分层已经失败。
清理前后消息示例
清理前,历史可能长成这样:
[
{"role": "assistant", "content": [{"type": "tool_use", "id": "toolu_1", "name": "run_tests", "input": {"cmd": "npm test"}}]},
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "数万字符测试日志..."}]},
{"role": "assistant", "content": "我看到 A、B、C 三个失败..."}
]
清理后,不应破坏最近未闭合状态;对已归档历史,可以改写成:
[
{"role": "system", "content": "工具结果归档摘要可通过 audit_ref 复现。"},
{"role": "user", "content": [{
"type": "archived_tool_result_summary",
"tool_use_id": "toolu_1",
"status": "error",
"summary": "npm test 失败;关键失败为 tool_result 配对缺失;完整日志见 audit_ref。",
"audit_ref": "audit/tool-results/2026-06-11/toolu_1.log",
"sha256": "..."
}]},
{"role": "assistant", "content": "基于归档摘要,当前待处理问题是 tool_result 配对缺失。"}
]
这类 archived_tool_result_summary 是兼容层内部表示,不应假装成 Anthropic 官方 block。渲染到具体模型前,要根据 provider profile 转成普通文本摘要、系统 side channel,或模型支持的 context editing 格式。
清理算法
一个可落地的清理流程如下:
1、闭合检查:确认没有未返回结果的 tool_use。如果存在未闭合工具调用,禁止清理。
2、模型约束检查:如果目标模型要求回传 reasoning_content 或 thinking,保留真实原文引用和必要 block。
3、重要性评分:按当前任务、最近使用时间、工具类型、输出体积、错误状态给工具结果打分。
4、摘要生成:对可清理结果生成结构化摘要,例如“命令、退出码、关键错误、涉及文件、结论”。
5、外置原文:将原始结果写入审计存储,记录 hash 和路径。
6、替换上下文:用摘要块替代大结果,保留 tool_use_id 的历史关系或转成已归档事件。
7、缓存重算:如果清理影响了缓存前缀,更新 cache key;如果只影响 live window,不触碰 stable prefix。
摘要格式应避免自然语言过度发挥。更好的做法是结构化:
{
"tool": "run_tests",
"status": "error",
"exit_code": 1,
"key_findings": [
"tests/tool_use_mapper.test.ts failed",
"missing tool_result for toolu_abc123"
],
"raw_ref": "audit/tool-results/2026-06-11/toolu_abc123.log",
"sha256": "..."
}
摘要质量门槛
工具结果摘要必须可用于继续推理,也必须可用于审计复现。建议设置四个门槛:
1、可判定:保留工具名、状态、退出码或错误类型,不能只写“工具失败”。
2、可定位:保留关键文件、命令、URL、测试名或资源 id。
3、可复现:保留 raw_ref、hash、时间和模型会话 id。
4、可过期:标注摘要生成时间、所依据的文件 hash 或 commit,防止模型把旧结果当成当前事实。
如果工具结果会影响安全决策、费用支出、外部写入或用户数据,摘要不能替代审计原文,只能替代模型上下文里的大块内容。
与治理审计层对接
第 6 篇文章中的治理层已经定义了 append-only 审计事件。历史清理不应另起一套存储,而应复用同一条审计链:
type ArchivedToolResult = {
toolUseId: string;
auditEventId: string;
rawResultRef: string;
rawResultSha256: string;
summary: string;
redactionPolicy: "none" | "secrets" | "pii" | "full";
cacheBoundary: "task_state" | "live_window";
};
这样,治理层负责“原始事实存在且可追溯”,历史清理层负责“哪些事实还需要进入模型上下文”。两层职责分开后,清理策略可以更激进,但不会牺牲复现能力。
失败模式
| 失败模式 | 触发原因 | 修复策略 |
|---|---|---|
| 清理后 API 400 | 删除了紧邻 tool_use 的 tool_result |
只清理已闭合且不在最近窗口内的历史 |
| DeepSeek thinking mode 报错 | 清理或空填了真实 reasoning_content |
独立保存真实 reasoning,并按轮次重注入 |
| prompt cache 命中率骤降 | 工具结果进入稳定前缀或清理改写前缀 | 固定 stable prefix,把工具历史放入 live window |
| 模型重复读取旧文件 | 摘要未标注时间和文件版本 | 摘要记录时间、hash、commit 或 mtime |
| 无法复现工具异常 | 只保留摘要,丢失原始日志 | 原始结果外置存储,摘要中记录 raw_ref 和 hash |
验证清单
• 构造连续 20 轮工具调用,确认上下文大小按预期收敛。
• 构造并行 tool_use,清理后仍能保持所有 id 与结果可追溯。
• 构造 DeepSeek thinking + tool call,多轮后确认回传的是真实 reasoning 内容。
• 构造大测试日志,确认模型上下文只保留结构化摘要,完整日志可通过 raw_ref 找回。
• 开启 prompt cache 指标,比较清理前后 cache hit rate、输入 token、首 token 延迟。
• 对清理策略做回归测试:任何未闭合工具调用不得被删除或摘要替换。
回归测试矩阵
| 场景 | 输入 | 期望 |
|---|---|---|
| 未闭合工具调用 | assistant 有 tool_use,下一条 user 还未出现 |
禁止清理,返回 pending 状态 |
| 大日志已闭合 | tool_result 超过阈值且已有 assistant 后续总结 |
原文进入审计存储,上下文保留摘要 |
| DeepSeek thinking | tool call 前后存在真实 reasoning_content |
reasoning 原文仍按轮次可重注入 |
| 并行工具结果 | 同一 turn 有多个 tool_use |
摘要仍保留所有 tool_use_id 和 ordinal |
| 缓存前缀 | stable prefix 未变化,只清理 live window | system_key 和 tool_schema_key 不变 |
| 敏感日志 | 工具输出包含 secret 或 token | 模型上下文使用脱敏摘要,审计层按策略留存或加密 |
工程落地顺序
历史清理应该放在兼容层后半段实现。推荐顺序是:
1、先完成 ToolCallIR / ToolResultIR、pending ledger 和工具结果配对。
2、接入推理状态原文存储,确保 DeepSeek reasoning_content 和 Anthropic thinking block 不被清理破坏。
3、增加治理审计层,保存原始工具输入、输出、hash、错误和成本。
4、在上下文构造器中分离 stable prefix、task state、live tool window 和 audit references。
5、最后打开自动清理策略,并用 cache hit rate、输入 token、错误率和复现成功率做灰度指标。
结论
工具历史清理的目标不是让上下文“变短”这么简单,而是在协议正确性、模型状态连续性、成本、缓存和审计之间做平衡。对于 Claude Code 跨模型兼容层,最稳的设计是把工具历史视为可管理的数据生命周期:实时上下文只保留必要状态,完整执行证据进入审计存储,稳定前缀尽量不被工具结果污染。这样才能在 DeepSeek、Qwen 等模型和其他经过能力探测的后端之间切换时,既不破坏 tool use 状态机,也不让长历史拖垮 agent。
参考链接
• Anthropic Context editing
• Anthropic Tool Runner SDK
• Anthropic Handle tool calls
• AWS Bedrock Anthropic Claude tool use
• Vercel AI SDK Anthropic Provider
• Anthropic Engineering: Advanced tool use