Arazzo Specification 拆開看:步驟失敗了怎麼辦,規格把重試與跳轉寫進文件
流程跑到第三支 API,回來一個 503。重試、跳去別的步驟、整件事就地收工,三條路都說得通,問題是誰來決定。
這個決定總得有人寫下來。寫在程式碼裡,換個語言就要重寫一次;寫在 agent 的提示詞裡,每次執行都可能被重新詮釋。Arazzo 的做法是第三條路:把它寫進一份跟語言無關的文件裡。我想拆的就是這一塊,因為它是整份規格裡最像「機制」的部分,其他多半是描述欄位。
我認為這份規格最值得讀的不是「它能串 API」,而是它預設什麼都不替你做:失敗了就停,重試要自己寫,沒寫 retryLimit 就只重試一次。這種保守比功能多更可信。哪些情況會讓我收回這個判斷,留在文末。
一個步驟怎麼算成功
規格的 README 把定位寫得很直白:Arazzo 是 OpenAPI Initiative 底下、由社群推動的開放規格,用一種與程式語言無關的方式,描述一串呼叫以及它們之間的相依,對象是 OpenAPI、AsyncAPI 這類 API 描述。README 的用途清單裡還有一項 “deterministic API invocation by AI-based LLMs”。repo 的簡介更乾脆,叫它 “A Tapestry for Deterministic API Workflows”。
重點在「deterministic」這個字。呼叫順序和失敗怎麼處置,不交給模型臨場判斷,而是先寫死。
拆到最小單位就是 step。成功與否由 successCriteria 決定:它是一串斷言,規格要求全部都滿足,這個步驟才算成功;而且只要寫了,就至少要有一個 Criterion。每個 Criterion 有四種寫法:simple、regex、jsonpath、xpath。
simple 條件長得最像平常寫的判斷式,例如 $statusCode == 200。它有幾條容易被忽略的規則:字串比較不分大小寫;null 只等於 null,跟任何其他值比都是 false;運算式算出來是 null,整個條件就算不成立。
另外三種多一道手續。regex、jsonpath、xpath 的條件字串裡,runtime expression 必須用 {} 包起來,先算出值、代換進字串,再拿去做比對。同樣是引用 $statusCode,simple 直接寫,其餘三種要包大括號。我只讀了規格,沒實際跑過,所以這個差別會不會在工具裡造成誤用,我不知道。
失敗之後,預設是停
判定失敗後會發生什麼,規格用 onFailure 回答,而且預設值寫得很清楚:省略的話,行為是 break and return。多個失敗動作的條件相近時,依序第一個符合的動作被執行。
反過來,onSuccess 省略時,預設是執行下一個步驟。
失敗動作只有三種型別,成功動作只有兩種:
| 型別 | 失敗動作 | 成功動作 |
|---|---|---|
| end | 工作流結束,帶著已有的 outputs 回到呼叫端 | 有 |
| retry | 重試目前步驟,受 retryAfter 與 retryLimit 限制 |
沒有 |
| goto | 單向把控制權轉給指定的 workflowId 或 stepId |
有 |
retry 有個細節很容易漏掉:如果同時指定了 stepId 或 workflowId,規格說的是先執行那個參照、把控制權還回來,然後再重試目前步驟。也就是說,「503 之後先去換一張 token,再回來重打」可以不離開文件。
retry 的小字
retryLimit 寫的是「失敗整個步驟之前,最多重試幾次」,非負整數。沒指定的話,只重試一次。而且它必須先耗盡,才輪得到後面的失敗動作。
retryAfter 是失敗後等幾秒再試。小字在這裡:如果被呼叫的操作回了 HTTP Retry-After 標頭,規格說它應該(SHOULD)蓋過這個欄位的值。
規格自己的範例就是照這三個欄位寫的:
1 | name: retryStep |
所以「確定性」要縮小解讀。確定的是規則寫在哪、誰先誰後,不代表每次執行的時間長得一樣:伺服器的 Retry-After 可以蓋掉你寫的秒數。沒寫 retryLimit 時只重試一次這條預設,各工具是否照做,我沒查證,只有規格文字。
步驟之間怎麼接
前一步的輸出,怎麼變成下一步的輸入?靠 runtime expression。頂層詞彙一共這些:$url、$method、$statusCode、$request.、$response.、$message.、$inputs.、$outputs.、$steps.、$workflows.、$sourceDescriptions.、$components.,以及 1.1.0 加進來的 $self。步驟的輸出以 $steps.<stepId>.outputs.<name> 取用,而 stepId 只能用英數、連字號和底線,不能有點。
有意思的是順序。1.1.0 讓 step 層級也能寫 dependsOn,但規格同時給了一個很克制的建議:只有同步步驟的工作流,建議照 steps 陣列的順序排,不要用 dependsOn。而且 dependsOn 只建立「先決關係」,不會去觸發被參照的步驟。另外,工具必須把 $steps.stepId.outputs.field 這種輸出引用當成隱含的相依;沒用 dependsOn 時,如果一個步驟引用了排在它後面的步驟的輸出,實作應該報錯。
這段背後有過一場爭論。早期的 issue #153 有人直接問:dependsOn 說是為了確保步驟「不平行處理」,但 steps 本來就是有序清單,第一版真的需要把平行執行的複雜度帶進來嗎?對照 1.1.0 的現況,step 層的 dependsOn 被明定為主要用來協調非同步操作。我沒看到那個 issue 之後的完整討論,所以不敢說這就是當時的結論,只能說規格現況和那個質疑指的是同一塊地方。
1.1.0 多了什麼
發版時間線很短:
Arazzo 的發版
2021-08-25
repo 建立(GitHub created_at)
2024-09-25
1.0.0 發佈
2025-01-20
1.0.1 發佈
2026-05-18
1.1.0 發佈,三個 release 皆非 prerelease
2026-07-17
issue #523 開出,提案 1.2 加入 Function Object 與 functionId 步驟目標(尚未進規格)
1.0.1 到 1.1.0 的差異,對照兩份規格全文整理成下表。
| 項目 | 1.0.1 | 1.1.0 |
|---|---|---|
| Source Description 的 type | openapi、arazzo |
再加 asyncapi |
step 的 channelPath、action、correlationId、timeout |
無 | 有 |
step 層 dependsOn |
無(只有 workflow 層) | 有 |
$self 身分式引用 |
全文 grep 0 命中 | 有 |
querystring |
全文 grep 0 命中 | 有(對齊 OAS 3.2) |
| JSONPath 版本 | 只允許 draft-goessner-dispatch-jsonpath-00 |
允許 rfc9535 與前者,預設 rfc9535 |
release notes 另外列了:支援 AsyncAPI v3、在原本支援 JSONPointer 的地方也支援 JSONPath 與 XPath、讓 workflow inputs 能在成功與失敗動作裡傳遞。
這張表能證明的只有「規格新增了這些欄位」。只認 1.0.1 的工具讀 1.1.0 文件會壞在哪,我沒實測,不能寫。
AsyncAPI 那一塊是我覺得最耐讀的,因為規格在這裡直接承認了限制。對 AsyncAPI 的 send 操作,步驟在訊息送出後立刻完成,原文是 “Arazzo does not model broker acknowledgment or delivery confirmation”。receive 則是:沒寫 successCriteria 時,在 timeout 時間內收到任何符合 correlationId(若有指定)的訊息,就算成功;逾時就失敗,並觸發定義好的 onFailure。
送出不等於送達。用 Arazzo 描述 AsyncAPI 的 send 步驟時,「步驟成功」只代表訊息送出去了,broker 有沒有收到,規格明說它不管。
它沒管的
順著同一個思路,再看兩個空白。
安全:規格寫得很不客氣,Arazzo 文件常由不受信任的第三方撰寫、部署在公開網路上,處理一份文件可能對任意網路資源執行安全與不安全的操作,確保操作無害是文件消費者的責任;規格本身也說不強制任何安全機制。
人在迴圈:我 grep 1.1.0 全文的 “human”,只命中兩處,都是在講文件「人類可讀」,沒有任何跟暫停等人輸入有關的東西;Step Object 的欄位清單裡也沒有。issue #353 就是為這個開的:作者希望某個步驟能依前面步驟的回應,回頭問使用者要更多輸入,舉的例子是選完航班之後付款。底下的留言還有人舉了 S3 pre-signed URL 上傳,以及 IoT 裝置上線時 HTTP 呼叫中間夾一段藍牙步驟的情境。那個 issue 在 2026-03-18 關閉,註記改到 #449(Actor-in-the-loop)。#449 現在是什麼狀態,我沒查。
把規則寫進文件,值不值
回到開頭那個 503。我的結論是:只要你的流程全是 HTTP 或訊息呼叫、失敗的分支用有限幾種動作就能講完,把重試與跳轉寫成文件,比寫在提示詞或散落在各語言的程式碼裡好得多。好在它迫使你明講預設:沒寫 onFailure 就是停,沒寫 retryLimit 就只有一次。
這個判斷會被兩種情況推翻。一是你的流程需要中途問人,目前規格沒有對應機制,那就老老實實寫程式。二是你要串的東西不在 OpenAPI 或 AsyncAPI 的描述裡。#523 的提案內文自己就說:現有每一種步驟目標,都指向某處已描述過的東西;MCP tool 不一定有 OpenAPI 或 AsyncAPI 描述,今天沒有任何步驟目標能碰到它。提案的第二條規則也值得抄下來:文件裡沒有任何東西授予權限,binding 只是「這個函式怎麼被呼叫」的宣稱,絕不是呼叫的許可。這只是 open 的提案,能不能進 1.2,我不知道。
這份規格最後教我的,是把「出錯時怎麼辦」當成必須事先寫完的資料,而不是執行時的即興動作。
本文只讀了規格全文(1.1.0 與 1.0.1)、README、release notes 與幾個 issue,沒有實際執行過任何 Arazzo 工作流,也沒有測過任何工具對 1.1.0 的支援。repo 現況取自 2026-10-29:未封存、Apache-2.0、GitHub 顯示 487 顆星;contributors API 回傳 24 位(不含匿名,不等於規格作者數)。










