摘要

Claude Code 这类 coding agent 的上下文不是普通聊天记录,而是由用户意图、模型推理、工具调用、工具结果、错误反馈和代码片段共同组成的执行日志。工具调用越多,历史越快膨胀;如果直接把所有 tool_result 长期保留在上下文里,模型会被过期输出、重复堆栈、大段文件内容和无关日志污染。反过来,如果清理过猛,又会破坏 tool_use.idtool_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-27clear_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_keytool_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_contentthinking,保留真实原文引用和必要 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_usetool_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_keytool_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