博客 • Open Responses
Open Responses 加入 AAIF 之后,把一轮工具调用重放到另一家模型前,JSON 里的 type、arguments 和 status 要先查什么?
2026 年 10 月 22 日,Agentic AI Foundation 宣布 Open Responses 成为托管项目。规范把一轮模型交互收成 item:message、function_call、function_call_output 是可移植核心;type 里带供应商前缀的记录只属于那一家实现。
把日志里的 item 发给另一家模型之前,先在本地看三栏。type 里如果有冒号,就不要当成通用 function_call。arguments 在规范示例里是 JSON 字符串,要先 parse 再对照工具的 JSON Schema。status 不是 completed 时,不要追加 function_call_output。
昨天写的 Bedrock 结构化输出 管的是模型文本的形状。Chat Completions 里 arguments 也常是字符串,那是另一套信封,见 AI Agent 与 JSON Schema。schema 通过仍不等于可以执行,见 权限决定。本篇只写 Open Responses 这一圈 item。
- 可移植 item 的 type 是 message、function_call 或 function_call_output。
- 带冒号的 type 按供应商扩展处理,重放时不要当成通用工具调用。
- arguments 必须是字符串;解析后的对象再用工具 schema 验 region 这类字段。
- status 为 in_progress 或 incomplete 时停住;只有 completed 才写 function_call_output,且 call_id 必须对上。
先记住这一点:AAIF 收下的是共享 item 形状,一条 JSON 仍可能只属于某一家供应商。下面按 type、arguments、status 的顺序看,重放前哪一栏会拦住。
10 月 22 日,共享 item 进了开放治理
当天在 AGNTCon 与 MCPCon,AAIF 宣布四个新项目。OpenAI 贡献的 Open Responses 与 MCP、A2A 并列,进入 Linux Foundation 旗下的开放治理。公告见 AAIF 公告。同一天还收下了 Anthropic 的 Agent Skills、Hugging Face 的 OpenEnv 和阿里巴巴的 OpenSandbox。本篇不写这三份目录或技能格式,只处理 Open Responses 的 item JSON。
AWS 同一天的开源博客写明,Amazon Bedrock 已支持 Responses API,并参与这项规范。见 AWS 开源博客。字段含义以 Open Responses 规范 为准:非流式响应用 application/json,item 用 type 区分。你要解决的是,日志里已经有一条 function_call 时,换供应商或隔天重放,哪三个字段会让下一请求不该发出去。
一条可重放的 function_call 至少有哪几个字段?
可以进入下一步检查的 function_call,至少带 type、id、name、call_id、status、arguments。arguments 的值是字符串,里面才是工具参数对象。下面这条 region 为 apac,后面会用同一份 schema 再验一次。
{
"type": "function_call",
"id": "fc_001",
"name": "get_latest_sales_report",
"call_id": "call_123",
"status": "completed",
"arguments": "{\"region\":\"apac\"}"
}
工具在你这边执行完之后,下一请求追加的是 function_call_output,不是 Chat Completions 的 tool_calls 数组。规范里的续写示例用同一个 call_id,并带上 output。服务端若把结果再作为 item 返回,那时再补 id 和 status。
{
"type": "function_call_output",
"call_id": "call_123",
"output": "sales report for apac"
}
type 里出现冒号时,为什么不能换供应商执行?
规范允许实现方增加自己的 item,但 type 必须带供应商 slug 前缀,例如 openai:web_search_call。这类记录可以留在原会话里回灌给同一家,规范写明不应假定它能在别的 Open Responses 服务器上移植。本地规则:type 含冒号就标成 namespaced,不生成 function_call_output。
{
"type": "openai:web_search_call",
"id": "ws_001",
"status": "completed",
"action": { "type": "search", "query": "apac sales report" }
}
| 看到的 type | 能否换供应商重放 | 本地动作 |
|---|---|---|
function_call | 可以进入下一步 | 继续查 arguments 和 status。 |
message | 可以放进 input | 当作对话文本,不要当成工具结果。 |
openai:web_search_call | 不可移植 | 留在原供应商,或从跨供应商日志里拆出去。 |
arguments 为什么必须先是字符串,再做 schema?
流式事件 response.function_call_arguments.delta 会在对象完成前追加文本,所以落盘时 arguments 是字符串,不是对象。把对象直接写进 arguments 的日志,下一跳会在 parse 失败,或被另一家实现拒绝。先确认它是 string,再 json.loads,再用工具的 JSON Schema。下面这份 schema 只接受 apac、emea、amer。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["region"],
"properties": {
"region": {
"type": "string",
"enum": ["apac", "emea", "amer"]
}
}
}
下面这条字符串能 parse,但 region 是 global,不在 enum 里。闸门应返回 arguments_schema,工具不要执行。
{
"type": "function_call",
"id": "fc_009",
"name": "get_latest_sales_report",
"call_id": "call_129",
"status": "completed",
"arguments": "{\"region\":\"global\"}"
}
| 看到的 arguments | 闸门结果 | 本地动作 |
|---|---|---|
| 字符串,内容是 region 为 apac 的对象 | ok | 可以执行工具,再写 function_call_output。 |
| arguments 本身是对象,不是字符串 | arguments_type | 拒绝。先改回字符串再 parse。 |
| 字符串能 parse,但 region 为 global | arguments_schema | 不执行。把校验错误写回,不要猜地区。 |
status 不是 completed 时,为什么不能写工具结果?
规范把 item 收成状态机:in_progress 表示还在输出,incomplete 是终态并且必须是该响应的最后一项,整段 response 也必须是 incomplete。completed 之后才不再改这条 item。incomplete 时 arguments 可能被截断,半截 JSON 不该执行。
| status | 能否写 function_call_output |
|---|---|
in_progress | 否。等这条流结束。 |
incomplete | 否。终态,参数可能被截断。 |
completed | 是。再确认 type 没有冒号,arguments 通过 schema,call_id 与下一条相同。 |
| 其它值,包括 failed | 否。不要执行副作用。 |
下面这段闸门和本文的样本是同一份代码:冒号、非 function_call、status、arguments 类型、parse、schema,按这个顺序返回原因。只有 ok 才可以执行工具并追加 function_call_output。
def actual_type(data: object) -> str:
if data is None:
return "null"
if isinstance(data, bool):
return "boolean"
if isinstance(data, list):
return "array"
if isinstance(data, int):
return "integer"
if isinstance(data, float):
return "number"
if isinstance(data, str):
return "string"
if isinstance(data, dict):
return "object"
raise TypeError(type(data))
def validate(data: object, schema: dict) -> list[str]:
errors: list[str] = []
if not isinstance(schema, dict):
return errors
types = schema.get("type")
if isinstance(types, str):
types = [types]
actual = actual_type(data)
if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
errors.append("type")
if "enum" in schema and not any(value == data for value in schema["enum"]):
errors.append("enum")
if isinstance(data, dict) and schema.get("properties"):
for key in schema.get("required") or []:
if key not in data:
errors.append(f"required:{key}")
if schema.get("additionalProperties") is False:
for key in data:
if key not in schema["properties"]:
errors.append(f"additional:{key}")
for key, sub in schema["properties"].items():
if key in data:
errors.extend(validate(data[key], sub))
if isinstance(data, list) and schema.get("items"):
for item in data:
errors.extend(validate(item, schema["items"]))
return errors
def replay_gate(item: dict, schema: dict) -> str:
item_type = item.get("type")
if not isinstance(item_type, str) or ":" in item_type:
return "namespaced"
if item_type != "function_call":
return "not_function_call"
if item.get("status") != "completed":
return "status"
arguments = item.get("arguments")
if not isinstance(arguments, str):
return "arguments_type"
try:
parsed = json.loads(arguments)
except json.JSONDecodeError:
return "arguments_parse"
if validate(parsed, schema):
return "arguments_schema"
return "ok"
在 JSONNote 里怎样把这三栏分开看?
- 1先看 arguments 是不是字符串
打开 JSON 格式化,把整条 item 贴进去。arguments 如果展开成对象而不是一行字符串,落盘形状已经偏了,闸门会返回 arguments_type。
- 2再验解开后的参数
把 arguments 字符串解开后的对象贴进 JSON Schema。schema 用上面的 region enum。global 会标出来,不要在执行前改成 apac。
- 3最后对比两家 item
打开 JSON Diff。左边放原供应商 item,右边放准备重放的 item。type 从 function_call 变成带冒号的扩展,或 call_id 对不上,就不要发下一请求。
常见问题
Open Responses 进了 AAIF,同一份 JSON 就能直接换到 Bedrock 或别的实现吗?
不能。可移植的是 message、function_call、function_call_output 这一核心。带供应商前缀的 item 不应假定能跨服务器重放。Bedrock 上 Claude 的结构化输出管的是另一层:模型文本的 JSON Schema,不是这条 item 信封。
arguments 为什么不直接存成对象?
规范示例把 arguments 写成 JSON 字符串。流式时用 function_call_arguments.delta 逐段追加,status 还没到 completed 之前,这段字符串可能还不完整。不要提前 parse 了就执行。
status 是 incomplete 时,能不能先跑工具?
不要。incomplete 是终态。规范要求它是该响应的最后一项,并且整段 response 也是 incomplete。半截 arguments 往往通不过 schema;就算碰巧通过,也不该当成完整调用执行。
allowed_tools 已经限制了工具名,还要本地 schema 吗?
要。allowed_tools 限制的是这次请求里哪些工具名可以真正执行,工具定义仍可以全部放在 tools 里。它不检查 arguments 里的 region 是否在 enum 中。名字被允许,参数仍要 parse 再做 schema。
Chat Completions 的 tool_calls 能和 Open Responses item 放进同一个数组吗?
不要。Open Responses 用 type、call_id 和字符串 arguments。Chat Completions 用 tool_calls 数组。混在一条日志里,重放时会把错误的信封发给下一家。用 Diff 把两份文档分开比。
总结
Open Responses 进 AAIF 之后,跨供应商能共用的是 item 形状,不是每一条日志。
直接结论:type 带冒号就不要当通用 function_call;arguments 先确认是字符串再按工具 schema 校验;status 不是 completed 就不写 function_call_output。
下一步:用格式化看 arguments 是不是字符串,用 Schema 验解开后的参数,用 Diff 对比两家 item。