把 coding agent 接進產品時,別急著把它們偽裝成同一個 API
一個內部「修復工作台」原本只接一種 coding agent。使用者送出 bug report,agent 修改程式、跑測試,最後回傳一個 PR。畫面上有進度、取消按鈕和綠色的完成狀態,看起來很單純。
直到團隊接入第二種 agent harness。
新的 backend 也能接收任務,但它的取消只會中止前端等待,遠端 session 可能仍在執行;它遇到工具權限時只輸出一段文字,不會送出結構化的核准事件;它說自己完成了,交回來的卻只有 session 結束訊息,沒有 patch,也沒有測試結果。
此時最先壞掉的通常不是 prompt,而是產品對「執行中」、「可取消」和「完成」的理解。
把兩個 harness 包成同一個 run(prompt) -> text 介面,確實可以很快做出 demo。正式接進產品後,這個過度乾淨的抽象反而會藏住最需要被看見的差異。
Model router 和 agent adapter 是兩件事
模型路由解決的是另一層問題。單一 API key、fallback、費用追蹤與模型 trace,可以降低更換 provider 或 model 的摩擦。但 coding agent 不只有模型呼叫。它還有 session、工具權限、sandbox、timeout、取消、續跑,以及最後產生的 branch、patch、PR、測試紀錄或截圖。
因此,「模型可以替換」不代表「整段 agent workflow 可以替換」。前者關心請求要送去哪裡,後者必須回答一次工作由誰持有、現在走到哪裡、使用者還能做什麼,以及執行結束後留下了什麼。
OpenAI 把 Codex 的整合方向從 codex mcp-server 往 app server 移動,Vercel AI SDK 7 也以 HarnessAgent 納入 Codex、Claude Code、OpenCode 與 Pi。這些變化指向同一件事:產品接進來的單位,逐漸不再只是 model endpoint,而是一個帶著完整執行行為的 harness。
Adapter 當然仍然有用,只是它不該假裝這些 harness 天生擁有相同能力。
先宣告能力,再決定介面
我會讓每個 adapter 在執行前先交出 capability declaration。內容不用複雜,但至少要說清楚它是否支援工具核准、sandbox、真正的 cancellation、resume 或 replay、結構化產物,以及 trace 存取。
unsupported 是正常答案,而且比模糊的「可能可以」更有用。
前端可以依這份宣告決定要不要顯示 Cancel、Resume 或 Replay。工作流也能在派送任務前判斷,某個需要人工核准的任務是否適合送到這個 backend。若 adapter 靜默模擬缺少的能力,產品表面上比較一致,實際上卻開始對使用者做出無法兌現的承諾。
以 Cancel 為例,按鈕背後至少可能有三種行為:向遠端工作送出停止要求、只關閉本地 session,或單純讓 UI 停止輪詢。這三種行為不能共用同一句「已取消」。否則使用者以為任務停止了,agent 卻可能繼續改檔或消耗資源。
統一生命週期,不要統一能力
產品確實需要一套共用語彙,否則每加一個 harness,狀態頁、通知和自動化都得重寫一次。適合統一的是生命週期事件,例如:
queued
running
approval_required
artifact_ready
failed
completed
這些事件描述 host application 必須理解的狀態,不代表每個 backend 的內部流程完全一樣。Executor 特有的 event、原始輸出與識別資訊仍可附在 provenance 裡,供除錯與稽核使用。
這也比解析終端文字可靠。若 UI 必須搜尋「waiting for approval」或「done」之類的句子才能猜狀態,只要工具改了輸出文案、語言或換行方式,產品就會誤判。自然語言適合讓人讀,不適合充當長期穩定的狀態協定。
控制語意也要跟事件一樣明確。Adapter 的契約應註明 timeout 之後遠端工作是否仍在執行,retry 會沿用原本的 session 或建立新的 run。如果 backend 不支援 resume,就直接把操作標成依照舊 input revision 重新執行,不要讓前端把它包裝成「繼續」。
Completed 應該附帶可以接手的結果
最危險的假一致性,是所有 backend 最後都回傳 completed: true。
對 coding agent 來說,執行程序結束和工作完成並不是同一件事。Reviewer 需要知道實際使用了哪個 harness,model 資訊是否可得,任務根據哪一版輸入執行,產生了哪些 branch、patch 或 PR,跑過哪些 checks,還有哪些未決問題。
因此,共用結果最好是一個 result envelope。它的價值不在於格式漂亮,而是讓下一個人能判斷是否可以接手。某個 backend 交回 patch 與完整測試紀錄,另一個只交回一段 session transcript,兩者都可以放進共用格式,但 final status 與 artifact 欄位必須忠實呈現差異,不能因為 schema 相同就被當成同一種保證。
回到那個修復工作台。兩種 harness 可以共用任務頁和生命週期畫面,產品也不必為每個 executor 重做整套 UI。不過,只有宣告支援的 backend 才顯示 Resume 或 Replay;真正收到停止確認後,畫面才能顯示已取消;進入 completed 前,系統還要檢查預期的 patch 與測試結果是否存在。
這種 adapter 看起來沒有 run(prompt) -> text 那麼優雅,卻更接近產品真的需要維護的邊界。
可替換性來自差異可見
Agent harness 會繼續增加,整合入口也會變。追求一個最小共同 API 很合理,但共同層若把 unsupported、控制差異與產物缺口全部藏起來,換 backend 時只會把問題延後到使用者按下按鈕之後。
比較可靠的做法,是穩定 host application 必須理解的生命週期、控制語意與結果格式,同時保留每個 executor 的能力宣告與 provenance。這樣的介面不會承諾無損替換,卻能讓團隊看清楚每次替換到底改變了什麼。
對需要長期維護 agent 產品的人來說,統一生命週期、保留能力差異,也許沒有一個萬用 API 那麼漂亮,卻比假裝所有 backend 都一樣實用。