Day 28 證明了一件事:最終回答不能代表整個 Agent run。既然真正的證據在執行軌跡裡,我就能再問更實際的問題:這次到底呼叫幾次模型、花多少 Token、等多久、為什麼重試,以及失敗後有沒有留下可以重現的案例?
如果這些問題只能靠使用者回報「今天好像比較慢」,我還沒有建立可維運的 Agent。
Day 29 我讓 Gemini 真的提出 SOP 查詢,再用固定故障注入讓 mock 查詢連續逾時。這次我不只看最後是否降級,而是把模型 usage、模型延遲、guardrail 判斷、工具失敗與重試次數收進同一個 run。最後再把去識別化的 failure envelope 轉成 Promptfoo case。
我在這個專案裡把三者分開:
Day 29 的網頁仍以單次 trace 為主;指標則從這個 run 的事件與 provider usage 彙整。正式環境會把相同欄位送到監控後端,而不是靠瀏覽器頁面長期保存。
OpenTelemetry 的 GenAI semantic conventions 已定義 gen_ai.usage.input_tokens、gen_ai.usage.output_tokens、模型與 provider 等欄位;輸入與輸出內容本身可能含有敏感資料,官方也特別提醒這類內容不應預設全部記錄。OpenTelemetry:Gen AI attributes
Provider 有回傳 usage 時,我可以記錄 input、output 與 total tokens。沒有回傳時,畫面就顯示 unavailable,不用字數偷偷估一個「實際 Token」。
成本還多一個條件:我要知道這個部署當下的 input/output 費率。相同模型可能有不同供應方式、快取與內部計價,Promptfoo 的 Agent 評估文件也提醒,某些路徑不會自動推算美元成本。Promptfoo:Evaluate OpenAI Agents
因此 Day 29 的 Live 畫面會呈現真實 provider usage 與實測 latency,但沒有設定費率時,成本維持 unavailable。
固定回歸案例才使用明寫在測試裡的 mock usage 與 mock 費率:
input tokens:2400
output tokens:500
total tokens:2900
test input rate:$1 / 1M tokens
test output rate:$2 / 1M tokens
estimated test cost:$0.0034
這個 $0.0034 只驗證公式,不是 Gemini 或其他模型的公開報價。
Day 29 的 Live latency 從送出模型請求前開始,到收到完整 AIMessage 為止。它不是整個頁面載入時間,也不含後面的 mock SOP retry。
若改成串流,還要分開 time to first token 與完整回覆時間。OpenTelemetry 使用 duration 類型描述一次離散操作的經過時間,GenAI instrumentation 也可用 operation duration 觀察模型呼叫。OpenTelemetry:Metric naming
只有一筆 latency 不能代表模型表現。我現在的頁面只用來檢查單次 run;正式監控會看 p50、p95、p99,並依模型、工具與錯誤類型分組。
比例沒有分母就沒有意義,所以我先固定定義:
guardrail interception rate
= blocked 或 quarantined 的 guardrail decisions
/ 全部 guardrail decisions
tool failure rate
= failed 或 timed_out 的工具執行
/ completed、failed、timed_out 的工具執行總數
requested、skipped 與 pending 不放進工具執行分母。它們分別代表請求、未執行與等待,不應和一次真正完成或失敗的 handler 混在一起。
攔截率也不是越高越好。突然升高可能代表攻擊增加,也可能是規則誤殺;突然降低可能是輸入變乾淨,也可能是 rail 根本沒被呼叫。我要把它和輸入分布、人工申訴與攻擊標籤一起看。
這次固定故障會留下:
search_it_sop timed_out
retry_attempt 1_of_2
search_it_sop timed_out
retry_attempt 2_of_2
search_it_sop failed
fallback_policy blocked
只看最後的 failed,我不知道前面已經多等兩次;只看模型 latency,又會誤以為慢的是 LLM。Trace 把模型、工具、backoff 與停止原因串在同一個 run 裡,我才能找到真正拖慢流程的節點。

這個學生專案目前沒有 production traffic,所以我沒有假裝拿到真實事故。FAIL-MOCK-029 是一份 production-shaped fixture:格式比照正式失敗事件,但內容、usage、價格與延遲都是固定測試資料。
真正上線後,我會把失敗回灌流程拆成四步:
Promptfoo CLI 也支援依先前結果重跑失敗案例;我這裡選擇把人工確認過的失敗降成版本控制內的固定 case,避免一份可能含敏感內容的原始 trace 直接進 CI。Promptfoo:Command Line
Live smoke test 已確認 Gemini 會提出 SearchItSopProposal,usage 與 latency 會進入 run_metrics。固定 regression case 則使用不變的資料,驗證 Token、測試成本、延遲、攔截率、工具失敗率與 retry count。
npx --yes promptfoo@latest eval \
-c evals/promptfooconfig.yaml \
--no-progress-bar \
--no-cache \
--filter-metadata day=29
實際結果是 1 passed、0 failed、0 errors。這只代表 FAIL-MOCK-029 的指標定義與降級行為沒有回歸,不代表正式環境的 p95 latency 或成本已達標。
Day 29 做完後,我有了從失敗 trace 回到測試案例的路徑。最後一天要把單元測試、Promptfoo 攻擊矩陣、trajectory、side effect 與已知風險接進同一個 release gate。
成本只有在部署明確提供費率時才計算:
cost = None
if input_per_million_usd is not None and output_per_million_usd is not None:
cost = (
input_tokens * input_per_million_usd / 1_000_000
+ output_tokens * output_per_million_usd / 1_000_000
)
固定失敗案例把指標與回灌結果一起回傳:
return {
"failure_id": "FAIL-MOCK-029",
"metrics": asdict(metrics),
"ticket_count": 0,
"regression_case_generated": True,
"trace": trace.as_list(),
}