Evaluation — วัดผล prompt และ agent ด้วยตัวเลข ไม่ใช่ความรู้สึก

⏱ 30 นาทีลองในClaude ↗

⚠️ บทนี้มีข้อมูลที่เปลี่ยนบ่อย (ฟีเจอร์ ราคา ชื่อเมนู) ตรวจกับเว็บทางการก่อนนำไปใช้

📎 ไฟล์ตัวอย่างสำหรับบทนี้

เป้าหมาย

สร้างชุดทดสอบจากเคสจริง ให้คะแนนอัตโนมัติ รู้ความแม่นยำและต้นทุนต่องาน แล้วปรับ prompt อย่างมีหลักฐาน

ทำไมต้อง eval

"ลองสามข้อแล้วดูดี" ไม่พอ → แก้ prompt ให้เคสใหม่ผ่าน เคสเก่าอาจพังเงียบๆ (regression) eval ให้ตัวเลขที่เทียบกันได้ทุกครั้งที่แก้

6 ขั้นทำ eval

  1. เก็บเคสจริง 20–100 เคส จากตั๋ว/อีเมล/log จริง (ลบข้อมูลส่วนบุคคลก่อน) ใส่เคสยากและกำกวมด้วย
  2. เขียนคำตอบที่คาดหวัง ให้คนที่รู้งานเป็นคนกำหนด ไม่ใช่ให้ AI เดา
  3. เลือกวิธีให้คะแนน
    grader ใช้กับ ข้อดี / ข้อเสีย
    exact match หมวดหมู่ ตัวเลข ใช่/ไม่ใช่ แม่น ถูก / ใช้กับข้อความอิสระไม่ได้
    code check JSON ถูก schema, มีเลขออเดอร์, ยาวไม่เกิน N เร็ว / ตรวจได้แค่รูปแบบ
    LLM-as-judge + rubric คุณภาพคำตอบ น้ำเสียง ความครบ ยืดหยุ่น / ต้องเขียน rubric ชัด และสุ่มตรวจตัว judge
  4. แบ่ง dev / test — ปรับ prompt ดูแค่ dev, test เก็บไว้วัดตอนท้าย กันการ "จูนจนจำข้อสอบ"
  5. วัด baseline ก่อน แล้ว เปลี่ยนทีละอย่าง (prompt หรือโมเดล หรือ effort) จดผลทุกครั้ง
  6. ดูต้นทุนต่องาน คู่กับความแม่นยำ — แม่นขึ้น 2% แต่แพงขึ้น 3 เท่า อาจไม่คุ้ม

ลองเลย 🧪

ทดลอง A — สคริปต์ eval จัดหมวดตั๋วลูกค้า (pip install anthropic pydantic และตั้ง ANTHROPIC_API_KEY ใน env) บันทึกเป็น eval_tickets.py ไว้ในโฟลเดอร์ระดับนี้ (ข้าง samples/)

import csv
import sys
from typing import Literal

import anthropic
from pydantic import BaseModel

TASK_MODEL = "claude-sonnet-5-5"
JUDGE_MODEL = "claude-opus-5-5"
PRICE = {"claude-sonnet-5-5": (2, 10), "claude-opus-5-5": (4, 20)}  # USD ต่อ 1M token (input, output)

SYSTEM_PROMPT = """จัดหมวดข้อความลูกค้าร้านกาแฟออนไลน์ และร่างคำตอบสั้นภาษาไทย ไม่เกิน 3 บรรทัด
หมวด: billing (การเงิน ใบแจ้งหนี้ ส่วนลด), shipping (การจัดส่ง), refund (ขอคืนเงิน/คืนสินค้า),
technical (แอป เว็บ ล็อกอิน), other (อื่นๆ)"""

RUBRIC = """ให้คะแนนคำตอบถึงลูกค้า 1-5: สุภาพ ตรงประเด็น ไม่สัญญาสิ่งที่ร้านอาจทำไม่ได้ (เช่น คืนเงินทันที)
ไม่ขอข้อมูลบัตรเต็มเลข passed=true เมื่อ score >= 4"""


class Ticket(BaseModel):
    category: Literal["billing", "shipping", "refund", "technical", "other"]
    reply: str


class Grade(BaseModel):
    score: int
    passed: bool
    reason: str


client = anthropic.Anthropic()
total_cost = 0.0


def ask(model, system, text, schema):
    global total_cost
    r = client.messages.parse(model=model, max_tokens=16000, system=system,
                              messages=[{"role": "user", "content": text}], output_format=schema)
    p_in, p_out = PRICE[model]
    total_cost += (r.usage.input_tokens * p_in + r.usage.output_tokens * p_out) / 1_000_000
    return r.parsed_output


split = sys.argv[2] if len(sys.argv) > 2 else "dev"  # dev = T01-T14, test = T15-T20
rows = [r for r in csv.DictReader(open(sys.argv[1], encoding="utf-8"))
        if split == "all" or (int(r["id"][1:]) <= 14) == (split == "dev")]
correct = good_reply = 0
for row in rows:
    out = ask(TASK_MODEL, SYSTEM_PROMPT, row["input"], Ticket)
    if out is None:
        print(row["id"], "ไม่ได้ผลลัพธ์ (อาจถูกปฏิเสธ)")
        continue
    ok = out.category == row["expected_category"]
    grade = ask(JUDGE_MODEL, RUBRIC, f"ข้อความลูกค้า: {row['input']}\nคำตอบ: {out.reply}", Grade)
    correct += ok
    good_reply += bool(grade and grade.passed)
    if not ok or not (grade and grade.passed):
        print(f"{row['id']} คาด={row['expected_category']} ได้={out.category} judge={grade.score if grade else '-'} {grade.reason if grade else ''}")

n = len(rows)
print(f"\n[{split}] หมวดถูก {correct}/{n} = {correct / n:.0%} | คำตอบผ่าน rubric {good_reply}/{n}")
print(f"ต้นทุนรวม ${total_cost:.4f} | ต่อเคส ${total_cost / n:.4f} (รวมค่า judge)")
python eval_tickets.py samples/eval-cases.csv dev

→ จดตัวเลข baseline: % หมวดถูก, % คำตอบผ่าน, ต้นทุนต่อเคส แล้วอ่านเคสที่พิมพ์ออกมา (ผิดเพราะ prompt หรือเพราะเฉลยกำกวม?)

ทดลอง B — ปรับทีละอย่าง แล้ววัดซ้ำ แก้ SYSTEM_PROMPT อย่างเดียว เช่นเพิ่มกติกา "ถ้าลูกค้าขอเงินคืน ให้เป็น refund แม้สาเหตุคือสินค้าเสีย" → รัน dev ใหม่ → ดีขึ้นค่อยรัน test ครั้งเดียว → ลองอีกรอบโดยเปลี่ยนแค่ TASK_MODEL เป็นรุ่นเล็กกว่า เทียบความแม่น vs ต้นทุน

ทดลอง C — ให้ Claude ช่วยหาจุดอ่อนของ rubric

นี่คือ rubric ที่ใช้ให้ AI ให้คะแนนคำตอบลูกค้า: [วาง RUBRIC]
ยกตัวอย่างคำตอบ 3 แบบที่ได้คะแนนสูงตาม rubric แต่จริงๆ แย่ แล้วเสนอ rubric ที่รัดกุมขึ้น

eval ใน CI (กัน regression)

รันสคริปต์ใน GitHub Actions ทุกครั้งที่ไฟล์ prompt เปลี่ยน แล้วให้ fail ถ้าความแม่นต่ำกว่าเกณฑ์ (เช่น 90%) — เก็บ API key เป็น secret ของ repo เท่านั้น

⚠️ ต้องตรวจสอบ: ชื่อรุ่นและราคาต่อ 1M token ใน PRICE ที่ platform.claude.com ก่อนใช้ตัวเลขต้นทุนจริง

สรุปจำง่าย

เคสจริง + เฉลยจากคน + grader อัตโนมัติ + เปลี่ยนทีละอย่าง + ดูต้นทุนต่องาน