一個典型的 eval workflow
從 dataset 到 grader 到分數報告——五步走完一輪 eval,用一個 meal plan prompt 做示範。
TL;DR
- 流程 = draft prompt → 生 dataset → 跑 prompt → grade → 看分數 + 報告
- Test data 可以叫 Claude(用 Haiku 省錢)自動生——你給任務描述 + 輸出 spec
- 報告比分數更重要——分數告訴你高低,報告告訴你哪裡爛
一個情境:meal plan prompt 的第一版只拿 2.3 分
你要做一個「給用戶身高體重和目標,回一份一週飲食計畫」的功能。第一版 prompt:
prompt = f"""
請根據以下資訊產生一週的飲食計畫:
{user_info}
"""
跑 eval,平均 2.3 / 10(以下分數都是示意,不是實測)。
第一反應通常是「太爛了,prompt 寫得不夠細」。但「太爛」這件事正是 baseline 的價值:你現在有一個明確的起點,下次改完跑出來如果是 5.1,你就知道改的方向是對的。
如果 v1 直接寫到 8 分,後面要證明「你又把它改到更好」就難了。第一版刻意爛是合理策略。
Workflow 五步驟
| 步驟 | 做什麼 | 產出 |
|---|---|---|
| 1 | Draft 一個 prompt(簡單就好) | prompt_v1 |
| 2 | 生 eval dataset | dataset.json(10–100 個 case) |
| 3 | 把每個 case 餵進 Claude | 每個 case 一個 output |
| 4 | 把 (input, output) 給 grader 打分 | 每個 case 一個 score + reasoning |
| 5 | 平均分 + 看哪些 case 爛 | 知道下一步要改什麼 |
改完 prompt 跑 step 3–5,比較分數。就這樣。
Step 1:先寫一版(爛沒關係)
Draft 的目的不是寫對,是有個東西可以打分。一行 prompt 也行:
def build_prompt(user_info):
return f"請根據以下資訊產生一週的飲食計畫:\n{user_info}"
Step 2:叫 Claude 自動生 dataset
手寫 dataset 慢又容易偏。直接讓 Haiku 生(生資料這種任務不需要 Sonnet/Opus):
GEN_PROMPT = """
產生一個 prompt eval 用的 dataset。每筆是一個會去呼叫「飲食計畫產生器」的真實 user input。
回傳 JSON array,每個物件有:
- "user_info": 一段描述身高、體重、年齡、目標的文字
- "constraints": 飲食限制(如 vegan、無麩質、堅果過敏)
涵蓋多樣的目標(增肌、減脂、維持)和限制。產生 10 筆。
"""
messages = [{"role": "user", "content": GEN_PROMPT}]
# 用 prefill + stop_sequences 確保拿到乾淨 JSON
messages.append({"role": "assistant", "content": "```json"})
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=2048,
messages=messages,
stop_sequences=["```"],
)
dataset = json.loads(response.content[0].text.strip()) # strip 防 model 多吐換行
with open("dataset.json", "w") as f:
json.dump(dataset, f, indent=2, ensure_ascii=False)
兩個技巧:
- Prefill
\``json`:強制 Claude 從 JSON 開始,不要寫前言 stop_sequences=["\``"]`:碰到收尾的 backtick 就停,不要寫後話
存成檔案是因為你會反覆 load——每次跑 eval 用同一份 dataset 才能比較。
Step 3 + 4:跑 prompt + grade
把 dataset 塞進 prompt,每個 case 拿到 output,再交給 grader:
def run_test_case(test_case):
prompt = build_prompt(test_case["user_info"])
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}],
)
output = response.content[0].text
# grader 在下一篇實作;本篇先當 stub
grade = grade_by_model(test_case, output) # → {"score": int, "reasoning": str}
return {
"input": test_case,
"output": output,
"score": grade["score"],
"reasoning": grade["reasoning"],
}
def run_eval(dataset):
results = [run_test_case(c) for c in dataset]
avg = sum(r["score"] for r in results) / len(results)
print(f"Average: {avg:.2f}")
return results, avg
第一次跑可能要 30 秒到幾分鐘(看 dataset 大小)。後面想加速可以平行化(asyncio / Batch API),但先讓它能跑比讓它跑得快重要。
Step 5:報告比分數重要
一個只看 Average: 2.3 的 eval 是廢的——你不知道為什麼是 2.3。Grader 要連 reasoning 一起回:
{
"score": 2,
"strengths": ["有列出三餐"],
"weaknesses": [
"沒考慮 vegan 限制(菜單裡有雞蛋)",
"沒給份量(克數或熱量)",
"只有 3 天不是 7 天"
],
"reasoning": "用戶是 vegan、要減脂,但輸出含動物性食材且天數不足。"
}
看 5–10 個低分 case 的 weaknesses,你會發現問題集中在某幾類,比如「忽略限制」「沒給份量」。下一版 prompt 就針對這幾點改,不是隨便改。
分數是 KPI,reasoning 是 root cause。沒 reasoning 的 eval 等於只看儀表板紅燈不看 log。
一個 v1 → v2 的對照
| v1 prompt | v2 prompt(針對 reasoning 改) | |
|---|---|---|
| 寫法 | 「產生一週飲食計畫」 | 加上「嚴格遵守 constraints、每餐標份量(克)、必須 7 天」 |
| 平均分 | 2.3 | 6.8 |
| 為什麼有效 | — | weaknesses 點出的三件事直接補上 |
進步 4.5 分不是來自靈感,而是看 grader 的 reasoning 改出來的。
接下來
下一篇 grading-strategies 處理整個 workflow 最關鍵也最容易做爛的環節:grader 怎麼設計。Code-based vs model-based 各自適合什麼?grader 自己會不會有 bias?怎麼避免 grader 跟 generator 互相背書?

