GPT-5.6 Programmatic Tool Calling 教程:对比 Function Calling(Python / Node)
用 Function Calling 做多步工具调用时,常见情况是:模型调一次工具,你把整段 JSON 塞回上下文;再调一次,再塞一次。中间结果越大,上下文越臃肿,token 和延迟都会上去。
Programmatic Tool Calling(PTC) 解决的就是这类问题:模型先写一段 JavaScript,在 OpenAI 托管的隔离 V8 里跑;程序里可以循环、过滤、Promise.all 并行调工具;中间结果可以留在运行时,不必全部进模型上下文;最后只把一小段结构化结果返回给模型。
官方文档:
重要限制: PTC 只在 Responses API 上提供,Chat Completions 没有。
一、和普通 Function Calling 有什么区别
1. 调用流程对比
普通 Function Calling:
模型 → 调 get_inventory
你 → 把库存 JSON 塞回上下文
模型 → 调 get_demand
你 → 把需求 JSON 塞回上下文
模型 → 自己算 shortage,写回答
每一步工具结果都会进入上下文,join / 过滤也靠模型多轮「想」。
Programmatic Tool Calling:
模型 → 生成一段 JS program
V8 → await Promise.all([tools.get_inventory(...), tools.get_demand(...)])
→ 在代码里算 shortage
→ text(JSON.stringify({ ... }))
模型 → 根据最终结构化结果写回答
你的业务函数(查库、调 HTTP、读密钥)仍然在你自己的服务里跑。变的是:编排逻辑从「模型多轮决策」挪到了「一段 JS」。
2. 对照表
| 项目 | 普通 Function Calling | Programmatic Tool Calling |
|---|---|---|
| 谁负责编排 | 模型多轮 | 模型写 JS + 托管 V8 执行 |
| 中间结果 | 默认进上下文 | 可留在程序运行时 |
| 并行 | 模型一轮发多个 call | 代码里 Promise.all |
| 适合场景 | 单次调用、要语义判断、要审批的写操作 | 控制流清楚、中间数据量大 |
| 支持 API | Chat Completions / Responses | 仅 Responses API |
适合 PTC 的典型任务:查很多 SKU、join 两表、过滤、汇总后再回答。
不适合:问一句、查一次就结束的简单对话。
二、运行环境:V8 沙箱能做什么
模型生成的程序跑在 隔离 V8 里,注意它不是完整 Node.js:
| 有 | 没有 |
|---|---|
top-level await | npm 包 |
调用你声明的 tools.* | 网络、文件系统、子进程 |
text(...) / image(...) 输出结果 | console、跨 program 共享状态 |
因此:
- 程序不能直接访问你的数据库或内网
- 只能通过你在请求里声明、并且
allowed_callers允许的工具间接访问 - 鉴权、权限、审批仍然在你这边做,不要因为
caller是 program 就跳过校验
另外:store: false 只表示这次响应不按 store: true 那套持久化续跑,不等于零数据保留(ZDR)。两件事不要混。
三、怎么开启:两处配置
要启用 PTC,需要同时做两件事。
1. 在 tools 里声明 PTC
{ "type": "programmatic_tool_calling" }
2. 给可被程序调用的工具加 allowed_callers
{
"type": "function",
"name": "get_inventory",
"description": "Return sku and available_units.",
"parameters": {
"type": "object",
"properties": {
"sku": { "type": "string" }
},
"required": ["sku"],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"available_units": { "type": "number" }
},
"required": ["sku", "available_units"],
"additionalProperties": false
},
"allowed_callers": ["programmatic"]
}
3. allowed_callers 取值
| 配置 | 含义 |
|---|---|
省略 或 ["direct"] | 只有模型能直接调 |
["programmatic"] | 只有 program 能调 |
["direct", "programmatic"] | 两边都能调 |
4. output_schema 建议写清楚
程序要读字段,工具返回结构要稳定。
output_schema 尽量写完整;返回一大段自然语言的工具,不适合放进 programmatic 路径。
四、响应结构:你要处理哪些字段
Responses 的 output 里常见这些类型:
| 类型 | 含义 |
|---|---|
program | 模型生成的 JS,含 call_id、fingerprint 等 |
function_call | 程序触发的工具调用,可能带 caller |
program_output | 程序执行结果 |
message | 最终自然语言回答 |
最容易踩的坑:回传 caller
当 function_call 带有:
"caller": { "type": "program", "caller_id": "..." }
你回 function_call_output 时,必须把 caller 原样带回。
漏掉这个字段,program 续跑会对不上,这是 PTC 协议里最常见的错误。
模型生成的 program 长什么样(示意)
const [stock, demand] = await Promise.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,
),
}),
);
注意:
- 这段 JS 由 OpenAI 在 V8 里执行
- 你只负责实现并执行
get_inventory、get_demand等真实业务函数 - 看到
program_output不等于结束;要以是否出现最终message为准
五、调用循环怎么写
和普通工具循环相比,PTC 多三点:
- 把整段
response.output都推进input(program、reasoning、function_call都不要丢) - 回传
function_call_output时带上caller - 以出现
message作为结束条件,不要看到program_output就 break
1. Node.js 示例
import OpenAI from "openai";
const client = new OpenAI();
const implementations = {
get_inventory: async ({ sku }) => ({ sku, available_units: 42 }),
get_demand: async ({ sku }) => ({ sku, requested_units: 31 }),
};
const tools = [
{
type: "function",
name: "get_inventory",
description: "Return sku and available_units.",
parameters: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
additionalProperties: false,
},
output_schema: {
type: "object",
properties: {
sku: { type: "string" },
available_units: { type: "number" },
},
required: ["sku", "available_units"],
additionalProperties: false,
},
allowed_callers: ["programmatic"],
},
{
type: "function",
name: "get_demand",
description: "Return sku and requested_units.",
parameters: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
additionalProperties: false,
},
output_schema: {
type: "object",
properties: {
sku: { type: "string" },
requested_units: { type: "number" },
},
required: ["sku", "requested_units"],
additionalProperties: false,
},
allowed_callers: ["programmatic"],
},
{ type: "programmatic_tool_calling" },
];
const input = [
{ role: "user", content: "Compare inventory with demand for sku_123." },
];
while (true) {
const response = await client.responses.create({
model: "gpt-5.6",
store: false,
input,
tools,
include: ["reasoning.encrypted_content"],
});
if (response.status !== "completed") {
throw new Error(`status ${response.status}`);
}
// 整段 output 都要保留
input.push(...response.output);
const calls = response.output.filter((i) => i.type === "function_call");
if (calls.length === 0) {
if (response.output.some((i) => i.type === "message")) {
console.log(response.output_text);
break;
}
continue;
}
for (const call of calls) {
const run = implementations[call.name];
if (!run) throw new Error(`unknown tool: ${call.name}`);
const result = await run(JSON.parse(call.arguments));
input.push({
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
caller: call.caller, // 必须回传
});
}
}
2. Python 示例
import json
from openai import OpenAI
client = OpenAI()
def get_inventory(sku: str):
return {"sku": sku, "available_units": 42}
def get_demand(sku: str):
return {"sku": sku, "requested_units": 31}
implementations = {
"get_inventory": get_inventory,
"get_demand": get_demand,
}
tools = [
{
"type": "function",
"name": "get_inventory",
"description": "Return sku and available_units.",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
"additionalProperties": False,
},
"output_schema": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"available_units": {"type": "number"},
},
"required": ["sku", "available_units"],
"additionalProperties": False,
},
"allowed_callers": ["programmatic"],
},
{
"type": "function",
"name": "get_demand",
"description": "Return sku and requested_units.",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
"additionalProperties": False,
},
"output_schema": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"requested_units": {"type": "number"},
},
"required": ["sku", "requested_units"],
"additionalProperties": False,
},
"allowed_callers": ["programmatic"],
},
{"type": "programmatic_tool_calling"},
]
input_items = [
{"role": "user", "content": "Compare inventory with demand for sku_123."}
]
while True:
response = client.responses.create(
model="gpt-5.6",
store=False,
input=input_items,
tools=tools,
include=["reasoning.encrypted_content"],
)
if response.status != "completed":
raise RuntimeError(f"status {response.status}")
input_items.extend(
item.model_dump(exclude_none=True) for item in response.output
)
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
if any(i.type == "message" for i in response.output):
print(response.output_text)
break
continue
for call in calls:
run = implementations.get(call.name)
if run is None:
raise ValueError(f"unknown tool: {call.name}")
result = run(**json.loads(call.arguments))
input_items.append(
{
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
"caller": call.caller.model_dump() if call.caller else None,
}
)
使用说明:
- 把假数据换成真实业务实现即可
- 模型 ID 以你账号实际可用的 GPT-5.6 系列为准
store: true时可用previous_response_id续跑,不必每次重放全部 historystore: false时才需要像上面这样手动把 output 全推进input
六、普通 Function Calling 对照(同一任务)
如果不走 PTC,同一任务通常是:
- 模型调
get_inventory→ 你把库存 JSON 塞回上下文 - 模型调
get_demand→ 再塞 - 模型在两段 JSON 上做计算,输出自然语言
优点:协议简单,没有 program / caller。
缺点:中间结果全进上下文,join 逻辑靠模型推理。
怎么选:
- 数据小、步数少 → 普通 FC 更简单
- 中间数据大、控制流固定 → 值得上 PTC
七、提示词和工具设计建议
1. 别只写「请尽量用 PTC」
两种模式同时开着时,最好在系统提示里写清楚边界,例如:
库存/需求对比走 Programmatic Tool Calling:
- 只使用 get_inventory、get_demand
- 能并行就并行
- 输出固定 JSON 字段:sku, available_units, requested_units, shortage_units
- 缺字段返回结构化失败,不要编造
任何修改库存的动作走 direct 调用,并且需要审批。
比较稳妥的默认拆法:
- 读路径 / 汇总路径 →
programmatic - 写路径 / 高影响操作 →
direct+ 应用层审批
2. 工具设计
- 返回短、结构化的数据
- 尽量幂等
- 错误形态写进
description - 高影响动作永远在应用层做审批,和
caller类型无关
3. 和 Tool Search 的关系
- Tool search 不能在 JS program 里调用
- 使用
defer_loading的工具,要先被模型 load,后续 program 才能用 - 已经在跑的 program 中途搜不到新工具
八、什么时候用、什么时候别用
适合用 PTC
- 中间结果大(多 SKU、多表 join、批量过滤)
- 控制流事先能画出来(先查 A、再查 B、再算 C)
- 你已经明显感到普通 FC 把大 JSON 反复塞进 prompt
不适合用 PTC
- 一次查询就结束
- 每一步都要重新做语义判断(搜什么、信哪段)
- 写操作、转账、删库、发生产通知等需要人工审批的动作
- 只是想「少写一点 while 循环」——PTC 不会让你少写循环,协议反而更啰嗦
上线前建议对比 4 个数
同一批任务,先跑普通 FC 做 baseline,再开 PTC,对比:
- 正确性
- 证据是否还在(有没有为了省 token 把关键细节丢掉)
- 总 token
- 端到端延迟
token 降了但答案丢证据,不算赚。
九、常见问题
Q:Chat Completions 能用 PTC 吗?
不能。只有 Responses API。
Q:程序是在我本机跑的吗?
不是。JS program 在 OpenAI 托管 V8 里跑;你的工具实现仍在你自己的服务里跑。
Q:为什么回了工具结果,program 却接不上?
检查 function_call_output 有没有原样带回 caller。这是最常见原因。
Q:看到 program_output 就可以结束循环了吗?
不建议。以最终 message 为准;program_output 只是中间阶段。
Q:store: false 是不是零数据保留?
不是。store: false 和 ZDR 是两回事。
Q:所有工具都设成 allowed_callers: ["programmatic"] 可以吗?
可以,但不一定合适。需要模型直接点名调用、或需要人工审批的写操作,通常保留 direct。
十、简要对照
| 你想做的事 | 做法 |
|---|---|
| 启用 PTC | tools 里加 { "type": "programmatic_tool_calling" } |
| 允许程序调某工具 | 该工具加 allowed_callers: ["programmatic"] 或双开 |
| 写调用循环 | 保留完整 output + 回传 caller + 以 message 结束 |
| 续跑会话 | store: true 用 previous_response_id;store: false 手动 replay |
| 控制读写边界 | 读/汇总走 programmatic,写操作走 direct + 审批 |
一句话总结:
Programmatic Tool Calling 不是新概念包装,而是把「多工具编排」从模型多轮上下文里挪到托管 V8 的 JS 里跑,从而减少中间 JSON 对上下文的污染。
接好 allowed_callers,回传时别丢 caller,用 Responses API 按完整 output 循环即可落地。
字段和模型名以你账号当前文档为准,接入前再核对官方最新说明。