博客 • 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。

先记住这一点: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 再验一次。

可进入后续检查的 function_call。arguments 是字符串。
{
  "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。

下一请求里的 function_call_output,call_id 与上一条相同。
{
  "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。

带供应商前缀的 item。不要当成 get_latest_sales_report 再执行一遍。
{
  "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。region 只能是 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,工具不要执行。

parse 成功,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 为 globalarguments_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。

重放前的本地闸门。返回值不是 ok 就不执行工具。
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. 1
    先看 arguments 是不是字符串

    打开 JSON 格式化,把整条 item 贴进去。arguments 如果展开成对象而不是一行字符串,落盘形状已经偏了,闸门会返回 arguments_type。

  2. 2
    再验解开后的参数

    把 arguments 字符串解开后的对象贴进 JSON Schema。schema 用上面的 region enum。global 会标出来,不要在执行前改成 apac。

  3. 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。

返回博客