iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0

Day 28 證明了一件事:最終回答不能代表整個 Agent run。既然真正的證據在執行軌跡裡,我就能再問更實際的問題:這次到底呼叫幾次模型、花多少 Token、等多久、為什麼重試,以及失敗後有沒有留下可以重現的案例?

如果這些問題只能靠使用者回報「今天好像比較慢」,我還沒有建立可維運的 Agent。

Day 29 我讓 Gemini 真的提出 SOP 查詢,再用固定故障注入讓 mock 查詢連續逾時。這次我不只看最後是否降級,而是把模型 usage、模型延遲、guardrail 判斷、工具失敗與重試次數收進同一個 run。最後再把去識別化的 failure envelope 轉成 Promptfoo case。

Trace、metric 和 log 不是同一件事

我在這個專案裡把三者分開:

  • Trace:回答「這次 run 依序經過哪些模型、工具與政策節點」。
  • Metric:回答「一段時間內用了多少 Token、延遲分布如何、失敗率是否上升」。
  • Log:保留某個事件的補充資訊,但不應把完整 Prompt、密碼或 token 全部寫進去。

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

Token 與成本不能混為一談

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 根本沒被呼叫。我要把它和輸入分布、人工申訴與攻擊標籤一起看。

重試要在同一條 Trace 裡看

這次固定故障會留下:

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 裡,我才能找到真正拖慢流程的節點。

image

把 production failure 回灌成 Promptfoo case

這個學生專案目前沒有 production traffic,所以我沒有假裝拿到真實事故。FAIL-MOCK-029 是一份 production-shaped fixture:格式比照正式失敗事件,但內容、usage、價格與延遲都是固定測試資料。

真正上線後,我會把失敗回灌流程拆成四步:

  1. 移除 Prompt、工具參數與文件中的 PII、secret 和 tenant 識別資訊。
  2. 保留造成問題所需的最小輸入、政策版本、模型版本與 trace 事件。
  3. 寫出不依賴當時外部狀態的通過條件。
  4. 把案例提交進 repo,讓修正前會失敗、修正後會通過。

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(),
}

本日程式碼

day-29-trace-metrics


上一篇
Day 28|Agent 嘴巴說不做,手卻已經呼叫危險工具了?
下一篇
Day 30|我把這個 Agent 放進紅隊測試後,還剩下多少風險?
系列文
從 LLM、Agent 到 Guardrails:30 天打造可控、安全、可驗證的 AI Agent 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言