OpenAI Agents API 的 JSON 数据结构详解:Agent、Tool Call、Tool Result、Handoff 如何通过 JSON 连接起来?
公测后最容易糊的不是「会不会调工具」,而是这四个对象怎么钉在同一条链上。官方没有一个叫 handoff 的 item。连接靠 ID:session_id、turn_id、call_id、agent_id。本文只拆这条 JSON 链。
阅读前速览
| 对象 | 官方形状 | 钉子 |
|---|---|---|
| Agent | 模型、工具表、是否开 multi_agent | session 创建时写入 |
| Tool Call | required_actions 里的 function_call | turn_id + call_id |
| Tool Result | agent.session.input.tool_result | 同一对 ID 回写 |
| Handoff | create / send / wait / interrupt items | agent_id / subagent_id |
历史里出现 function_call,不等于还在等结果。等不等,看 required_actions。
四个对象靠 ID 相连,不靠一段自然语言
Agents API 里,Agent 是创建 session 时写下的配置。Tool Call 是 session 停住时,required_actions 里的 function_call。Tool Result 是你打回事件流的 agent.session.input.tool_result。所谓 Handoff,在这套 API 里是一组协调 item,不是 SDK 里那个 handoff 对象。
- 同一对 turn_id / call_id:调用和结果必须对得上。对不上,harness 不会接着转。
- 历史不够:items 里有 function_call,只说明曾经调过。还在不在等,读
required_actions。 - Handoff 不是一个 type:看
create_subagent_call和subagent_id,不要找名为 handoff 的字段。
工具怎么调,见 OpenAI Agents API 完整解析。长任务怎么续,见 长时间运行的 Agents API。循环该不该托管,见 Agents API vs Responses API。若 Agent 和 Function Calling 还糊在一起,先读 AI Agent 完整指南。
Agent:配置对象,不是一次回复
创建 session 时一次写进模型、指令、工具和 multi_agent。这是后面所有 item 的主语。官方口是 POST /v1/agents/sessions,header 带 OpenAI-Beta: agents=v1:
{
"agent": {
"model": "gpt-6-astra",
"instructions": "Look up orders. Confirm before any refund.",
"tools": [
{
"type": "function",
"name": "lookup_order",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^[A-Z]-\\d{4}$" }
},
"required": ["order_id"],
"additionalProperties": false
}
}
],
"multi_agent": { "enabled": true, "max_concurrent_subagents": 3 }
},
"environment": { "type": "openai_hosted" },
"input": [{ "role": "user", "content": [{ "type": "input_text", "text": "Look up A-1042." }] }]
}
Agent 配置跟着 session 走。改模型、推理力度,要更新这份 session 的设置;已保存的 Agent 模板只作用于新 session。环境是另一份资源:openai_hosted 或自建沙箱,删 session 不会自动清 sandbox。
Tool Call:只信 required_actions
需要函数结果时,session 发 agent.session.requires_action。待处理的调用在 required_actions 里,形状是带 turn_id / call_id 的 function_call:
{
"type": "function_call",
"turn_id": "turn_2",
"call_id": "call_3c91",
"name": "lookup_order",
"arguments": { "order_id": "A-1042" }
}
参数必须过 Schema。宽 object 等于没有门。口径见 AI 为什么需要 JSON Schema。items 历史里同样可能出现 function_call;官方写明:单凭历史不能断定还在等结果。
Tool Result:同一对 ID,写回事件流
执行函数后,向 session 的 events 端点发送 agent.session.input.tool_result。turn_id 和 call_id 从待处理动作原样抄过来。成功写 success: true 和字符串 output;失败写 success: false 和 error:
{
"type": "agent.session.input.tool_result",
"turn_id": "turn_2",
"call_id": "call_3c91",
"success": true,
"output": "{\"order_id\":\"A-1042\",\"status\":\"paid\"}"
}
对象要先序列化成字符串。断线后若已经跑过函数,用同一对 ID 再交一次保存的结果。不要新开 session「重试」。harness 收齐所需结果才继续这一 turn。
Handoff:协调 item,不是一个字段名
打开 multi_agent.enabled 后,harness 自带创建、传话、等待、打断。你不用声明这些工具。items 里出现的是协调动作:
{
"data": [
{
"type": "create_subagent_call",
"agent_id": "agent_main",
"task": "Compare A-1042 packing notes with the invoice."
},
{
"type": "send_subagent_input_call",
"subagent_id": "sub_7a12",
"input": "Focus on line items only."
},
{
"type": "agent_message",
"from": "sub_7a12",
"text": "Invoice lines match packing notes for A-1042."
}
]
}
- create_subagent_call:派出去。item 上的
agent_id是谁请求的。完成不等于子任务结束。 - send / wait / interrupt:传话、等待、打断。另有
agent_message承载可见的对内文本,流不会给你完整对话稿。 - SDK 的 handoff 对象:那是你进程里的 runner。和这些 item 不是同一份资源。
真正交给下一个主语的,应该是可审计的 JSON:订单号、意图、依据。怎么收成 Schema,见 JSON Schema 与 AI Agent 工作流。
钉子对齐,整条链才成立
把四个对象压成你本地能重建的链。虚构订单,不要贴真实密钥:
{
"session_id": "sess_8f21",
"links": [
{ "object": "agent", "id": "agent_main" },
{ "object": "turn", "id": "turn_2" },
{ "object": "function_call", "call_id": "call_3c91" },
{ "object": "tool_result", "call_id": "call_3c91" },
{ "object": "subagent", "id": "sub_7a12", "via": "create_subagent_call" }
]
}
| 对象 | 你看见的 type | 连到谁 | 常见错位 |
|---|---|---|---|
| Agent | session.agent | session_id | 另开 session 当重试 |
| Tool Call | function_call | turn_id + call_id | 只看 items 历史 |
| Tool Result | input.tool_result | 同一对 ID | output 仍是对象不是字符串 |
| Handoff | create_subagent_call | agent_id / subagent_id | 去找名为 handoff 的字段 |
| Turn | turn.completed / failed | turn_id;根 Agent 的 subagent_id 为空 | 空闲当成成功 |
MCP 那一行只是另一张工具表,不另造一套 ID。边界见 MCP Tool Schema vs Function Calling。
复盘这条链,先摊开两段 JSON
对不上,多半是钉子漂了,不是模型变笨。
- 把 required_actions 和回写事件 格式化,确认
call_id一字不差。 - 用 JSONPath 提取 抽出
turn_id、call_id、subagent_id。 - 同一笔订单的调用和结果 JSON Diff。漂了,先看 ID,再怪模型。
- 主 Agent 的 items 和某个 subagent 的 items 分开列。混在一页,协调会对错人。
- 订单、session、内网 MCP 不要贴进公共格式化站。JSONSort 在浏览器本地跑。
两段轨迹都能按 ID 重建,你才是在读结构,不是在读口号。
常见误区
- 把 items 里的 function_call 当成还在等。等不等,看 required_actions。
- 回写时换了一对 ID。harness 对不上,这一 turn 就停在那。
- output 直接丢对象。官方要字符串或受支持的 content 数组。
- 去 payload 里找 type: handoff。Agents API 用的是 create / send / wait / interrupt。
- 让 subagent 跑你的 function。子代理不支持 function 工具。查单留在主 Agent。
- 把 SDK handoff 和 session items 当成同一份。删一份不会自动清另一份。
FAQ
Tool Call 和 Tool Result 必须成对出现吗?
待处理的调用必须回写。已经结束的历史 function_call 不必再交结果。判断标准是 required_actions,不是 items 列表长度。
Agents API 有没有名为 handoff 的 JSON 字段?
没有作为一等 item type。协调看 create_subagent_call、send_subagent_input_call、wait_for_subagents_call、interrupt_subagent_call。SDK 的 handoff 是另一套对象。
call_id 和 item_id 有什么差别?
call_id 把一次函数调用和它的结果钉在一起。item_id 标识保存下来的那条 item。事件流用 item_id 把增量贴回同一段内容。两套钉子,不要混用。
为什么 turn 完成了,工具却失败了?
官方写明:completed 不等于每个工具都成功。还要看 tool_result 的 success,以及该 turn 的 items。
能不能自己发明一个 handoff JSON 在应用里传?
能,而且该传。那是你的业务对象:订单号、意图、依据。不要指望官方 item 替你记住金额。把它写成 Schema,和 session items 分开存。
结论
Agent、Tool Call、Tool Result、Handoff 不是四段散文。它们是同一条 JSON 链上的四个对象,钉子是 ID。官方把回写写成事件,把交接写成协调 item。
先写清你能不能只凭 call_id 把调用和结果对上。能,结构才算连起来。模型会换;钉子应该先稳定。
延伸阅读
- OpenAI Agents API 完整解析 Session、Tool Calling、Schema 怎么接
- 长时间运行的 Agents API Session、compact、Subagents
- Agents API vs Responses API 循环该由谁跑
- AI 为什么需要 JSON Schema Schema / Function Calling / Structured Output
- JSON Schema 与 Agent 工作流 把工具和状态收成 Schema
- JSONSort 本地工具箱 格式化、校验、Diff
- 全部文章 更多 JSON 与 AI 开发笔记
更新记录: 首次发布