วิธีสร้างระบบประเมินผล AI Agent แบบ Self-hosted ด้วยตัวเอง
สร้างระบบประเมินผล AI Agent ที่คุณควบคุมเองได้ด้วยการใช้ Golden cases จาก Trace จริง ตรวจสอบด้วย Logic พื้นฐานก่อนใช้ LLM Judge พร้อมติดตาม Pass rate ในทุก Commit
การประเมินผล AI agent แบบ self-hosted คืออะไร
การประเมินผล (evals) แบบ self-hosted สำหรับ AI agent ประกอบด้วย 4 ส่วนที่คุณเก็บไว้ใน repository ของคุณเอง ได้แก่ ไฟล์ชุดข้อมูลทดสอบที่บันทึกไว้, สคริปต์สำหรับรัน agent เพื่อทดสอบข้อมูลเหล่านั้น, ชุดการตรวจสอบเพื่อประเมินคำตอบแต่ละรายการ และตารางผลลัพธ์ที่คุณสามารถสืบค้นได้ ทั้งหมดนี้ไม่จำเป็นต้องพึ่งพาผู้ให้บริการภายนอก วงจรการทำงานทั้งหมดประกอบด้วยโค้ด Python เพียงไม่กี่ร้อยบรรทัดและไฟล์ SQLite หนึ่งไฟล์
Agent ทำงานได้ดีในตัวอย่าง demo เพราะคุณเลือกข้อมูลนำเข้า 5 รายการนั้นด้วยตัวเอง แต่มันกลับพังในสัปดาห์ที่สองเนื่องจากมีการเปลี่ยนบรรทัดใน prompt, การเปลี่ยนโมเดล หรือการเปลี่ยนคำอธิบายเครื่องมือ (tool description) โดยไม่มีการวัดผลใดๆ ครอบคลุมถึงการเปลี่ยนแปลงเหล่านี้ วงจรการประเมินผลจะเปลี่ยนความรู้สึกที่ว่า "ตอนนี้มันแย่ลง" ให้กลายเป็นตัวเลขที่ชัดเจน เช่น "อัตราการผ่านลดลงจาก 58 ใน 60 เหลือ 51 ใน 60 ที่ commit 4f1c9ab"
วงจรนี้มี 4 ขั้นตอน และคู่มือนี้จะแบ่งเนื้อหาออกเป็นส่วนละหนึ่งขั้นตอน ได้แก่ การรวบรวม trace จริง, การคัดเลือก trace ที่น่าสนใจมาเป็นกรณีทดสอบ, การประเมินผลทุกกรณีในทุกการเปลี่ยนแปลง และการจัดเก็บอัตราการผ่านไว้คู่กับ commit ที่สร้างผลลัพธ์นั้น วงจรเดียวกันนี้สามารถใช้งานได้ไม่ว่าคุณจะรัน agent บนระบบใดก็ตาม และ framework สำหรับ AI agent แบบ self-hosted ที่น่าใช้งาน จะมีความแตกต่างกันหลักๆ อยู่ที่ปริมาณข้อมูล trace ที่ framework เหล่านั้นเตรียมไว้ให้คุณใช้งานได้ทันที
เหตุใดเอเจนต์จึงทำงานผิดพลาดในสัปดาห์ที่สอง
เอเจนต์ประกอบด้วยพรอมต์, โมเดล, ชุดคำจำกัดความของเครื่องมือ และบริบทที่ถูกดึงมาใช้ในขณะรันไทม์ ทั้งสี่ส่วนนี้สามารถเปลี่ยนแปลงได้โดยที่โค้ดแอปพลิเคชันของคุณยังคงเหมือนเดิม ดังนั้นการรีวิวโค้ดตามปกติจึงไม่พบสิ่งผิดปกติใดๆ
สาเหตุที่พบบ่อยที่สุดคือการแก้ไขพรอมต์ คุณเพิ่มประโยคหนึ่งเข้าไปเพื่อป้องกันการตอบกลับที่ไม่สุภาพ ประโยคนั้นเปลี่ยนพฤติกรรมในการรับอินพุตที่ไม่มีใครทดสอบซ้ำ และร่องรอยการทำงาน (trace) ก็แสดงให้เห็นชัดเจน: trace ของสัปดาห์ที่แล้วสำหรับคำถามเดียวกันมีการเรียกใช้เครื่องมือ create_refund แต่ของสัปดาห์นี้กลับไม่มี และผลลัพธ์ที่ได้คือคำขอโทษที่สุภาพแทน ไม่มีข้อผิดพลาดเกิดขึ้น จึงไม่มีการแจ้งเตือนใดๆ ทำงาน
สาเหตุที่สองคือตัวโมเดล ให้บันทึกสตริงชื่อโมเดลที่แน่นอนที่คุณส่งไปในการรันทุกครั้ง ไม่ใช่ claude-haiku-4-5-20251001 หรือชื่อย่อที่คุณจำไว้ในหัว เพราะอัตราความสำเร็จที่ลดลงในวันที่คุณเปลี่ยนโมเดลจะสามารถวิเคราะห์ได้ก็ต่อเมื่อมีข้อมูลโมเดลระบุไว้ในบันทึกเท่านั้น
สาเหตุที่สามคือเครื่องมือ การปรับเปลี่ยนคำอธิบายเครื่องมือจะส่งผลต่อการตัดสินใจของโมเดลว่าจะเรียกใช้เครื่องมือเมื่อใด หากเครื่องมือของคุณถูกส่งผ่าน MCP servers ที่รันอยู่บน VPS สคีมาจะอยู่ในอีกโพรเซสหนึ่ง ซึ่งสามารถเปลี่ยนแปลงได้โดยที่คุณไม่เห็นความแตกต่างใดๆ ใน repository ของคุณเลย สาเหตุที่สี่คือการดึงข้อมูล (retrieval): คำถามเดิมอาจไปตรงกับดัชนีที่ถูกสร้างใหม่ในชั่วข้ามคืน ทำให้คำตอบเปลี่ยนไปตามเอกสารชุดใหม่นั้น
สร้างชุดข้อมูลมาตรฐาน (Golden Set) จากร่องรอยข้อมูลที่คุณจัดเก็บไว้แล้ว
ห้ามสร้างกรณีทดสอบขึ้นมาเอง ให้ดึงข้อมูลมาจากทราฟฟิกจริง หากคุณใช้งาน การทำ tracing ด้วย Langfuse สำหรับเอเจนต์ของคุณ อยู่แล้ว ทุกคำขอจะถูกจัดเก็บพร้อมอินพุต การเรียกใช้เครื่องมือ และเอาต์พุต ซึ่งเป็นวัตถุดิบที่จำเป็นสำหรับกรณีทดสอบอย่างแท้จริง
ส่งออกข้อมูลช่วงเวลาของ root observations ผ่านทาง public API โดยใช้การยืนยันตัวตนแบบ basic authentication ซึ่งใช้ public key ของคุณเป็นชื่อผู้ใช้ และ secret key เป็นรหัสผ่าน
export LF_HOST="https://langfuse.example.com"
curl -sS -u "$LF_PUBLIC_KEY:$LF_SECRET_KEY" \
"$LF_HOST/api/public/v2/observations?limit=50&isRootObservation=true&fromStartTime=2026-07-01T00:00:00Z" \
| jq '.data[0]'อ่านข้อมูลหนึ่งระเบียนก่อนเริ่มเขียนโค้ดแยกวิเคราะห์ ข้อมูลจะถูกส่งกลับมาภายใต้ data แต่ชื่อฟิลด์ที่เก็บคำถามและคำตอบจะขึ้นอยู่กับวิธีการที่เอเจนต์ของคุณทำ instrumentation ในแต่ละ span ดังนั้นให้แมปข้อมูลตามที่คุณเห็นจริงแทนที่จะคาดเดา จากนั้นให้เขียนกรณีทดสอบด้วยตนเอง โดยใช้รูปแบบ JSON object หนึ่งรายการต่อหนึ่งบรรทัดใน evals/cases.jsonl:
{"id": "refund-double-charge", "tags": ["smoke"], "input": "I was charged twice for order 41822.", "must_call": ["lookup_order", "create_refund"], "must_not_include": ["I cannot help"], "rubric": "The reply confirms exactly one refund for order 41822 and states the amount."}กฎ 5 ข้อที่จะช่วยให้ชุดข้อมูลนี้มีประสิทธิภาพในการใช้งาน:
- จำนวน 40 ถึง 80 กรณีทดสอบเพียงพอสำหรับการเริ่มต้น หากน้อยกว่า 20 กรณีทดสอบที่ทำงานไม่เสถียรเพียงรายการเดียวจะส่งผลต่ออัตราความสำเร็จถึง 5 จุด ซึ่งจะทำให้ตัวเลขที่แกว่งไปมาโดยไม่มีสาเหตุถูกละเลยไป
- ทุกบั๊กในระบบผลิตที่คุณแก้ไข จะต้องกลายเป็นกรณีทดสอบในวันที่คุณแก้ไขบั๊กนั้น นิสัยนี้คือสิ่งที่ทำให้ชุดข้อมูลเติบโตไปในทิศทางที่ถูกต้อง
- หนึ่งพฤติกรรมต่อหนึ่งกรณีทดสอบ กรณีทดสอบที่ตรวจสอบทั้งยอดเงินคืนและน้ำเสียงไปพร้อมกัน จะไม่สามารถบอกอะไรคุณได้เลยเมื่อเกิดความล้มเหลว
- ค่า
idต้องไม่มีการเปลี่ยนแปลง เพราะ id คือสิ่งที่ใช้เปรียบเทียบผลการรันของวันนี้กับเดือนที่แล้ว - ทำการปกปิดข้อมูลส่วนบุคคล (Redact) ก่อนทำการ commit เนื่องจากไฟล์นี้จะถูกเก็บไว้ใน git ดังนั้นให้ลบชื่อลูกค้าและหมายเลขคำสั่งซื้อใดๆ ที่ไม่ใช่ของคุณออกไป
ตรวจสอบด้วยวิธีที่กำหนดผลลัพธ์ได้แน่นอนก่อน เพราะไม่มีค่าใช้จ่าย
สิ่งใดที่มีคำตอบที่ถูกต้องชัดเจน ให้ใช้การยืนยันผล (assertion) แบบปกติ ไม่ต้องเรียกใช้โมเดล ไม่เสียค่าใช้จ่าย และไม่มีความคลุมเครือ การตรวจสอบแบบกำหนดผลลัพธ์ได้แน่นอนจะช่วยตรวจจับความถดถอยเชิงโครงสร้าง ซึ่งเป็นสิ่งที่ทำให้ระบบรอบตัวเอเจนต์ของคุณพัง เช่น JSON ไม่สามารถ parse ได้, ไม่มีการเรียกใช้เครื่องมือ, มีการตอบกลับด้วยวลีที่ห้ามใช้ หรือคำตอบไม่มีการอ้างอิงแหล่งที่มา
มีเพียงฟังก์ชันเดียวเท่านั้นที่รู้จักเอเจนต์ของคุณ ส่วนประกอบอื่นทั้งหมดในชุดทดสอบควรเป็นแบบทั่วไป
import json, os, urllib.request
def run_agent(case):
req = urllib.request.Request(
os.environ["AGENT_URL"],
data=json.dumps({"input": case["input"]}).encode(),
headers={"content-type": "application/json"},
)
with urllib.request.urlopen(req, timeout=120) as resp:
return json.load(resp)
def deterministic(case, result):
text = result.get("output", "")
called = [c["name"] for c in result.get("tool_calls", [])]
failures = []
for tool in case.get("must_call", []):
if tool not in called:
failures.append(f"tool not called: {tool}")
for phrase in case.get("must_not_include", []):
if phrase.lower() in text.lower():
failures.append(f"forbidden phrase: {phrase}")
if len(called) > case.get("max_tool_calls", 12):
failures.append(f"too many tool calls: {len(called)}")
return failuresให้คงงบประมาณการใช้เครื่องมือไว้ในรายการนั้น เอเจนต์ที่แก้ปัญหาได้ใน 3 ครั้งในวันนี้ แต่ใช้ 11 ครั้งในวันพรุ่งนี้ ถือว่ามีความถดถอยเกิดขึ้น แม้ว่าคำตอบสุดท้ายจะถูกต้องก็ตาม เพราะคุณต้องเสียค่าใช้จ่ายสำหรับการเรียกใช้ทุกครั้ง
การใช้ LLM เป็นผู้ตัดสินและข้อผิดพลาด 4 ประการที่มักเกิดขึ้น
เมื่อผ่านการตรวจสอบด้วย assertions แล้ว สิ่งที่จำเป็นต้องมีคือตัวประเมินผลที่สามารถอ่านเนื้อหาได้ การใช้ LLM เป็นผู้ตัดสินคือการเรียกใช้โมเดลอีกตัวหนึ่ง โดยโมเดลจะได้รับคำถาม คำตอบของเอเจนต์ และเกณฑ์การตัดสินหนึ่งข้อ จากนั้นจึงส่งผลการตัดสินกลับมา นี่เป็นวิธีเดียวที่ใช้งานได้จริงในการประเมินว่า "คำตอบนั้นตอบสิ่งที่ผู้ใช้ถามหรือไม่"
กฎ 4 ข้อที่ทำให้การใช้ LLM เป็นผู้ตัดสินมีประสิทธิภาพ:
- ใช้ผลลัพธ์แบบไบนารี (ผ่าน/ไม่ผ่าน) ห้ามใช้คะแนน 1 ถึง 10 เพราะสเกลคะแนนมักจะให้ค่า 7 หรือ 8 กับเกือบทุกอย่าง ทำให้ตัวเลขไม่เปลี่ยนแปลงและคุณจะไม่ได้รับข้อมูลที่เป็นประโยชน์ใดๆ
- ใช้เกณฑ์การตัดสินหนึ่งข้อต่อการเรียกใช้งานหนึ่งครั้ง ให้ถามเรื่องจำนวนเงินคืนหรือเรื่องน้ำเสียงอย่างใดอย่างหนึ่ง ไม่ควรถามทั้งสองอย่างพร้อมกัน
- ให้คำตอบที่คาดหวังแก่ผู้ตัดสินเสมอในกรณีที่มีคำตอบที่ถูกต้องชัดเจน การให้คะแนนโดยเทียบกับข้อมูลอ้างอิงนั้นง่ายกว่าการประเมินแบบลอยๆ มาก
- บังคับรูปแบบของผลลัพธ์และตรวจสอบ (parse) อย่างเคร่งครัด
from anthropic import Anthropic
client = Anthropic() # reads ANTHROPIC_API_KEY from the environment
def judge_prompt(case, output):
return (
"You grade one answer against one criterion.\n"
"Reply with JSON only, in this exact shape:\n"
'{"verdict": "pass", "confidence": "high", "reason": "one short sentence"}\n'
f"Criterion: {case['rubric']}\n"
f"Question: {case['input']}\n"
f"Answer: {output}\n"
"Length is not a criterion. Judge only the criterion above."
)
def judge(case, output, model):
msg = client.messages.create(
model=model,
max_tokens=200,
messages=[{"role": "user", "content": judge_prompt(case, output)}],
)
return json.loads(msg.content[0].text)ต่อไปคือรูปแบบความล้มเหลว แต่ละข้อมีวิธีทดสอบที่คุณสามารถทำได้ในบ่ายวันนี้ และการทดสอบเหล่านี้มีความสำคัญ เพราะผู้ตัดสินที่ไม่มีการตรวจสอบจะสร้างตัวเลขที่ดูแม่นยำแต่ไม่มีความหมายใดๆ
อคติจากความยาว (Length bias): คำตอบที่ยาวกว่ามักจะผ่านการประเมินบ่อยกว่า ให้ทดสอบโดยนำคำตอบ 10 ข้อที่ผู้ตัดสินเคยให้สอบตกมาเติมเนื้อหาที่ดูมั่นใจแต่ไม่มีข้อมูลใหม่เพิ่มเข้าไป 2 ย่อหน้า แล้วให้ผู้ตัดสินประเมินใหม่อีกครั้ง หากผลลัพธ์เปลี่ยนเป็นผ่าน แสดงว่าเกิดอคติจากความยาว และคุณต้องแก้ไขเกณฑ์การตัดสิน (rubric)
อคติเข้าข้างตัวเอง (Self-preference): ผู้ตัดสินมักให้คะแนนผลลัพธ์จากโมเดลตระกูลเดียวกันอย่างใจดีกว่าผลลัพธ์จากโมเดลตระกูลอื่น ให้ทดสอบโดยใช้ผู้ตัดสินจากสองตระกูลที่ต่างกันประเมินคำตอบชุดเดียวกัน 30 ข้อ แล้วเปรียบเทียบผลลัพธ์ทีละกรณี หากผลลัพธ์ไม่ตรงกัน ให้คุณอ่านกรณีนั้นด้วยตัวเอง
อคติจากตำแหน่ง (Position bias): หากคุณใช้ผู้ตัดสินเพื่อเปรียบเทียบคำตอบสองข้อคือ A และ B ให้ลองสลับลำดับแล้วรันการทดสอบใหม่อีกครั้ง หากผลการตัดสินเปลี่ยนไปเมื่อสลับตำแหน่ง แสดงว่าการเปรียบเทียบแบบจับคู่ยังไม่ปลอดภัยสำหรับเกณฑ์นั้น
เกณฑ์การตัดสินคลาดเคลื่อน (Rubric drift): เกณฑ์ที่คลุมเครือจะทำให้ผู้ตัดสินใจง่ายเกินไป เช่น "คำตอบมีประโยชน์หรือไม่" จะทำให้ผ่านเกือบทุกอย่าง แต่ถ้าใช้ "คำตอบระบุจำนวนเงินคืนเป็นดอลลาร์หรือไม่" จะทำให้ผ่านเฉพาะสิ่งที่ถูกต้องเท่านั้น ให้เขียนเกณฑ์ใหม่จนกว่าจะระบุข้อเท็จจริงที่ต้องการตรวจสอบได้อย่างชัดเจน
มีมาตรการป้องกันหนึ่งข้อที่ครอบคลุมทั้ง 4 ปัญหา คือการเก็บชุดข้อมูล 30 กรณีที่คุณประเมินด้วยมือไว้ และนำมาเปรียบเทียบกับคะแนนของผู้ตัดสินทุกครั้งที่คุณเปลี่ยนโมเดลผู้ตัดสินหรือเปลี่ยน prompt ของผู้ตัดสิน หากผลลัพธ์ของผู้ตัดสินไม่ตรงกับคุณเกิน 1 ใน 10 กรณี ให้แก้ไขเกณฑ์การตัดสินก่อนที่จะเชื่อถืออัตราการผ่านที่ผู้ตัดสินรายงานออกมา ผู้ตัดสินถือเป็นโค้ดประเภทหนึ่ง ดังนั้นจึงต้องมีการทำ version control และการตรวจสอบ (review) เช่นเดียวกับโค้ดทั่วไป
จัดลำดับความสำคัญของโมเดลตามราคาและขยับขยายไปใช้โมเดลระดับแนวหน้าเมื่อจำเป็น
การใช้โมเดลที่แพงที่สุดประเมินผลทุกกรณีในทุก commit จะทำให้ค่าใช้จ่ายในการประเมินผลสูงเกินกว่าตัว agent ที่กำลังทดสอบ ให้จัดลำดับตัวประเมินผล (grader) ตามราคา และหยุดการประเมินทันทีที่ได้คำตอบที่ชัดเจน
The data behind this chart
[
{
"label": "Haiku 4.5, Batch API",
"usd_per_1000_judge_calls": "0.90"
},
{
"label": "Haiku 4.5",
"usd_per_1000_judge_calls": "1.80"
},
{
"label": "Sonnet 5",
"usd_per_1000_judge_calls": "3.60"
},
{
"label": "Opus 5",
"usd_per_1000_judge_calls": "9.00"
}
]ตัวเลขเหล่านี้คำนวณจาก input token ประมาณ 1,200 token และ output token 120 token ต่อการเรียกใช้งาน 1 ครั้ง ซึ่งเป็นขนาดที่สมเหตุสมผลสำหรับ 1 คำถาม 1 คำตอบ และ 1 เกณฑ์การประเมิน การประเมิน 1,000 กรณีจะมีค่าใช้จ่าย 1.80 ดอลลาร์สหรัฐบน Claude Haiku 4.5 และ 9.00 บน Claude Opus 5 ส่วนต่างนี้อาจดูเล็กน้อยจนกว่าจะคูณจำนวนครั้งจริง ชุดทดสอบ 60 กรณีที่ประเมินทุก commit โดยมีการ commit 40 ครั้งต่อสัปดาห์ จะเท่ากับการเรียกใช้งาน 2,400 ครั้งต่อสัปดาห์ ก่อนที่จะเริ่มรันงาน nightly job เสียอีก
มีส่วนลด 2 รูปแบบที่ใช้ได้ผลดีกับงานประเมินผลและสามารถใช้ร่วมกันได้ งานประเมินผลไม่จำเป็นต้องโต้ตอบแบบเรียลไทม์ ดังนั้น Batch API จึงลดราคา input และ output ลงครึ่งหนึ่งเพื่อแลกกับการส่งผลลัพธ์แบบไม่พร้อมกัน ซึ่งแสดงอยู่ในแถวแรกของตาราง ส่วน rubric และคำสั่งนั้นเหมือนกันทุกประการในทุกการเรียกใช้งาน จึงสามารถใช้ prompt caching ได้ โดยการอ่านจาก cache มีค่าใช้จ่ายเพียงหนึ่งในสิบของราคา input ปกติ และการเขียน cache 5 นาทีมีค่าใช้จ่าย 1.25 เท่าของราคา input ปกติ ดังนั้น cache จะคุ้มทุนหลังจากมีการเรียกใช้เพียงครั้งเดียว ราคาเหล่านี้เป็นราคาตามรายการของ Anthropic ณ เดือนสิงหาคม 2026 และ Sonnet 5 อยู่ในราคาช่วงแนะนำจนถึงวันที่ 31 สิงหาคม 2026 ดังนั้นแถบที่สามจะมีราคาสูงขึ้นหลังจากวันที่ดังกล่าว
ลำดับขั้นการประเมินมีดังนี้:
- ตรวจสอบด้วยวิธี deterministic ในทุกกรณี ซึ่งไม่มีค่าใช้จ่าย API
- ใช้โมเดลขนาดเล็กประเมินในกรณีที่ผ่านการตรวจสอบขั้นแรกแล้ว
- ใช้โมเดลระดับแนวหน้าประเมินเฉพาะกรณีที่โมเดลขนาดเล็กระบุว่าไม่ผ่าน หรือระบุว่าผ่านแต่มีความมั่นใจต่ำ
- ตรวจสอบโดยมนุษย์ในกลุ่มตัวอย่างขนาดเล็กสัปดาห์ละครั้ง
CHEAP = "claude-haiku-4-5-20251001"
STRICT = "claude-opus-5"
def grade(case, result):
hard = deterministic(case, result)
if hard:
return False, "deterministic", "; ".join(hard)
first = judge(case, result["output"], CHEAP)
if first["verdict"] == "pass" and first["confidence"] == "high":
return True, CHEAP, first["reason"]
second = judge(case, result["output"], STRICT)
return second["verdict"] == "pass", STRICT, second["reason"]วิธีนี้เป็นการแลกความแม่นยำในการประเมินกับค่าใช้จ่าย ดังนั้นควรวัดผลการแลกเปลี่ยนนี้แทนการคาดเดา เดือนละครั้งให้ประเมินชุดข้อมูลทั้งหมดด้วยตัวประเมินที่เข้มงวดแล้วเปรียบเทียบผลลัพธ์ทั้งสองคอลัมน์ หากผลลัพธ์ไม่ตรงกันในหลายกรณี แสดงว่า rubric ของคุณหลวมเกินไปสำหรับโมเดลขนาดเล็ก และสิ่งที่คุณต้องแก้ไขคือตัว rubric เอง การควบคุมค่าใช้จ่ายของตัว agent เองเป็นอีกงานหนึ่ง ซึ่งครอบคลุมอยู่ใน การควบคุมค่าใช้จ่ายสำหรับ AI agent บน VPS
ติดตามอัตราความสำเร็จเมื่อเวลาผ่านไปในระบบที่คุณดูแล
อัตราความสำเร็จที่คุณไม่สามารถเชื่อมโยงกับ commit ได้นั้นเป็นเพียงความรู้สึก ให้จัดเก็บข้อมูลหนึ่งแถวต่อหนึ่งกรณีทดสอบต่อหนึ่งรอบการรัน โดยระบุ commit และ model ไว้ภายในแถวนั้นด้วย
CREATE TABLE IF NOT EXISTS results (
run_id TEXT NOT NULL,
ran_at TEXT NOT NULL,
git_sha TEXT NOT NULL,
agent_model TEXT NOT NULL,
case_id TEXT NOT NULL,
passed INTEGER NOT NULL,
graded_by TEXT NOT NULL,
reason TEXT
);SELECT run_id, git_sha, agent_model,
count(*) AS cases,
round(100.0 * sum(passed) / count(*), 1) AS pass_pct
FROM results
GROUP BY run_id
ORDER BY ran_at DESC
LIMIT 10;โหลด schema ด้วย sqlite3 evals/results.db < evals/schema.sql จากนั้นอ่านแนวโน้มด้วย sqlite3 -box evals/results.db < evals/passrate.sql การรันรายวันตลอดหนึ่งปีสำหรับ 60 กรณีทดสอบ จะมีข้อมูลประมาณ 22,000 แถว ดังนั้นที่เก็บข้อมูลนี้จึงไม่กลายเป็นโปรเจกต์ที่ต้องดูแลแยกต่างหาก การใช้งาน SQLite ในสภาพแวดล้อม production บน VPS ครอบคลุมถึงการตั้งค่าที่เริ่มมีความสำคัญหากไฟล์นี้ถูกใช้งานร่วมกันระหว่างหลายเครื่อง
ตัวรัน (runner) จะแสดงข้อมูลเดียวกันนี้สำหรับผู้ใช้งาน:
run 2026-08-05T09:14:22Z sha 4f1c9ab model claude-sonnet-5 58/60 pass (96.7%)
FAIL refund-double-charge deterministic: tool not called: create_refund
FAIL pto-policy-question judge(opus): reply gives no dollar amountให้รันชุดทดสอบกับการเปลี่ยนแปลงที่อาจส่งผลกระทบต่อ agent ซึ่งหมายถึงการแก้ไข prompt, การเปลี่ยน model และการเปลี่ยนเครื่องมือ แทนที่จะรันทุก commit ใน repository โดยใช้ pre-push hook เพื่อครอบคลุมชุดทดสอบย่อยที่รวดเร็ว:
cat > .git/hooks/pre-push <<'EOF'
#!/bin/sh
python3 evals/run.py --set smoke || exit 1
EOF
chmod +x .git/hooks/pre-pushการรันแบบเต็มรูปแบบจะใช้เวลานานกว่าและควรตั้งเวลาไว้ โดยใช้ systemd service และ timer บน VPS เพื่อรันชุดทดสอบทั้งหมดเทียบกับ prompt ที่ใช้งานจริง ซึ่งเป็นวิธีที่ช่วยตรวจจับการเปลี่ยนแปลงที่มาจากภายนอก repository ของคุณ เช่น เครื่องมือที่โฮสต์ไว้ภายนอกซึ่งมีการเปลี่ยนแปลงพฤติกรรมไปจากเดิม
การตรวจสอบโดยมนุษย์แบบสุ่มแทนการตรวจสอบทั้งหมด
ตัวตัดสิน (judge) จะถูกปรับเทียบโดยอ้างอิงจากป้ายกำกับที่มนุษย์กำหนด ดังนั้นจึงจำเป็นต้องมีผู้สร้างป้ายกำกับเหล่านั้น ให้สุ่มอ่านตัวอย่างทุกสัปดาห์ โดยเลือกทุกกรณีที่ตัวตัดสินประมวลผลพลาด รวมกับกรณีที่ผ่านอีก 10 กรณีที่เลือกมาแบบสุ่ม กรณีที่ผ่านแบบสุ่มถือเป็นส่วนที่สำคัญ เพราะตัวตัดสินที่เริ่มปล่อยคำตอบที่ผิดพลาดออกมาอย่างเงียบๆ อาจดูสมบูรณ์แบบบนแดชบอร์ดที่สร้างจากผลการตัดสินของตัวมันเอง
การตรวจสอบ 15 กรณีโดยใช้เวลากรณีละ 3 นาที จะใช้เวลา 45 นาทีต่อสัปดาห์ ซึ่งจะช่วยให้สามารถแก้ไขเกณฑ์การประเมินในส่วนที่คุณและตัวตัดสินมีความเห็นไม่ตรงกัน รวมถึงได้กรณีใหม่ๆ สำหรับรูปแบบความล้มเหลวที่ไม่มีใครคาดคิดมาก่อน ให้บันทึกผลการตัดสินของมนุษย์ลงในตารางเดียวกันโดยตั้งค่า graded_by เป็น human เพื่อให้การตรวจสอบความสอดคล้องระหว่างตัวตัดสินและมนุษย์กลายเป็นการสืบค้นข้อมูล (query) แทนการใช้ความจำ
สิ่งที่ทำให้ระบบประเมินผล (eval harness) ทำงานผิดพลาด
anthropic.RateLimitError ในการรันรอบแรกแบบเต็มรูปแบบ การส่งเคสพร้อมกัน 60 รายการในคราวเดียวเกินขีดจำกัดของจำนวนคำขอหรือโทเค็นในระดับสิทธิ์ของคุณ ให้จำกัดการทำงานพร้อมกันไว้ที่ 4 เวิร์กเกอร์ และย้ายการรันรอบกลางคืนไปใช้ Batch API แทน
json.JSONDecodeError: Expecting value: line 1 column 1 (char 0) จากตัวตัดสิน (judge) โมเดลตอบกลับมาเป็นข้อความปกติ หรือครอบ JSON ไว้ใน code fence ให้ลองใหม่หนึ่งครั้ง จากนั้นจึงบันทึกเคสนั้นเป็นข้อผิดพลาด ห้ามปล่อยให้ความล้มเหลวในการ parse ถูกนับเป็นผ่าน เพราะชุดทดสอบที่เปลี่ยนข้อผิดพลาดให้เป็นผลผ่านจะทำให้ค่าความสำเร็จพุ่งเข้าใกล้ 100% ในขณะที่ตัวเอเจนต์กลับทำงานแย่ลง
เคสที่ไม่เสถียร (flaky cases) อินพุตเดียวกันผ่านในการรันรอบหนึ่งแต่ล้มเหลวในรอบถัดไป เนื่องจากเอเจนต์มีการสุ่มผลลัพธ์ ให้รันเคสที่ไม่เสถียรนั้น 3 ครั้งแล้วบันทึกเป็นสัดส่วนแทนที่จะลบเคสนั้นทิ้ง เคสที่ผ่าน 2 ใน 3 ครั้งถือเป็นบั๊กด้านความทนทานที่แท้จริง และลูกค้าจะพบปัญหานี้อย่างแน่นอน
ชุดคำตอบมาตรฐาน (golden set) เสื่อมสภาพ มีคนแก้ไขคำตอบที่คาดหวังเพื่อให้ชุดทดสอบผ่าน ให้ตรวจสอบ diff ของ evals/cases.jsonl อย่างละเอียดเช่นเดียวกับ diff ของตัวเอเจนต์ เพราะไฟล์นั้นคือคำจำกัดความของความถูกต้องที่คุณเขียนไว้
ชุดทดสอบที่ไม่เคยล้มเหลว อัตราการผ่านที่คงอยู่ที่ 100% เป็นเวลาหนึ่งเดือนหมายความว่าชุดทดสอบไม่ได้ติดตามการเปลี่ยนแปลงของผลิตภัณฑ์อีกต่อไป ให้ดึง trace ล่าสุดมา 10 รายการ ค้นหารายการที่เอเจนต์จัดการได้ไม่ดี แล้วเพิ่มเข้าไป จากนั้นให้จงใจทำให้บางอย่างพังเพื่อยืนยันว่าการรันจะแสดงผลเป็นสีแดง ซึ่งเป็นการตรวจสอบ การทดสอบแบบกลายพันธุ์ (mutation testing) ที่ใช้กับชุดทดสอบ และเป็นวิธีเดียวที่จะทราบว่าชุดทดสอบของคุณยังคงมีประสิทธิภาพอยู่
FAQ
ชุดประเมินผล (eval set) สำหรับ AI agent ต้องมีกี่กรณีทดสอบ?
ให้เริ่มที่ 40 ถึง 80 กรณี แล้วค่อยขยายชุดทดสอบจากความล้มเหลวที่เกิดขึ้นจริง หากมีกรณีทดสอบน้อยกว่า 20 ผลลัพธ์ที่แปรปรวนเพียงครั้งเดียวจะทำให้ค่าอัตราความสำเร็จ (pass rate) แกว่งไปถึง 5 จุด ซึ่งจะทำให้ตัวเลขดังกล่าวขาดความน่าเชื่อถือ หากเกินกว่าไม่กี่ร้อยกรณี การรันแต่ละครั้งจะสิ้นเปลืองทั้งเงินและเวลาจริง ในขณะที่กรณีทดสอบส่วนเพิ่มให้ความครอบคลุมเพียงเล็กน้อย สิ่งที่สำคัญไม่ใช่จำนวน แต่คือสัดส่วนของประเภทความล้มเหลวใน production ที่คุณรู้จัก ซึ่งควรปรากฏอยู่ในชุดทดสอบอย่างน้อยหนึ่งครั้ง
ฉันสามารถเชื่อใจ LLM judge ในการให้คะแนน agent ของฉันได้หรือไม่?
เชื่อได้ก็ต่อเมื่อคุณได้วัดผลเทียบกับชุดข้อมูลที่คุณติดป้ายกำกับ (labels) ไว้เองแล้วเท่านั้น ให้เก็บกรณีทดสอบ 30 กรณีที่คุณให้คะแนนด้วยตนเองไว้ และใช้ทดสอบ judge ทุกครั้งที่คุณเปลี่ยนโมเดลหรือเปลี่ยน prompt ของ judge ตัว judge มักแสดงอคติเรื่องความยาว (length bias) ซึ่งคำตอบที่ยืดเยื้อจะผ่านบ่อยกว่า และอคติเรื่องความชอบในโมเดลตระกูลเดียวกัน (self-preference) ซึ่งผลลัพธ์จากโมเดลตระกูลเดียวกับมันจะได้รับคะแนนที่ใจดีกว่า ทั้งสองกรณีนี้สามารถทดสอบได้โดยการเพิ่มเนื้อหาเข้าไปในคำตอบที่ล้มเหลวแล้วให้ judge ตรวจใหม่ หรือใช้ judge จากตระกูลอื่นมาตรวจคำตอบชุดเดียวกัน หาก judge ให้คะแนนไม่ตรงกับป้ายกำกับของคุณเกินกว่า 1 ใน 10 กรณี แสดงว่าเกณฑ์การให้คะแนน (rubric) นั้นคลุมเครือเกินกว่าจะนำไปใช้งาน
ควรใช้โมเดลใดในการตรวจประเมินผล?
ให้ตรวจด้วยวิธีที่ประหยัดก่อนแล้วค่อยขยับขยาย การตรวจสอบแบบกำหนดเงื่อนไขตายตัว (deterministic assertions) ไม่มีค่าใช้จ่าย จึงควรให้รันเป็นอันดับแรกในทุกกรณี ส่วนโมเดลขนาดเล็กสามารถจัดการกับกรณีที่ผ่านชัดเจนได้ และส่งเฉพาะกรณีที่ล้มเหลวหรือกรณีที่โมเดลมีความมั่นใจต่ำไปยังโมเดลระดับแนวหน้า (frontier model) ณ ราคาตลาดในเดือนสิงหาคม 2026 การตรวจประเมิน 1,000 กรณีจะมีค่าใช้จ่ายประมาณ 1.80 ดอลลาร์สหรัฐด้วย Claude Haiku 4.5 และประมาณ 9.00 ด้วย Claude Opus 5 และเนื่องจากการรัน eval เป็นแบบอะซิงโครนัส การใช้ Batch API จะช่วยลดค่าใช้จ่ายทั้งสองกรณีลงได้ครึ่งหนึ่ง
การประเมินผล (evals) สามารถทดแทนการตรวจสอบใน production (production monitoring) ได้หรือไม่?
ไม่ได้ เพราะทั้งสองอย่างตอบคำถามที่แตกต่างกัน ชุดการประเมินผลจะบอกคุณว่าการเปลี่ยนแปลงที่คุณกำลังจะนำไปใช้งานจริงนั้น ทำให้ชุดกรณีทดสอบที่กำหนดไว้ดีขึ้นหรือแย่ลง ส่วนการติดตาม (tracing) และการตรวจสอบ (monitoring) จะบอกคุณว่าผู้ใช้จริงกำลังประสบปัญหาอะไรอยู่ในขณะนี้ รวมถึงข้อมูลนำเข้าที่ไม่มีกรณีทดสอบใดครอบคลุม ทั้งสองอย่างนี้ส่งเสริมซึ่งกันและกัน โดย traces จะจัดหากรณีทดสอบใหม่ๆ ให้ และชุดการประเมินผลจะตัดสินว่าการแก้ไขของคุณได้ผลจริงหรือไม่