如何建立自架 AI agent eval 流程
用 repository 保存真實 trace、案例與 SQLite 結果,先執行便宜的 deterministic checks,再用 LLM judge 評分,並追蹤每個 commit 的通過率。
AI agent 的 self-hosted eval 是什麼
AI agent 的 self-hosted eval 包含 4 項內容,全部保存在自己的 repository 中:儲存案例的檔案、讓 agent 執行這些案例的 script、評分每個回答的一組檢查,以及可供查詢的結果表。這些內容都不需要依賴 vendor。整個流程只需要幾百行 Python 和 1 個 SQLite 檔案。
demo 中的 agent 能正常運作,是因為 5 個輸入都是你自行挑選的。到了第 2 週,它可能因為 prompt 中的某一行、model 或 tool description 變更而失效,但沒有任何量測涵蓋這些變更。eval 流程會把「現在感覺變差了」轉化為「在 commit 4f1c9ab 上,通過率從 60 個案例中的 58 個降至 60 個案例中的 51 個」。
這個流程包含 4 個步驟,本指南每個步驟各有 1 個 section:收集實際 trace、將值得注意的 trace 提升為案例、針對每次變更為每個案例評分,以及將通過率儲存在產生該結果的 commit 旁邊。無論 agent 執行什麼工作,都能使用相同流程;而 值得執行的 self-hosted agent frameworks 主要差異在於它們會免費提供多少 trace 內容。
第二週 agent 為何會失效
agent 由提示、模型、工具定義,以及執行時擷取的內容組成。這4項都可能在應用程式程式碼未變更的情況下改變,因此一般的程式碼審查不會發現任何異常。
最常見的原因是修改提示。你加入一句話,想避免 agent 回覆無禮。這句話會改變未重新測試之輸入的行為,而追蹤記錄會清楚顯示差異:上週相同問題的追蹤記錄包含 create_refund 工具呼叫,本週則完全沒有,回覆反而變成禮貌的道歉。過程中沒有發生錯誤,因此也沒有觸發警示。
第二個原因是模型。每次執行都記錄實際傳送的完整模型字串,例如 claude-haiku-4-5-20251001,不要只依賴自己記得的簡稱。只有在模型名稱存在於該筆資料中,才能診斷通過率是否在切換模型當天下降。
第三個原因是工具。重新措辭工具描述會改變模型決定呼叫工具的時機。如果工具是透過在 VPS 上執行的 MCP 伺服器提供,結構描述會存在於另一個程序中,因此可能在你的儲存庫完全沒有差異的情況下自行變更。第四個原因是擷取內容:相同問題命中一個在夜間重新建立的索引,而答案也隨著新文件而改變。
從既有追蹤資料建立基準測試集
不要自行編造評估案例,應從實際流量中取得。如果你已經為 agent 執行 自架 Langfuse 追蹤,每個請求都會儲存輸入、工具呼叫與輸出,這些正是建立案例所需的原始資料。
透過公開 API 匯出一段時間範圍內的 root observations。此 API 使用基本驗證,public key 作為使用者名稱,secret key 作為密碼。
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'先讀取一筆記錄,再撰寫任何解析程式。資料列會放在 data 底下,但問題與回覆所使用的欄位名稱,取決於 agent 如何對 span 進行 instrumentation。因此,應根據實際看到的內容進行對應,不要假設欄位名稱。接著手動撰寫案例,每行放置一個 JSON 物件,寫入 evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}以下 5 項規則可讓測試集維持實用性:
- 一開始準備 40 到 80 個案例即可。少於 20 個時,一個不穩定的案例就可能讓通過率變動 5 個百分點;沒有明確原因而大幅變動的數字,通常會被忽略。
- 每個修正的 production bug,都應在修正當天轉成一個案例。這項習慣能讓測試集朝正確方向成長。
- 每個案例只測試一種行為。如果同一個案例同時檢查退款金額與語氣,失敗時就無法判斷原因。
id永遠不能變更,因為系統會依據此 id 比較今天與上個月的執行結果。- 在 commit 前先移除敏感資料。此檔案會存入 git,因此應移除客戶名稱,以及任何不屬於你的訂單編號。
先執行確定性檢查,因為這些檢查不需額外成本
凡是有明確正確答案的項目,都直接使用 assertion。不需呼叫模型,不產生成本,也沒有歧義。確定性檢查能捕捉結構性退化,而這些問題會破壞代理程式周邊的系統:JSON 無法解析、工具從未被呼叫、禁用的片語再次出現,或回答未引用任何來源。
只有一個函式需要知道你的代理程式。測試 harness 中的其他部分都保持通用。
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failures將工具呼叫預算保留在該清單中。代理程式今天用 3 次呼叫解決案例,明天卻需要 11 次,即使最終答案正確,仍代表系統已退化,因為每次呼叫都會產生成本。
LLM 作為評審,以及它出錯的 4 種方式
通過這些斷言的內容,仍需要能夠閱讀答案的評分器。LLM 評審是另一次模型呼叫:它會收到問題、代理程式的答案及一項評分標準,然後回傳判定。這是評估「回覆是否回答使用者所詢問內容」的唯一實用方式。
以下 4 項規則能讓評審具備實用性:
- 使用二元判定,絕不要使用 1 到 10 的分數。評分尺度幾乎所有內容都會得到 7 或 8,因此數字不會變化,你也無法從中學到任何資訊。
- 每次呼叫只使用一項評分標準。詢問退款金額,或詢問語氣,不要同時詢問兩者。
- 案例有預期答案時,務必提供給評審。依據參考答案評分,比抽象地評分容易得多。
- 強制指定輸出格式,並嚴格解析。
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)以下是失效模式。每一項都有你今天下午就能執行的測試。執行這些測試很重要,因為未經檢查的評審會產生看似精確、實際上毫無意義的數字。
長度偏差。 較長的答案更常通過。測試方式:取 10 個評審判定失敗的答案,在每個答案後加入 2 個只包含自信語氣、沒有新增事實的段落,然後重新評分。若有任何判定從失敗變成通過,就是長度偏差;此時需要修正的是評分規範。
自我偏好。 評審通常會比較寬容地評估來自相同模型家族的輸出,而不是來自其他模型的輸出。測試方式:使用來自 2 個不同模型家族的評審,為相同的 30 個答案評分,然後逐案例比較判定結果。若結果不一致,請自行閱讀該案例。
位置偏差。 若使用評審比較 2 個答案 A 和 B,請交換順序後重新執行。交換順序後判定改變,表示該評分規範目前不適合進行成對比較。
評分規範漂移。 模糊的標準會產生只想同意的評審。「答案是否有幫助」幾乎任何內容都能通過。「答案是否以美元說明退款金額」則只會通過符合指定要求的內容。請持續改寫每項標準,直到它明確指出要檢查的事實。
一項防護措施即可涵蓋以上 4 種問題。保留 30 個由人工標註的案例,每次變更評審模型或評審提示時,都使用這些標註評估評審。如果評審在 10 個案例中有超過 1 個案例與你的判定不同,請先修正評分規範,再信任它產生的任何通過率。評審也是程式碼,因此應像程式碼一樣進行版本控管與審查。
讓便宜模型先評分,需要時再升級到 frontier model
每次提交都用最昂貴的模型評估所有案例,會讓評估費用超過受測 agent 本身的成本。請依價格排列評分器,並在答案已足夠明確時立即停止。
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]這些數字假設每次評分呼叫約包含 1,200 個輸入 tokens 和 120 個輸出 tokens。對單一問題、答案與評分標準而言,這是合理的大小。評估 1,000 個案例時,使用 Claude Haiku 4.5 的成本為 1.80 美元,使用 Claude Opus 5 的成本則為 9.00 美元。差距看似不大,但乘開後就很可觀。若一組包含 60 個案例,且每次提交都進行評估,每週提交 40 次,則在執行 nightly job 前,每週就會產生 2,400 次評分呼叫。
評估工作適合套用兩項折扣,而且可以疊加使用。評估執行不是互動式工作,因此 Batch API 可透過非同步交付,將輸入與輸出價格都減半;這是圖表中的第一列。每次呼叫中的評分規準與指示都完全相同,因此適合使用 prompt caching:讀取快取的費用是基本輸入價格的十分之一,寫入快取 5 分鐘的費用是基本輸入價格的 1.25 倍,因此快取只要命中一次就能回本。這些是截至 2026 年 8 月的 Anthropic 定價;Sonnet 5 的 introductory pricing 會持續到 2026 年 8 月 31 日,因此第三根長條會在該日期後上升。
評估階梯如下:
- 每個案例都執行 deterministic checks。不產生任何 API 費用。
- 對通過這些檢查的案例,使用小型模型評分。
- 只有在小型模型判定失敗,或判定通過但信心不足時,才使用 frontier judge。
- 每週一次,抽取少量案例進行人工審查。
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]這種做法會以部分評分準確度換取成本降低,因此應測量兩者之間的取捨,不要直接假設結果。每月一次,使用嚴格的 judge 評估整組案例,再比較兩欄結果。如果兩者在超過少數案例上出現差異,表示評分規準對小型模型而言過於寬鬆;應修正的是評分規準。控制 agent 自身的支出是另一項工作,請參閱 VPS 上 AI agent 的成本控制。
隨時間追蹤自有系統的通過率
無法與 commit 對應的通過率只是一種感覺。每次執行的每個案例各儲存一列,並在該列中記錄 commit 與模型。
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;使用 sqlite3 evals/results.db < evals/schema.sql 載入 schema,再使用 sqlite3 -box evals/results.db < evals/passrate.sql 讀取趨勢。一年每天執行一次、每次 60 個案例,約為 22,000 列,因此資料儲存不會反過來成為獨立的專案。在 VPS 上於 production 執行 SQLite 說明檔案需要在多台機器之間共用時,開始需要注意的設定。
runner 會為人員列印相同資訊:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amount在可能使 agent 故障的變更上執行測試套件,包括 prompt 編輯、模型變更與工具變更,而不是對 repository 中的每個 commit 都執行。pre-push hook 可涵蓋快速子集:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-push完整執行速度較慢,適合排程執行。每晚執行的 VPS 上的 systemd service 與 timer 會針對已部署的 prompt 執行整套測試,這能捕捉 repository 外部引入的變更,例如託管工具的行為發生變化。
人工審查,採樣而非詳盡檢查
評審器是依據人工標註校準的,因此必須有人產生這些標註。每週閱讀一批樣本:評審器判定失敗的所有案例,再隨機選取 10 個通過案例。隨機選取的通過案例尤其重要,因為評審器若已悄悄開始放過不良答案,任何只根據自身判定建立的儀表板都會顯示正常。
每週審查 15 個案例、每個 3 分鐘,共需 45 分鐘。這能找出你與評審器意見不一致時應修正的評分規範,也能補充原先未預想到的失敗類型案例。將人工判定寫入相同的資料表,並將 graded_by 設為 human,如此一來,評審器與人工判定是否一致就能透過查詢取得,不必依賴記憶。
評估測試工具本身的故障點
anthropic.RateLimitError 出現在第一次完整執行時。 一次展開 60 個案例會超過您方案的請求數或 token 限制。將並行工作數限制為 4 個,並將每晚執行的工作移至 Batch API。
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) 來自評審器。 模型以散文形式回覆,或將 JSON 包在程式碼區塊中。重試 1 次,然後將該案例記錄為錯誤。解析失敗絕不能算作通過,因為將錯誤轉為通過的測試套件,會在 agent 變差時仍逐步逼近 100%。
不穩定案例。 相同輸入在某次執行時通過,下一次卻失敗,原因是 agent 會對輸出進行取樣。讓不穩定案例執行 3 次,並記錄通過比例,不要刪除該案例。某案例在 3 次中通過 2 次,代表確實存在穩健性問題,客戶也會發現這個問題。
基準資料集腐化。 有人修改預期答案,讓測試套件顯示為通過。檢視指向 evals/cases.jsonl 的差異時,應與檢視 agent 的差異同樣仔細,因為該檔案是您對正確結果的書面定義。
永遠不會失敗的測試套件。 通過率連續 1 個月維持在 100%,表示資料集已停止反映產品狀態。擷取最近的 10 筆追蹤記錄,找出 agent 處理不佳的案例,並將其加入資料集。
FAQ
AI agent eval set 需要多少案例?
先從 40 到 80 個案例開始,再根據實際失敗案例擴充集合。少於約 20 個案例時,單一不穩定結果就可能讓通過率變動 5 個百分點,因此這個數值不再具備參考價值。超過幾百個案例後,每次執行都會消耗實際的費用與時間,而新增案例帶來的涵蓋範圍有限。真正重要的不是案例數量,而是已知的正式環境失敗類型中,有多少比例至少出現在集合中 1 次。
可以信任 LLM judge 為我的 agent 評分嗎?
只有在使用你自己的標註資料進行測量後,才可以。保留 30 個由人工評分的案例;每次變更 judge model 或 judge prompt 時,都要用這些案例評估 judge。judge 可能出現長度偏誤,讓填充較多的答案更容易通過;也可能出現自我偏好,對相同 model family 產生的輸出給予較寬鬆的評分。這兩種偏誤都能測試:將失敗答案加長後重新評分,或使用其他 model family 的 judge 評分相同答案。如果 judge 與你的標註在超過每 10 個案例中的 1 個案例上不一致,表示評分規範過於模糊,無法使用。
應該由哪個 model 為 evals 評分?
先使用低成本方式評分,再逐步升級。Deterministic assertions 不需費用,因此每個案例都先執行。小型 model 處理明確通過的案例。只有失敗案例與低信心判定才交由 frontier model 處理。以 2026 年 8 月的牌價計算,使用 Claude Haiku 4.5 為 1,000 個案例評分,約需 1.80 美元;使用 Claude Opus 5 則約需 9.00 美元。由於 eval 執行是非同步的,Batch API 可將兩者的費用減半。
evals 可以取代正式環境監控嗎?
不行,因為兩者回答的是不同問題。eval suite 會告訴你,即將發布的變更是否讓固定案例集合的結果變好或變差。Tracing 與 monitoring 則會告訴你實際使用者目前遇到的問題,包括尚未涵蓋在任何案例中的輸入。兩者會互相提供資料:traces 提供新案例,而 eval suite 判定修正是否真正生效。