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