GPT-5.6 和 Claude Sonnet 4.5 几乎在同一时间段把 Programmatic Tool Calling(PTC,程序化工具调用) 推到了台前。名字一样,目标也一样:让模型不再「调一个工具 → 等结果 → 再想下一步」,而是写一段程序,在沙箱里批量协调工具、过滤中间结果,最后只把精炼后的结论送回上下文。
但两家实现路径并不相同。理解这些差异,直接影响你怎么选型、怎么写集成代码、以及哪些场景该用 PTC、哪些不该用。
先看传统模式。无论 OpenAI 还是 Anthropic,经典 tool calling 都是同一个五步循环:
用户提问 → 模型返回 tool_use → 你的服务执行工具 → 你把结果塞回上下文 → 模型再推理
每调一次工具,至少消耗一轮模型推理。任务稍复杂,代价就会叠加:
问题 | 典型场景 |
|---|---|
延迟叠加 | 查 20 个员工的报销记录 = 20 次模型往返,每次数百毫秒到数秒 |
Token 膨胀 | 每次工具结果都进上下文,2000 行报销明细 × N 次 = 数百 KB |
编排脆弱 | 模型用自然语言「记住」上一步结果,多步依赖容易出错 |
无法并行 | 除非 API 支持 parallel tool calls,否则串行等待 |
PTC 的核心思路是:把「编排逻辑」从模型上下文里挪到沙箱代码里。
传统模式:
模型 → 工具A → 模型 → 工具B → 模型 → 工具C → 模型 → 答案
PTC 模式:
模型 → 写程序 → [工具A → 工具B → 工具C](在沙箱内,不进上下文)→ 程序输出 → 模型 → 答案
Anthropic 的工程博客给过一个具体数字:团队报销合规检查,传统方式要 20 次模型往返、把数千行明细都拉进上下文;用 PTC 后,一个脚本跑完 20 次查询、过滤、聚合,模型最终只看到超预算的 2–3 个人——上下文从约 200KB 降到约 1KB。在 BrowseComp 等 Agent 搜索基准上,PTC 平均提升 11% 准确率,同时减少 24% 输入 token。
理解 PTC,最容易混淆的一点是:用户侧到底执行什么? 一个常见的误解是——模型生成代码后,由用户下载并运行这段代码。实际上并非如此。
生成代码后的执行、暂停、恢复,全部发生在模型服务商的后台沙箱里。 用户只负责一件事:当沙箱里的程序需要调用「你提供的工具」时,由你的服务执行这些工具,并把结果回传给模型接口。
你(调用方) 模型服务商后台
│ │
│ ① 发请求(prompt + 工具定义) │
│ ──────────────────────────────────>│
│ │ ② 模型生成编排代码(JS 或 Python)
│ │ ③ 在沙箱里开始执行这段代码
│ │
│ │ ④ 代码执行到「需要调你的工具」→ 暂停
│ ⑤ 收到响应: │
│ - 生成的代码(供观察/调试) │
│ - 需要调用的工具名和参数 │
│ - caller / container 等续跑信息 │
│ <──────────────────────────────────│
│ │
│ ⑥ 你的服务执行工具 │
│ │
│ ⑦ 把工具结果 + 续跑信息回传 │
│ ──────────────────────────────────>│
│ │ ⑧ 沙箱从断点继续执行代码
│ │ (可能再次暂停、再次要工具)
│ │
│ ... 可能循环多次 ... │
│ │
│ ⑨ 代码跑完,输出精炼结果 │
│ ⑩ 模型基于最终结果生成回答 │
│ <──────────────────────────────────│
和传统 Tool Calling 的对比如下:
传统 Tool Calling | Programmatic Tool Calling | |
|---|---|---|
谁编排多步逻辑 | 模型(每步都要推理) | 模型写的程序(在沙箱里跑) |
每步工具结果 | 都进模型上下文 | 中间结果留在沙箱,不进上下文 |
用户执行什么 | 工具 | 只有工具,不执行生成代码 |
模型推理次数 | N 次工具 ≈ N 次推理 | 通常 1–2 次(写程序 + 看最终结果) |
1. 代码不是你的服务执行的
❌ 用户拿到代码 → 自己跑这段 JS/Python
✅ 服务商沙箱跑代码 → 碰到你的工具时暂停 → 你把结果送回 → 沙箱继续跑
OpenAI 在隔离 V8 运行时里执行 JavaScript;Claude 在 Code Execution 容器里执行 Python。响应里返回的代码主要是给你观察、调试、审计用的,不是你的执行入口。
2. 回传的不只是「工具结果」,还有续跑凭证
不能把工具结果简单塞进任意上下文位置。必须带上运行时恢复程序所需的关联信息:
OpenAI 回传 function_call_output,并原样保留 caller:
{
"type":"function_call_output",
"call_id":"call_inventory_123",
"output":"{\"sku\": \"sku_123\", \"available_units\": 42}",
"caller":{"type":"program","caller_id":"call_prog_123"}
}
Claude 回传 tool_result,且请求中必须带上 container ID:
{
"role":"user",
"content":[{
"type":"tool_result",
"tool_use_id":"toolu_def456",
"content":"[{\"customer_id\": \"C1\", \"revenue\": 45000}]"
}]
}
缺少 caller 或 container,后台无法从断点恢复执行。
3. 一次请求里可能暂停多次
不是「调一次工具就结束」。如果生成的代码里有循环:
for employee in employees: # 20 个人
expenses = await get_expenses(employee) # 每次都会暂停
# 在沙箱内处理...
可能暂停 20 次——你回传 20 次工具结果,沙箱每次从断点继续,最后才一次性输出聚合结果。
4. 并非所有工具都要用户执行
类型 | 谁执行 | 例子 |
|---|---|---|
客户端工具(client-owned) | 你的服务 | 查数据库、调内部 API |
托管工具(hosted) | 模型服务商后台 | web search、code interpreter |
只有你的自定义工具会「弹回」给你;托管工具在沙箱内直接完成,通常不会出现在你的回调里。
5. 用一句话概括角色分工
模型后台写程序、跑程序;程序需要你的工具时暂停并把请求弹回给你;你执行工具、送回结果;模型后台从断点继续跑程序;跑完后只把精炼结果交给模型做最后回答。
你的角色是工具提供者,不是代码执行者——这也是它叫 Programmatic Tool Calling,而不是「把代码发给你跑」的原因。
尽管实现细节不同,两家 PTC 共享一套架构模式:
allowed_callers不是每个工具都默认可被程序调用。你必须在工具定义上显式标注:
调用方式 | OpenAI | Claude |
|---|---|---|
仅直接调用 | 省略或 ["direct"] | 省略或 ["direct"] |
仅程序内调用 | ["programmatic"] | ["code_execution_20260120"] |
两者皆可 | ["direct", "programmatic"] | ["direct", "code_execution_20260120"] |
官方都建议:对每个工具只选一种模式,给模型更清晰的引导,而不是同时开两种。
caller 字段程序发起的工具调用,响应里会带 caller 字段,标明「这次调用是谁发起的」:
caller.type = "direct"caller 指向对应的程序 / code execution 实例你的应用在回传工具结果时,必须原样保留 caller,运行时才能正确恢复暂停的程序。
程序执行到需要客户端工具时暂停,API 把 tool_use / function_call 返回给你;你执行工具、回传结果;程序从断点继续。循环直到程序跑完,才把最终输出交给模型。
这是 PTC 最大的价值点。工具返回的原始数据在沙箱内被过滤、聚合、转换,只有程序的 stdout / program_output 进入模型的下一轮推理。
OpenAI 的 PTC 是 Responses API 的专属能力,随 GPT-5.6 正式推出(更早的 GPT-5.4+ 可能有限支持,需查模型页)。
┌─────────────────────────────────────────────────────┐
│ Responses API Request │
│ tools: [ │
│ { type: "function", name: "get_inventory", │
│ allowed_callers: ["programmatic"], ... }, │
│ { type: "programmatic_tool_calling" } ← 开关 │
│ ] │
└──────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 模型生成 program 项(JavaScript 代码) │
│ OpenAI 在隔离 V8 运行时中执行 │
│ - 支持 top-level await │
│ - 无 Node.js、无网络、无文件系统、无 npm │
│ - 只能通过 tools.* 调用已启用的工具 │
└──────────────────────┬──────────────────────────────┘
▼ 程序调用 client-owned 工具时暂停
┌─────────────────────────────────────────────────────┐
│ response.output 包含: │
│ - program(代码 + call_id + fingerprint) │
│ - function_call(caller.caller_id → program) │
└──────────────────────┬──────────────────────────────┘
▼ 你的应用执行工具
┌─────────────────────────────────────────────────────┐
│ 回传 function_call_output(保留 call_id + caller) │
└──────────────────────┬──────────────────────────────┘
▼ 程序恢复执行
┌─────────────────────────────────────────────────────┐
│ program_output(status: completed/incomplete) │
│ → 模型收到精炼结果,生成最终 message │
└─────────────────────────────────────────────────────┘
1. 启用 PTC
在 tools 数组中加入 {"type": "programmatic_tool_calling"},并对允许被程序调用的工具设置 allowed_callers: ["programmatic"]。
2. 定义 output_schema
OpenAI 要求你为 function 工具定义 output_schema——描述工具返回的 JSON 结构。这样生成的 JavaScript 可以可靠地解析返回值:
{
"type":"function",
"name":"get_inventory",
"description":"Return an object with sku (string) and available_units (number).",
"parameters":{
"type":"object",
"properties":{"sku":{"type":"string"}},
"required":["sku"]
},
"output_schema":{
"type":"object",
"properties":{
"sku":{"type":"string"},
"available_units":{"type":"number"}
},
"required":["sku","available_units"]
},
"allowed_callers":["programmatic"]
}
3. 模型生成的代码长什么样
响应中的 program 项包含 JavaScript,通过 tools.* 命名空间调用工具:
const [stock, demand] = awaitPromise.all([
tools.get_inventory({ sku: 'sku_123' }),
tools.get_demand({ sku: 'sku_123' })
]);
text(JSON.stringify({
sku: stock.sku,
available_units: stock.available_units,
requested_units: demand.requested_units,
shortage_units: Math.max(demand.requested_units - stock.available_units, 0)
}));
程序用 text(...) 或 image(...) 输出最终结果。
PTC 在 response.output 中引入三类新 item:
生成的 JS 代码、call_id、fingerprint(用于恢复/重放)示例(程序暂停,等待两个并行工具调用):
[
{
"type":"program",
"call_id":"call_prog_123",
"code":"const [stock, demand] = await Promise.all([...]); text(JSON.stringify({...}));",
"fingerprint":"opaque_replay_state"
},
{
"type":"function_call",
"call_id":"call_inventory_123",
"name":"get_inventory",
"arguments":"{\"sku\":\"sku_123\"}",
"caller":{"type":"program","caller_id":"call_prog_123"}
},
{
"type":"function_call",
"call_id":"call_demand_123",
"name":"get_demand",
"arguments":"{\"sku\":\"sku_123\"}",
"caller":{"type":"program","caller_id":"call_prog_123"}
}
]
while (true) {
const response = await client.responses.create({
model: "gpt-5.6",
store: false,
input,
tools,
});
input.push(...response.output);
const calls = response.output.filter(item => item.type === "function_call");
if (calls.length === 0) {
// 收到最终 message,结束
break;
}
for (const call of calls) {
const result = awaitexecuteTool(call.name, JSON.parse(call.arguments));
input.push({
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
caller: call.caller, // 必须原样保留
});
}
}
JavaScript(V8 隔离运行时)Tool search 是 Responses API 的顶层工具,不能从程序内部的 JavaScript 调用。流程是:模型先用 tool search 加载 deferred 工具 → 再启动 program 调用它们。已在运行的 program 无法触发 tool search。
Claude 的 PTC 不走独立工具类型,而是建立在已有的 Code Execution 工具之上——模型写 Python,在沙箱容器里跑,容器内的代码以 async 函数形式调用你的工具。
2025 年 11 月作为 Advanced Tool Use 的一部分发布(Beta);2026 年 2 月随 Sonnet 4.6 发布转为 GA,不再需要 beta header。
┌─────────────────────────────────────────────────────┐
│ Messages API Request │
│ tools: [ │
│ { type: "code_execution_20260120", │
│ name: "code_execution" }, ← 沙箱开关 │
│ { name: "query_database", │
│ allowed_callers: ["code_execution_20260120"] } │
│ ] │
└──────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ 模型生成 server_tool_use(Python 代码) │
│ Anthropic 在 Code Execution 容器中执行 │
│ - 工具被暴露为 async Python 函数 │
│ - 支持 asyncio.gather 并行 │
│ - REPL 状态可跨 cell 持久化(20260120 版本) │
└──────────────────────┬──────────────────────────────┘
▼ 代码调用 client-owned 工具时暂停
┌─────────────────────────────────────────────────────┐
│ response 包含: │
│ - server_tool_use(code_execution 代码) │
│ - tool_use(caller 指向 code_execution 实例) │
│ - container.id(必须回传以恢复执行) │
│ - stop_reason: "tool_use" │
└──────────────────────┬──────────────────────────────┘
▼ 你的应用执行工具
┌─────────────────────────────────────────────────────┐
│ 回传 tool_result(user 消息,仅含 tool_result) │
│ 必须带 container ID │
└──────────────────────┬──────────────────────────────┘
▼ 代码从断点恢复
┌─────────────────────────────────────────────────────┐
│ code_execution_tool_result(stdout/stderr) │
│ → 模型收到精炼结果,生成最终 text │
└─────────────────────────────────────────────────────┘
1. 启用 PTC
不需要额外的 programmatic_tool_calling 开关。只需:
tools 中加入 code_execution_20260120allowed_callers: ["code_execution_20260120"]2. 工具描述要详细
Claude 官方强调:在 tool description 里写清楚返回格式(比如「返回 JSON 对象列表」),模型才能在 Python 代码里正确 json.loads() 处理结果。
3. 模型生成的代码长什么样
import json
rows = json.loads(await query_database({"sql": "SELECT ..."}))
top_customers = sorted(rows, key=lambda x: x['revenue'], reverse=True)[:5]
print(f'Top 5 customers: {top_customers}')
工具在容器内被暴露为 async Python 函数:接收一个 dict 参数,返回你回传的 tool_result 字符串。
Claude 不引入新的 item 类型,而是复用 Messages API 的现有结构:
模型发起的 code execution 请求,包含 Python 代码程序暂停时的响应示例:
{
"role":"assistant",
"content":[
{"type":"text","text":"I'll query the purchase history and analyze the results."},
{
"type":"server_tool_use",
"id":"srvtoolu_abc123",
"name":"code_execution",
"input":{"code":"import json\nrows = json.loads(await query_database({...}))\n..."}
},
{
"type":"tool_use",
"id":"toolu_def456",
"name":"query_database",
"input":{"sql":"SELECT ..."},
"caller":{
"type":"code_execution_20260120",
"tool_id":"srvtoolu_abc123"
}
}
],
"container":{
"id":"container_xyz789",
"expires_at":"2026-01-20T14:30:00Z"
},
"stop_reason":"tool_use"
}
messages = [{"role": "user", "content": "..."}]
container_id = None
whileTrue:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=4096,
messages=messages,
tools=tools,
container=container_id, # 续传时必传
)
messages.append({"role": "assistant", "content": response.content})
container_id = response.container.idif response.container elseNone
# 检查是否有程序发起的 tool_use
programmatic_calls = [
block for block in response.content
if block.type == "tool_use"
and block.caller.type.startswith("code_execution")
]
ifnot programmatic_calls:
break# 程序已结束或没有工具调用
# 回传工具结果:user 消息只能包含 tool_result,不能有 text
tool_results = []
for call in programmatic_calls:
result = execute_tool(call.name, call.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": call.id,
"content": json.dumps(result),
})
messages.append({"role": "user", "content": tool_results})
Python(沙箱容器)Claude 还把 PTC 的原则应用到了 web search——Dynamic Filtering(web_search_20260318)。模型写 Python 预处理搜索结果,过滤无关内容后再进入上下文。这不是独立的 PTC,而是同一套「在代码里过滤,少进上下文」哲学的原生工具层实现。
维度 | OpenAI (GPT) | Anthropic (Claude) |
|---|---|---|
API | Responses API | Messages API |
启用方式 | 独立的 programmatic_tool_calling 工具 | 复用 code_execution_20260120 |
程序语言 | JavaScript(V8 隔离运行时) | Python(沙箱容器) |
Opt-in 字段 | allowed_callers: ["programmatic"] | allowed_callers: ["code_execution_20260120"] |
程序 item | program(含 fingerprint) | server_tool_use(code_execution) |
工具调用 item | function_call | tool_use |
结果回传 | function_call_output + caller | tool_result + container ID |
程序完成 item | program_output | code_execution_tool_result |
状态续传 | fingerprint / previous_response_id | container ID(等待工具结果时必填) |
维度 | OpenAI | Claude |
|---|---|---|
并行调用 | Promise.all | asyncio.gather |
返回 schema | 显式 output_schema 字段 | 依赖 tool description 描述 |
可编排的托管工具 | MCP、shell、code_interpreter、apply_patch | 仅 client-side 自定义工具 |
MCP 程序化调用 | 支持(含 approval 暂停) | 不支持 |
Tool Search 配合 | 需先加载 deferred 工具再开 program | 独立的 tool search 工具 |
ZDR 兼容 | 支持(无需持久 container) | 不支持 |
发布时间 | 2026.07(GPT-5.6) | 2025.11 Beta → 2026.02 GA |
限制 | OpenAI | Claude |
|---|---|---|
强制特定工具 | 不可用 tool_choice 强制 program 调用 | 不可用 tool_choice 强制 |
Strict schema 工具 | 未明确禁止 | strict: true 工具不可用 |
禁用并行工具 | 未明确禁止 | disable_parallel_tool_use: true 不可用 |
递归 $ref schema | 未提及 | 返回 400 错误 |
回传格式 | function_call_output 可混合其他 item | 等待 program 结果时 user 消息只能含 tool_result |
工具结果超时 | 未明确 | 约 4 分钟 |
以「查三个区域销售数据,找出收入最高的区域」为例。
1. POST /v1/responses
tools: [get_sales(region), programmatic_tool_calling]
input: "查 West/East/Central 三地销售,找出最高收入区域"
2. 响应 output:
├─ program: "const results = {}; for (const r of ['West','East','Central']) {
│ results[r] = await tools.get_sales({region: r});
│ } text(JSON.stringify(results));"
├─ function_call: get_sales({region: "West"}) caller→program
├─ function_call: get_sales({region: "East"}) caller→program
└─ function_call: get_sales({region: "Central"}) caller→program
3. 你的服务并行执行三个 get_sales,回传三个 function_call_output
4. 响应 output:
└─ program_output: {"West": 120000, "East": 95000, "Central": 110000}
5. 响应 output:
└─ message: "Central 区域收入最高,为 $110,000。"
你的应用跑了 1 次 API 请求 + 1 次续传(回传工具结果)+ 可能 1 次最终响应。模型推理 2 次,工具执行 3 次(并行)。
1. POST /v1/messages
tools: [code_execution_20260120, get_sales(allowed_callers: code_execution)]
messages: [{role: user, content: "查三地销售..."}]
2. 响应:
├─ server_tool_use: code_execution (Python 循环代码)
├─ tool_use: get_sales({region: "West"}) caller→code_execution
├─ container: {id: "container_abc", expires_at: "..."}
└─ stop_reason: "tool_use"
3. 你的服务执行 get_sales,回传 tool_result
⚠️ 必须带 container: "container_abc"
⚠️ user 消息只能含 tool_result
4. (可能重复 2-3 步,直到代码中所有工具调用完成)
5. 响应:
├─ code_execution_tool_result: stdout="{'West': 120000, ...}"
└─ text: "Central 区域收入最高,为 $110,000。"
流程类似,但多了一步 container 管理,且回传格式约束更严格。
两家官方指南高度一致:
两家都支持 allowed_callers: ["direct", "programmatic"](或等价的双模式),并建议在 prompt 里明确划分:
<tool_orchestration>
对 [bounded stage] 使用 Programmatic Tool Calling,仅调用 [eligible tools]。
并行执行独立调用。处理中间结果后,输出恰好 [schema]。
对 [semantic judgment / approval / validation] 使用 direct tool calling。
</tool_orchestration>
OpenAI 额外强调:如果工具返回结构在写程序前不确定,应保持 direct calling,让模型先看结果再决定怎么用。
程序需要解析,不是读自然语言OpenAI | Claude | |
|---|---|---|
入门难度 | 中等:需理解 program / program_output 新 item 类型 | 中等:复用 Messages API 结构,但 container 管理增加复杂度 |
状态管理 | store: false 时 replay 全部 output items;或 previous_response_id | 必须跟踪 container ID,等待工具结果时必填 |
调试 | program.code 可直接看到生成的 JS | server_tool_use.input.code 可直接看到生成的 Python |
SDK 支持 | OpenAI SDK 原生支持 Responses API | Anthropic SDK 支持,code_execution_20260120 需显式配置 |
如果你的组织有 Zero Data Retention 要求:
这可能是某些场景下的决定性因素。
不要假设 PTC 一定更快或更便宜。官方建议:
以 direct tool calling 为 baseline,在代表性任务上对比两种方案。
衡量维度:
GPT 和 Claude 的 Programmatic Tool Calling 解决的是同一个问题——Agent 外循环(outer loop)的效率。传统 tool calling 让模型既当「调度器」又当「处理器」,每一步都要把中间结果读进上下文、做一次推理;PTC 把调度逻辑下沉到沙箱代码,模型只在开始(写程序)和结束(读结果)时出现。
但「同题异构」体现在:
OpenAI | Anthropic | |
|---|---|---|
哲学 | 新增独立的 PTC 运行时(V8 + JS) | 复用 Code Execution 容器(Python) |
接口 | Responses API 新 item 类型 | Messages API 现有 block + container |
扩展 | PTC 可编排 MCP、shell 等托管工具 | PTC 仅限 client-side 工具,MCP 被排除 |
合规 | ZDR 友好 | 标准 retention |
成熟度 | 2026.07 随 GPT-5.6 发布 | 2025.11 Beta → 2026.02 GA,生态更久 |
对开发者来说,PTC 不是「升级版 tool calling」——它是一种工作流阶段的选型。Bounded、可预测、数据密集的阶段用 PTC;需要模型实时判断、审批、或保留完整引用的阶段用 direct calling。两家都支持混合模式,关键是在 prompt 和工具定义里把边界划清楚。
下一阶段的竞争,可能不是「谁的 PTC 更强」,而是谁能让开发者更低成本地在 direct 和 programmatic 之间切换——OpenAI 的 output_schema + fingerprint 重放,Claude 的 Dynamic Filtering 原生扩展,都是这个方向上的探索。
本文基于 OpenAI Programmatic Tool Calling 文档、Anthropic Programmatic Tool Calling 文档 及 Advanced Tool Use 工程博客(截至 2026 年 7 月)整理分析。