Code-based vs model-based grading
兩種打分方式的取捨——能用 regex / parser 驗的就用 code,開放性產出才用 LLM。
TL;DR
- Code grading:能用 regex / JSON parse / 算數驗證的就用——快、便宜、確定
- Model grading:開放性產出(meal plan、摘要、回信)只能用 LLM 評
- 混搭:一個 case 同時跑 code(必要條件)+ model(品質);grader 永遠用比 generator 強的模型
一個情境:「這個輸出 8 分」是什麼意思?
你的 grader 跟你說「這個 meal plan 8 分」。你問為什麼,它說「整體還不錯」。
這個分數沒有用。因為你不知道下次怎麼讓它變 9 分。
好的 grader 做兩件事:
- 把「好不好」拆成可單獨檢查的條件
- 對每個條件給可驗證的判斷(pass/fail 或 1–10)
「整體還不錯」不是 grader,是讀者感想。Grader 必須能精準回答「這次哪一條沒做到」。
三種 grader
| 類型 | 怎麼打分 | 適合什麼 | 缺點 |
|---|---|---|---|
| Code | 寫程式驗(regex、parse、算數、長度) | 格式、syntax、必要條件 | 只能驗「規則化」的東西 |
| Model | 叫另一個 LLM 評 | 品質、相關性、是否照做 | 慢、貴、會 bias |
| Human | 真人讀 | 最終品質基準 | 太慢,只用於採樣 |
實務上是 code + model 混搭:能用 code 驗的條件先過 code(確定性高、便宜),剩下開放性的判斷交給 model。Human 留給離線抽樣校正用。
Code grader:能驗就驗
任何能寫成函式的判斷都是 code grader。常見:
import ast, json, re
def validate_python(text: str) -> int:
try:
ast.parse(text.strip())
return 10
except SyntaxError:
return 0
def validate_json(text: str) -> int:
try:
json.loads(text.strip())
return 10
except json.JSONDecodeError:
return 0
def validate_regex(text: str) -> int:
try:
re.compile(text.strip())
return 10
except re.error:
return 0
其他常見的 code check:
- 長度:output 不能超過 280 字(推文 generator)
- 必含關鍵字:summary 必須出現原文的某個專有名詞
- 不能含某些字:客服回信不能罵人、不能承諾退款
- 數值範圍:價格估算必須是正數 < 10000
Code grader 的好處:0 token 成本、ms 級延遲、結果 deterministic。
Model grader:給開放性產出打分
「這份 meal plan 有沒有照 vegan 限制」「這封回信語氣是不是夠專業」這種 code 寫不出來,要叫 LLM 評。
最關鍵的設計:不要只問「打幾分」。直接問「打幾分」,model 會永遠給 6 分(中間值偏好)。要連 reasoning 一起要:
GRADER_PROMPT = """
你是嚴格的飲食教練。評估以下 AI 產生的 meal plan:
User 輸入:{user_info}
限制:{constraints}
AI 輸出:{output}
評估標準:
1. 是否嚴格遵守限制(vegan / 過敏 / 熱量目標)
2. 是否提供完整 7 天 × 3 餐
3. 每餐是否標明份量
回 JSON:
{{
"strengths": ["最多 3 點"],
"weaknesses": ["最多 3 點"],
"reasoning": "為什麼給這個分數",
"score": 1-10 整數
}}
"""
實作(沿用 prefill + stop_sequence 拿乾淨 JSON):
def grade_by_model(test_case, output):
prompt = GRADER_PROMPT.format(
user_info=test_case["user_info"],
constraints=test_case["constraints"],
output=output,
)
messages = [
{"role": "user", "content": prompt},
{"role": "assistant", "content": "```json"},
]
response = client.messages.create(
model="claude-opus-4-7", # grader 用比 generator 強的
max_tokens=1024,
messages=messages,
stop_sequences=["```"],
)
return json.loads(response.content[0].text)
Rubric 的寫法決定 grader 品質。模糊的 rubric(「品質好不好」)會拿到模糊的分數;具體可驗的條目(「7 天 × 3 餐」「每餐標份量」)才會拿到能行動的反饋。
混搭:一個 case 跑兩種 grader
Code 驗硬規則(必要條件),model 驗品質(充分條件):
def grade(test_case, output):
# Code 端:硬規則
syntax_score = validate_format(output, test_case["format"])
# Model 端:開放性品質
model_grade = grade_by_model(test_case, output)
model_score = model_grade["score"]
# 簡單平均,或 syntax 不過直接 0
if syntax_score == 0:
return {"score": 0, "reasoning": "Format invalid", **model_grade}
return {
"score": (syntax_score + model_score) / 2,
**model_grade,
}
一個寫法的取捨:
| 合併方式 | 行為 | 用在哪 |
|---|---|---|
| 平均 | 兩邊都算 | 大多數情境 |
| 加權平均 | code 70% / model 30% | 格式比品質重要時 |
| Gating(code 0 → 整體 0) | format 不過直接 fail | output 必須是合法 JSON / Python 等 |
Code vs model 的決策表
| 你要驗的東西 | 用什麼 grader |
|---|---|
| 是不是合法 JSON / Python / regex | Code |
| 長度有沒有超過 | Code |
| 有沒有出現禁字 / 必含字 | Code |
| 數值是不是在合理範圍 | Code |
| 回信語氣專不專業 | Model |
| 摘要有沒有抓到重點 | Model |
| 有沒有照 user 的 constraints | Model(除非可規則化) |
| 翻譯是否流暢 | Model(或人工抽樣) |
判斷原則:寫得出 if/else 的就用 code,寫不出來的才上 model。
接下來
到這裡你有完整的 eval pipeline——dataset、prompt runner、grader。但「改 prompt」這件事本身還沒講方法論:要怎麼改?一次改幾個地方?怎麼確認改的有效?
下一節進入 Prompt Engineering:把 eval 當實驗台,每次只改一個變數,跑分、比較、保留贏家。沒 eval 沒 PE——這兩件事是同一回事。

