程序化工具调用
Programmatic Tool Calling(PTC)

让模型「写代码来调工具」,而不是「每调一次工具就回模型一次」——Claude 平台高级工具使用三件套中,对 Agent 工程影响最深的一项。

Agent 工程Tool Use上下文效率调研日期 2026-07-15

⚡ 开篇速览:30 秒版本

传统的工具调用,每调一次工具,结果都要原封不动塞回模型的上下文,模型看完再决定下一步——调 20 次工具就是 20 次模型往返,几千条中间数据全堆在上下文里。

程序化工具调用(PTC)换了个思路:Claude 直接写一段 Python 脚本,在沙箱容器里运行,脚本内部去调用你的工具、循环、过滤、汇总,中间结果一概不进模型上下文,Claude 只看到脚本最后 print 出来的结论。

传统工具调用 程序化工具调用(PTC) 模型(每一步都要思考一次) 工具 ×20 20 次往返,中间结果全进上下文 模型(写一段脚本,等最终结果) 沙箱容器:脚本循环调工具、过滤 工具 ×20 模型只看到最终汇总,1 次往返
同一个「调 20 次工具」的任务:左边模型往返 20 次;右边脚本在沙箱里跑完,模型只拿结论。(依据官方文档描述绘制)
-37%
复杂研究任务平均 token 消耗(43,588 → 27,297)
+11%
BrowseComp / DeepSearchQA 等搜索基准平均提升
-38%
75 工具项目管理 Agent 基准的计费输入 token

数据来源: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 读写数千行的表格而不撑爆上下文。

2025 前后社区探索:Code Mode 等 2025-11-24Anthropic 发布 beta(三件套) 2026-07(调研时)正式能力,code_execution_20260120+
PTC 从社区模式到官方正式能力的时间线(依据官方博客与文档整理)

🧩 核心概念

程序化工具调用(Programmatic Tool Calling, PTC)
Claude 在代码执行(code execution)沙箱容器中,用自己写的 Python 代码去调用你定义的工具。脚本可以循环、并行、加条件判断、过滤结果;工具的中间返回值由脚本消化,不进模型上下文,只有代码的最终输出回到模型。
官方定义参考:「allows Claude to write code that calls your tools programmatically within a code execution container, rather than requiring round trips through the model for each tool invocation.」

三个必须搞懂的字段/机制

1. allowed_callers ——「这个工具允许谁来调」
加在工具定义上的字段。["direct"] 表示只让模型直接调(默认);["code_execution_20260120"] 表示引导模型只在代码里调;两个都写则两种都行。官方建议二选一,指向更明确。注意:它是引导而非硬拦截,官方明确说不要把它当安全边界——你的客户端仍要准备好处理任何工具的直接调用。
2. caller ——「这次调用是谁发起的」
响应里每个 tool_use 块都带 caller 字段:{"type":"direct"} 是传统直调;{"type":"code_execution_20260120","tool_id":"srvtoolu_..."} 则说明是某次代码执行发起的,tool_id 能对应到那段代码的 server_tool_use 块。你的代码靠它区分两种调用。
3. 容器(container)生命周期
PTC 复用代码执行的沙箱容器。响应里返回 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 步:暂停的是沙箱里的脚本,不是模型——模型在整个过程中只被采样一次(写代码时)加最后一次(看结果时)。

Claude 写编排代码。它不再逐个请求工具,而是生成一段 Python:里面可能有循环、并行、前后处理逻辑。
代码在沙箱容器里运行。通过 code execution 工具执行,环境是 Anthropic 托管的、为 Claude 调优的 Python 沙箱。
脚本调到你的工具时,执行暂停。API 返回一个带 caller 字段的 tool_use 块和容器 ID,stop_reasontool_use
你回传 tool_result,脚本继续跑。结果被脚本消费而不是模型——这一步是省 token 的关键。可能多次重复 3↔4。
代码跑完,模型只看最终输出。只有 stdout(如 print 的汇总结果)进入上下文,Claude 基于它给出最终回答。
① 你的请求tools + allowed_callers ② Claude 写 Pythonserver_tool_use: code ③ 沙箱执行,遇工具暂停tool_use + caller + 容器ID ④ 你回传 tool_result须带同一容器 ID + 同一 tools 脚本消化结果,继续执行不进模型上下文 ⑤ 代码跑完 → 模型只看到 stdout 最终输出code_execution_tool_result → Claude 给出最终回答 脚本再调下一个工具时,重复 ③↔④
一次 PTC 交互的请求-暂停-续传循环(依据官方文档 5 步工作流绘制)
🔑 为什么这招有效?

官方给的解释很朴素:Claude 的代码能力极强,把工具呈现为可调用的 Python 函数,循环、条件、组合就成了普通的代码控制流,而不是一连串模型往返;大结果集在代码里过滤聚合,只有结论进上下文;两次工具调用之间也不需要重新采样模型。

🛠️ 怎么用:完整上手流程

前置条件

第一步:在工具定义上加 allowed_callers

下面是官方 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 时,续传请求有三个硬要求(官方文档原文归纳):

  1. 承载结果的 user 消息只能包含 tool_result 块,连一句文字都不能带(传统直调没有这个限制);
  2. 必须回传暂停响应里的容器 ID,否则请求被拒;
  3. tools 数组要和原请求一致,code execution 工具必须还在,暂停的代码才能恢复。

第三步:循环直到代码跑完

每个续传响应要么再次暂停(还有新的程序化 tool_use,继续回传),要么返回 code_execution_tool_result(内含 stdout)并让 Claude 给出最终回答。用 stop_reasoncaller 字段区分这两种情况。

🧠 四种高级模式

官方文档给出四个典型代码模式,正好覆盖 PTC 的四类价值(以下代码均摘自官方文档):

1. 批量循环:N 次往返变 1 次

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]}")

2. 提前终止:满足条件就停

for endpoint in ["us-east", "eu-west", "apac"]:
    status = await check_health({"endpoint": endpoint})
    if status == "healthy":
        print(f"Found healthy endpoint: {endpoint}")
        break  # 后面的不再查

3. 条件选工具:按中间结果分叉

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})

4. 数据过滤:只让结论进上下文

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。

⚠️ 常见坑与限制

以下均为官方文档明确列出的限制(非社区传闻)

📚 学习资源

🎯 学完检验:三个自测问题

① 为什么说 PTC 省 token 的本质是「中间结果不进上下文」而不是「少调了工具」?② 一个每步都依赖模型判断的顺序工作流,该不该开 PTC?为什么官方在 τ²-bench 上成本反升 8%?③ 续传请求忘带容器 ID 会发生什么?