Skip to content
← 文章目次 HCYTLOG / 文章與筆記

Agent 任務 API 怎麼設計?分清完成、排隊與等待核准

模型有輸出,不代表 Agent 任務已完成。以互動資料流拆開完成、排隊、拒絕與故障,再把 SDK 的等待核准狀態接到產品 API,避免前端誤判與重複執行。

本文目錄

Agent 任務可能查詢資料、執行工具,或在修改外部資料前等待使用者核准。產品 API 必須把這些狀態交代清楚:畫面應該顯示結果、繼續等待,還是請使用者做決定?不能只因 SDK 回傳了一個物件,就讓前端顯示「已完成」。

假設前端呼叫摘要 API,後端回傳 success: true 與任務 ID。前端因此顯示「摘要完成」,但工作還躺在佇列裡。使用者重新整理看不到結果,又送一次請求。現在同一份資料正在生成兩次摘要,後端卻認為每一次回覆都沒有錯。

這裡需要修正的是契約:請求已接受、工作已完成、結果可取得,以及等待核准,是不同狀態。LLM 本身可能花多久只是其中一部分,網路重送與背景處理才是客戶端必須理解的邊界。

把任務狀態寫進公開契約

先讓回覆型別表達狀態

同步請求等待結果完成後回覆內容;非同步請求在可靠地接受任務後回覆任務 ID 與查詢位置。依 HTTP 語意,202 Accepted 表示已接受處理,但尚未完成,也不保證最終成功。回覆應提供目前狀態與查詢方式。RFC 9110,15.3.3

我會避免把所有回覆塞成一組可選欄位,改用可辨識的聯集型別。以下省略一般的追蹤資訊,留下客戶端需要判斷的部分:

type Reply<T> =
  | { kind: "completed"; data: T }
  | { kind: "accepted"; jobId: string; statusUrl: string }
  | { kind: "pending_approval"; jobId: string; approvalId: string }
  | { kind: "failed"; code: string; retryable: boolean };

function renderReply<T>(reply: Reply<T>) {
  switch (reply.kind) {
    case "accepted": return `等待處理:${reply.jobId}`;
    case "pending_approval": return "需要核准後才能繼續";
    case "completed": return "結果已完成";
    case "failed": return `處理失敗:${reply.code}`;
  }
}

這個型別不會替 HTTP 決定狀態碼,但能防止程式在 accepted 分支讀取不存在的結果。正式端點還要讓 HTTP 狀態、回覆內容與文件相符,客戶端才能知道何時等待、何時修正輸入、何時可以重試。

互動圖解 · 教學模型 收到請求,排進佇列,不等於完成工作

比較同步回應與非同步工作查詢。HTTP 狀態、工作狀態與內容拒絕各有自己的語意。

HTTP 200,工作狀態 completed,生成結果已可取得。

HTTP 狀態
200
工作狀態
completed
生成結果
可取得
  1. 01
    1. 輸入驗證完成

    格式檢查通過。

  2. 02
    2. 安全政策完成

    範例政策通過。

  3. 03
    3. 工作執行完成

    已取得生成結果。

  4. 04
    4. 狀態與結果完成

    結果與 completed 狀態已可讀取。

  5. 05
    5. HTTP 回應完成

    HTTP 200;依範例契約回傳。

圖中的資料只用來說明流程,不是實際服務的測試結果。 狀態碼是這份範例契約的選擇,不是所有 API 都必須採用的對應方式。非同步查詢的 200 只表示成功取得工作紀錄;仍須檢查 job.status。沒有真正建立佇列或發送請求。

這個互動圖解先聚焦同步與背景工作,狀態與回覆是教學示例。在圖解選擇成功、輸入不合法、政策拒絕或供應商故障,觀察請求停在哪一個階段。再切換同步與非同步模式;非同步情境提供排隊中與 worker 已執行的兩個觀察時點。worker 執行後可能完成,也可能失敗;輸入不合法與政策拒絕則在建立工作前被拒絕,不能當成已接受。

試著選非同步、供應商故障與 worker 已執行,會看到查詢任務的 HTTP 狀態為 200,工作本身卻是 job.status: "failed"。HTTP 成功表示成功讀取任務紀錄,沒有把失敗的工作變成成功。正式契約還需要定義執行中、取消等狀態,以及它們允許的轉換。

拒絕與故障不能只靠訊息猜

輸入格式錯誤,需要使用者修改參數;政策拒絕,需要說明服務範圍;供應商暫時故障,可能允許之後重試。三者如果全部回傳同一個內部錯誤,前端會給出錯的操作建議,監控也會把正常的政策執行算成系統故障。

如果依拒絕訊息中的文字選擇 HTTP 狀態碼,改文案、翻譯或模型改寫就可能改變 API 行為。比較可維護的做法是由服務層回傳固定錯誤種類,例如 INVALID_INPUT、POLICY_REFUSAL、PROVIDER_UNAVAILABLE,API 層再依公開契約轉成對應狀態碼。

retryable 也需要有具體意義。驗證失敗不應重送相同輸入;暫時故障只能在截止時間與重試次數限制內嘗試。若前一次工作可能已執行,重試前要處理重複效果。供應商回覆的原始錯誤或內部堆疊留在受控紀錄中,不應為了方便偵錯直接送給所有客戶端。

把 SDK 執行狀態轉成產品狀態

OpenAI Agents SDK(@openai/agents)的一次執行可能正常完成,也可能為了核准工具呼叫而暫停。暫停時,finalOutput 可以仍為空,interruptions 列出待決定的操作,state 則保留可繼續執行的狀態。產品可把這種結果轉成 pending_approval,保存狀態與任務的關聯,核准或拒絕後再接續同一次執行。OpenAI:Results and state

這是 SDK 的執行狀態,並不是 HTTP 202。若核准要稍後進行,應用程式還得保存任務、驗證決策者權限,並處理狀態到期或動作內容已變更的情境。不能把前端重送同一句話當成「核准」,也不能在使用者按下核准時另開一個沒有原狀態的新任務。OpenAI:Guardrails and human review

Claude Agent SDK(@anthropic-ai/claude-agent-sdk)提供 Claude Code 的 Agent 執行環境。若設定 outputFormat: { type: "json_schema", schema },應從 result 訊息的 structured_output 取資料,同時確認 subtype === "success" 且欄位存在,再驗證產品自己的規則。不能因收到一個 result,就把格式重試失敗或缺少資料的情境轉成完成。Claude:Agent SDK overview、Structured outputs

若產品功能是檢查專案或修改程式碼,Codex SDK(@openai/codex-sdk)控制的則是本機 Codex thread,可透過 thread.run() 執行並接續工作。它與泛用的 OpenAI Agents SDK 是不同介面;不論接哪一種執行環境,產品的任務 ID、HTTP 回覆、查詢權限與防止重複執行的規則仍要由應用程式設計。OpenAI:Codex SDK

可靠交接與防止重複執行

接受背景任務,要先有可靠的交接

回覆 202 前,任務至少需要被可靠保存,之後才能查詢。只在 HTTP 處理函式中啟動一個未等待的 Promise,程序若重啟,任務可能就消失。API 回得快,卻無法證明背景工作已交給 worker。

一種設計是把任務資料與待送出的工作事件寫入同一筆資料庫交易,之後由發送程序把事件交給佇列。這樣可以檢查尚未發送的事件,補做交接。即使佇列重送,worker 也要能辨識已完成的工作。可靠保存與交接需要一起設計,否則 API 的「已接受」就沒有後續執行依據。

狀態查詢要重新驗證權限。知道某個 jobId,不代表可以讀取它的結果。任務表應記錄擁有者或租戶、輸入版本、建立時間、更新時間與可保存到何時。若結果已被清除,查詢回覆也要讓客戶端知道,不能永遠停在「處理中」。

冪等鍵不能只是「先查再做」

如果先查 requestId 是否有結果,沒有才執行,結束後保存,仍然無法避免同時重送。兩個同時到達的請求可能都查不到,接著同時呼叫模型或工具。這是競爭條件,即使後來只留下一份結果,重複費用或外部寫入已經發生。

需要的是原子地取得「這個請求由誰負責執行」的權利。概念上的儲存介面可以是:

type Reservation =
  | { kind: "owner"; jobId: string }
  | { kind: "existing"; jobId: string }
  | { kind: "conflict" };

interface IdempotencyStore {
  reserveOrRead(input: {
    scope: string; key: string; bodyHash: string;
  }): Promise<Reservation>;
}

reserveOrRead 必須由資料庫唯一限制與交易,或等價的原子機制實作;函式名稱本身不會提供保障。scope 應涵蓋租戶與操作,bodyHash 綁定正規化後的請求內容。同一個鍵卻帶不同參數,應明確拒絕衝突,不能把舊結果當成新請求的回覆。

也要處理取得執行權後的中斷。若 worker 可能接手逾期工作,需要租約與工作版本,避免舊 worker 回來又寫入結果。外部副作用更需要下游的冪等支援或可查詢狀態;只在自己的資料庫中避免重複處理,無法保證第三方只執行一次。

驗證契約,要包含會讓客戶端誤判的情境

測試應從前端看得到的回覆開始:同步成功時取得結果;非同步接受時只取得任務資訊;輸入失敗不啟動模型;政策拒絕保留固定分類;供應商故障不冒充完成。等待核准則另外檢查三件事:等待中不顯示完成、拒絕後不執行原工具,以及重複核准不重複寫入。驗證 JSON 結構也要驗證 HTTP 狀態與查詢權限,TypeScript 型別不能檢查網路上收到的任意資料。

冪等測試則同時送出多個相同鍵的請求,檢查只有一個執行者;再用相同鍵、不同 body,確認被判為衝突。插入程序中斷、交接失敗、佇列重送與結果保存失敗,確認任務能恢復,或進入明確的失敗狀態。不要只測連續送兩次的順利情境。

版本相容性也包括狀態與錯誤碼。新增欄位通常較容易維持相容,但新增狀態可能使列舉判斷失效,改變 retryable 會改變客戶端行為。保存一組舊版客戶端的請求與回覆測試,才能檢查改動是否仍符合承諾。

API 的完成條件應能由客戶端判斷。只要「接受」與「完成」仍被混用,後端成功回覆再多次,使用者也不會知道工作到底做了沒有。

延伸閱讀

SDK 文件參考日期:2026-10-07。

系列導覽

上一篇:多步驟任務的效能。下一篇:Agent 的觀測與故障定位。也可以從產品架構開始。