Applied AI School
v0 · 規劃中
Anthropic

一個典型的 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 五步驟

步驟做什麼產出
1Draft 一個 prompt(簡單就好)prompt_v1
2生 eval datasetdataset.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 promptv2 prompt(針對 reasoning 改)
寫法「產生一週飲食計畫」加上「嚴格遵守 constraints、每餐標份量(克)、必須 7 天」
平均分2.36.8
為什麼有效weaknesses 點出的三件事直接補上

進步 4.5 分不是來自靈感,而是看 grader 的 reasoning 改出來的。

接下來

下一篇 grading-strategies 處理整個 workflow 最關鍵也最容易做爛的環節:grader 怎麼設計。Code-based vs model-based 各自適合什麼?grader 自己會不會有 bias?怎麼避免 grader 跟 generator 互相背書?