AG Day 34 LLM-as-judge:用模型評模型
執行需求:CPU+API key。AG Day 33(原文連結)建立了黃金問題評估集,也把每題的評分標準拆成「規則式可自動判定」與「需要語意判斷」兩類,但後面那一類至今完全沒有被真正執行過。今天要把它真正接上:用 RESEARCH_AGENT_MODEL 指定的模型,讀取報告全文與對應的評分標準,回傳結構化的判斷結果,這種做法業界通稱 LLM-as-judge(用大型語言模型當評審)。這篇需要真正呼叫模型 API 才能看到完整效果,如果手邊沒有 OPENAI_API_KEY 或 ANTHROPIC_API_KEY,程式一樣可以透過 --dry-run 用固定的模擬評分跑完整條流程,只是評分結果是示範用的假資料,完全不代表任何真實的語意判斷結果。
引言
「用模型評模型」聽起來有點像自己出題、自己改考卷,直覺上會讓人擔心公正性。這個擔心是對的,也正是今天要花不少篇幅討論偏誤的原因。但反過來想,語意層面的品質判斷(這份報告有沒有正確理解問題、論述是否合理、有沒有掉入常見的誤解)本來就沒有更便宜、更快速的替代方案——找真人專家一題一題審閱固然理想,但成本高、速度慢,不可能每次程式碼一改動就跑一輪人工評審。LLM-as-judge 的定位是:在「完全沒有語意判斷」與「昂貴的人工全面審查」之間,提供一個雖不完美但可重複、可規模化、成本相對低的中間選項。
今天的實作分成三個部分:先設計評分提示詞(judge prompt),把 AG Day 33 的評分標準轉換成模型能理解、能給出結構化判斷的指令;接著實作評分函式本身,包含離線模擬模式的降級路徑;最後把評分結果整合進 AG Day 32 的 EvalResult,讓規則式檢查與模型判斷的結果可以並排比較、互相參照。過程中我們會不斷提醒自己 LLM-as-judge 不是萬能的真理,而是一個需要持續校準、需要人工定期抽查驗證的評分工具,用錯了反而會給人一種虛假的安心感。
原理/觀念
LLM-as-judge 常見的三種偏誤
第一種是位置偏誤(position bias):如果讓模型比較兩份報告的優劣,模型常常偏好排在前面(或後面)的那一份,跟內容本身的品質無關,只跟呈現順序有關。今天我們評分的方式是「單一報告打分數」而不是「兩兩比較」,暫時避開這個問題,但如果未來要做版本比較(例如比較改動前後的兩份報告),就必須刻意把順序隨機化、或兩個方向都跑一次取平均,才能抵銷這個偏誤。
第二種是verbosity偏誤(也稱長度偏誤):模型常常誤把「寫得更長、更詳細」等同於「品質更好」,即使內容本質上沒有更正確或更有幫助。設計評分提示詞時,必須明確要求模型忽略長度、只針對評分標準裡列出的具體條件打分,並在提示詞裡舉例說明「簡短但正確」應該打高分、「冗長但答非所問」應該打低分。
第三種是自我偏好(self-preference bias):如果評分模型跟產生報告的模型是同一家廠商甚至同一個模型,有研究顯示模型會傾向給自己風格相近的輸出更高的分數。今天的做法是讓 RESEARCH_AGENT_MODEL 同時扮演產生報告與評分兩種角色,這在教學情境下可以接受,但正式專案裡建議用不同廠商或不同版本的模型來擔任評審,降低自我偏好造成的評分虛高。
評分要基於明確標準,不要問模糊的「好不好」
今天的評分提示詞不會問模型「這份報告寫得好嗎,給幾分」,而是把 AG Day 33 RUBRICS 裡標記為 auto_checkable=False 的每一條標準,逐條列給模型,要求它針對每一條分別回答「符合」或「不符合」,並附上簡短理由。這種逐條判斷的方式比要求一個綜合分數更可靠,因為每一條的判斷範圍更窄、更具體,模型出錯的機會也相對較小,而且逐條記錄下來的理由本身也是有用的除錯資訊,能讓人快速看出模型判斷的依據是否合理。
完整實作
第一步:在 evals.py 定義 judge 用的結構化輸出模型 RubricJudgement 與 JudgeResult,沿用 AG Day 8 學過的 Pydantic 結構化輸出習慣:
# research-agent/src/research_agent/evals.py(新增:judge 結構化輸出)
from pydantic import BaseModel, Field
class RubricJudgement(BaseModel):
"""模型對單一評分標準的判斷。"""
description: str
satisfied: bool = Field(description="這份報告是否符合這條評分標準")
reasoning: str = Field(description="簡短說明為什麼符合或不符合,一到兩句話")
class JudgeResult(BaseModel):
"""一次 LLM-as-judge 評分的完整結果,包含逐條判斷。"""
case_id: str
judgements: list[RubricJudgement]
satisfied_count: int
total_count: int
第二步:設計評分提示詞。這裡把系統提示詞跟評分標準清單組合起來,明確要求模型逐條判斷、忽略長度、只根據標準本身給結論:
# research-agent/src/research_agent/evals.py(新增:評分提示詞)
JUDGE_SYSTEM_PROMPT = """你是一個嚴格但公正的研究報告評審。
你會收到一份研究報告與一組評分標準,請針對每一條標準逐一判斷「符合」或「不符合」。
評分原則:
1. 只根據列出的標準判斷,不要因為報告寫得長或用詞華麗就給比較寬鬆的判斷。
2. 每一條都要附上一到兩句話的簡短理由,理由要能對應報告裡的具體內容。
3. 若報告完全沒有處理某條標準對應的內容,一律判定為不符合。
"""
def build_judge_prompt(report: str, rubric_items: list[RubricItem]) -> str:
"""把報告與評分標準組成要交給模型的使用者提示詞。"""
standards = "\n".join(f"{i+1}. {item.description}" for i, item in enumerate(rubric_items))
return f"評分標準:\n{standards}\n\n報告內容:\n{report}"
第三步:實作 judge_report,離線模擬模式下回傳固定的示範判斷(一律標示為模擬結果、不代表真實判斷),正式模式則呼叫既有的 llm.chat 搭配結構化輸出:
# research-agent/src/research_agent/evals.py(新增:judge_report 主函式)
from research_agent.config import load_settings
def judge_report(case_id: str, report: str, rubric_items: list[RubricItem]) -> JudgeResult:
"""對一份報告逐條套用評分標準,回傳結構化的判斷結果。"""
if not rubric_items:
return JudgeResult(case_id=case_id, judgements=[], satisfied_count=0, total_count=0)
settings = load_settings()
if settings.is_dry_run:
judgements = [
RubricJudgement(description=item.description, satisfied=True,
reasoning="離線模擬模式:固定回傳符合,非真實模型判斷。")
for item in rubric_items
]
else:
from research_agent.llm import chat # AG Day 3-8 建立的對話與結構化輸出介面
prompt = build_judge_prompt(report, rubric_items)
class JudgementList(BaseModel):
items: list[RubricJudgement]
response = chat(
messages=[
{"role": "system", "content": JUDGE_SYSTEM_PROMPT},
{"role": "user", "content": prompt},
],
response_model=JudgementList,
)
judgements = response.items
satisfied = sum(1 for j in judgements if j.satisfied)
return JudgeResult(case_id=case_id, judgements=judgements, satisfied_count=satisfied, total_count=len(judgements))
第四步:呼應「常見錯誤與踩雷」會提到的呼叫失敗問題,先把 judge_report 包一層跟 AG Day 7 一致的重試邏輯,避免單次模型呼叫逾時或格式不符就讓整個評估中斷:
# research-agent/src/research_agent/evals.py(新增:帶重試的 judge 呼叫)
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def judge_report_with_retry(case_id: str, report: str, rubric_items: list[RubricItem]) -> JudgeResult:
"""對 judge_report 包一層指數退避重試,逾時或暫時性錯誤時自動重試三次。"""
return judge_report(case_id, report, rubric_items)
第五步:寫一個批次評分函式 run_full_evaluation,把整組黃金問題的規則式基準評估與 LLM-as-judge 評分一次跑完,彙整成一份可以直接列印或存檔的總表:
# research-agent/src/research_agent/evals.py(新增:整合規則與模型評分)
def run_full_evaluation(cases: list[EvalCase], reports: dict[str, str], known_sources: list[str]) -> list[dict]:
"""對每一筆評估案例同時跑規則式基準評估與 LLM-as-judge,回傳整合後的總表。"""
rows = []
for case in cases:
report = reports.get(case.case_id, "")
baseline = run_baseline_eval(case, report, known_sources, step_count=3)
_, manual_items = split_rubric(case.case_id)
judge_result = judge_report_with_retry(case.case_id, report, manual_items)
rows.append({
"case_id": case.case_id,
"baseline_passed": baseline.passed_baseline,
"judge_satisfied": judge_result.satisfied_count,
"judge_total": judge_result.total_count,
})
return rows
第六步:把 LLM-as-judge 的結果跟 AG Day 32 的規則式基準評估並排呈現,寫一個整合示範腳本:
# research-agent/scripts/run_judge_demo.py
from research_agent.evals import EvalCase, run_baseline_eval, judge_report, split_rubric
def main():
case = EvalCase(case_id="gq-001", question="什麼是檢索增強生成(RAG)?",
required_keywords=["檢索", "向量", "生成"], min_citations=1)
demo_report = (
"# 什麼是檢索增強生成(RAG)?(示範報告)\n"
"RAG 先把文件向量化存進向量資料庫,再用檢索找出相關片段,"
"最後把片段交給模型生成答案。[來源:示範資料]"
)
baseline = run_baseline_eval(case, demo_report, ["示範資料"], step_count=3)
print(f"規則式基準評估:{'通過' if baseline.passed_baseline else '未通過'}")
_, manual_items = split_rubric(case.case_id)
judge_result = judge_report(case.case_id, demo_report, manual_items)
print(f"LLM-as-judge:{judge_result.satisfied_count}/{judge_result.total_count} 條標準符合")
for j in judge_result.judgements:
print(f" [{'符合' if j.satisfied else '不符合'}] {j.description}:{j.reasoning}")
if __name__ == "__main__":
main()
執行示範(離線模擬模式):
cd research-agent
uv run python scripts/run_judge_demo.py
示範輸出:
規則式基準評估:通過
LLM-as-judge:1/1 條標準符合
[符合] 有解釋向量化與相似度檢索的基本概念:離線模擬模式:固定回傳符合,非真實模型判斷。
若設定了 OPENAI_API_KEY 或 ANTHROPIC_API_KEY,同一支腳本會改成真正呼叫模型評分,理由欄位會變成模型針對報告內容給出的具體說明,而不是這句固定的模擬提示。
常見錯誤與踩雷
第一個常見錯誤是把 LLM-as-judge 的結果當成絕對真理,不再做任何人工抽查。模型評審跟模型本身一樣會犯錯,尤其在評分標準寫得不夠具體、或報告內容處於模糊地帶時,判斷結果的可靠度會下降。實務上建議定期抽出一部分評分結果(例如每次評估集執行後抽 10%)讓人工複核,追蹤模型評審跟人工判斷的一致率,一致率明顯下降時就要重新檢視評分提示詞或評分標準本身。
第二個常見錯誤是評分提示詞裡混雜了太多條標準,導致模型在一次回應裡要同時判斷五六條甚至更多標準,判斷品質會隨著標準數量增加而下降。如果一題的評分標準特別多,建議拆成多次呼叫,每次只處理兩三條標準,雖然會增加 API 呼叫次數與成本,但能提升每一條判斷的可靠度,這是成本與品質之間需要視情況拿捏的取捨。今天的示範裡每題只有一到兩條需要模型判斷的標準,還沒有真正碰到這個問題,但評估集規模一大,這個取捨就會變得很現實。
第三個常見錯誤是忽略了模型呼叫本身可能失敗(逾時、格式不符預期),沒有像 AG Day 7 那樣做重試與例外處理,一旦評分過程中某次呼叫失敗,整個評估集的執行就會中斷。正式串接時 judge_report 應該包一層跟 AG Day 7 一致的重試邏輯,並在多次重試失敗後明確標記這一題「評分失敗」,而不是讓整個批次評估因為一題出錯而全部停擺。
效能與實務提醒
LLM-as-judge 每評一題都要付出一次真實的 API 呼叫成本,如果評估集有上百題、每題又有好幾條需要模型判斷的標準,一次完整評估的花費可能不小。實務上建議先用今天的離線模擬模式驗證整條流程的邏輯是否正確(提示詞組裝、結構化輸出解析、結果彙整),確認沒有程式錯誤之後,再花真正的 API 費用跑一次完整評估,避免因為程式邏輯的小錯誤而重複燒錢。
另外,評分模型的選擇也值得思考:不一定要用跟產生報告一樣強大(也一樣昂貴)的模型來當評審。很多情境下,一個較小、較便宜的模型只要評分提示詞設計得夠明確、標準夠具體,也能做出合理的判斷,這跟 AG Day 36 要討論的模型分級策略是同一個思路——不是所有工作都需要動用最貴的模型。
最後,LLM-as-judge 的評分結果本身也應該被記錄下來、可追蹤版本歷史,這樣才能回答「這次改了評分提示詞之後,同一批舊報告的評分有沒有系統性地變高或變低」這種校準問題,而不是每次評分都是一次性的、無法比較的孤立事件。這也是為什麼今天 run_full_evaluation 回傳的是一份結構化的清單而不是直接印出來就丟掉,方便之後接上 AG Day 35 的追蹤平台時,能直接把這份總表寫進去,不需要重新設計資料格式。
小結
今天我們把 AG Day 33 標記為「需要語意判斷」的評分標準真正接上模型:設計了逐條判斷、忽略長度、附理由的評分提示詞,實作了 judge_report 搭配離線模擬的降級路徑,也把結果整合進既有的評估流程,跟規則式基準評估並排呈現。同時我們也誠實面對了 LLM-as-judge 本身的三種常見偏誤:位置偏誤、長度偏誤、自我偏好偏誤,這些偏誤不會因為换了模型就自動消失,需要在提示詞設計與抽查流程裡持續留意。
新增的術語:LLM-as-judge(用大型語言模型評估其他輸出品質的評分方法)、位置偏誤與長度偏誤(模型評分時容易受呈現順序或篇幅長短影響、而非只看內容本質的系統性誤差)、自我偏好偏誤(模型傾向給風格相近的輸出更高評分的現象)。這三種偏誤都不是理論上的假設,而是實際應用 LLM-as-judge 時經常遇到的真實問題,設計評分流程時務必提前考慮。
結語
有了規則式基準評估與 LLM-as-judge 兩層評分機制,research-agent 現在已經有一套相對完整的品質評估流程。但這些評分結果目前都只是印在終端機上,跑完就消失,沒有任何地方能長期留存、視覺化呈現,也沒辦法輕易看出「這個月的平均通過率跟上個月比起來如何」這種趨勢問題。
明天,我們會進入「AG Day 35 追蹤平台:Langfuse 觀測實戰」,把 research-agent 的執行過程與今天的評估結果接上正式的可觀測性平台,讓每一次代理執行、每一次評分都留下可以回溯、可以視覺化查詢的完整紀錄,這需要一個外部服務帳號才能完整體驗全部功能。
延伸資源
- Anthropic 官方文件關於評估與 LLM 評審的討論:
https://docs.anthropic.com/。說明結構化評分提示詞設計與常見偏誤的緩解方式。 - OpenAI 官方文件的結構化輸出(Structured Outputs)指南:
https://platform.openai.com/docs/。response_model對應的底層機制請以官方文件為準。 - Pydantic 官方文件:
https://docs.pydantic.dev/。巢狀模型(如JudgementList內含RubricJudgement清單)的驗證行為可在此查閱完整規格。
留言
張貼留言