摘要
跨模型 tool use 兼容层最容易被低估的部分,不是把 Anthropic 的 tool_use 转成 OpenAI-compatible 的 tool_calls,也不是为 Qwen 的 XML tool call 写一个 parser。真正决定 agent 是否可长期运行的,是兼容层能否承担治理责任:每一次工具调用为什么发生、模型原始输出是什么、归一化后的调用是什么、工具是否真的执行、失败如何回传给模型、堆栈和敏感信息如何分层保存、这一次调用消耗了多少 token 和费用、异常应该归因到模型、兼容层、工具实现还是下游 API。
Claude Code 这类 coding agent 的工具调用链很长:模型可能读取文件、搜索代码、执行测试、访问浏览器、调用远程 API,然后根据工具结果继续规划。只要其中一环没有审计,问题就会变成不可复现的“模型偶发不稳定”。而 Anthropic tool runner 对工具异常的包装、ANTHROPIC_LOG=info/debug 暴露的调试信息、tool_use / tool_result 的严格状态机,以及 DeepSeek、Qwen 等模型在社区 agent 框架中暴露出的兼容问题,都指向同一个结论:跨模型 tool use 兼容层必须是一个运行时治理层。
问题边界:协议转换解决不了运行时责任
最小实现通常只做三件事:
1、把上游模型输出解析成内部工具调用对象。
2、调用本地或远程工具。
3、把工具结果转换回目标模型要求的消息格式。
这能跑通 demo,但很难支撑真实 agent。原因是 tool use 不是单次 RPC,而是一个跨越模型、兼容层、工具执行器、日志系统和下一轮模型请求的状态机。
以 Anthropic 协议为例,模型返回 stop_reason: "tool_use" 后,assistant message 中会包含一个或多个 tool_use block,每个 block 有 id、name 和 input。应用执行工具后,必须在紧随其后的 user message content 数组中返回对应的 tool_result,并通过 tool_use_id 关联原始调用。失败时,工具结果可以设置 is_error: true,让错误作为模型可见的反馈进入下一轮推理。
这套机制表面上是格式约束,本质上是治理约束:兼容层必须知道哪个工具调用处于 pending,哪个已经 completed,哪个失败但已经回传给模型,哪个失败只写入了内部日志却没有进入上下文。如果只做字段映射,一旦出现解析失败、工具超时、异常堆栈过大、敏感参数泄露或 token 账单异常,就没有足够证据定位问题。
治理对象
工具治理层至少要覆盖七类对象。
| 对象 | 要治理的问题 | 典型风险 |
|---|---|---|
| 模型原始输出 | 模型到底生成了什么 | parser 误判、流式增量丢字段、XML/JSON 格式不完整 |
| 归一化工具调用 | 兼容层认为应该执行什么 | name 映射错误、参数默认值污染、schema coercion 错误 |
| 工具执行请求 | 实际传给工具的参数 | 敏感路径、token、命令参数泄露 |
| 工具执行结果 | 工具真实返回了什么 | 大日志污染上下文、二进制/截图无法直接入模 |
| 错误对象 | 失败属于哪一层 | 模型格式错、协议顺序错、工具异常、网络错误混淆 |
| 回传消息 | 给模型看的结果是什么 | is_error 丢失、错误过度清洗、状态机断裂 |
| 成本记录 | 每轮消耗多少 token 和钱 | prompt cache 失效、重试放大费用、工具调用风暴 |
这里的关键是区分“事实层”和“模型可见层”。事实层保存原始证据,包括原始 assistant 输出、解析前后的差异、完整错误对象和原始工具结果。模型可见层只回传完成下一步推理所需的信息,经过大小限制、敏感信息脱敏和错误摘要。两者不能混为一谈。
状态机:先把生命周期建模清楚
推荐把每个工具调用建模为独立生命周期,而不是附着在一条聊天消息上的临时对象:
model_output_received
-> parsed
-> normalized
-> policy_checked
-> dispatching
-> running
-> succeeded | failed | timed_out | cancelled
-> result_mapped
-> returned_to_model
-> archived
每个状态转换都应产生审计事件。这样做有三个好处。
第一,协议错误能被明确定位。例如 Anthropic 要求 tool_result 紧跟对应的 tool_use,DeepSeek thinking mode 在工具调用后还要求完整回传必要的 reasoning_content。如果状态机里只有“执行了工具”这个布尔值,系统无法判断是模型没生成、parser 没识别、兼容层没回放,还是回放顺序不对。
第二,重试策略不会破坏幂等性。工具超时后是否重试,取决于工具是否有副作用、是否已有外部可见结果、是否能用 idempotency key 去重。没有状态机,重试很容易变成重复写文件、重复提交表单或重复调用计费 API。
第三,成本可以按状态归因。一次失败可能消耗在模型输入 token、模型输出 token、工具运行时间、浏览器截图、向量检索或外部 API 上。治理层需要知道钱花在哪里,而不只是知道“这一轮失败了”。
审计日志 schema
审计日志应采用 append-only 事件流,而不是只在最后写一条 summary。summary 适合看板,事件流适合复现。
下面是一个可落地的事件 schema。字段可以按存储系统拆分,但语义不应丢失。
{
"event_id": "evt_01J...",
"trace_id": "tr_20260611_001",
"conversation_id": "conv_...",
"turn_id": "turn_00042",
"tool_use_id": "toolu_...",
"parent_tool_use_id": null,
"event_type": "tool_call.normalized",
"timestamp": "2026-06-11T10:20:30.123Z",
"model": {
"provider": "anthropic",
"model": "claude-sonnet-...",
"request_id": "req_...",
"raw_stop_reason": "tool_use"
},
"protocol": {
"source_format": "anthropic_messages",
"target_format": "internal_tool_call_v1",
"parallel_group_id": "pg_...",
"requires_immediate_tool_result": true,
"requires_reasoning_replay": false
},
"tool": {
"name": "run_command",
"version": "2026-06-01",
"schema_hash": "sha256:...",
"side_effect": "workspace_write",
"idempotency_key": "..."
},
"raw": {
"assistant_output_ref": "audit/raw-model/turn_00042.json",
"assistant_output_sha256": "sha256:..."
},
"normalized": {
"input_ref": "audit/normalized/toolu_....json",
"input_sha256": "sha256:...",
"input_redaction_profile": "secrets_masked"
},
"result": {
"status": "pending",
"is_error_for_model": null,
"model_visible_result_ref": null,
"raw_result_ref": null
},
"usage": {
"input_tokens": 18432,
"output_tokens": 611,
"cache_read_tokens": 12000,
"cache_write_tokens": 512,
"tool_runtime_ms": null,
"estimated_cost_usd": "0.0731"
},
"security": {
"data_classification": "internal",
"contains_secret": false,
"redaction_rules": ["env_secret", "api_key", "absolute_home_path"],
"retention_days": 30
}
}
几个字段尤其重要。
raw.assistant_output_ref 保存模型原始输出。它用于排查“模型没有生成 tool call”还是“模型生成了但 parser 没识别”。对于 Qwen3-Coder 这类可能使用 XML-style tool call 的模型,原始文本尤其关键,因为 JSON-only parser 可能会把有效工具调用当成普通文本。
normalized.input_ref 保存兼容层实际执行的工具调用。它用于排查 schema coercion、工具名映射、默认参数补齐、枚举值大小写转换等兼容层行为。
result.model_visible_result_ref 与 result.raw_result_ref 必须分开。给模型看的结果应短、小、脱敏、结构化;原始结果可以很大,可能包含完整堆栈、命令输出、HTTP response、截图 OCR 或浏览器 console log。
usage 不应只记录模型 token。一次工具调用还可能引发二次模型总结、embedding 检索、浏览器快照、外部 API 计费和重试成本。治理层至少要保留可以后算费用的计量数据。
错误回传策略:让模型知道失败,但不要泄露运行时
Anthropic tool runner 的一个重要工程细节是:工具执行异常可以被包装成带 is_error: true 的 tool_result 回传给模型;同时,开发者可以通过 ANTHROPIC_LOG=info 或 ANTHROPIC_LOG=debug 查看更详细的调试信息和堆栈。这体现了一个值得复用的分层原则:模型需要可行动的错误摘要,开发者需要完整证据,二者不应是同一份文本。
推荐把错误分成四层。
| 层级 | 受众 | 内容 | 存储/回传策略 |
|---|---|---|---|
| Model-safe error | 模型 | 错误类别、可重试性、下一步建议、短摘要 | 作为 is_error: true 或等价错误结果回传 |
| Operator error | 开发/运维 | 退出码、异常类型、关键堆栈、request id | 写入受控日志,默认脱敏 |
| Forensic artifact | 调试人员 | 完整堆栈、原始 stdout/stderr、HTTP payload | 加密存储,短留存,按需授权 |
| User-facing error | 最终用户 | 不含内部路径和秘密的解释 | 只在产品界面展示 |
模型可见错误可以采用结构化格式,而不是直接塞入完整 traceback:
{
"ok": false,
"error": {
"category": "tool_runtime_error",
"retryable": false,
"message": "The test command failed with exit code 1.",
"key_findings": [
"Unit test failed in tool_use_mapper.test.ts",
"Failure is related to missing tool_result correlation"
],
"debug_ref": "audit/errors/err_01J..."
}
}
对应到 Anthropic 协议时,这个对象可以成为 tool_result.content,并设置 is_error: true。对应到 OpenAI-compatible 或其他模型时,也应该保留等价语义,例如内部消息里标注 tool_error: true,再按目标模型的最佳实践转换。
不要把完整堆栈直接回传给模型,原因有三点。
第一,堆栈经常包含本机路径、环境变量、URL、header、临时文件名或用户数据。第二,大堆栈会快速污染上下文,增加 token 成本。第三,模型真正需要的是“下一步怎么修”,不是全部运行时细节。完整堆栈应该通过 debug_ref 留给开发者追溯。
异常分类:先归因,再重试
兼容层应把异常分类作为一等能力。建议至少包含以下类别:
| 类别 | 例子 | 是否回传给模型 | 是否自动重试 |
|---|---|---|---|
model_malformed_tool_call |
JSON 不完整、XML tag 未闭合、参数类型错误 | 是,提示格式问题或请求重新生成 | 通常不重试同一输出 |
protocol_state_error |
tool_result 未紧跟 tool_use、id 不匹配、reasoning 回放缺失 |
是,但同时触发兼容层告警 | 不应盲目重试 |
policy_denied |
工具越权、命令被策略拒绝、访问敏感路径 | 是,给出可行边界 | 不重试 |
tool_validation_error |
参数不符合工具 schema | 是,提示缺失字段或非法值 | 可让模型修正 |
tool_runtime_error |
命令退出码非零、测试失败、文件不存在 | 是,摘要关键失败 | 由模型决定下一步 |
tool_infra_error |
sandbox 故障、网络中断、浏览器崩溃 | 是,标记 retryable | 可有限重试 |
provider_api_error |
上游 API 400/429/5xx | 视情况回传摘要 | 429/5xx 可退避重试 |
cost_guardrail_triggered |
token 超预算、工具调用风暴 | 是,说明预算约束 | 不自动重试 |
社区 agent 框架中关于 DeepSeek thinking mode、Qwen3-Coder tool call、OpenAI-compatible streaming 等问题的案例,很多表面上是“模型不兼容”,实际可以归入这些类别:有的是 reasoning 状态没有回放,有的是 XML tool call 没有被 parser 识别,有的是流式增量和最终消息不一致,有的是工具调用生成了但执行器没有进入 dispatch 状态。只有分类清楚,才知道该修 parser、修状态机、修 prompt,还是修工具执行器。
成本和 token 统计:按调用链计账
tool use 的成本统计不能只看一次模型请求的 usage。一个工具调用链通常包含:
1、发起工具调用前的模型输入 token。
2、模型生成 tool_use 的输出 token。
3、工具执行产生的大结果进入下一轮上下文后的输入 token。
4、错误摘要、工具结果清理、代码总结等二次模型调用。
5、prompt cache read/write token。
6、外部工具成本,例如搜索、浏览器、embedding、数据库、第三方 API。
7、重试成本,包括模型重试和工具重试。
因此,成本统计至少要支持四个维度。
| 维度 | 目的 |
|---|---|
| per turn | 看单轮对话是否异常膨胀 |
| per tool call | 找出最贵的工具类型和失败调用 |
| per trace/session | 看一个任务从开始到完成的总成本 |
| per provider/model | 比较不同模型和后端的真实运行成本 |
推荐记录如下聚合指标:
{
"trace_id": "tr_20260611_001",
"turn_id": "turn_00042",
"tool_use_id": "toolu_...",
"cost": {
"model_input_tokens": 18432,
"model_output_tokens": 611,
"cache_read_tokens": 12000,
"cache_write_tokens": 512,
"tool_result_tokens_added": 3800,
"retry_count": 1,
"estimated_model_cost_usd": "0.0731",
"estimated_tool_cost_usd": "0.0040",
"estimated_total_cost_usd": "0.0771"
},
"attribution": {
"provider": "anthropic",
"model": "claude-sonnet-...",
"tool_name": "run_tests",
"error_category": "tool_runtime_error",
"cache_hit": true
}
}
tool_result_tokens_added 是非常关键的指标。工具本身不一定收费,但它的输出会进入下一轮模型输入,间接制造大量 token 成本。测试日志、搜索结果、文件读取结果和浏览器快照都可能在这里放大成本。第 7 篇讨论的工具历史清理与缓存协同,实际上应该消费这里的计量结果:哪些工具结果最贵,哪些结果应该摘要,哪些原文应该外置。
还要把成本和失败关联起来。一个经常失败但每次失败只花少量 token 的工具,优先级可能低于一个偶发失败但每次失败会触发十轮重试和大量日志回灌的工具。治理层的目标不是“记录账单”,而是让工程团队知道成本失控的机制。
敏感信息分层与留存
工具调用经常接触敏感信息:本地路径、源代码片段、环境变量、API key、cookie、用户输入、数据库记录、HTTP header。治理层必须把“可审计”与“无限期保存原文”区分开。
推荐使用三类存储。
| 存储 | 内容 | 留存 |
|---|---|---|
| 热日志 | trace id、状态、错误类别、token、hash、脱敏摘要 | 较长,可用于看板和检索 |
| 调试 artifact | 原始模型输出、原始工具输入输出、完整堆栈 | 较短,按项目策略清理 |
| 受限密文 | 含秘密或用户数据的 payload | 最短,强访问控制和审计 |
脱敏不应只靠正则。正则适合处理 API key、Bearer token、邮箱和本机路径,但工具 schema 本身也应声明字段敏感度,例如 password、token、cookie、private_key、authorization_header。兼容层在写审计日志、回传模型、展示 UI 和发送遥测时,应使用同一套字段级 redaction policy。
同时,审计日志要保存 hash。即使原文因留存策略被删除,仍可以证明某次模型输出或工具结果是否被篡改,也可以在短期窗口内通过 hash 关联调试 artifact。
可观测性与告警指标
治理层应暴露指标,而不是只写日志。推荐从四类指标开始。
协议正确性
| 指标 | 告警含义 |
|---|---|
tool_call_parse_failure_rate |
模型输出无法解析,可能是 prompt、parser 或模型格式漂移 |
tool_result_missing_rate |
存在未闭合工具调用,可能破坏 Anthropic 状态机 |
tool_use_id_mismatch_count |
tool_use.id 与 tool_result.tool_use_id 配对错误 |
reasoning_replay_missing_count |
thinking 模型所需 reasoning 状态缺失 |
parallel_tool_join_latency_ms |
并行工具调用等待最慢工具的时间 |
工具可靠性
| 指标 | 告警含义 |
|---|---|
tool_error_rate_by_name |
某个工具版本异常升高 |
tool_timeout_rate |
工具执行器或外部依赖不稳定 |
tool_retry_amplification |
一次用户请求触发过多工具/模型重试 |
policy_denied_rate |
模型频繁尝试越权操作,可能需要调整工具描述或权限模型 |
成本与上下文
| 指标 | 告警含义 |
|---|---|
tokens_per_turn_p95 |
上下文增长过快 |
tool_result_tokens_added_p95 |
工具输出过大,需要摘要或外置 |
cache_hit_rate |
稳定前缀被工具历史污染或 schema 不稳定 |
cost_per_completed_task |
真实任务成本上升 |
failed_cost_ratio |
花在失败路径上的费用比例过高 |
安全与合规
| 指标 | 告警含义 |
|---|---|
secret_redaction_hit_count |
工具输出中频繁出现秘密 |
raw_artifact_access_count |
调试原文访问异常 |
sensitive_tool_call_count |
高权限工具调用量变化 |
retention_policy_violation_count |
artifact 超期未清理 |
这些指标最好都带上 provider、model、tool_name、tool_version、agent_version、workspace_id 等 label。跨模型兼容问题往往不是全局故障,而是某个模型、某个 parser、某个工具 schema 或某种 streaming 模式的组合故障。
实现建议:把治理层放在工具执行路径正中间
一个稳健的架构可以分为六层:
Model Adapter
-> Raw Output Recorder
-> Tool Call Parser / Normalizer
-> Policy & Budget Gate
-> Tool Dispatcher
-> Result Mapper / Error Wrapper
-> Conversation State Machine
-> Audit & Metrics Sink
Model Adapter 负责各模型协议差异,例如 Anthropic block、OpenAI tool calls、DeepSeek reasoning 字段、Qwen XML tool call。
Raw Output Recorder 在任何解析前保存原始输出,避免 parser bug 抹掉证据。
Tool Call Parser / Normalizer 输出统一内部结构,并记录从原始格式到内部格式的映射。
Policy & Budget Gate 在执行前检查权限、预算、并发、超时和副作用等级。被拒绝的调用也要作为 tool result 回传给模型,让模型能在约束内调整方案。
Tool Dispatcher 执行工具,提供 idempotency key、timeout、取消、并发 join 和 sandbox 边界。
Result Mapper / Error Wrapper 把成功结果或异常转换为模型可见结果。这里应实现 Anthropic 风格的 is_error 语义,即失败也要闭合工具调用,而不是只在内部日志里报错。
Conversation State Machine 负责确保下一轮消息满足目标模型协议:Anthropic 的 tool_result 顺序、DeepSeek 的 reasoning 回放、并行工具调用的配对、清理历史时不破坏未闭合状态。
Audit & Metrics Sink 以异步方式写入日志、指标和 artifact。注意,审计写入失败不应默认吞掉;至少要进入降级队列,否则最需要审计的故障路径反而没有记录。
结论
跨模型 tool use 兼容层如果只做协议转换,最多能解决“能不能调用工具”。真实 agent 还需要回答更难的问题:调用是否合规,失败是否回传,状态机是否闭合,错误能否复现,敏感信息是否泄露,成本是否可控,异常是否能被归因。
Anthropic 的 tool use 状态机和 tool runner 异常包装提供了一个清晰信号:工具失败不是框架内部的小插曲,而是模型推理的一部分,应以 is_error 或等价语义回传;完整堆栈则通过 ANTHROPIC_LOG=info/debug 这类开发者通道进入调试层。DeepSeek、Qwen 等模型在 agent 框架中的问题案例进一步说明,跨模型兼容的核心不是“字段名翻译”,而是把模型输出、工具执行、状态回放、错误处理、审计日志和成本统计串成一个可观测、可治理的运行时系统。
参考链接
• Anthropic Tool use overview
• Anthropic Define tools
• Anthropic Handle tool calls
• Anthropic Parallel tool use
• Anthropic Tool Runner SDK
• DeepSeek Thinking Mode
• DeepSeek Tool Calls
• Qwen Function Calling
• Alibaba Cloud Model Studio: Function calling
• QwenLM/Qwen3-Coder #475
• ggml-org/llama.cpp #15012
• vercel/ai #10778
• anomalyco/opencode #24114
• openclaw/openclaw #71435
• Anthropic Engineering: Advanced tool use
• Anthropic Engineering Postmortem