AI 助理給出錯誤的答案,不一定代表模型有問題。它的回應可能只是整條服務鏈的最後一步——資訊先被檢索、結構化並傳遞給模型,最後才被轉譯成自然語言。
當 Aurora Shop 的助理說耳機「有庫存、明天出貨」時,那筆訂單實際上歷經九天缺貨等待,最終被退款。日誌顯示,助理只是照實轉述庫存 API 提供的資料;真正的問題在於產品頁、結帳系統、倉儲儀表板與助理各自以不同的方式解讀同一個 API。AI 並沒有憑空捏造這個錯誤——它只是這場服務之間未明說的分歧中,最顯眼的傳話者。
契約是介面對外做出的公開承諾,必須精確到機器可以驗證的程度。它至少應該定義路徑與方法(例如 GET /inventory/{sku}/availability)、每個必填與選填欄位及其型別、每個錯誤碼的意義、逾時或部分失敗時的行為,以及呼叫方必須遵守的頻率限制或配額。在 Aurora Shop 的案例中,這些全都不存在於任何書面文件。stock 可能是整數也可能是 null,而沒有人決定過 null 是什麼意思。ship_date 是一個沒有時區的日期字串。一次落後的倉儲同步回傳了舊的快取值,卻帶著一個若無其事的 200 狀態碼,彷彿什麼都沒發生。每個團隊都用自己合理的猜測填補這些缺口,而各自合理卻互不一致的猜測,正是系統在每一個單項測試都亮綠燈的同時崩壞的原因。
先寫契約把這些猜測變成決策。stock 變成不可為 null 的整數,並另設 status 欄位(in_stock、backordered、unknown),讓「我們不知道」永遠不會被誤認成「我們還有一些」。ship_date 變成帶有明確時區偏移的 ISO 8601 格式。過期回應必須標記 stale: true 並附上 synced_at 時間戳記,同步逾時則回傳 503 與 JSON 錯誤主體,而不是把舊數字偽裝成新資料。當契約以機器可讀規格的形式存在,例如 OpenAPI 片段加上簡短的語意說明,它就成為提供方與消費方共同驗證的唯一依據。當實作由 AI 生成時,這點尤其划算:契約是你能給程式生成模型最強的提示詞約束,防止它憑空發明欄位,同時也給審查者一份具體的清單,用來接受或退回結果。
有了書面契約,仍有一個問題懸而未決:契約中哪些部分真正重要,對誰重要?消費方驅動契約測試的回答方式,是讓每個消費方明確宣告自己依賴什麼。在 Aurora Shop,助理會聲明它需要 stock 為整數、status 為三個已知值之一。結帳系統會宣告它依賴每個回應都帶有 stale 欄位。倉儲儀表板會記錄它期望 ship_date 帶有時區、同步失敗時回傳 503。如此一來,「誰依賴誰」就不再是只存在於兩年前寫下整合程式碼的人腦中的部落知識,而變成一份掛在提供方之上、有名有姓且可測試的期望清單。
這套工作流程通常有三個步驟:消費方記錄自己的期望,這些契約被發布到共用位置,然後提供方在自己的 CI 管線中執行所有契約測試,任何會破壞任何消費方的變更都會讓建置失敗。如果某位庫存服務開發者把 stock 改名為 quantity,或決定用 null 偷懶表示「未知」,建置會立即失敗,錯誤訊息還會指名受影響的消費方——而不是等到 staging 才浮現、繞過 pre-production、最終在正式環境抵達客戶面前。這並非取代整合測試,而是為它排序:契約測試用低成本、盡早攔下破壞性變更,讓整合測試得以專注在「各部分如何協同運作」,而不是「它們到底還合不合得起來」。
AI 服務需要經典 REST 契約的一切,外加幾個一般 API 鮮少在意的維度。回應結構排在第一位:環繞模型輸出的結構化欄位,例如引用來源、狀態旗標或被引用的 SKU,必須保持穩定,即使模型本身在其背後被替換。逾時與重試語意也更為關鍵,因為模型呼叫更慢、更難預測;契約應該說明呼叫方要等多久、重試是否安全、部分回應長什麼樣。錯誤碼需要比「出了問題」更細的區分:錯誤輸入、模型不可用與超出配額各自需要不同的反應——修正請求、降級轉人工客服,或退避稍後再試。頻率限制與配額也變得事關業務,因為促銷活動期間被悄悄調低的配額,可能讓助理在最不該沉默的時刻整個沉默。
有兩個維度幾乎是 AI 獨有的。成本欄位,例如輸入與輸出 token 數或花費估算,讓消費方能監控並預算用量,而一份悄悄拿掉這些欄位的契約,會同時破壞建立在它之上的每個儀表板與警示。最棘手的維度是非確定性欄位:自然語言答案。要求契約保證確切措辭會讓每個測試都變得不穩定,因為同一個問題可以產生多種都算正確的說法。按照慣例,契約只承諾該欄位存在、型別正確、長度不超過上限;它從不承諾內容。「有庫存、明天出貨」究竟是不是事實,屬於另一種檢查,例如黃金題組與人工評分對照可接受區間。契約的職責更窄但至關重要:在文字自由移動的同時,不讓結構漂移。
下表把每個介面對應到依賴它的消費方、負責守諾的提供方、最可能破壞承諾的具體變更,以及應該攔下它的測試層級。橫著讀一列,你就不只知道要測什麼,還知道出問題時該找誰:「契約」列可以在提供方的 CI 中驗證,不必啟動整個系統;「契約+整合」表示風險還涉及逾時等執行期行為;「整合(寬鬆斷言)」標記的是非確定性的模型輸出,只檢查結構與上限,不檢查確切文字。套用到 Aurora Shop 的庫存情境,清單如下:
| 介面 | 消費方 | 提供方 | 破壞風險 | 測試層級 |
|---|---|---|---|---|
| 產品問答 API | 網站客服 | 產品服務 | citations 被改名或移除 |
契約(消費方驅動) |
| 產品問答 API | 行動版客服 | 產品服務 | 逾時回傳非 JSON | 契約+整合 |
| 錯誤碼語意 | 報表工作 | 產品服務 | 200 空答案與 404 混淆 | 契約 |
| 頻率限制 | 所有消費方 | 閘道 | 配額未經通知被調低 | 契約 |
| 非確定性欄位 | 所有消費方 | 模型服務 | 答案超過長度上限 | 整合(寬鬆斷言) |
庫存回應範例(排練資料):
{
"sku": "SKU-EXAMPLE-07",
"stock": 0,
"status": "backordered",
"ship\_date": "2026-10-06T00:00:00+08:00",
"stale": false,
"synced\_at": "2026-09-27T09:14:00Z"
}
助理回應範例(排練資料):
{
"answer": "這款耳機目前缺貨中,預計 10 月 6 日前後出貨。",
"citations": \[{"sku": "SKU-EXAMPLE-07"}\],
"tokens": {"input": 298, "output": 41},
"latency\_ms": 764
}
AI 助理給出錯誤答案是待調查的症狀,而非對模型的判決,而追查它最快的路徑,就是貫穿幕後連接各服務的契約。先寫契約再寫實作、由消費方視角在 CI 中驗證契約,並為契約擴充 AI 專屬維度——穩定的回應結構、成本欄位與非確定性輸出規則——把團隊之間未明說的分歧變成可測試、可判失效的承諾。當這些承諾被明確寫下,「這款耳機有庫存嗎」這個問題就不再取決於由哪個服務來回答,而助理也終於成為它本該成為的角色:一位準確的傳信者,而非一場無聲故障中最顯眼的目擊者。