跳到主要內容

AG Day 32 評估基礎:Agent 為什麼難測

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=...) 用法以此為準。

留言

這個網誌中的熱門文章

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 中,資料型別決定我們可以對變數進行哪些操作...

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 等工具能處理和分析龐...

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 建構深度學習模型。 開發者與研究人員 :想更深入了...