第三週我們把評測平台從 fake baseline 推進到真實 LLM baseline。
第二週結束時,平台已經可以做到:
evals/cases.json
-> batch runner
-> SimpleAgent
-> trace logging
-> evaluator
-> eval_run_*.json
但當時主要還是使用 FakeLLMClient。
FakeLLMClient 很適合建立平台骨架,因為它穩定、不需要 API key,也不會產生成本。
可是它不能代表真實 Agent 行為。
第三週便把注意力轉到真實模型:
接上真實 LLM,觀察真實錯誤,並把錯誤整理成可以改善的方向。
這篇先停下來回顧第三週。
我們不新增大功能,而是把 Day 15 到 Day 20 完成的東西串起來,回答一個問題:
從目前的 eval 結果來看,下一步最值得改善什麼?
這次要做的是:
整理第三週的真實 LLM baseline、failure analysis 和 prompt A/B testing 結果,形成第四週改善策略的依據。
內容分成幾個部分:
以下項目先不處理:
這次先把問題看清楚,不急著一次修完所有錯誤。
第三週主要處理「真實 LLM 評測」與「失敗分析」。
| 天數 | 主題 | 完成內容 |
|---|---|---|
| Day 15 | 從 Fake 到 Real | 新增 GeminiLLMClient,讓 runner 可以用 Gemini Flash 跑 eval |
| Day 16 | Failure Type | 在 evaluation result 加入 failure_type |
| Day 17 | Failure Dashboard | 用 Streamlit 顯示失敗案例、錯誤分布與 task type 失敗率 |
| Day 18 | Prompt A/B Testing | 加入 prompt versioning,讓 baseline prompt 和 improved prompt 可以比較 |
| Day 19 | Retry Strategy | 針對 format_error 讓 Agent 有一次修正機會 |
| Day 20 | Schema Guardrail | 把 JSON schema validation 視為輸出進入後續流程前的 guardrail |
這六天串起來後,現在平台的流程變成:
evals/cases.json
-> 選擇 LLM_PROVIDER
-> 選擇 PROMPT_VERSION
-> SimpleAgent.run()
-> trace logging
-> schema validation guardrail
-> evaluator
-> failure type classifier
-> optional retry
-> schema validation guardrail again
-> evaluator again
-> eval_run_*.json
-> Failure Dashboard
和第二週相比,平台多了六項能力:
這些能力讓我們開始從「平台能不能跑」進入「平台能不能幫我們做工程決策」。
到 Day 21 為止,專案大致有這些模組。
agent-testing-platform/
app.py
trace_viewer_app.py
failure_dashboard_app.py
agents/
simple_agent.py
prompts.py
fake_llm.py
gemini_llm.py
client_factory.py
tools/
calculator.py
tracing/
models.py
storage/
database.py
schema.sql
evals/
cases.json
runner.py
evaluators.py
guardrails/
json_schema.py
ui/
trace_viewer.py
failure_dashboard.py
data/
agent_traces.db
eval_runs/
eval_run_*.json
tests/
test_json_schema_guardrail.py
每個模組的責任也比一開始更清楚。
| 模組 | 責任 |
|---|---|
agents/simple_agent.py |
執行 Agent,建立 messages,處理 final answer / tool call,產生 trace |
agents/prompts.py |
管理 system prompt 與 prompt version |
agents/client_factory.py |
根據 LLM_PROVIDER 建立 fake 或 Gemini client |
evals/runner.py |
讀取 cases,批次執行 Agent,保存 eval run |
evals/evaluators.py |
根據 grading method 評分,並標記 failure type |
guardrails/json_schema.py |
檢查 JSON output 是否符合簡化 schema |
ui/failure_dashboard.py |
讀取 eval run,顯示失敗分析 |
ui/trace_viewer.py |
查看單次 Agent 執行 trace |
tests/test_json_schema_guardrail.py |
用本地測試驗證 JSON schema guardrail |
第四週會開始加 tool guardrails、latency、cost 和最終實驗整理,這時模組邊界就會派上用場。
如果前面的邊界不清楚,後面很容易變成所有邏輯都塞進 runner.py 或 SimpleAgent。
第二週的 baseline 使用 FakeLLMClient。
它的行為大致是:
看到「計算」
-> 嘗試產生 calculator tool call
其他任務
-> 回傳 Fake response for: 原始問題
所以 fake baseline 的通過案例通常集中在 calculation。
例如:
{
"case_id": "case_001",
"task_type": "calculation",
"actual": "The result is 3780",
"passed": true
}
但 keyword QA、general QA、instruction following 和 JSON output 大多不會真的被理解。
這讓 fake baseline 的用途偏向:
確認平台流程能不能跑通。
Day 15 接上 Gemini 後,baseline 的意義改變了。
Gemini baseline 開始能回答真實任務。
例如:
請回答 HTTP 狀態碼 404 通常代表什麼
Gemini 可能會回答:
HTTP 狀態碼 404 通常代表找不到請求的資源。
這種結果讓 contains evaluator 可以正確判定通過。
所以 Gemini baseline 的用途變成:
觀察真實 LLM 在固定 eval dataset 上的表現。
這兩種 baseline 都有價值,但用途不同。
| baseline | 主要用途 |
|---|---|
| fake baseline | 測平台流程、測 trace / eval / dashboard 是否正常 |
| Gemini baseline | 測真實 LLM 在資料集上的能力與失敗模式 |
Fake baseline 的成功率低,不代表 Agent 測試平台做錯。
它本來就不是用來回答知識問題的。
例如 fake client 回:
Fake response for: 請回答台灣的首都是哪裡
這不代表 evaluator 太嚴格,也不代表 task case 有問題。
它只是說明 fake client 不具備這個能力。
反過來說,如果 fake client 某題剛好通過,也不一定代表它真的答對。
Day 14 提過 contains evaluator 的 false positive:
{
"case_id": "case_007",
"input": "請回答 Python 中 list 是可變還是不可變資料型別",
"expected": "可變",
"actual": "Fake response for: 請回答 Python 中 list 是可變還是不可變資料型別",
"passed": true
}
這題通過是因為 actual 裡剛好包含「可變」。
但那個「可變」來自原始問題,不是 Agent 的答案。
所以 fake baseline 比較適合當作工程 smoke test:
runner 有沒有跑完?
trace 有沒有存?
eval result 有沒有輸出?
dashboard 有沒有讀到資料?
真正要分析 Agent 行為,還是要看 Gemini baseline。
第三週接上 Gemini 後,也遇到一個很實際的工程問題:
API rate limit
也就是 Gemini API 回傳 429。
這種錯誤通常不是 prompt、evaluator 或 guardrail 可以解決的。
它屬於執行環境和 API quota 問題。
所以 Day 19 和 Day 20 的驗證策略要稍微調整:
真實 LLM 行為
-> 用 Gemini baseline 觀察
平台邏輯是否正確
-> 優先用 fake client 或本地 unit test 驗證
例如 Day 20 的 schema guardrail,不需要每次都打 Gemini 才能驗證。
可以直接跑:
python3 -m pytest tests/test_json_schema_guardrail.py
如果測試結果是:
4 passed
代表 JSON schema guardrail 本身可以正確處理:
接著再跑:
RETRY_ENABLED=0 LLM_PROVIDER=fake python3 -m evals.runner
這不是用來評估模型能力。
它的用途是確認 runner 串接沒有壞掉:
output_schema 不會讓非 JSON cases 壞掉。之所以拆開驗證,是因為 real LLM API 可能受 quota、網路、服務狀態影響。
但平台內部邏輯應該盡量可以在本地穩定驗證。
這三週下來,evaluation result 逐步變得更完整。
Day 11 時,我們只記錄執行結果:
{
"case_id": "case_001",
"status": "completed",
"actual": "The result is 3780",
"trace_session_id": "..."
}
Day 12 加入自動評分:
{
"case_id": "case_001",
"passed": true,
"failure_reason": null
}
Day 16 加入 failure type:
{
"case_id": "case_013",
"passed": false,
"failure_type": "format_error",
"failure_reason": "Output is not valid JSON: Expecting value"
}
Day 18 加入 prompt version:
{
"run_id": "eval_run_20260908_180929",
"llm_provider": "gemini",
"prompt_version": "baseline",
"total_cases": 15,
"results": []
}
Day 19 加入 retry metadata:
{
"case_id": "case_013",
"retry_enabled": true,
"retry_count": 1,
"initial_actual": "```json\n{\"answer\": 15}\n```",
"initial_failure_type": "format_error",
"initial_failure_reason": "Output is not valid JSON: Expecting value"
}
Day 20 加入 guardrail metadata:
{
"case_id": "case_013",
"guardrail_passed": false,
"guardrail_type": "json_schema",
"guardrail_reason": "Output is not valid JSON: Expecting value"
}
到現在,一份 eval run 不只是一份 pass / fail 清單。
它開始包含可分析的實驗 metadata:
| 欄位 | 用途 |
|---|---|
llm_provider |
知道這次用 fake 還是 Gemini |
prompt_version |
知道這次使用哪個 prompt |
task_type |
分析不同任務類型表現 |
grading_method |
理解評分規則 |
passed |
計算成功率 |
failure_type |
統計失敗類型 |
failure_reason |
追查具體失敗原因 |
retry_enabled |
知道這次是否啟用 retry |
retry_count |
知道某個 case 是否靠 retry 才通過 |
guardrail_passed |
知道 output 是否通過 schema guardrail |
guardrail_reason |
追查 guardrail 擋下 output 的原因 |
trace_session_id |
回到 Trace Viewer 看執行過程 |
後面的 reliability improvement 都會用到這些欄位。
以 Day 16 和 Day 17 的 Gemini baseline 為例,最常看到的錯誤通常不是模型完全不會回答。
不過這裡要補上一個 Day 18 之後的觀察:
加入
json_strictprompt 後,目前這組 JSON 題可能已經全數通過。
所以 Day 21 回顧 format_error 時,不是說它現在一定還大量存在。
更精確的說法是:
format_error 是 baseline 階段暴露出來的代表性問題,
Day 18 用 prompt 先降低它,
Day 19 用 retry 建立復原機制,
Day 20 用 schema guardrail 建立檢查邊界。
比較常見的是這幾類。
最典型的是 JSON 題。
使用者要求:
請用 JSON 格式回傳 10 + 5 的答案,欄位名稱使用 answer
預期是:
{
"answer": 15
}
但模型可能回:
```json
{
"answer": 15
}
```
這對人類來說很清楚。
但對 json.loads() 來說,整段文字不是合法 JSON。
所以 evaluator 會判定:
Output is not valid JSON: Expecting value
並分類成:
format_error
這類錯誤就是 Day 19 retry 和 Day 20 schema guardrail 要處理的目標。
因為模型其實已經產生接近正確的答案,只是格式不符合機器解析要求。
wrong_answer 在目前平台裡的意思是:
在目前 evaluator 規則下,actual 不符合 expected。
它不一定代表模型語意上完全錯。
例如:
expected: 紀錄
actual: Trace 是指記錄並追蹤程式或請求在系統中執行的完整歷程...
這題在人類眼中可能可以接受,因為「記錄」和「紀錄」在這裡語意接近。
但目前 evaluator 是 contains。
它只做字串包含檢查,所以這題會失敗。
這類錯誤要小心解讀。
改善方向可能是:
這篇先不動這些地方。
但在回顧中要記住:
failure analysis 也會暴露 evaluator 本身的限制。
如果使用者要求:
請只回覆 OK
但 Agent 回:
OK。
或:
好的,OK
這就會被 exact_match 判定失敗。
這類錯誤可以先嘗試用 prompt 改善。
Day 18 的 json_strict prompt 裡已經加入:
If the user asks for a single exact word or phrase, output only that word or phrase.
如果這類錯誤仍然發生,未來也可以把 retry 條件從 format_error 擴充到部分 instruction_error。
execution_error 和前面幾種不同。
它代表 Agent 執行流程本身失敗。
例如:
{
"failure_type": "execution_error",
"error": "GEMINI_API_KEY is not set"
}
或:
{
"failure_type": "execution_error",
"error": "'NoneType' object has no attribute 'session_id'"
}
這類錯誤通常不是 prompt 可以解決的。
例如 GEMINI_API_KEY is not set 要修設定。
trace 是 None 則要修 AgentResult 回傳資料結構。
所以看到大量 execution_error 時,不應該先改 prompt。
應該先修平台流程,讓 evaluation 能正常跑完。
Day 17 做的 Failure Dashboard 可以用來整理第三週結果。
啟動 dashboard:
streamlit run failure_dashboard_app.py
可以先選擇一份 Gemini baseline eval run,或選擇 Day 18 前後不同 prompt version 的結果。
如果選的是較早的 baseline run,Summary 可能長這樣:
Total cases: 15
Passed: 10
Failed: 5
Success rate: 66.7%
這是整體結果。
如果選的是 Day 18 後的 json_strict run,JSON 題可能已經通過,成功率也可能更高。
所以 Day 21 讀 dashboard 時,不是只看單一 run,而是要把不同階段的 run 放在一起理解:
fake baseline
Gemini baseline
Gemini + json_strict
Gemini + retry
Gemini + schema guardrail
接著看 Failure Type Distribution。
例如 baseline run 可能是:
| failure_type | count |
|---|---|
format_error |
3 |
wrong_answer |
2 |
從這裡可以先看出:
baseline 階段最集中的問題不是所有任務都失敗,而是 JSON 格式輸出不穩定。
如果切到 json_strict run,format_error 可能會下降甚至歸零。
表示 Day 18 的 prompt 修改確實對準了問題。
再看 Task Type Failure Rate。
例如 baseline run 可能是:
| task_type | total | failed | failure_rate |
|---|---|---|---|
json_output |
3 | 3 | 100.0% |
general_qa |
3 | 2 | 66.7% |
calculation |
4 | 0 | 0.0% |
instruction_following |
2 | 0 | 0.0% |
keyword_qa |
3 | 0 | 0.0% |
這個表格比 success rate 更有用。
因為它指出:
json_output 是第三週後段最值得優先改善的 task type。
而 Day 19 和 Day 20 正是在處理這個方向。
Dashboard 幫我們找到失敗分布。
但真正要理解單一案例,還是要回到 Trace Viewer。
流程是:
Failure Dashboard
-> 找到 failed case
-> 複製 trace_session_id
-> 開啟 Trace Viewer
-> 查看每一步
啟動 Trace Viewer:
streamlit run trace_viewer_app.py
例如 dashboard 顯示:
case_013
failure_type: format_error
trace_session_id: 4a2d...
進 Trace Viewer 後要看:
如果 trace 顯示模型真的輸出了:
```json
{
"answer": 15
}
```
那就代表問題不是模型不知道答案,而是輸出格式沒有符合 evaluator。
這種問題就是 Day 19 retry 要處理的情境:
第一次輸出不是合法 JSON
-> 把錯誤訊息回饋給 Agent
-> 要求它重新輸出純 JSON
Day 18 加入了 prompt versioning。
現在可以用環境變數切換:
PROMPT_VERSION=baseline LLM_PROVIDER=gemini python3 -m evals.runner
以及:
PROMPT_VERSION=json_strict LLM_PROVIDER=gemini python3 -m evals.runner
這兩次 run 會分別記錄:
{
"prompt_version": "baseline"
}
和:
{
"prompt_version": "json_strict"
}
比較時,不要只看成功率。
假設 baseline 是:
| prompt_version | passed | failed | success_rate |
|---|---|---|---|
baseline |
10 | 5 | 66.7% |
而 json_strict 是:
| prompt_version | passed | failed | success_rate |
|---|---|---|---|
json_strict |
12 | 3 | 80.0% |
這看起來是改善。
但要再看 failure type:
| prompt_version | format_error | wrong_answer | instruction_error |
|---|---|---|---|
baseline |
3 | 2 | 0 |
json_strict |
1 | 2 | 0 |
這樣比較能說:
json_strict確實降低了 format_error,而且沒有明顯增加 wrong_answer。
如果結果是:
| prompt_version | format_error | wrong_answer | instruction_error |
|---|---|---|---|
baseline |
3 | 2 | 0 |
json_strict |
1 | 5 | 1 |
那就不能只說 json_strict 比較好。
它可能改善 JSON,但讓一般回答或指令遵循變差。
因此,prompt A/B testing 不能只看總分:
不只看總分,也要看錯誤分布是否往正確方向移動。
以目前實作來看,json_strict 很可能已經讓 JSON 題全數通過。
這時 Day 19 的 retry 不一定會被觸發。
如果 eval result 裡看到:
{
"retry_enabled": true,
"retry_count": 0
}
這不能直接解讀成 retry 沒有用。
它只代表:
這次 prompt 已經讓第一次輸出通過,所以 recovery path 沒有被用到。
如果要驗證 retry 機制,可以切回 baseline prompt,或使用本地 fake/stub client 製造一次 JSON format error。
第三週結束後,我們可以開始分類改善策略。
有些錯誤適合先改 prompt。
例如:
| 錯誤 | 為什麼適合改 prompt |
|---|---|
| JSON 外面包 markdown code fence | 可以明確要求只輸出 raw JSON |
| exact match 題多了說明文字 | 可以要求只輸出指定字串 |
| 回答太長導致格式不穩定 | 可以要求簡潔輸出 |
| 忘記包含必要欄位 | 可以在 prompt 中列出格式要求 |
這些問題的共同點是:
模型大致知道要做什麼,但輸出風格或格式沒有被控制好。
這時 prompt 可能有效。
Day 18 的 json_strict 就是這類改善。
也有一些錯誤不適合只靠 prompt。
例如:
| 錯誤 | 更適合的方向 |
|---|---|
| API key 沒設定 | 修環境變數或設定檢查 |
trace 是 None |
修 AgentResult 和 runner 的資料流 |
| JSON 偶爾還是包 code fence | retry 或 output cleanup |
| 欄位型別錯誤 | schema validation guardrail |
| tool 不存在或工具輸入錯誤 | tool guardrail |
| evaluator false positive | 改 evaluator 或 test case |
這些問題如果只靠 prompt,通常不穩。
例如你可以在 prompt 裡寫:
Always output valid JSON.
但模型仍然可能偶爾輸出:
```json
{ "answer": 15 }
```
所以第三週後段改用 retry 和 schema validation guardrail,讓處理方式更明確。
可靠性改善有很多可以做的方向:
那為什麼 Day 19 和 Day 20 先做 retry 與 schema validation?
因為從第三週的結果來看,format_error 很適合作為第一個 retry 目標。
原因可以拆成四點。
第一,錯誤可以被 evaluator 明確偵測。
例如:
Output is not valid JSON: Expecting value
第二,錯誤訊息可以回饋給模型。
例如:
Your previous output was not valid JSON.
Please return only valid JSON matching this expected structure:
{"answer": 15}
第三,重試一次有機會修正。
因為模型通常已經知道答案,只是格式錯。
第四,retry 的效果可以被現有 eval pipeline 測量。
我們已經有:
所以 Day 19 加 retry 後,可以直接比較:
retry disabled
retry enabled
而 Day 20 的 schema validation 則把這件事再往前推一步。
Retry 是:
失敗後,給 Agent 一次修正機會。
Schema validation 是:
在輸出進入後續流程前,先確認格式符合要求。
兩者的關係是:
schema validation
-> 偵測格式是否合格
-> 不合格時標記 failure
-> retry 可以根據這個錯誤產生修正任務
前面幾天累積的 trace、eval、failure type 和 dashboard,也在這裡串成完整流程。
第三週真正留下來的,不只是某一次 Gemini 成功率,而是一套可重複的分析流程。
現在每次要改善 Agent,都可以照這個流程:
1. 固定 eval dataset
2. 選定 LLM provider
3. 選定 prompt version
4. 執行 batch evaluation
5. 保存 eval run JSON
6. 查看 Failure Dashboard
7. 回 Trace Viewer 檢查代表性失敗
8. 決定下一個改善策略
9. 用 fake client 或 unit test 驗證平台邏輯
10. 再用 real LLM baseline 觀察真實行為
這個流程讓 Agent 開發比較像工程實驗,而不是憑感覺調 prompt。
尤其是 prompt 改動。
沒有 eval 時,prompt 修改常常像這樣:
改一句 prompt
手動測三題
覺得比較好
就留下來
有 eval 後,我們可以改成:
新增 prompt version
跑完整 dataset
比較 success rate
比較 failure type
檢查是否造成其他 task type 退步
必要時加入 retry 或 guardrail
用本地測試確認平台邏輯
再決定是否保留
有了這些紀錄,prompt 修改才有可比較的依據。
雖然第三週平台變得更完整,但目前仍有幾個限制。
目前只有 15 筆 cases。
所以一題通過或失敗,就會造成明顯百分比變化。
例如:
1 / 15 = 6.7%
目前的 success rate 適合看方向,不適合拿來下太精細的結論。
目前主要評分方式是:
contains
exact_match
json_exact
這些 evaluator 很適合 MVP。
但它們對語意等價、同義詞、部分正確答案的判斷還很粗。
例如:
紀錄 vs 記錄
目前會被視為不同。
Day 16 的 classify_failure() 是規則式分類。
它可以把很多錯誤整理成:
format_error
wrong_answer
instruction_error
tool_error
execution_error
unknown_failure
但它還不能完全理解錯誤背後的語意。
所以 dashboard 的分類要搭配 actual output 和 trace 一起看。
真實 LLM 輸出可能有隨機性。
更嚴謹的做法是每個 prompt version 跑多次,再看平均結果。
但目前先做單次 run,是為了讓流程簡單、可教學、可操作。
後面資料集變大後,再做更完整的統計比較。
Gemini API 可能出現 429。
通常是短時間內請求太多,或 API key / project quota 已經被限制。
遇到這種情況時,不應該一直重跑完整 eval。
比較合理的做法是:
第三週先把這件事記下來。
Day 23 討論成本與延遲時,會再回來處理 API call 的限制。
Day 20 的 output_schema 是本系列自訂的簡化格式。
目前只處理:
它還不是完整 JSON Schema。
所以它適合教學和 MVP,但還不是 production-grade schema validation。
第三週我們完成了從 fake baseline 到真實 LLM baseline 的轉換。
完成內容包含:
GeminiLLMClient。LLM_PROVIDER 可以切換 fake / Gemini。failure_type。agents/prompts.py 加入 prompt versioning。PROMPT_VERSION 可以切換 baseline / json_strict。llm_provider 和 prompt_version。evals/runner.py 加入 RETRY_ENABLED。format_error 和 json_exact 實作 retry once。retry_count、initial_actual 和 retry 前的失敗原因。這些功能讓我們可以把失敗對應到具體的改善方向。
例如:
| 觀察 | 改善方向 |
|---|---|
format_error 很多 |
prompt、retry、schema validation |
wrong_answer 很多 |
prompt、dataset、evaluator |
instruction_error 很多 |
prompt、retry |
execution_error 很多 |
修平台設定或資料流 |
tool_error 很多 |
tool guardrail |
平台現在開始能回答:
下一步該修哪裡?
而不只是:
通過幾題?
Day 22 會進入 Tool Guardrail。
前面幾天處理的是輸出可靠性:
prompt
retry
schema validation
接下來要處理的是工具使用邊界。
Agent 可以使用工具,但不是每個任務都應該使用每個工具。
Day 22 會開始加入工具權限限制,例如:
allowed_tools
blocked tool call
tool violation
流程會變成:
test case 定義允許工具
-> Agent 嘗試呼叫工具
-> runner 檢查是否授權
-> 未授權就阻擋並記錄 tool_error
這會讓 Guardrails 從輸出格式擴展到工具使用。
到那時候,我們可以開始回答:
Agent 不只答案是否正確,
它使用工具的方式是否也符合限制?
這會讓 Agent 可靠性改善從「分析問題」進入「驗證改善」。