跳到主要內容

AG Day 33 建立評估集:黃金問題與評分標準

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 逃脫序列,方便直接閱讀存下來的檔案。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...