AG Day 33 建立評估集:黃金問題與評分標準
執行需求:CPU 可跑。AG Day 32(原文連結)把「評估代理系統為什麼困難」的道理講清楚了,也在 evals.py 裡搭好了 EvalCase、EvalResult 與幾個規則式檢查函式。但昨天的示範案例是臨時寫的一筆資料,跑完就丟,沒有真正累積下來。今天要把這件事做得正式而且能長期沿用:建立一組固定的「黃金問題」(golden questions)評估集,涵蓋研究助理常見的各種使用情境,並為每一題寫下明確的評分標準(rubric)。有了這組固定的評估集,之後每次改動 research-agent 的邏輯,都可以重新跑一次同一批題目,客觀比較「這次改動是讓它變好還是變差」,不再只靠肉眼隨機抽看幾個例子。今天全部範例都用離線模擬資料,CPU 就能順利跑完,不需要任何 API 金鑰或外部服務。
引言
「黃金問題」這個詞借用自機器學習評估裡的「黃金標準(gold standard)」概念:找出一組有代表性、答案品質可以被清楚判斷的題目,固定下來反覆使用,讓評估結果具有可比較性。對 research-agent 來說,黃金問題應該覆蓋幾種典型情境:主題單一明確、容易找到資料的簡單問題;需要綜合多個來源、有一定難度的問題;資訊本身就有爭議或矛盾、考驗代理如何處理衝突說法的問題;還有故意刁鑽、資料稀少或主題冷門的邊界情況。只測簡單問題會讓評估結果過於樂觀,只測困難問題又會讓通過率長期偏低而失去鑑別力,好的評估集需要在難度分佈上有意識地設計。
評分標準(rubric)則是把「這題答得好不好」從模糊的主觀判斷,轉換成一組具體、可檢查的條件清單。以「這篇報告解釋清楚了 RAG 的基本原理嗎」為例,一個模糊的評語沒辦法重複驗證,但拆成「有沒有提到向量化」「有沒有提到檢索步驟」「有沒有至少一個引用來源」這種條件清單,就變成任何人(或任何模型)都能一致執行的檢查流程。今天我們會把黃金問題與對應的評分標準存成結構化資料,寫一個載入與批次執行的流程,替接下來 AG Day 34 的 LLM-as-judge 打好資料基礎。
原理/觀念
黃金問題的難度分層設計
今天採用三層難度:easy(單一明確主題,一次搜尋通常就夠)、medium(需要整合兩三個來源才能完整回答)、hard(資訊零散、可能有矛盾說法,或主題本身比較冷門)。每一題除了問題本身,還要標明難度、要求代理達成的必要條件(例如至少幾個引用、必須提到哪些關鍵概念),這些必要條件會被轉成 AG Day 32 定義的 EvalCase 欄位。難度分層的價值在於:當你看到「簡單題通過率 95%、困難題通過率 40%」這種分佈,比只看一個籠統的整體通過率更能定位問題出在哪個能力層次。
評分標準要拆成「客觀可檢查」與「需要判斷」兩類
並不是所有評分條件都適合寫成規則式檢查。「有沒有引用來源」「引用數量是否達標」這類是客觀可檢查的,昨天的 check_citation_validity 已經能處理;但「這個論述的邏輯是否合理」「有沒有正確理解問題的語意」這類需要理解語言含義的條件,規則式檢查做不到,必須交給模型或人工判斷。今天設計評分標準時,會刻意把每一條都標記成這兩類之一,讓 evals.py 知道哪些條件可以自動判定、哪些要留給 AG Day 34 的 LLM-as-judge 處理,而不是含糊地混在一起。
評估集要能版本化與重複執行
評估集本身也是會演進的資產:發現代理系統某個弱點時,應該把對應的題目加進評估集,而不是每次都臨時想一題測一測就忘了。今天把黃金問題存成 data/evals/golden_questions.jsonl,用 JSON Lines 格式(每行一個 JSON 物件),方便之後用版本控制追蹤題目的增減,也方便用簡單的逐行讀取就能載入,不需要額外的資料庫或解析工具。
完整實作
先建立存放評估集的目錄與檔案:
mkdir -p research-agent/data/evals
touch research-agent/data/evals/golden_questions.jsonl
第一步:寫入五筆黃金問題到 golden_questions.jsonl,涵蓋三種難度。每一行是一個完整的 JSON 物件,欄位對應 AG Day 32 定義的 EvalCase:
# research-agent/scripts/seed_golden_questions.py
import json
from pathlib import Path
GOLDEN_QUESTIONS = [
{"case_id": "gq-001", "question": "什麼是檢索增強生成(RAG)?", "difficulty": "easy",
"required_keywords": ["檢索", "向量", "生成"], "min_citations": 1},
{"case_id": "gq-002", "question": "LangGraph 的 checkpointer 解決了什麼問題?", "difficulty": "easy",
"required_keywords": ["檢查點", "狀態"], "min_citations": 1},
{"case_id": "gq-003", "question": "多代理系統的 supervisor 模式與去中心化交接有什麼取捨?", "difficulty": "medium",
"required_keywords": ["supervisor", "worker", "交接"], "min_citations": 2},
{"case_id": "gq-004", "question": "Model Context Protocol 跟傳統 function calling 的差異是什麼?", "difficulty": "medium",
"required_keywords": ["MCP", "工具"], "min_citations": 2},
{"case_id": "gq-005", "question": "在資料稀少、說法互相矛盾的冷門主題上,代理應該如何呈現不確定性?", "difficulty": "hard",
"required_keywords": ["不確定", "來源"], "min_citations": 1},
]
def main():
path = Path("data/evals/golden_questions.jsonl")
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
for item in GOLDEN_QUESTIONS:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
print(f"已寫入 {len(GOLDEN_QUESTIONS)} 筆黃金問題到 {path}")
if __name__ == "__main__":
main()
第二步:在 evals.py 裡新增 load_golden_questions,把 JSONL 檔案讀成 EvalCase 物件清單,難度欄位額外保留供後續分層統計使用:
# research-agent/src/research_agent/evals.py(新增:載入評估集)
import json
from pathlib import Path
def load_golden_questions(path: Path) -> list[EvalCase]:
"""從 JSON Lines 檔案載入黃金問題,轉成 EvalCase 物件清單。"""
cases = []
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
data = json.loads(line)
cases.append(EvalCase(
case_id=data["case_id"],
question=data["question"],
required_keywords=data.get("required_keywords", []),
min_citations=data.get("min_citations", 1),
))
return cases
第三步:定義評分標準的資料結構 RubricItem,明確標記每一條標準是「客觀可檢查」還是「需要判斷」,並為今天的五道題目各寫一份對應的評分標準清單:
# research-agent/src/research_agent/evals.py(新增:評分標準結構)
from dataclasses import dataclass
@dataclass
class RubricItem:
"""一條評分標準:描述內容,以及是否能用規則式程式自動判定。"""
description: str
auto_checkable: bool
RUBRICS: dict[str, list[RubricItem]] = {
"gq-001": [
RubricItem("有解釋向量化與相似度檢索的基本概念", auto_checkable=False),
RubricItem("至少引用一個可查證的來源", auto_checkable=True),
],
"gq-003": [
RubricItem("有明確比較兩種拓樸的優缺點,不是只列名詞", auto_checkable=False),
RubricItem("至少引用兩個可查證的來源", auto_checkable=True),
],
}
第四步:寫一個批次執行腳本,把整組黃金問題跑一輪基準評估(沿用 AG Day 32 的 run_baseline_eval),並依難度分組統計通過率,這樣才能一眼看出「代理在哪個難度層級表現比較弱」:
# research-agent/scripts/run_golden_eval.py
import json
from pathlib import Path
from research_agent.evals import load_golden_questions, run_baseline_eval, summarize_results
GOLDEN_PATH = Path("data/evals/golden_questions.jsonl")
def load_difficulty_map(path: Path) -> dict[str, str]:
"""額外讀出每題的難度標籤,run_baseline_eval 本身不需要這個欄位。"""
mapping = {}
with path.open("r", encoding="utf-8") as f:
for line in f:
data = json.loads(line)
mapping[data["case_id"]] = data["difficulty"]
return mapping
def main():
cases = load_golden_questions(GOLDEN_PATH)
difficulty_map = load_difficulty_map(GOLDEN_PATH)
results_by_difficulty: dict[str, list] = {"easy": [], "medium": [], "hard": []}
for case in cases:
# 離線模擬模式:用固定的示範報告,實際串接代理後改用真實輸出
demo_report = f"# {case.question}(示範報告)\n" + "\n".join(
f"{kw} 相關內容[來源:示範資料]" for kw in case.required_keywords
)
result = run_baseline_eval(case, demo_report, ["示範資料"], step_count=3)
results_by_difficulty[difficulty_map[case.case_id]].append(result)
for level in ("easy", "medium", "hard"):
summary = summarize_results(results_by_difficulty[level])
print(f"難度 {level}:{summary['total']} 題,通過率 {summary['pass_rate']:.0%}")
if __name__ == "__main__":
main()
第五步:在 evals.py 新增 split_rubric,把某一題的評分標準拆成「可自動判定」與「需要模型判斷」兩個子清單,供批次流程知道還有哪些條件沒有真正被檢查過:
# research-agent/src/research_agent/evals.py(新增:拆分評分標準)
def split_rubric(case_id: str) -> tuple[list[RubricItem], list[RubricItem]]:
"""把某一題的評分標準拆成可自動判定與需要模型判斷兩組。"""
items = RUBRICS.get(case_id, [])
auto_items = [item for item in items if item.auto_checkable]
manual_items = [item for item in items if not item.auto_checkable]
return auto_items, manual_items
第六步:在批次腳本裡加上一段輸出,明確列出每題還有哪些評分標準尚未被自動判定,讓「今天做到哪、明天要接手哪」一清二楚,不會誤以為基準評估的通過就等於整題完全合格:
# research-agent/scripts/run_golden_eval.py(追加:列出待模型判斷的標準)
from research_agent.evals import split_rubric
def report_pending_manual_checks(cases) -> None:
"""列出每題還有哪些評分標準需要交給 LLM-as-judge 處理。"""
for case in cases:
_, manual_items = split_rubric(case.case_id)
if not manual_items:
continue
print(f"{case.case_id} 尚待模型判斷:")
for item in manual_items:
print(f" - {item.description}")
執行整個流程:
cd research-agent
uv run python scripts/seed_golden_questions.py
uv run python scripts/run_golden_eval.py
示範輸出:
已寫入 5 筆黃金問題到 data/evals/golden_questions.jsonl
難度 easy:2 題,通過率 100%
難度 medium:2 題,通過率 100%
難度 hard:1 題,通過率 100%
因為今天的示範報告是照著 required_keywords 硬湊出來的,通過率自然都是 100%;等到 AG Day 34 把真正的模型輸出接進來,通過率才會反映代理系統真正的表現,才有鑑別力可言。
常見錯誤與踩雷
第一個常見錯誤是黃金問題的難度分佈嚴重失衡,例如全部都是簡單題。這樣一來,不管代理系統實際表現如何,通過率都會長期維持在高位,評估集就失去了「能區分好壞」的鑑別力。今天特別把五題分成 2 easy、2 medium、1 hard,雖然題數還不多,但已經體現了分層的意識,之後擴充題數時也應該維持類似的比例,而不是隨手加題目。
第二個常見錯誤是評分標準寫得太模糊,導致同一份報告在不同人(或不同模型)眼裡有不同的判定結果。「回答得夠不夠好」這種標準沒辦法重複驗證,必須拆成更具體的描述,例如「有沒有解釋 A 概念」「有沒有比較 B 與 C 的差異」,這也是為什麼今天把 RubricItem 的 description 寫得盡量具體,而不是寫一句籠統的形容詞。
第三個常見錯誤是把 required_keywords 當成完整的評分標準,忽略了真正重要的語意判斷條件(放在 RUBRICS 裡、標記為 auto_checkable=False 的那些)。今天的 run_golden_eval.py 示範只跑了規則式基準檢查,還沒有真正執行 RUBRICS 裡需要語意判斷的條件,這是刻意留給明天處理的部分,避免今天的骨架被過早的模型呼叫細節干擾。
第四個常見錯誤是評估集寫死之後就再也不更新。黃金問題應該隨著系統演進持續補充:每當在正式使用中發現代理系統答錯了某個問題,就該把這個問題(連同正確或可接受的答案特徵)加進評估集,讓「曾經犯過的錯」變成「永久回歸測試的一部分」,不會在下一次修改時悄悄重蹈覆轍。這個習慣跟一般軟體工程裡「每修一個 bug 就補一個回歸測試」的精神完全一致,只是這裡的「測試」換成了黃金問題與評分標準。
效能與實務提醒
今天的評估集只有五題,執行成本幾乎為零;但當評估集擴充到幾十題、甚至上百題,而且每題都要真正呼叫模型跑一次代理流程時,一次完整的評估執行可能要花上幾分鐘甚至更久,成本也會累積。實務上建議把評估集拆成「每次修改都跑的小型子集」(例如十題以內,涵蓋各難度的代表題)與「重大版本才跑的完整集」,平衡驗證頻率與成本。
難度分層統計也適合搭配 AG Day 9 的觀測紀錄一起看:如果困難題的通過率低,同時步數與 token 用量也明顯偏高,代表代理在困難題上是「知道自己不確定、多繞了幾圈想辦法補救」;但如果困難題通過率低、步數卻跟簡單題差不多,就代表代理可能沒有意識到問題困難,直接給出了過度自信的錯誤答案,這兩種情況需要的改進方向完全不同。
最後,JSON Lines 格式的好處是可以用版本控制系統清楚看出每次評估集的變動(新增了哪一行、修改了哪一行),這在多人協作維護評估集時特別重要,比起把所有題目塞進一個大 JSON 陣列、任何修改都會讓整份差異變得難以閱讀,逐行的格式對版本控制更友善。同樣的道理也適用於 RUBRICS 這個字典:目前用 Python 原始碼直接定義,優點是能享有型別檢查與自動完成,缺點是非工程背景的人不容易貢獻新的評分標準;如果團隊裡有內容專家需要參與設計評分標準,日後可以考慮把 RUBRICS 也改成外部檔案,跟評估集題目統一存放。
小結
今天我們把評估工作從「臨時想一題測一次」升級成「固定的黃金問題評估集」:五道題目分成三個難度層級,存成 JSON Lines 格式方便日後用版本控制追蹤變動;每題搭配對應的評分標準,並明確區分「規則式可自動判定」與「需要語意判斷」兩類條件;批次執行腳本可以依難度分組統計通過率,看出代理系統在不同能力層次上的表現差異。
新增的術語:黃金問題(golden questions,固定不變、有代表性的評估題目集合)、評分標準(rubric,把品質判斷拆成具體條件清單)、難度分層(difficulty tiering,依任務難度分組統計評估結果)。這幾個概念合起來,讓評估工作從「臨時起意的抽樣檢查」變成一套可以長期累積、可以追蹤趨勢的正式資產。
結語
今天的評分標準裡,凡是標記 auto_checkable=False 的條件,目前完全沒有被真正執行——我們只是先把它們定義下來、列出清單,還沒有辦法回答「這份報告有沒有正確解釋 RAG 的基本概念」這種需要語意理解才能判斷的問題。要回答這類問題,光靠規則式程式是不夠的,必須引入模型本身的判斷能力。
明天,我們會進入「AG Day 34 LLM-as-judge:用模型評模型」,把今天標記為需要判斷的評分標準,交給 RESEARCH_AGENT_MODEL 用結構化輸出的方式評分,並討論這個做法本身的偏誤與侵蝕評估公正性的風險——用模型評模型聽起來像是用左手評右手,需要謹慎設計評分提示詞與抽樣策略,才能真正做到可靠、可信。
延伸資源
- Anthropic 官方文件的評估相關指南:
https://docs.anthropic.com/。討論如何為語言模型應用設計具代表性的評估題目與評分標準。 - OpenAI 官方文件的 Evals 框架:
https://platform.openai.com/docs/。說明評估集的資料格式與批次執行的常見做法,可與今天的 JSON Lines 設計互相對照。 - Python 官方文件的
json模組:json.dumps(..., ensure_ascii=False)確保中文內容不被轉成 Unicode 逃脫序列,方便直接閱讀存下來的檔案。
留言
張貼留言