驗(yàn)結(jié)果的邊界與解讀)
高可用實(shí)驗(yàn)結(jié)果的邊界與解讀高可用實(shí)驗(yàn)最容易出現(xiàn)兩種誤讀一次壓測(cè)通過(guò)就認(rèn)為系統(tǒng)能夠承受同等線上流量一次 Agent 循環(huán)失控又把所有問(wèn)題歸因于模型不確定。前者忽略了請(qǐng)求分布、依賴和故障注入后者則掩蓋了本應(yīng)由程序控制的重試、并發(fā)與取消邊界。實(shí)驗(yàn)結(jié)論只覆蓋已記錄的條件。把變量、樣本、拓?fù)浜蛷?fù)現(xiàn)步驟寫(xiě)完整失敗結(jié)果才有資格進(jìn)入架構(gòu)決策。先把要驗(yàn)證的假設(shè)寫(xiě)成問(wèn)題以 Agent 調(diào)用內(nèi)部工具為例可以分別驗(yàn)證參數(shù)缺失時(shí)系統(tǒng)是否在發(fā)出網(wǎng)絡(luò)請(qǐng)求前拒絕下游返回不可重試錯(cuò)誤時(shí)任務(wù)是否立即結(jié)束或詢問(wèn)用戶臨時(shí)錯(cuò)誤發(fā)生時(shí)重試是否受次數(shù)和總時(shí)限約束用戶取消后排隊(duì)、模型請(qǐng)求和工具調(diào)用是否停止入口流量高于處理能力時(shí)隊(duì)列是否有上限并能恢復(fù)。這些問(wèn)題不能用同一條“峰值 QPS”回答。實(shí)驗(yàn)應(yīng)先跑沒(méi)有故障的基線再一次注入一種失敗。輸入分布也要固定包括任務(wù)步驟數(shù)、工具類型、參數(shù)大小和返回時(shí)間。若所有樣本都只調(diào)用一個(gè)快速只讀接口結(jié)果不能外推到包含寫(xiě)操作和長(zhǎng)任務(wù)的工作流。流量逐級(jí)增加每一級(jí)等待系統(tǒng)進(jìn)入相對(duì)穩(wěn)定狀態(tài)觀察入口、Agent 執(zhí)行器和下游隊(duì)列。負(fù)載生成器自身的 CPU、網(wǎng)絡(luò)與錯(cuò)誤也要記錄否則測(cè)到的上限可能只是生成端先到瓶頸。先描述現(xiàn)象再解釋原因發(fā)現(xiàn)內(nèi)部調(diào)用量明顯高于入口流量時(shí)先計(jì)算每個(gè)任務(wù)的工具調(diào)用分布和步驟數(shù)。Agent 正常完成多步驟任務(wù)本來(lái)就會(huì)產(chǎn)生放大只有調(diào)用數(shù)持續(xù)增長(zhǎng)、同一任務(wù)無(wú)法結(jié)束或失敗后仍派生新步驟才說(shuō)明控制流可能失去邊界。排查記錄把事實(shí)、假設(shè)和驗(yàn)證分開(kāi)。事實(shí)可以是某類錯(cuò)誤增加、隊(duì)列不再回落、取消后仍有調(diào)用“模型陷入幻覺(jué)”只是解釋需要用調(diào)用軌跡證明。若發(fā)現(xiàn)同一參數(shù)連續(xù)失敗再查看失敗類別和重新規(guī)劃輸入確認(rèn)它是否真的在做相同動(dòng)作。不要補(bǔ)寫(xiě)沒(méi)有來(lái)源的精確流量、Token 倍數(shù)或協(xié)程數(shù)量。實(shí)驗(yàn)日志能支持到什么粒度就報(bào)告到什么粒度。證據(jù)不足時(shí)寫(xiě)明缺口比給出一條完整但無(wú)法復(fù)驗(yàn)的故事更有價(jià)值。Trace 記錄控制流不保存任務(wù)正文Agent Trace 可以在普通 HTTP Span 之外增加運(yùn)行標(biāo)識(shí)、步驟序號(hào)、工具名稱、參數(shù)校驗(yàn)結(jié)果、錯(cuò)誤類別、嘗試次數(shù)和剩余預(yù)算。敏感參數(shù)、模型 Prompt、工具返回正文不應(yīng)默認(rèn)進(jìn)入 Span 屬性它們?nèi)菀妆粚?dǎo)出到多個(gè)觀測(cè)后端。下面是一段脫敏后的示意結(jié)構(gòu)。args_fingerprint只用于同一次運(yùn)行內(nèi)識(shí)別重復(fù)動(dòng)作不能據(jù)此還原參數(shù)也不跨用戶做全局判斷。{ name: agent.tool.execute, attributes: { agent.run_id: run-example, agent.step: 3, tool.name: getUserDetail, tool.args_valid: false, tool.args_fingerprint: sha256:example, tool.attempt: 1, error.type: validation_error, retryable: false }, events: [ { name: agent.stopped, reason: missing_required_argument } ] }參數(shù)缺失屬于本地 Schema 校驗(yàn)錯(cuò)誤根本不應(yīng)發(fā)往下游。系統(tǒng)可以讓用戶補(bǔ)充信息或者終止當(dāng)前動(dòng)作。把相同的 400 響應(yīng)再次喂給模型只是消耗預(yù)算并不會(huì)創(chuàng)造缺失的業(yè)務(wù)數(shù)據(jù)。三層邊界分別解決不同問(wèn)題第一層是動(dòng)作協(xié)議。每個(gè)工具聲明參數(shù) Schema、權(quán)限、是否有副作用、冪等要求和可重試錯(cuò)誤。模型輸出通過(guò)校驗(yàn)后還要由服務(wù)端填入當(dāng)前用戶與租戶上下文。模型不能提供權(quán)限字段也不能直接決定目標(biāo)主鍵。第二層是單次運(yùn)行預(yù)算。最大步驟、總時(shí)限、模型調(diào)用、工具調(diào)用和并發(fā)任務(wù)都有上限。任一預(yù)算耗盡狀態(tài)機(jī)進(jìn)入明確的停止或人工接管節(jié)點(diǎn)。運(yùn)行預(yù)算必須隨取消信號(hào)一起傳遞到子任務(wù)不能只讓最外層請(qǐng)求返回。第三層是下游保護(hù)。連接池、并發(fā)許可、隊(duì)列和斷路器保護(hù)具體服務(wù)防止所有 Agent 同時(shí)重試。它與單個(gè)任務(wù)的循環(huán)檢測(cè)不同斷路器根據(jù)一段時(shí)間內(nèi)的下游失敗決定是否暫時(shí)停止調(diào)用而循環(huán)檢測(cè)只判斷本次運(yùn)行是否在重復(fù)沒(méi)有進(jìn)展的動(dòng)作。一個(gè)按運(yùn)行隔離的執(zhí)行示例下面的 Python 片段演示參數(shù)校驗(yàn)之后的異步執(zhí)行邊界。歷史只屬于一個(gè)RunGuard不會(huì)把不同用戶的相同查詢誤判為循環(huán)超時(shí)使用單調(diào)時(shí)鐘相同指紋只有在前一次失敗后才累計(jì)。import asyncio import hashlib import json import time from dataclasses import dataclass, field from typing import Any, Awaitable, Callable class ToolExecutionError(Exception): def __init__(self, message: str, *, retryable: bool) - None: super().__init__(message) self.retryable retryable dataclass class RunGuard: max_steps: int deadline: float repeated_failure_limit: int 2 steps: int 0 failures: dict[str, int] field(default_factorydict) def before_call(self, tool: str, args: dict[str, Any]) - str: if self.steps self.max_steps: raise ToolExecutionError(step budget exhausted, retryableFalse) if time.monotonic() self.deadline: raise ToolExecutionError(run deadline exceeded, retryableFalse) canonical json.dumps(args, sort_keysTrue, separators(,, :)) fingerprint hashlib.sha256(f{tool}:{canonical}.encode()).hexdigest() if self.failures.get(fingerprint, 0) self.repeated_failure_limit: raise ToolExecutionError(repeated failed action, retryableFalse) self.steps 1 return fingerprint def record_failure(self, fingerprint: str) - None: self.failures[fingerprint] self.failures.get(fingerprint, 0) 1 async def execute_tool( guard: RunGuard, tool_name: str, validated_args: dict[str, Any], call: Callable[..., Awaitable[Any]], step_timeout: float, ) - Any: fingerprint guard.before_call(tool_name, validated_args) try: return await asyncio.wait_for(call(**validated_args), timeoutstep_timeout) except ToolExecutionError as exc: guard.record_failure(fingerprint) if not exc.retryable: raise raise except asyncio.TimeoutError: guard.record_failure(fingerprint) raise ToolExecutionError(tool timeout, retryableTrue)示例沒(méi)有包含 Schema、鑒權(quán)、重試退避、冪等鍵和審計(jì)不能直接作為完整執(zhí)行器。wait_for發(fā)出取消后底層客戶端還必須支持取消若請(qǐng)求已經(jīng)到達(dá)有副作用的服務(wù)調(diào)用方應(yīng)先查詢結(jié)果再?zèng)Q定重試。相同調(diào)用出現(xiàn)兩次也不必一律禁止。輪詢一個(gè)最終一致的只讀狀態(tài)可能是合理行為關(guān)鍵是協(xié)議必須允許并有等待間隔和總時(shí)限。循環(huán)規(guī)則應(yīng)結(jié)合工具語(yǔ)義而不是把哈希次數(shù)寫(xiě)成全局常量。降級(jí)路徑不能依賴失效的 Agent當(dāng)模型或 Agent 執(zhí)行器不可用時(shí)入口仍需根據(jù)普通服務(wù)狀態(tài)決定返回靜態(tài)信息、進(jìn)入人工隊(duì)列還是拒絕任務(wù)。降級(jí)邏輯不能再次調(diào)用同一模型來(lái)判斷是否降級(jí)也不能繞過(guò)原有權(quán)限。高影響寫(xiě)操作失敗時(shí)優(yōu)先保留當(dāng)前狀態(tài)與冪等標(biāo)識(shí)不要自動(dòng)換模型重放。只讀問(wèn)答可以回到搜索結(jié)果或已有緩存但要說(shuō)明結(jié)果來(lái)源發(fā)生變化?;謴?fù)后從少量任務(wù)開(kāi)始放量確認(rèn)隊(duì)列、錯(cuò)誤和取消都回到預(yù)期。實(shí)驗(yàn)結(jié)論按范圍交付報(bào)告最后應(yīng)列出環(huán)境拓?fù)?、版本、樣本分布、?fù)載階梯、注入故障、指標(biāo)口徑和觀測(cè)開(kāi)銷。結(jié)論寫(xiě)成條件句在當(dāng)前步驟預(yù)算與隊(duì)列上限下某類失敗能夠收斂哪些工具和峰值尚未覆蓋則明確留空。失敗實(shí)驗(yàn)可以產(chǎn)生三類資產(chǎn)能穩(wěn)定回放的樣本、阻斷同類問(wèn)題的自動(dòng)測(cè)試以及故障時(shí)可執(zhí)行的限流與接管說(shuō)明。它們比一句“Agent 不適合高并發(fā)”更有用也比一張峰值流量圖更接近高可用設(shè)計(jì)真正需要的證據(jù)。