誤不是普通文本:補(bǔ)上失敗終態(tài)與圖像成本歸因)
Vercel AI SDK 工具選擇錯(cuò)誤不是普通文本補(bǔ)上失敗終態(tài)與圖像成本歸因核心判斷當(dāng)模型被要求調(diào)用工具卻返回普通文本時(shí)運(yùn)行時(shí)不能把它當(dāng)作“模型回答成功”當(dāng)一次圖像調(diào)用被網(wǎng)關(guān)拆成多個(gè)子請求時(shí)也不能用父請求的一條賬單記錄代替真實(shí)成本。前者需要獨(dú)立的 tool_choice_violation 終態(tài)后者需要按子請求建立可追溯的成本賬本。這篇文章解決一個(gè)具體問題AI 應(yīng)用已經(jīng)接入工具和圖像生成后為什么“請求成功”仍然可能代表業(yè)務(wù)失敗或成本統(tǒng)計(jì)失真讀者可以用本文的狀態(tài)模型、純函數(shù)樣例和驗(yàn)收清單給現(xiàn)有 Agent Loop、流式 UI 或網(wǎng)關(guān)適配層補(bǔ)上兩道門禁。1. 兩條版本變化2026-08-30Vercel AI SDK 發(fā)布了兩條與應(yīng)用工程直接相關(guān)的更新。ai7.0.85 修正 Gateway 分段圖像請求的成本求和并暴露單次圖像生成調(diào)用ai6.0.272 則把 generateText 在 required 或明確 selected tool choice 下收到不滿足工具選擇的響應(yīng)明確變成 ToolChoiceViolationError同時(shí)保留規(guī)范化后的響應(yīng)內(nèi)容讓調(diào)用方可以選擇是否恢復(fù)。這里有三個(gè)容易被忽略的詞不滿足、規(guī)范化、選擇性恢復(fù)。它們意味著 SDK 不再鼓勵(lì)調(diào)用方用一條“成功/失敗”布爾值吞掉差異。模型可能返回了可讀文字但沒有完成協(xié)議要求的工具動(dòng)作響應(yīng)仍然有結(jié)構(gòu)化內(nèi)容但內(nèi)容不能未經(jīng)檢查直接當(dāng)作下一步輸入恢復(fù)也不是默認(rèn)動(dòng)作而是業(yè)務(wù)層在重新校驗(yàn) Schema、權(quán)限和資源后作出的決定。另一條更新解決的是費(fèi)用粒度。父請求可以代表一次用戶意圖但網(wǎng)關(guān)可能把一次圖像生成拆成多個(gè)實(shí)際子請求例如不同尺寸、不同質(zhì)量或重試分支。每個(gè)子請求擁有自己的模型、usage、價(jià)格和結(jié)果狀態(tài)。若只在父請求結(jié)束時(shí)記一筆總費(fèi)用系統(tǒng)就無法回答“哪一步最貴”“重試是否重復(fù)計(jì)費(fèi)”“失敗的子請求有沒有產(chǎn)生費(fèi)用”。本文只依據(jù) Vercel AI SDK 官方 Release 與項(xiàng)目知識庫中的靜態(tài)核驗(yàn)結(jié)果。沒有在本機(jī)升級或運(yùn)行 SDK也沒有連接真實(shí) Provider。下面的 TypeScript 代碼是框架無關(guān)的最小門禁樣例可以單獨(dú)復(fù)制到 Node.js 環(huán)境驗(yàn)證數(shù)據(jù)流它不宣稱已經(jīng)替代 SDK 內(nèi)部實(shí)現(xiàn)。2. 為什么工具選擇違反必須是獨(dú)立終態(tài)一個(gè)典型 Agent Loop 大致如下模型讀取上下文選擇工具并生成參數(shù)運(yùn)行時(shí)校驗(yàn)參數(shù)工具執(zhí)行結(jié)果回填上下文再進(jìn)入下一輪。只要模型選擇了工具調(diào)用方就會把這輪響應(yīng)理解為一種協(xié)議消息而不僅是一段自然語言?,F(xiàn)在考慮兩種返回應(yīng)用要求 weather 工具模型給出 {toolCall: weather, args: {city: “嘉興”}}應(yīng)用要求 weather 工具模型卻說“嘉興今天多云建議帶傘”沒有任何工具調(diào)用。第二種返回的文字可能非常合理卻沒有經(jīng)過天氣工具的事實(shí)來源、權(quán)限和審計(jì)鏈路。如果 UI 把它渲染成綠色的“完成”用戶會誤以為系統(tǒng)已經(jīng)查過實(shí)時(shí)數(shù)據(jù)如果 Runtime 把它靜默改成普通文本又會讓后續(xù)重試、評測和成本統(tǒng)計(jì)失去依據(jù)。因此它至少要有獨(dú)立失敗狀態(tài)和普通模型錯(cuò)誤、工具參數(shù)校驗(yàn)失敗、審批后輸入漂移區(qū)分開。建議把一次運(yùn)行建模為明確的狀態(tài)機(jī)狀態(tài)含義是否允許自動(dòng)繼續(xù)streaming模型仍在輸出增量否等待完整響應(yīng)tool_call_ready已發(fā)現(xiàn)工具調(diào)用參數(shù)尚待校驗(yàn)否先過 Schemaexecuting_tool參數(shù)與權(quán)限通過工具執(zhí)行中否等待結(jié)果或取消tool_choice_violationrequired/selected 工具選擇不滿足默認(rèn)否需顯式恢復(fù)策略validation_failed工具參數(shù)不符合 Schema否提示修正或重新生成completed約定的文本或工具流程完成是結(jié)束本輪failed不可恢復(fù)的模型、網(wǎng)絡(luò)或業(yè)務(wù)錯(cuò)誤否進(jìn)入回滾/人工處理tool_choice_violation 的關(guān)鍵不是命名而是保留三個(gè)證據(jù)原始請求要求了什么工具、規(guī)范化響應(yīng)是什么、業(yè)務(wù)是否允許恢復(fù)。沒有這三項(xiàng)后續(xù)人工排查只能從聊天文本猜測。3. 恢復(fù)不是“把文本當(dāng)成功”ToolChoiceViolationError 提供了選擇性恢復(fù)的可能但恢復(fù)路徑必須比普通重試更嚴(yán)格。推薦順序是記錄失敗 → 判斷策略 → 重新檢查工具注冊表 → 重新校驗(yàn)用戶權(quán)限與資源 → 重新生成或請求補(bǔ)充信息 → 只在新一輪工具調(diào)用通過后繼續(xù)。下面的純函數(shù)只演示門禁順序。它沒有依賴 Vercel AI SDK因此可以放在業(yè)務(wù)層、BFF 或前端狀態(tài)適配層中作為統(tǒng)一的狀態(tài)轉(zhuǎn)換合同。typeToolChoice{mode:required|selected;name?:string};typeNormalizedResponse{text?:string;toolCalls:Array{name:string;args:unknown};};typeRunFailure{kind:tool_choice_violation|validation_failed;requested:ToolChoice;response:NormalizedResponse;canRecover:boolean;};functionclassifyToolChoice(requested:ToolChoice,response:NormalizedResponse,registeredTools:Setstring,):RunFailure|null{constcallsresponse.toolCalls;constselectedOkrequested.mode!selected||calls.some((call)call.namerequested.name);constrequiredOkrequested.mode!required||calls.length0;if(selectedOkrequiredOk)returnnull;constknownCallcalls.every((call)registeredTools.has(call.name));return{kind:tool_choice_violation,requested,response,canRecover:knownCallcalls.length0,};}在真實(shí)系統(tǒng)中registeredTools 還應(yīng)帶版本、租戶、權(quán)限和資源范圍而不是只有工具名稱。canRecover 也不等于“馬上重試”它只是把錯(cuò)誤交給一個(gè)可審計(jì)的策略函數(shù)。typeRecoveryDecision|{action:retry_tool_choice;reason:string}|{action:ask_user;reason:string}|{action:stop;reason:string};functiondecideRecovery(failure:RunFailure,opts:{retriesUsed:number;maxRetries:number;userCanApprove:boolean},):RecoveryDecision{if(failure.kind!tool_choice_violation){return{action:stop,reason:交給對應(yīng)錯(cuò)誤處理器};}if(!failure.canRecover){return{action:ask_user,reason:響應(yīng)未包含可驗(yàn)證的已注冊工具};}if(opts.retriesUsedopts.maxRetries){return{action:stop,reason:達(dá)到重試上限};}if(!opts.userCanApprove){return{action:stop,reason:當(dāng)前會話沒有恢復(fù)權(quán)限};}return{action:retry_tool_choice,reason:重新校驗(yàn)后再生成工具調(diào)用};}這段邏輯有意把“問用戶”和“停止”分開。對于寫入、刪除、發(fā)送消息、付款等副作用工具恢復(fù)時(shí)應(yīng)重新創(chuàng)建審批記錄不能沿用一次已經(jīng)失效的批準(zhǔn)。對于只讀工具也要限制重試次數(shù)、總耗時(shí)和預(yù)算避免模型在錯(cuò)誤狀態(tài)下自旋。4. 前端怎樣呈現(xiàn)這個(gè)狀態(tài)流式 UI 不能只維護(hù) isLoading 和 errorMessage 兩個(gè)變量。至少要能展示當(dāng)前運(yùn)行 ID、工具選擇要求、錯(cuò)誤類型、規(guī)范化響應(yīng)、是否需要用戶補(bǔ)充、恢復(fù)按鈕是否可用以及恢復(fù)后產(chǎn)生的新運(yùn)行 ID。一個(gè)可落地的消息部分模型如下typeMessagePart|{type:text;text:string}|{type:tool-call;name:string;args:unknown;status:ready|running|done}|{type:tool-choice-violation;requested:ToolChoice;response:NormalizedResponse;recoverable:boolean}|{type:error;code:string;retryable:boolean};渲染時(shí)tool-choice-violation 應(yīng)明確告訴用戶“本輪要求調(diào)用某工具但模型沒有按要求調(diào)用”而不是把模型普通文本放在成功氣泡里。若允許恢復(fù)按鈕文案應(yīng)是“重新校驗(yàn)并嘗試工具調(diào)用”而不是含糊的“重試”。用戶點(diǎn)擊后服務(wù)端重新檢查工具版本、Schema、權(quán)限和資源客戶端只顯示服務(wù)端確認(rèn)的狀態(tài)。斷線重連時(shí)客戶端還要按 runId 和序列號對賬。一個(gè)舊運(yùn)行的恢復(fù)結(jié)果不能覆蓋新運(yùn)行重復(fù)收到同一錯(cuò)誤事件不能讓計(jì)數(shù)器增加兩次終態(tài)之后的遲到文本不能把失敗運(yùn)行改回完成。這些是 UI 與 Runtime 的共同合同不是某一個(gè)組件的樣式問題。5. 圖像請求為何必須按子請求記成本圖像生成的“一次請求”經(jīng)常包含多種實(shí)際動(dòng)作網(wǎng)關(guān)解析參數(shù)、選擇模型、發(fā)起一次或多次生成、下載或存儲結(jié)果、重試失敗分支、執(zhí)行安全檢查。官方這次修正的是 Gateway 分段圖像請求的成本求和并暴露單次圖像生成調(diào)用。工程上可以把它理解為費(fèi)用賬本的最小單位應(yīng)接近真實(shí) Provider 調(diào)用而不是用戶點(diǎn)擊的按鈕。成本記錄建議至少包含這些字段字段用途注意事項(xiàng)runId關(guān)聯(lián)一次 Agent 運(yùn)行斷線重試仍保持可追蹤parentRequestId關(guān)聯(lián)用戶意圖或網(wǎng)關(guān)請求只用于聚合不直接計(jì)費(fèi)subRequestId標(biāo)識一次真實(shí) Provider 調(diào)用必須冪等重試生成新 IDmodel記錄實(shí)際模型不要只記錄路由別名usage輸入/輸出 token 或圖像計(jì)量按 Provider 原始單位保存unitPrice價(jià)格快照記錄生效時(shí)間和幣種cost本次子請求金額由 usage 與價(jià)格快照計(jì)算statussucceeded、failed、canceled失敗不等于零成本attempt第幾次嘗試用于發(fā)現(xiàn)重試放大父請求的總成本是子請求賬本的派生值。若網(wǎng)關(guān)收到四個(gè)圖像子請求就應(yīng)該能逐項(xiàng)列出四條記錄再匯總成一個(gè)總數(shù)。這樣才能區(qū)分第一張圖成功、第二張圖超時(shí)、第三張圖重試后成功以及第四張圖在安全檢查階段被拒絕等情況。6. 可復(fù)現(xiàn)的成本賬本樣例下面的代碼故意不調(diào)用網(wǎng)絡(luò)只驗(yàn)證“按子請求冪等寫入、再匯總”的核心規(guī)則。它可在 Node.js 22 或 Bun 1.3 環(huán)境運(yùn)行。typeUsage{inputTokens?:number;outputTokens?:number;images?:number};typeCostEntry{parentRequestId:string;subRequestId:string;model:string;usage:Usage;cost:number;status:succeeded|failed|canceled;attempt:number;};classCostLedger{privaterowsnewMapstring,CostEntry();append(row:CostEntry):void{constoldthis.rows.get(row.subRequestId);if(oldJSON.stringify(old)!JSON.stringify(row)){thrownewError(sub_request_conflict:row.subRequestId);}this.rows.set(row.subRequestId,row);}byParent(parentRequestId:string):CostEntry[]{return[...this.rows.values()].filter((row)row.parentRequestIdparentRequestId);}total(parentRequestId:string):number{constrawthis.byParent(parentRequestId).reduce((sum,row)sumrow.cost,0);returnMath.round(raw*1000)/1000;}}constledgernewCostLedger();ledger.append({parentRequestId:p-100,subRequestId:p-100-1,model:image-model-a,usage:{images:1},cost:0.126,status:succeeded,attempt:1});ledger.append({parentRequestId:p-100,subRequestId:p-100-2,model:image-model-a,usage:{images:1},cost:0.204,status:failed,attempt:1});ledger.append({parentRequestId:p-100,subRequestId:p-100-3,model:image-model-a,usage:{images:1},cost:0.204,status:succeeded,attempt:2});ledger.append({parentRequestId:p-100,subRequestId:p-100-2,model:image-model-a,usage:{images:1},cost:0.204,status:failed,attempt:1});console.log({rows:ledger.byParent(p-100).length,total:ledger.total(p-100)});預(yù)期輸出為{ rows: 3, total: 0.534 }為什么不是 0.330因?yàn)槭〉牡诙€(gè)子請求已經(jīng)實(shí)際消耗了資源不能因?yàn)樽罱K狀態(tài)是 failed 就強(qiáng)行記為零。真正計(jì)費(fèi)規(guī)則要以 Provider 返回的 usage 和價(jià)格有效期為準(zhǔn)這里的金額只是門禁樣例。生產(chǎn)實(shí)現(xiàn)還要處理幣種、稅費(fèi)、價(jià)格變更、舍入規(guī)則和賬單對賬。冪等檢查同樣重要。網(wǎng)關(guān)超時(shí)后調(diào)用方可能重放“寫入成本”事件。如果 subRequestId 相同且內(nèi)容一致第二次寫入應(yīng)該被視為重放如果同一 ID 攜帶不同模型或金額則必須報(bào)警并阻止覆蓋。沒有這道門禁成本看板會因?yàn)榫W(wǎng)絡(luò)重試隨機(jī)膨脹。7. 觀測字段怎樣與錯(cuò)誤終態(tài)對齊建議把工具選擇錯(cuò)誤和圖像成本放進(jìn)同一條 Trace但不要混成一條指標(biāo)。一次 Trace 可以包含run.started記錄用戶任務(wù)、模型路由和預(yù)算model.response.normalized保存規(guī)范化響應(yīng)摘要和工具選擇要求tool_choice.violation記錄錯(cuò)誤類型、可恢復(fù)性和策略決定tool.retry.started記錄重新校驗(yàn)后的新嘗試image.subrequest.completed每個(gè)子請求一條事件包含 usage、cost、狀態(tài)run.completed 或 run.failed匯總業(yè)務(wù)結(jié)果與總成本。指標(biāo)也要分開工具選擇違反率反映協(xié)議遵從性工具參數(shù)校驗(yàn)失敗率反映 Schema 或提示設(shè)計(jì)圖像子請求平均成本反映路由和質(zhì)量策略重試放大率反映故障與冪等設(shè)計(jì)。只看 HTTP 200 比例和父請求平均成本會掩蓋這些差異。日志中不要寫入完整用戶提示、圖片原始內(nèi)容、訪問令牌或環(huán)境變量??梢员4婀?、字段級摘要和脫敏后的錯(cuò)誤。對外部工具尤其要記錄調(diào)用者身份、工具版本、審批 ID 和資源范圍方便在出現(xiàn)越權(quán)或賬單爭議時(shí)回放。8. 四階段遷移路線如果現(xiàn)有應(yīng)用只有 success / error 兩種狀態(tài)不建議一次性重寫所有前端。可以按下面四個(gè)有依賴關(guān)系的階段遷移。階段一凍結(jié)協(xié)議與版本最小動(dòng)作是把 AI SDK 版本、工具選擇模式、工具 Schema、價(jià)格表版本寫入配置為 tool_choice_violation、validation_failed、failed 建立互不重疊的錯(cuò)誤碼。過關(guān)證據(jù)是同一份響應(yīng)在服務(wù)端日志、Trace 和 UI 中得到一致分類。適用邊界是單模型、少量工具的應(yīng)用多租戶系統(tǒng)還要把租戶權(quán)限加入分類輸入。階段二補(bǔ)齊恢復(fù)門禁最小動(dòng)作是實(shí)現(xiàn) classify → decide → revalidate → retry/ask/stop 鏈路并給恢復(fù)設(shè)置最大次數(shù)、時(shí)間和成本預(yù)算。過關(guān)證據(jù)是一個(gè)包含“普通文本返回、未知工具、已知工具、審批后參數(shù)漂移”的測試矩陣每個(gè)案例都有預(yù)期終態(tài)。適用邊界是恢復(fù)不會自動(dòng)執(zhí)行不可逆副作用支付、刪除和發(fā)布類工具仍需人工確認(rèn)。階段三建立子請求賬本最小動(dòng)作是為每個(gè)真實(shí) Provider 調(diào)用生成穩(wěn)定的 subRequestId保存模型、usage、價(jià)格快照、狀態(tài)和 attempt父請求總成本只能由賬本聚合。過關(guān)證據(jù)是重復(fù)事件不增加行數(shù)、相同 ID 內(nèi)容沖突會報(bào)警、失敗和取消路徑都有成本狀態(tài)。適用邊界是價(jià)格、稅費(fèi)和幣種規(guī)則仍需與財(cái)務(wù)系統(tǒng)對賬。階段四接入回歸評測最小動(dòng)作是從線上失敗樣本建立小型評測集比較版本升級前后的工具選擇違反率、恢復(fù)成功率、圖像成本分布和重試放大率。過關(guān)證據(jù)是 CI 或定期任務(wù)能輸出差異并能定位到運(yùn)行 ID 和子請求。適用邊界是靜態(tài)指標(biāo)不能證明答案質(zhì)量仍需人工審閱事實(shí)、引用和用戶體驗(yàn)。9. 回滾、異常和未驗(yàn)證邊界升級 SDK 前先把錯(cuò)誤碼和賬本字段做向后兼容。若新版本的錯(cuò)誤對象無法被舊客戶端識別服務(wù)端可以暫時(shí)映射成通用失敗但必須在日志中保留原始 ToolChoiceViolationError 類型不能把它降級成成功文本?;貪L時(shí)保留已經(jīng)寫入的子請求賬本禁止刪除賬單記錄后重新計(jì)算。需要特別測試的異常包括模型同時(shí)返回普通文本和錯(cuò)誤工具調(diào)用、工具名稱大小寫或別名不一致、網(wǎng)關(guān)只返回部分 usage、價(jià)格表在請求中途切換、重試跨越不同 Provider、客戶端重復(fù)點(diǎn)擊恢復(fù)按鈕、用戶在恢復(fù)過程中撤銷權(quán)限以及圖像子請求成功但結(jié)果上傳失敗。每一種情況都應(yīng)有確定的終態(tài)和補(bǔ)償動(dòng)作。本文沒有驗(yàn)證 Vercel AI SDK 在你項(xiàng)目中的具體 Provider 行為也沒有證明所有圖像模型都按同一單位計(jì)費(fèi)。ai7.0.85 與 ai6.0.272 的官方 Release 是事實(shí)來源“UI 需要獨(dú)立狀態(tài)”“成本應(yīng)按子請求歸因”“恢復(fù)前重校驗(yàn)”是基于這些變化做出的工程判斷。接入前仍要鎖定實(shí)際 SDK、Provider、價(jià)格頁和運(yùn)行時(shí)版本跑一遍自己的回歸矩陣。10. 發(fā)布前驗(yàn)收清單文章只回答一個(gè)問題工具選擇錯(cuò)誤和圖像成本為什么不能走普通成功路徑版本事實(shí)對應(yīng) ai7.0.85、ai6.0.272 官方 Releasetool_choice_violation、參數(shù)校驗(yàn)失敗、普通模型錯(cuò)誤和完成態(tài)互不混淆恢復(fù)前重新校驗(yàn)工具注冊表、Schema、權(quán)限、資源和預(yù)算子請求賬本具備冪等鍵、沖突檢測、usage、價(jià)格快照、狀態(tài)與 attempt失敗子請求不被靜默記為零成本父請求總額由子請求聚合示例代碼可在 Node.js 22/Bun 1.3 中獨(dú)立運(yùn)行輸出為 { rows: 3, total: 0.534 }未把靜態(tài)核驗(yàn)寫成 SDK 已在本機(jī)實(shí)測封面與第 2、6 節(jié)知識圖均為獨(dú)立 PNG分別從本地上傳到兩個(gè)平臺CSDN 與掘金平臺實(shí)際正文計(jì)數(shù)均不少于 5000 字預(yù)覽無 Markdown 標(biāo)記殘留、粘連表格或代碼塊損壞。來源與證據(jù)Vercel AI SDK ai7.0.85 ReleaseVercel AI SDK ai6.0.272 ReleaseAI SDK Tools and Tool CallingAI SDK Core Reference[[…/…/02-技術(shù)沉淀/01-AI工程/AI應(yīng)用工程知識地圖|AI 應(yīng)用工程知識地圖]]2026-08-31 更新[[…/…/02-技術(shù)沉淀/01-主題筆記/02-前端框架與原理/AI應(yīng)用前端與生成式UI狀態(tài)設(shè)計(jì)|AI 應(yīng)用前端與生成式 UI 狀態(tài)設(shè)計(jì)]]2026-08-31 更新[!warning] 驗(yàn)證邊界本文的版本事實(shí)來自官方 Release 靜態(tài)核驗(yàn)示例賬本是本地純邏輯演示未在本機(jī)升級或運(yùn)行 Vercel AI SDK未連接真實(shí) Provider也未對真實(shí)賬單金額作承諾。