让模型「写代码来调工具」,而不是「每调一次工具就回模型一次」——Claude 平台高级工具使用三件套中,对 Agent 工程影响最深的一项。
传统的工具调用,每调一次工具,结果都要原封不动塞回模型的上下文,模型看完再决定下一步——调 20 次工具就是 20 次模型往返,几千条中间数据全堆在上下文里。
程序化工具调用(PTC)换了个思路:Claude 直接写一段 Python 脚本,在沙箱容器里运行,脚本内部去调用你的工具、循环、过滤、汇总,中间结果一概不进模型上下文,Claude 只看到脚本最后 print 出来的结论。
数据来源:Anthropic 官方文档与工程博客(见文末来源清单),不同工作负载差异很大,后文有「什么时候反而更贵」的说明。
要理解 PTC,先看传统工具调用在任务变复杂后暴露的两个问题——这两个问题官方工程博客里讲得很直白:
让 Claude 分析一个 10MB 日志文件里的报错规律,传统做法是整个文件进上下文——哪怕 Claude 真正需要的只是「每类错误出现了多少次」这一行摘要。跨多张表查客户数据,每条记录不管有用没用都会堆进上下文,把真正重要的信息挤出窗口。
每个工具调用都要模型完整推理一遍。调完之后,模型还得用「肉眼」在自然语言层面读数据、比大小、拼结论。一个 5 步工具流就是 5 次推理 + 5 次人肉式解析,又慢又容易错。
传统工具调用像一个事必躬亲的老板:每查一个数字,下属都要跑回办公室汇报全部原始材料,老板看完再派下一个活。PTC 则像老板写了一张清晰的任务清单交给助理:「把 20 个人的报销都查一遍,只把超预算的名字报给我」。老板(模型)只被打扰一次,桌上(上下文)也不会堆满原始票据。
这个思路并非凭空出现。官方博客明确致谢了业界同类探索:Cloudflare 的 Code Mode、Anthropic 自己的《Code Execution as MCP》等——「让模型用代码编排工具」在 2025 年已是社区共识方向。Anthropic 于 2025 年 11 月 24 日把它作为 beta 功能(与 Tool Search Tool、Tool Use Examples 一起,合称「高级工具使用」)正式产品化;截至本文调研时(2026-07),官方文档已将其作为正式能力,依赖 code_execution_20260120 及以后版本的代码执行工具。官方产品 Claude for Excel 就用 PTC 读写数千行的表格而不撑爆上下文。
["direct"] 表示只让模型直接调(默认);["code_execution_20260120"] 表示引导模型只在代码里调;两个都写则两种都行。官方建议二选一,指向更明确。注意:它是引导而非硬拦截,官方明确说不要把它当安全边界——你的客户端仍要准备好处理任何工具的直接调用。tool_use 块都带 caller 字段:{"type":"direct"} 是传统直调;{"type":"code_execution_20260120","tool_id":"srvtoolu_..."} 则说明是某次代码执行发起的,tool_id 能对应到那段代码的 server_tool_use 块。你的代码靠它区分两种调用。container ID 和 expires_at;有挂起的程序化调用时,续传请求必须带容器 ID,否则 API 直接拒绝。闲置容器约 5 分钟回收,单个容器最长复用 30 天;挂起的工具调用约 4 分钟没等到结果,脚本里会抛 TimeoutError。你的每个工具会被暴露成一个异步 Python 函数:入参是一个 dict,返回值是字符串(即你回传的 tool_result 文本)。所以 Claude 的代码里常见 rows = json.loads(await query_database({"sql": "..."})) 这样的写法,还能用 asyncio.gather 并行调用多个工具。
官方文档把流程拆成 5 步。关键在于第 3、4 步:暂停的是沙箱里的脚本,不是模型——模型在整个过程中只被采样一次(写代码时)加最后一次(看结果时)。
caller 字段的 tool_use 块和容器 ID,stop_reason 为 tool_use。stdout(如 print 的汇总结果)进入上下文,Claude 基于它给出最终回答。官方给的解释很朴素:Claude 的代码能力极强,把工具呈现为可调用的 Python 函数,循环、条件、组合就成了普通的代码控制流,而不是一连串模型往返;大结果集在代码里过滤聚合,只有结论进上下文;两次工具调用之间也不需要重新采样模型。
code_execution_20260120 或更高版本,支持的模型包括 Claude Fable 5 / Mythos 5、Opus 4.8 / 4.7 / 4.6 / 4.5、Sonnet 5 / 4.6 / 4.5(官方文档兼容表)。下面是官方 Quick start 的 Python 版(代码来自官方文档,略有排版调整):
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
messages=[{
"role": "user",
"content": "查询 West、East、Central 三个区域的销售数据,告诉我哪个区收入最高",
}],
tools=[
{"type": "code_execution_20260120", "name": "code_execution"},
{
"name": "query_database",
"description": "对销售库执行 SQL。返回 JSON 对象组成的行列表。",
"input_schema": {
"type": "object",
"properties": {"sql": {"type": "string"}},
"required": ["sql"],
},
"allowed_callers": ["code_execution_20260120"], # 关键:允许代码调用
},
],
)
因为 Claude 要在代码里反序列化你的返回值,官方最佳实践强调:在工具 description 里把输出的 JSON 结构、字段类型写清楚(如「返回列表,每项含 id(str)、total(float)、status(str) …」)。写得越细,Claude 的解析代码越靠谱。
收到 stop_reason: "tool_use" 且 tool_use 块带代码执行 caller 时,续传请求有三个硬要求(官方文档原文归纳):
每个续传响应要么再次暂停(还有新的程序化 tool_use,继续回传),要么返回 code_execution_tool_result(内含 stdout)并让 Claude 给出最终回答。用 stop_reason 和 caller 字段区分这两种情况。
官方文档给出四个典型代码模式,正好覆盖 PTC 的四类价值(以下代码均摘自官方文档):
regions = ["West", "East", "Central", "North", "South"]
results = {}
for region in regions:
rows = json.loads(await query_database({"sql": f"..."}))
results[region] = sum(row["revenue"] for row in rows)
top_region = max(results.items(), key=lambda x: x[1])
print(f"Top region: {top_region[0]}")
for endpoint in ["us-east", "eu-west", "apac"]:
status = await check_health({"endpoint": endpoint})
if status == "healthy":
print(f"Found healthy endpoint: {endpoint}")
break # 后面的不再查
file_info = json.loads(await get_file_info({"path": path}))
if file_info["size"] < 10000:
content = await read_full_file({"path": path})
else:
content = await read_file_summary({"path": path})
log_text = await fetch_logs({"server_id": "srv-01"})
errors = [l for l in log_text.splitlines() if "ERROR" in l]
print(f"Found {len(errors)} errors")
for error in errors[-10:]: # 只返回最后 10 条
print(error)
再加上工具函数天然是异步的,可以 asyncio.gather 并行发起几十个调用——官方工程博客的「预算合规检查」例子就是并行拉 20 个人的报销、并行查各级别预算,最后只把两三个超支者的名字送回模型,上下文从 200KB 原始数据缩到 1KB 结论。
| 场景 | 效果 | 出处 |
|---|---|---|
| BrowseComp / DeepSearchQA 等 agentic 搜索基准 | 平均 +11% 表现,输入 token -24% | 官方文档 |
| 75 工具的项目管理 Agent 基准 | 计费输入 token 约 -38%,准确率不变 | 官方文档(内部评测) |
| 复杂研究任务 | 平均 token 43,588 → 27,297(-37%) | 官方工程博客 |
| 内部知识检索 / GIA 基准 | 25.6% → 28.5% / 46.5% → 51.2% | 官方工程博客 |
| 生产流量中 tools 数组含 10–49 个工具的请求 | 典型节省 20%–40% | 官方文档 |
| τ²-bench(每轮只有 1–2 个顺序调用) | 分数不变,成本反而 +8% | 官方文档(内部评测) |
最后一行值得特别注意:官方自己承认顺序单调用工作流不受益——PTC 是用「容器启动 + 生成脚本」的固定开销,换「省掉工具结果 token 和模型往返」的收益,负载形态决定这笔账划不划算。
| ✅ 强适配 | ❌ 弱适配 |
|---|---|
| 扇出/并行操作:查 50 个接口、20 条记录 | 严格顺序流:每步都要模型看完上一步结果再推理 |
| 大结果集需要先过滤、聚合、摘要 | 少量调用 + 小返回值(尤其会话第一轮,固定开销可能超过节省) |
| agentic 搜索/检索:迭代查询 + 结果过滤为主 | 每次调用之间需要用户即时反馈 |
| 中间数据不应影响模型判断的任务 | 模型应该看到并推理所有中间结果的任务 |
官方建议:在放开之前,用有代表性的流量样本,分别测「加 / 不加 allowed_callers」的计费输入 token,再决定是否全面启用。
「让模型写代码调工具」是个通用模式,官方文档列了三种实现路线,PTC 是其中的托管版:
| 路线 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| 客户端直接执行 你在本地执行模型写的代码 | 改造成本最小,环境完全自控 | 在沙箱外跑不可信代码,存在注入风险 | 应用本身能安全执行任意代码时 |
| 自管沙箱 自建受限容器跑代码 | 安全 + 基础设施自控 | 构建维护复杂,要自己解决沙箱内外的工具通信 | 安全要求极高、托管方案不满足需求时 |
| Anthropic 托管(即 PTC) | 默认安全,一个字段开启,环境为 Claude 调优 | 依赖托管平台(Bedrock/GCP 暂不可用),不符合 ZDR | 用 Claude API / AWS Claude Platform / Foundry 的大多数场景 |
PTC 与同期发布的 Tool Search Tool(按需搜索加载工具定义,官方测试在大工具库上 token 省约 85%、准确率显著提升)和 Tool Use Examples(在工具定义里给调用示例,复杂参数准确率 72%→90%)互补:一个管「找到对的工具」,一个管「高效执行」,一个管「参数填对」。官方建议按瓶颈选择:工具定义撑爆上下文 → Tool Search;中间结果污染上下文 → PTC;参数总填错 → Examples。
strict: true 的结构化输出工具不支持;tool_choice 不能强制程序化调用某个工具(指名的工具必须允许 direct,否则 400);disable_parallel_tool_use: true 不支持。Circular $ref detected)。解法:该工具保持 direct-only,或把递归展开成固定深度。① 为什么说 PTC 省 token 的本质是「中间结果不进上下文」而不是「少调了工具」?② 一个每步都依赖模型判断的顺序工作流,该不该开 PTC?为什么官方在 τ²-bench 上成本反升 8%?③ 续传请求忘带容器 ID 会发生什么?