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

返回博客