บทความ › AI

ทดสอบฟีเจอร์ AI แบบเขียนเทส: ชุด eval 30-100 เคส รันทุกครั้งที่แก้ prompt

ทำชุดทดสอบ LLM จาก input จริง ตรวจด้วยโค้ดหรือ LLM-as-judge รันทุกครั้งที่เปลี่ยน prompt หรือโมเดล พร้อมคำนวณว่าต้องกี่เคสถึงจะเห็นคะแนนตก 10%

กลาง อ่าน 11 นาที 22 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • ชุด eval คือ unit test ของฟีเจอร์ AI: 30-100 เคสจาก input จริง ผลที่ถูกเขียนไว้ล่วงหน้า รันทุกครั้งที่แก้ prompt หรือเปลี่ยนโมเดล
  • ตรวจด้วยโค้ดก่อนเสมอ ใช้ LLM-as-judge เฉพาะคำตอบที่ตรวจด้วยกฎไม่ได้ และต้องเทียบ judge กับคนก่อนเชื่อ
  • ดูรายเคสว่าอะไรพัง อะไรหาย ไม่ใช่ดูแค่คะแนนรวม 85% เป็น 80% อาจซ่อนเคสพัง 17 เคส
  • 100 เคสแยกคะแนนต่างกัน 10 จุดได้ก็ต่อเมื่อเทียบรายเคส ถ้าเทียบแค่ยอดรวมต้องใช้ราว 250 เคส

ฟีเจอร์ที่เรียก LLM มีพฤติกรรมเปลี่ยนได้ทุกครั้งที่แก้ prompt สลับโมเดล หรือผู้ให้บริการอัปเดตเวอร์ชัน ถ้าไม่มีชุดทดสอบ วิธีรู้ว่าพังคือผู้ใช้มาบอก ชุด eval เล็ก ๆ 30-100 เคสที่รันได้ในไม่กี่นาที ทำหน้าที่เดียวกับ unit test คือบอกว่าการแก้ครั้งนี้ทำให้อะไรดีขึ้น และอะไรพัง

ทดสอบบน Python 3.12 (สคริปต์ในบทใช้แค่ stdlib) ตัวเลขสถิติคำนวณใหม่ทุกตัวในบทความ ข้อแนะนำเรื่องการตรวจตรวจกับเอกสาร Anthropic, OpenAI และงานวิจัย MT-Bench (Zheng et al., 2023)

eval คืออะไร ต่างจากลองถามเล่นตรงไหน

การลองพิมพ์ถามสามสี่ข้อแล้วดูว่า “ดูดี” ไม่ใช่การทดสอบ เพราะครั้งหน้าคุณจะถามคนละข้อ และจำไม่ได้ว่าครั้งก่อนตอบอะไร eval มีสามส่วนที่ต้องเขียนลงไฟล์

  1. เคส input จริงที่ระบบต้องเจอ พร้อม id ถาวร
  2. ผลที่ถูก หรือเกณฑ์ตัดสินของแต่ละเคส เขียนไว้ก่อนรัน
  3. ตัวตรวจ โค้ดที่ให้ผล pass/fail ต่อเคส แล้วรวมเป็น pass rate

พอเป็นไฟล์ รอบไหนก็รันซ้ำได้ เทียบกับรอบก่อนได้ และเก็บไว้ใน git คู่กับ prompt ได้

เคสมาจากไหน

เอกสาร Anthropic สรุปไว้ว่าเคสต้อง เหมือนงานจริง รวม edge case อย่างพิมพ์ผิด ประชดประชัน ข้อความยาวยืดยาด ส่วน OpenAI แนะนำให้เก็บ log ตั้งแต่ช่วงพัฒนาเพื่อขุดเคสดี ๆ ออกมา สัดส่วนที่เราใช้

ประเภทสัดส่วนโดยประมาณตัวอย่าง (ระบบคัดแยก ticket)
เคสปกติจาก log จริง60%“ของยังไม่มา สั่งไป 5 วันแล้ว”
edge case25%ข้อความไทยปนอังกฤษ พิมพ์ผิด ถามสองเรื่องในข้อความเดียว
เคสที่เคยพังจริง15% ขึ้นไปทุก bug ที่ผู้ใช้เจอ กลายเป็นเคสถาวร

ข้อสุดท้ายสำคัญที่สุด ทุกครั้งที่ผู้ใช้เจอคำตอบผิด เพิ่มเคสนั้นเข้าชุดก่อนแก้ prompt เหมือนเขียน regression test ก่อนแก้ bug ลบข้อมูลส่วนตัวออกก่อนเก็บเป็นเคส

เก็บเคสจาก log จริง30-100 เคสแรกแก้ prompt หรือโมเดลทีละอย่างรัน eval ทุกเคสโค้ดตรวจก่อน judge ทีหลังเทียบรายเคสเคสไหนพัง เคสไหนหายเคสพังเป็นเคสใหม่เพิ่มเข้าชุดทดสอบถาวรแล้ววนกลับไปข้อแรก
ทุก bug ที่ผู้ใช้เจอ กลายเป็นเคสถาวรในชุดทดสอบ

ตรวจยังไง: โค้ดก่อน judge ทีหลัง

เอกสาร Anthropic จัดลำดับวิธีตรวจไว้ชัด ให้เลือกวิธีที่ เร็วที่สุด เชื่อถือได้ที่สุด และขยายได้ ก่อน

วิธีเหมาะกับข้อเสีย
ตรวจด้วยโค้ด (exact match, regex, parse JSON, ค่าตัวเลข)หมวดหมู่, field ที่ดึงออกมา, รูปแบบ outputตรวจคำตอบอิสระไม่ได้
LLM-as-judge ตาม rubricสรุปความ, น้ำเสียง, “ตอบตรงคำถามไหม”มีอคติ ต้องเทียบกับคนก่อน
คนตรวจสร้าง label ตั้งต้น, ตรวจ judgeช้า แพง

เคล็ดคือออกแบบ output ให้ตรวจด้วยโค้ดได้ตั้งแต่แรก ถ้าฟีเจอร์ตอบเป็น JSON ที่มี field ชัด (บทความ structured output) ครึ่งหนึ่งของ eval จะกลายเป็น == ธรรมดา เอกสาร Anthropic ยังบอกว่า เคสเยอะที่ตรวจอัตโนมัติ ดีกว่าเคสน้อยที่คนตรวจละเอียด

LLM-as-judge และอคติของมัน

เมื่อต้องใช้ judge ให้ทำตามสามข้อจากเอกสาร Anthropic คือ rubric ละเอียด (“ต้องเอ่ยถึงเลขออเดอร์ ไม่งั้นตก”) ให้ตอบแบบวัดได้ (pass/fail หรือ 1-5) และให้ judge คิดก่อนให้คะแนน (เปิด thinking) และ ใช้โมเดลคนละตัวกับที่สร้างคำตอบ

งานวิจัย MT-Bench (Zheng et al., 2023) วัดอคติของ judge ไว้สามแบบ (ทดลองกับโมเดลรุ่นปี 2023 ตัวเลขของโมเดลปัจจุบันต่างไป แต่ชนิดของอคติยังต้องระวังเหมือนเดิม)

  • position bias ชอบคำตอบตามลำดับที่วาง ในการทดลองนั้น แม้แต่ GPT-4 ก็ตัดสินเหมือนเดิมเมื่อสลับลำดับได้แค่ราว 65% ของกรณี
  • verbosity bias ชอบคำตอบยาวกว่า แม้ยาวเพราะพูดซ้ำ
  • self-enhancement อาจให้คะแนนคำตอบของตัวเองสูงกว่า (ผู้วิจัยบอกเองว่าข้อมูลยังน้อยเกินจะสรุปแน่ชัด)

วิธีลด: ถ้าเทียบสองคำตอบ ให้รันสองรอบสลับลำดับ นับว่าชนะเฉพาะเมื่อชนะทั้งสองรอบ ถ้าไม่จำเป็นต้องเทียบ ใช้ pass/fail ต่อคำตอบเดียวแทน และสุ่ม 20-30 เคสให้คนตรวจเทียบกับ judge ก่อนเชื่อตัวเลข OpenAI แนะนำแบบเดียวกัน คือขยายการใช้ judge เมื่อมันเห็นตรงกับคนอย่างสม่ำเสมอแล้วเท่านั้น

ตัวตรวจก็ต้องถูกทดสอบ judge ที่ยังไม่เคยเทียบกับคน ให้ตัวเลขที่ดูแม่นแต่ไม่มีความหมาย

สคริปต์ eval เล็กที่สุดที่ใช้ได้จริง

เก็บเคสใน cases.jsonl บรรทัดละเคส เช่น {"id": "c001", "input": "...", "expect": "refund"} แล้วใช้สคริปต์นี้ (stdlib ล้วน) predict คือฟังก์ชันที่เรียกฟีเจอร์ AI ของคุณ

import json, sys
from math import comb

def run(cases_path, predict, out_path):
    results = {}
    for line in open(cases_path, encoding="utf-8"):
        c = json.loads(line)
        got = predict(c["input"])
        results[c["id"]] = got.strip().lower() == c["expect"].lower()
    json.dump(results, open(out_path, "w"))
    print(f"pass {sum(results.values())}/{len(results)}")

def compare(old_path, new_path):
    old, new = json.load(open(old_path)), json.load(open(new_path))
    broke = [k for k in old if old[k] and not new[k]]
    fixed = [k for k in old if not old[k] and new[k]]
    n, k = len(broke) + len(fixed), min(len(broke), len(fixed))
    p = min(1.0, 2 * sum(comb(n, i) for i in range(k + 1)) / 2**n) if n else 1.0
    print(f"broke {len(broke)} {broke}\nfixed {len(fixed)} {fixed}\np = {p:.3f}")

if __name__ == "__main__":
    compare(sys.argv[1], sys.argv[2])
# เทียบผลรอบก่อนกับรอบนี้ (รันบนเครื่อง dev หรือใน CI)
python evalrun.py results/old.json results/new.json

compare ไม่ได้ดูแค่ยอดรวม มันบอก เคสที่พัง (เคยผ่าน ตอนนี้ตก) และ เคสที่หาย (เคยตก ตอนนี้ผ่าน) แล้วคำนวณ p-value แบบ McNemar exact test ซึ่งคือ binomial test บนเคสที่ผลเปลี่ยน (แบบเดียวกับ exact=True ใน statsmodels) ตอนลองกับข้อมูลจำลอง รอบเก่าผ่าน 85/100 รอบใหม่ 80/100 ดูแค่ยอดรวมเหมือนตกนิดเดียว แต่ข้างในพัง 17 เคส หาย 12 เคส p = 0.458 แปลว่าการแก้ครั้งนี้ทำให้ผลสั่นไปทั้งชุดโดยไม่ได้ดีขึ้น ต้องไปอ่าน 17 เคสนั้น

ตัวอย่างคำนวณ: ต้องกี่เคสถึงเห็นคะแนนตก 10 จุด

สมมติฟีเจอร์ผ่าน 85% แล้วแก้ prompt จนเหลือ 75% อยากรู้ว่าชุด eval ขนาดเท่าไรถึงจะแยกออกจากความบังเอิญ

ขั้น 1: ความคลาดเคลื่อนของ pass rate รอบเดียว standard error = √(p(1−p)/n) ที่ p = 0.85 คือ √(0.1275/n) ช่วง 95% ≈ ±1.96 × SE

จำนวนเคสSEช่วง 95%
306.5 จุด±12.8 จุด
505.0 จุด±9.9 จุด
1003.6 จุด±7.0 จุด
2502.3 จุด±4.4 จุด

ที่ 30 เคส ช่วงความไม่แน่นอนกว้างกว่าตัวที่อยากจับ (10 จุด) ส่วน 1 เคสเท่ากับ 3.3 จุด การตก 10 จุดคือแค่ 3 เคส

ขั้น 2: ถ้าเทียบยอดรวมแบบสองกลุ่มอิสระ สูตรขนาดตัวอย่างของสองสัดส่วน ที่ α = 0.05 (สองทาง, z = 1.96) power 80% (z = 0.84)

n = (1.96 + 0.84)² × (0.85×0.15 + 0.75×0.25) / 0.10² = 7.84 × 0.315 / 0.01 ≈ 247 เคส

ขั้น 3: ถ้าเทียบรายเคสบนชุดเดิม ใช้ได้เพราะเรารันเคสเดิมทั้งสองรอบ เหลือดูแค่เคสที่ผลเปลี่ยน ที่ 100 เคส คะแนนตก 10 จุดเกิดได้หลายแบบ

พังหายสุทธิp (McNemar exact)
100−100.002
122−100.013
155−100.041

ทุกแบบต่ำกว่า 0.05 คือ 100 เคสเทียบรายเคสพอจับการตก 10 จุดได้ ส่วนที่ 30 เคส ตก 10 จุด = พัง 3 หาย 0 ได้ p = 0.25 ยังบอกไม่ได้ว่าจริง

สรุปเชิงปฏิบัติ: 30 เคสพอจับ “พังยับ” กับ bug ชัด ๆ, ราว 100 เคสพร้อมเทียบรายเคสพอจับการเปลี่ยน 10 จุด, การเปลี่ยนเล็กกว่านั้นต้องเพิ่มเคสหรือรันหลายรอบ

noise ของโมเดลเอง

LLM ตอบไม่เหมือนเดิมทุกครั้ง ก่อนเชื่อผลใด ๆ ให้ รัน baseline สองรอบโดยไม่แก้อะไร แล้วใช้ compare ดูว่ามีกี่เคสสลับไปมาเอง ถ้าสลับ 5 เคสต่อรอบ การแก้ที่ทำให้พัง 5 เคสก็ไม่มีความหมาย เคสที่สลับบ่อยให้รัน 3 รอบแล้วนับผ่านเมื่อผ่านเสียงข้างมาก หรือแก้ตัวตรวจให้ไม่จู้จี้กับรูปแบบที่ไม่สำคัญ

รันเมื่อไร และติดตามยังไง

รันทุกครั้งที่ แก้ prompt, เปลี่ยนโมเดล, เปลี่ยน temperature หรือ tool, อัปเดต SDK (บทความเลือกโมเดล ต้องใช้ชุดนี้ตัดสิน) เก็บผลแต่ละรอบเป็นไฟล์ตั้งชื่อตาม commit แล้วดูเส้น pass rate ตามเวลา

ค่าใช้จ่ายต่ำกว่าที่คิด ตัวอย่าง 100 เคส เคสละ 1,000 token เข้า 200 token ออก บน Claude Sonnet 5.5 ($2 / $10 ต่อล้าน token ณ ตุลาคม 2026): 100 × (1,000 × $2 + 200 × $10) / 1,000,000 = $0.40 ต่อรอบ ถ้ามี judge อีกตัวก็ราวเท่าตัว ถูกกว่าเวลาที่เสียไปกับ bug ที่ผู้ใช้เจอเยอะ

ข้อผิดพลาดที่เจอบ่อย

  • จูน prompt จนจำชุดทดสอบ (overfit) แก้ทีละเคสจนผ่านครบ 100% แต่ของจริงแย่ลง กันเคส 20-30% เป็นชุด holdout ที่ไม่เปิดดูตอนจูน รันแค่ตอนตัดสินใจ
  • ชุด eval เก่า input จริงเปลี่ยนไปแล้วแต่เคสยังเป็นของสามเดือนก่อน เติมจาก log ทุกสัปดาห์
  • ดูแต่คะแนนรวม ตามตัวอย่างข้างบน 85 → 80 ซ่อนเคสพัง 17 เคส
  • เชื่อ judge ที่ไม่เคยเทียบกับคน หรือใช้โมเดลเดียวกันตรวจตัวเอง
  • เคสที่มีข้อมูลส่วนตัว ลบชื่อ เบอร์ เลขบัญชีออกก่อนเก็บ ชุด eval อยู่ใน git นานกว่าที่คิด
  • ลืมเคสโจมตี ถ้าฟีเจอร์รับข้อความจากภายนอก ใส่เคส prompt injection ด้วย (วิธีป้องกันฝั่งระบบ)

eval ที่ดีไม่ได้ตอบว่า “ดีแค่ไหน” แต่ตอบว่า “การแก้ครั้งนี้ทำให้เคสไหนพัง”

แหล่งอ้างอิง

คำถามที่พบบ่อย

ต้องมีกี่เคสถึงจะพอ

เริ่ม 30 เคสก็ได้ประโยชน์ทันที เพราะจับของพังชัด ๆ ได้ แต่ถ้าอยากแยกความต่าง 10 จุดออกจาก noise ให้ขยับไปราว 100 เคสและเทียบผลรายเคสกับรอบก่อน ถ้าเทียบแค่ % รวมแบบสองกลุ่มอิสระต้องใช้ราว 250 เคส

ใช้โมเดลตัวเดียวกันเป็น judge ได้ไหม

ได้แต่ไม่ควร เอกสาร Anthropic แนะนำให้ใช้โมเดลอื่นตรวจ และงานวิจัย MT-Bench พบว่า judge มีอคติเรื่องตำแหน่งคำตอบ ความยาว และอาจชอบคำตอบของตัวเอง ใช้ rubric ชัด ให้ตอบ pass/fail และสุ่มเคสมาให้คนตรวจเทียบเป็นระยะ

คะแนน eval สูงแล้ว ทำไมของจริงยังพัง

ส่วนใหญ่เพราะชุด eval ไม่เหมือน input จริงแล้ว หรือแก้ prompt จนจำชุดทดสอบได้ (overfit) ให้เพิ่มเคสจาก log ที่ผู้ใช้เจอปัญหาทุกสัปดาห์ และกันเคสไว้ส่วนหนึ่งที่ไม่ดูตอนจูน prompt

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#AIขั้นสูง

CaMeL: กัน prompt injection ด้วยการออกแบบระบบ ไม่ใช่ด้วย prompt ที่ดีกว่า

งานวิจัย CaMeL จาก Google DeepMind แยก control flow ออกจากข้อมูลที่ไม่น่าเชื่อถือ ติดป้าย capability ให้ทุกค่า แล้วตรวจ policy ตอนเรียก tool ผลบน AgentDojo และข้อจำกัด

อ่าน 8 นาที
#AIกลาง

Context rot: ยิ่งใส่ข้อมูลให้ AI ยาว ยิ่งตอบพลาด แล้วควรใส่อะไรใน context

งานวิจัยของ Chroma ทดสอบ 18 โมเดล พบว่าคุณภาพคำตอบลดลงเมื่อ input ยาวขึ้น และข้อมูลที่คล้ายแต่ผิดทำให้แย่ลงอีก พร้อมกฎใช้งานจริงว่าควรใส่อะไรใน context

อ่าน 9 นาที
#AIเริ่มต้น

รีวิวโค้ดที่ AI เขียน: จุดที่ AI พลาดบ่อย และ checklist ก่อน merge

AI เขียนโค้ดเร็ว แต่ชอบแต่ง package ที่ไม่มีจริง กลืน error เงียบ และเขียนเทสต์ที่ไม่ได้เทสต์อะไร checklist รีวิวทีละข้อ diff เล็ก รันก่อนอ่าน และคนเซ็นรับ

อ่าน 10 นาที