AG Day 32 評估基礎:Agent 為什麼難測
執行需求:CPU 可跑。AG Day 31(原文連結)讓 research-agent 具備了檢查點、恢復與背景執行的能力,多代理系統已經可以穩定地跑完一輪又一輪的研究任務。但截至目前為止,我們判斷「這次跑得好不好」的方法一直是:肉眼看一遍輸出的報告內容,覺得還可以就算過關。這種做法在系列前期驗證架構通不通時沒問題,但接下來要做的評估集、LLM-as-judge、追蹤平台、成本最佳化,全都需要一個更嚴謹的地基。今天先不寫任何評分程式,而是老老實實把「為什麼代理系統的測試比一般軟體困難」這個問題想清楚,找出評估代理系統真正該關注的幾個維度,幫接下來三天要陸續建立的評估集、評分標準與評分機制打好地基。這篇是純觀念與骨架整理,CPU 就能跑完所有範例,全程不需要任何 API 金鑰或外部服務帳號。
引言
寫一般軟體的單元測試時,我們習慣的既有模式是:給定輸入,斷言輸出等於某個固定值。這套思維放到 research-agent 上會立刻碰壁。同一個研究問題,跑兩次可能搜尋到不同的網頁、模型可能用不同的字句組織報告、甚至工具呼叫的順序都可能不一樣,但這兩次結果都可能是「合格」的。傳統的「輸出要完全等於期望值」的斷言,在這裡幾乎沒有用武之地。這不是代理系統做得不好,而是它的本質——多步驟決策加上語言模型的生成性——決定了它天生就不是決定性的系統。
今天我們要做三件事:先拆解代理系統為什麼難測的幾個根本原因,再把「評估」這件事拆成幾個獨立的維度(而不是想用一個單一的「好不好」分數概括一切),最後在 research_agent 套件裡新增 evals.py 的骨架,定義評估案例(EvalCase)的資料結構,並示範幾個不需要模型輔助、純規則式的基準檢查,作為之後 LLM-as-judge 評分之前的第一道防線。
原理/觀念
代理系統難測的三個根本原因
第一個原因是非決定性(non-determinism)。即使把模型的 temperature 設成很低甚至零,只要外部搜尋結果隨時間變化、或工具呼叫的順序有些微差異,最終輸出就可能不同。傳統測試假設「相同輸入必得相同輸出」,這個假設在代理系統上並不成立,必須換成「相同輸入應該得到符合某些性質的輸出」這種較弱但更務實的判定方式。
第二個原因是多步驟執行路徑。一個研究任務可能走「直接找到答案」的短路徑,也可能走「查了三次都不夠、換關鍵字再查一次」的長路徑,兩條路徑都可能得到同樣好的最終報告。如果評估只看「用了幾步」或「呼叫了哪些工具」,就會誤判某些其實表現良好但走法不同的執行;但如果完全不看過程、只看最終輸出,又會錯過「明明繞了一大圈才拿到答案,效率其實很差」的問題。這代表評估必須同時關注過程與結果,不能只挑一邊。
第三個原因:工具呼叫的副作用
代理呼叫的工具通常有真實的副作用:搜尋工具會發出真正的網路請求,寫入資料庫的工具會真的改動狀態。這跟測試一個純函式完全不同——你不能無限次重複執行同一個測試案例而不付出成本(網路流量、API 費用、資料庫狀態),也不容易在測試環境裡完整重現生產環境的外部條件(例如搜尋引擎當天回傳的結果)。這也是為什麼 AG Day 2 開始,我們一路都保留了 --dry-run 離線模擬模式:評估代理系統時,能用可控的模擬資料先驗證邏輯正確性,再用少量真實呼叫驗證整合是否正常,兩者缺一不可。
把「好不好」拆成幾個獨立維度
與其追求一個單一的「這次跑得好嗎」分數,更務實的做法是把評估拆成幾個彼此獨立的維度,各自用適合的方法衡量:
- 任務成功率(task success):最終報告是否真的回答了使用者的研究問題,這通常需要人工或模型判斷語意是否切題。
- 引用正確性(citation accuracy):報告裡的每一個引用是否真的對應到檢索出來的來源,這是規則式檢查就能做到的,不需要模型輔助。
- 效率(efficiency):花了幾步、呼叫了幾次工具、耗費多少 token 與時間,這些是可以直接從 AG Day 9 建立的觀測紀錄裡算出來的純數字指標。
- 安全性(safety):有沒有呼叫不該呼叫的工具、有沒有洩漏不該回應的內容,這部分會在 AG Day 37 深入討論。
把維度拆開的好處是,你可以針對每個維度用最合適、成本最低的方法:效率用純數字計算,引用正確性用規則檢查程式,任務成功率才真正需要模型或人工介入判斷。這個分層思路會直接影響接下來三天的設計——AG Day 33 建立評估集時就會按這幾個維度分別設計題目與評分標準。
完整實作
我們在 research_agent 套件裡新增 evals.py,先定義評估案例的資料結構,再實作幾個不需要模型輔助的規則式檢查函式:
touch research-agent/src/research_agent/evals.py
mkdir -p research-agent/data/evals
第一步:定義 EvalCase 與 EvalResult 兩個資料結構。EvalCase 描述一次評估要驗證什麼,EvalResult 描述評估跑完之後的各維度結果,呼應剛才拆出的幾個獨立維度:
# research-agent/src/research_agent/evals.py(1/4:資料結構)
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class EvalCase:
"""一筆評估案例:一個研究問題,加上判斷是否合格的必要條件。"""
case_id: str
question: str
required_keywords: list[str] = field(default_factory=list)
min_citations: int = 1
@dataclass
class EvalResult:
"""一次評估的結果,拆成幾個獨立維度,不合併成單一分數。"""
case_id: str
keyword_coverage: float
citation_count: int
citation_valid: bool
step_count: int
passed_baseline: bool
第二步:實作規則式的引用正確性檢查。這個檢查不需要模型,只需要拿報告全文比對它宣稱引用的來源,是否真的存在於檢索結果裡:
# research-agent/src/research_agent/evals.py(2/4:引用正確性)
import re
def check_citation_validity(report: str, known_sources: list[str]) -> tuple[int, bool]:
"""數出報告裡標記的引用數量,並確認每個引用都能對應到已知來源。"""
citation_markers = re.findall(r"[來源:([^]]+)]", report)
if not citation_markers:
return 0, False
all_valid = all(
any(marker in source or source in marker for source in known_sources)
for marker in citation_markers
)
return len(citation_markers), all_valid
第三步:實作關鍵字覆蓋率檢查,作為「任務成功率」最粗略但完全不需要模型、零成本的替代指標。這不能取代真正的語意判斷,但可以當作第一道快速過濾:
# research-agent/src/research_agent/evals.py(3/4:關鍵字覆蓋率)
def check_keyword_coverage(report: str, required_keywords: list[str]) -> float:
"""計算報告裡出現了多少比例的必要關鍵字,回傳 0.0 到 1.0 之間的覆蓋率。"""
if not required_keywords:
return 1.0
hit = sum(1 for kw in required_keywords if kw in report)
return hit / len(required_keywords)
第四步:把上面幾個規則式檢查組成一個基準評估函式 run_baseline_eval,這是在導入 LLM-as-judge(AG Day 34)之前,成本最低、最快能跑的第一層防線:
# research-agent/src/research_agent/evals.py(4/4:基準評估)
def run_baseline_eval(case: EvalCase, report: str, known_sources: list[str], step_count: int) -> EvalResult:
"""跑一次不需要模型輔助的基準評估,作為 LLM-as-judge 之前的快速檢查。"""
coverage = check_keyword_coverage(report, case.required_keywords)
citation_count, citation_valid = check_citation_validity(report, known_sources)
passed = (
coverage >= 0.5
and citation_count >= case.min_citations
and citation_valid
)
return EvalResult(
case_id=case.case_id,
keyword_coverage=coverage,
citation_count=citation_count,
citation_valid=citation_valid,
step_count=step_count,
passed_baseline=passed,
)
第五步:寫一個示範腳本,拿 AG Day 29-31 產生的示範報告跑一次基準評估,驗證整套骨架可以運作:
# research-agent/scripts/run_baseline_eval_demo.py
from research_agent.evals import EvalCase, run_baseline_eval
def main():
case = EvalCase(
case_id="demo-001",
question="多代理架構的優勢與限制",
required_keywords=["多代理", "supervisor", "worker"],
min_citations=1,
)
demo_report = (
"# 多代理架構的優勢與限制(示範報告,離線模擬模式)\n\n"
"1. 多代理架構把 supervisor 與 worker 分開,各自負責路由與執行。"
"[來源:多代理架構的優勢與限制 示範資料 1]"
)
known_sources = ["多代理架構的優勢與限制 示範資料 1"]
result = run_baseline_eval(case, demo_report, known_sources, step_count=4)
print(f"關鍵字覆蓋率:{result.keyword_coverage:.2f}")
print(f"引用數量:{result.citation_count}(有效:{result.citation_valid})")
print(f"基準評估結果:{'通過' if result.passed_baseline else '未通過'}")
if __name__ == "__main__":
main()
第六步:真實情境裡不會只評估一筆案例,而是一整批。寫一個彙整函式 summarize_results,把多筆 EvalResult 彙整成通過率與平均步數,這是接下來 AG Day 33 建立整組評估集之後,馬上會用到的彙整邏輯:
# research-agent/src/research_agent/evals.py(5/5:彙整多筆結果)
def summarize_results(results: list[EvalResult]) -> dict:
"""把一整批評估結果彙整成通過率與平均步數,供人快速掌握整體品質。"""
if not results:
return {"total": 0, "pass_rate": 0.0, "avg_step_count": 0.0}
total = len(results)
passed = sum(1 for r in results if r.passed_baseline)
avg_steps = sum(r.step_count for r in results) / total
return {
"total": total,
"pass_rate": passed / total,
"avg_step_count": avg_steps,
}
執行示範:
cd research-agent
uv run python scripts/run_baseline_eval_demo.py
示範輸出:
關鍵字覆蓋率:1.00
引用數量:1(有效:True)
基準評估結果:通過
這個結果只回答了「這份報告有沒有明顯的低品質特徵」,遠遠不能回答「這份報告寫得好不好、有沒有真正切題」,這正是接下來三天要逐步補上的部分,也是評估工作裡真正困難、最需要花心思設計的部分。
常見錯誤與踩雷
第一個常見錯誤是把關鍵字覆蓋率當成唯一的評估指標。關鍵字比對非常粗糙,一份報告可能剛好塞進了所有必要關鍵字卻答非所問,也可能用完全不同的措辭正確回答了問題卻沒命中任何關鍵字。今天特別把它定位為「基準」而不是「評估」,就是要提醒自己:這只是低成本的第一道過濾,真正的語意判斷必須留給後面章節的模型輔助評估或人工審查。
第二個常見錯誤是評估時用了跟生產環境不同的資料格式,導致規則式檢查誤判。今天 check_citation_validity 用固定的正規表達式 [來源:...] 去比對引用格式,如果 report.py 之後改變了引用的呈現方式(例如換成方括號數字引用),這個檢查函式就會完全失效卻不會報錯,只會靜靜地回傳「沒有引用」。規則式檢查的維護成本正是在這裡:格式一變就要跟著改,這也是為什麼引用格式從一開始(AG Day 23)就該固定下來、全系統共用同一套慣例。
第三個常見錯誤是把單一次執行的結果當成代理系統整體表現的定論。因為非決定性是代理系統的本質,任何評估如果只跑一次就下結論,很容易被單次的運氣影響。之後導入評估集時,每一題都應該考慮跑多次取平均或看變異程度,而不是只看一次的成功或失敗。
效能與實務提醒
規則式的基準評估幾乎沒有額外成本,可以在每次修改程式碼後隨時執行,甚至可以整合進未來的持續整合流程裡當作最基本的迴歸檢查。這跟需要呼叫模型的 LLM-as-judge 評估(AG Day 34)成本差很多——後者每跑一次評估集都要付出 API 費用與等待時間,適合在比較重要的改動之後才執行,不適合每次存檔就跑一次。
實務上建議把評估拆成兩層:第一層是今天的規則式基準檢查,快速、免費、可以常跑;第二層是模型輔助或人工審查的深度評估,慢、有成本,但能真正判斷語意品質,只在重要版本或重大改動時執行。這種分層策略在軟體工程裡很常見(單元測試 vs. 整合測試的關係),代理系統評估的道理是一樣的。
另外,效率維度(步數、token、時間)的資料如果沒有從一開始就系統性紀錄,事後很難補救。這也是為什麼 AG Day 9 一開始就把觀測基礎建好——今天的 step_count 直接沿用那套紀錄機制,評估與觀測是互相依賴的兩件事,缺一不可。
小結
今天我們把「為什麼代理系統難測」拆成三個根本原因:非決定性、多步驟執行路徑、工具呼叫的副作用,並據此把評估這件事拆成任務成功率、引用正確性、效率、安全性四個獨立維度,避免用單一分數概括一切。實作上,evals.py 有了 EvalCase、EvalResult 兩個資料結構,加上關鍵字覆蓋率與引用正確性兩個規則式檢查,組成了不需要模型輔助的基準評估。
新增的術語:非決定性(non-determinism,相同輸入不保證相同輸出的特性)、基準評估(baseline eval,成本最低的第一層規則式檢查)、評估維度(evaluation dimension,把整體品質拆成的各個獨立面向)、彙整(summarize,把多筆個別結果整理成整體趨勢的統計量)。這些骨架看起來簡單,卻是讓評估從「憑印象判斷」變成「有數字依據」的第一步。
結語
今天的基準評估只能抓出明顯的低品質特徵,完全沒有觸及「這份報告是不是真的切題、論述是不是合理」這種需要語意理解的問題。要回答這些問題,我們需要一組有代表性的題目,以及清楚的評分標準,而不是臨時想一個問題就測一次。
明天,我們會進入「AG Day 33 建立評估集:黃金問題與評分標準」,把研究助理常見的使用情境整理成一組固定的黃金問題(golden questions),並為每一題設計明確的評分標準(rubric),讓評估從「臨時測一下」變成「有固定基準、可以重複執行、可以比較版本前後差異」的正式流程,也讓今天寫的 EvalCase 骨架真正派上用場。
延伸資源
- OpenAI 官方文件的 Evals 相關指南:
https://platform.openai.com/docs/。討論如何為大型語言模型應用設計評估案例,適合對照今天拆出的評估維度。 - Anthropic 官方文件:
https://docs.anthropic.com/。關於建構可靠代理系統與評估方法的討論,可與今天的維度拆解互相參照。 - Python 官方文件的
dataclasses模組:EvalCase、EvalResult的欄位預設值與field(default_factory=...)用法以此為準。
留言
張貼留言