Agent รายงานว่าเสร็จแล้ว แต่ฟีเจอร์ไม่ทำงาน: ทำไมถึงเกิดขึ้นและจะตรวจสอบงานของ AI agent อย่างไร
Agent รายงานว่า «เสร็จแล้ว» เพราะมันประเมินงานของตัวเองและเห็นเฉพาะสิ่งที่ตรวจไปแล้ว ซึ่งมักเป็นยูนิตเทสต์ที่มองไม่เห็นจุดเชื่อมต่อระหว่างโมดูล migration การตั้งค่า และสถานการณ์ปลายทางถึงปลายทาง การแก้ไขต้องใช้โครงสร้าง ไม่ใช่พรอมป์ต ได้แก่ Definition of Done ใน AGENTS.md การบังคับรัน E2E สคริปต์ตรวจสอบที่เป็นผู้เดียวที่ตั้งสถานะ passing และผู้ประเมินอิสระที่มีบริบทสะอาด
ทำไมถึงเกิดขึ้น
สถานการณ์นี้คุ้นเคยสำหรับทุกคนที่ทำงานกับ agent: agent รายงานว่า «เสร็จแล้ว» แต่ฟีเจอร์ไม่ทำงาน มันไม่ได้โกหกโดยเจตนา มันรายงานสิ่งที่ตรวจไปแล้ว และเชื่อสนิทใจว่านั่นเพียงพอ ช่องว่างระหว่างคำแถลงกับความเป็นจริงเป็นคุณสมบัติของรูปแบบ «ผู้ทำประเมินงานของตัวเอง» เอง ไม่ใช่ข้อบกพร่องของโมเดลตัวใดตัวหนึ่ง
มีสามสาเหตุ สาเหตุแรก โมเดลมีแนวโน้มมั่นใจเกินจริง ความมั่นใจของคำตอบสูงกว่าความแม่นยำของมัน สาเหตุที่สอง agent ประเมินงานในบริบทเดียวกับที่ทำ มันโน้มน้าวตัวเองไปแล้วว่าคำตอบถูก และคำขอให้ «ตรวจอย่างเป็นกลางกว่านี้» ไม่ได้เปลี่ยนบริบทนั้น สาเหตุที่สามซึ่งปฏิบัติจริงที่สุด agent เข้าถึงการตรวจสอบได้เพียงบางส่วน: ยูนิตเทสต์เขียวได้เร็ว ส่วนสถานการณ์ปลายทางถึงปลายทางไม่มีใครรัน
บันทึกเหตุการณ์ «เสร็จ» ที่ผิดพลาดแบบฉบับ
Agent ได้รับมอบหมายให้ทำการรีเซ็ตรหัสผ่าน มันเปลี่ยน schema ของฐานข้อมูล เพิ่ม endpoint เชื่อมเทมเพลตอีเมล และรันยูนิตเทสต์ ทุกอย่างเขียว ในแชตปรากฏคำว่า «เสร็จแล้ว» อย่างมั่นใจ
ความจริงที่เกิดขึ้นคือ:
- migration ล้มกลางคันและทิ้ง schema ไว้ในสถานะไม่สอดคล้องกัน
- เทมเพลตอีเมลมีอยู่ แต่บริการส่งอีเมล (SMTP) ยังไม่ได้ตั้งค่าในสภาพแวดล้อม อีเมลจึงไม่ถูกส่ง
- สถานการณ์ «ผู้ใช้คลิกลิงก์ → กรอกรหัสผ่านใหม่ → ล็อกอิน» ไม่เคยถูกรันเลย
ยูนิตเทสต์ไม่สามารถจับปัญหาเหล่านี้ได้เลย เพราะฐานข้อมูลถูก mock การส่งอีเมลถูก mock และไม่มีการรันปลายทางถึงปลายทางเลย
ทำไมเทสต์เขียวจึงไม่พิสูจน์ว่าฟีเจอร์ทำงาน
ยูนิตเทสต์ตรวจสอบโมดูลแบบแยกโดด ส่วนประกอบถูกแทนด้วย mock และโลกภายนอกถูกตัดออก สิ่งนี้ให้ความเร็วและความแม่นยำ แต่ก็ทำให้เกิดจุดบอดอย่างเป็นระบบ จุดบอดนี้ไม่ใช่บั๊กของเทสต์ แต่เป็นการออกแบบของมัน ยูนิตเทสต์เขียวพิสูจน์ได้เพียงว่าแต่ละรายละเอียดทำงานแยกกันได้ ไม่ได้พิสูจน์ว่ารายละเอียดเหล่านั้นทำงานร่วมกันได้
มีข้อบกพร่องสี่ประเภทที่ยูนิตเทสต์มองไม่เห็นโดยหลักการ:
| ประเภทบั๊ก | ตัวอย่าง | ทำไมยูนิตเทสต์จับไม่ได้ |
|---|---|---|
| อินเทอร์เฟซไม่ตรงกัน | ส่วนหนึ่งส่งคืน path แบบสัมพัทธ์ อีกส่วนต้องการ path แบบสัมบูรณ์ | แต่ละส่วนถูกเทสต์ด้วย mock ของตัวเอง ซึ่ง «ยอม» รับทุกรูปแบบ |
| การแพร่กระจายของสถานะ | schema ของฐานข้อมูลเปลี่ยนไป แต่แคชของ ORM ยังเก็บของเก่า | ยูนิตเทสต์ไม่มีฐานข้อมูลจริงและไม่มีแคช |
| วงจรชีวิตของทรัพยากร | ตัวอธิบายไฟล์และการเชื่อมต่อไม่ถูกปล่อย หน่วยความจำรั่วกับไฟล์ขนาดใหญ่ | เทสต์รันกับข้อมูลเล็กและจบก่อนที่ทรัพยากรจะหมด |
| สภาพแวดล้อมและการตั้งค่า | ไม่มีตัวแปรสภาพแวดล้อม SMTP ยังไม่ได้ตั้งค่า migration ยังไม่ถูกนำไปใช้ | เทสต์ไม่ได้เปิดแอปด้วยการตั้งค่าจริง |
ระดับการตรวจสอบซ้อนกันเป็นชั้น: สิ่งที่ยูนิตจับได้ การทดสอบเชิงบูรณาการก็จับได้ และสิ่งที่การทดสอบเชิงบูรณาการจับได้ การรัน E2E ก็จับได้เช่นกัน แต่ละระดับถัดไปมองเห็นมากกว่าอย่างเคร่งครัด ดังนั้นสิทธิ์ในการประกาศว่า «เสร็จแล้ว» ควรเป็นของระดับบนสุดเท่านั้น
วัดปัญหา: verification gap
ก่อนแก้ไข ให้นับก่อน หยิบงาน 10 ชิ้นล่าสุดที่ agent ทำเสร็จ สำหรับแต่ละชิ้นตอบสองคำถาม: agent รายงานว่า «เสร็จแล้ว» หรือไม่ และการตรวจสอบอิสระยืนยันหรือไม่ (คุณทดสอบสถานการณ์เอง หรือเทสต์ E2E ผ่าน) สัดส่วนของกรณีที่ไม่ตรงกันคือ verification gap ของคุณ
ตัวเลขนี้มีประโยชน์ด้วยเหตุผลสองประการ มันแสดงขนาดของปัญหาโดยไม่ต้องเถียงกันว่า «ดูเหมือนจะทำงานได้» และมันเป็นจุดอ้างอิง: หลังการเปลี่ยนแปลงทุกครั้งในกระบวนการ ให้คำนวณใหม่บนชุดงานที่คล้ายกันและดูว่าดีขึ้นหรือไม่
การตรวจสอบจริง: สามชั้นของการยืนยัน
การเปรียบเทียบที่เข้าใจง่ายคือศูนย์บริการรถยนต์ รถต้องผ่านสามจุด และข้ามจุดใดไม่ได้
- การวินิจฉัย (ไม่กี่วินาที): ไวยากรณ์ ชนิดข้อมูล linter จับพิมพ์ผิดและข้อผิดพลาดหยาบ ๆ ได้
- แท่นทดสอบ (ไม่กี่นาที): ยูนิตเทสต์และเทสต์เชิงบูรณาการ แอปเริ่มทำงานได้ เครื่องยนต์ทำงาน แต่รถยังจอดอยู่กับที่
- ทดลองขับ: สถานการณ์ปลายทางถึงปลายทางตามที่ผู้ใช้ทำจริง ผ่าน API เบราว์เซอร์ หรือ CLI พร้อมฐานข้อมูลและการตั้งค่าจริง
หลักการลำดับ: ห้ามขับทดลองขณะที่การวินิจฉัยยังมีข้อผิดพลาดอยู่ และห้ามส่งมอบรถโดยไม่ผ่านการทดลองขับ ยูนิตเทสต์ที่เขียวคือจุดที่สองที่ผ่านแล้ว ไม่ใช่จุดที่สาม
สามารถรวมทุกอย่างไว้ที่จุดเข้าเดียวได้:
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
ที่นี่ E2E เปิดฐานข้อมูลจริงและเซิร์ฟเวอร์อีเมลทดสอบ ใช้ migration และรันสถานการณ์ทั้งหมด migration ที่ล้มเหลวหรืออีเมลที่ยังไม่ได้ตั้งค่าจะปรากฏทันที ไม่ใช่ที่ลูกค้า
Definition of Done ใน AGENTS.md
agent ไม่ควรตัดสินเองว่า «เสร็จ» หมายความว่าอะไร เกณฑ์ถูกเขียนลงในไฟล์คำสั่งและกำหนดให้ตรวจสอบได้:
## Definition of Done
ฟีเจอร์จะถือว่าเสร็จเมื่อผ่านครบ "ทุก" ข้อต่อไปนี้:
1. make check จบด้วยรหัส 0
2. make e2e จบด้วยรหัส 0 และมีสถานการณ์ของฟีเจอร์ใน tests/e2e
3. migration ใช้ได้กับฐานข้อมูลสะอาดและย้อนกลับได้
4. ตัวแปรสภาพแวดล้อมใหม่ถูกอธิบายใน .env.example
5. PROGRESS.md บันทึกสิ่งที่ทำและวิธีการตรวจสอบ
สิ่งที่ "ไม่ใช่" ตัวบ่งชี้ว่าเสร็จ:
- «เขียนโค้ดแล้วและดูถูกต้อง»
- «ยูนิตเทสต์ผ่าน»
- «ในเครื่องของฉันทำงานได้»
รายการที่ชัดเจนของสิ่งที่ ไม่ ถือเป็นความพร้อมได้ผลดีกว่าที่คิด agent จะหยุดไม่ไปจบแค่การรันเขียวครั้งแรก
ใครมีสิทธิ์ตั้งสถานะ «passing»
ขั้นตอนต่อไปคือเอาสถานะความพร้อมออกจากมือ agent โดยสมบูรณ์ ฟีเจอร์เก็บไว้ในรายการ (เช่น feature_list.json) พร้อมฟิลด์ «พฤติกรรม» «คำสั่งตรวจสอบ» «สถานะ» มีเพียงสคริปต์เท่านั้นที่เปลี่ยนฟีเจอร์เป็น passing: มันรัน make check รวม แล้วรันคำสั่งตรวจสอบของฟีเจอร์นั้น และเมื่อสำเร็จจึงบันทึกสถานะพร้อมแฮชของ commit เป็นหลักฐาน ถ้าไม่ผ่าน ฟีเจอร์ยังคง active ไม่มีคำว่า «เกือบได้แล้ว»
{
"id": "F04",
"behavior": "ผู้ใช้ได้รับอีเมลพร้อมลิงก์และเปลี่ยนรหัสผ่าน",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
มีกับดักอยู่ตรงนี้: สคริปต์ตรวจสอบเพียงสิ่งที่เขียนไว้ในฟิลด์ verification ถ้าการตรวจสอบเชิงสถาปัตยกรรมอยู่ใน Definition of Done แต่ไม่อยู่ในคำสั่งของฟีเจอร์ มันจะกลายเป็นช่องโหว่ที่มองไม่เห็น ดังนั้น make check รวมต้องครอบคลุมการตรวจสอบบังคับทั้งหมด ไม่ใช่เพียงเทสต์
ผู้ประเมินอิสระ
คำรายงานของผู้ทำไม่เคยเป็นแหล่งความจริงอิสระ วิธีแก้เชิงโครงสร้างคือแยกบทบาท:
- ผู้วางแผน แตกข้อกำหนดเป็นฟีเจอร์พร้อมเกณฑ์การยอมรับ และไม่เขียนโค้ด
- ผู้สร้างโค้ด ลงมือทำหนึ่งฟีเจอร์และรันเทสต์ แต่ไม่ตัดสินเองว่าเสร็จหรือยัง
- ผู้ประเมิน ออกคำตัดสินตามรายการเกณฑ์
ผู้ประเมินต้องไม่เห็นเหตุผลของผู้สร้างโค้ด ไม่เช่นนั้นมันจะรับความมั่นใจของผู้สร้างมาด้วย มันรับเพียงข้อกำหนด diff และผลการรัน ใน Claude Code ทำได้ง่ายด้วย subagent ซึ่งมีบริบทสะอาดของตัวเอง และดียิ่งขึ้นถ้าผู้ประเมินเป็นโมเดลอื่น เพราะมันจะมีจุดบอดต่างกัน การประเมินแบบ «ดูโอเคดี» ไม่อนุญาต ทุกข้อสรุปต้องอ้างอิงไฟล์ บรรทัด หรือ log
ข้อความแจ้งข้อผิดพลาดที่ agent แก้ไขเองได้
การตรวจสอบมีประโยชน์ก็ต่อเมื่อผลลัพธ์นำไปสู่การแก้ไข «Test failed» หรือ AssertionError เปล่า ๆ ไม่บอกอะไร agent ข้อความที่ดีบอกว่าอะไรพัง ทำไมกฎนี้จึงมีอยู่ และแก้อย่างไร:
ERROR arch-check: app/services/refund.py import app/db/session.py
WHY: service ไม่เข้าถึงฐานข้อมูลโดยตรง ต้องผ่าน repository เท่านั้น
FIX: ใช้ RefundRepository จาก app/repositories/refund.py
รูปแบบนี้เปลี่ยนการตรวจสอบจากข้อห้ามเป็นคำแนะนำ agent แก้การละเมิดได้เองโดยไม่ต้องให้คุณเข้ามาช่วย
การตรวจสอบด้วยมือ: ยังจำเป็นตรงไหน
ระบบอัตโนมัติไม่ได้ตัดมนุษย์ออก แต่ทำให้บทบาทของมนุษย์แคบลง การตรวจด้วยมือจำเป็นในจุดที่เกณฑ์ยากจะทำให้เป็นทางการ เช่น หน้าตาของอินเทอร์เฟซ ข้อความ และความสะดวกของสถานการณ์ ให้ทำตามรายการเกณฑ์การยอมรับ อย่าแค่ «คลิกดู» ถ้าคุณเขียนคอมเมนต์เดิมให้ agent เป็นครั้งที่สอง นั่นคือสัญญาณให้เปลี่ยนมันเป็นการตรวจสอบอัตโนมัติ: เทสต์ กฎ linter หรือ grep guard ใน make check
E2E เปลี่ยนพฤติกรรมของ agent แม้ก่อนรัน
คุณค่าของเทสต์ปลายทางถึงปลายทางไม่ได้อยู่แค่บั๊กที่จับได้ ถ้า agent รู้ล่วงหน้าว่างานจะถูกตรวจด้วยสถานการณ์ E2E มันจะเขียนโค้ดต่างออกไป: คิดถึงสัญญากับส่วนประกอบข้างเคียง ไม่ใช่แค่ฟังก์ชันของตัวเอง จัดการเส้นทางข้อผิดพลาด และไม่ละเมิดขอบเขตสถาปัตยกรรม เพราะการละเมิดจะปรากฏตอนรัน การตรวจสอบทำหน้าที่เป็นข้อจำกัดที่ทางเข้า ไม่ใช่เพียงตัวกรองที่ทางออก
มีเงื่อนไข: E2E มีความหมายเมื่อระบบมีขอบเขตที่ชัดเจน บนสถาปัตยกรรมแบบสปาเกตตี เทสต์ปลายทางถึงปลายทางพิสูจน์ได้เพียงว่าสปาเกตตียังทำงานอยู่ และไม่บอกว่าเจตนาการออกแบบถูกละเมิดตรงไหน ดังนั้นต้องกำหนดชั้นและกฎการพึ่งพาก่อน แล้วจึงสร้างสถานการณ์ปลายทางถึงปลายทาง agent ชอบคัดลอกรูปแบบที่มีอยู่แล้วใน repository แม้เป็นรูปแบบที่ไม่ดี ดังนั้นควรใส่ข้อจำกัดตั้งแต่วันแรก
ความผิดพลาดที่พบบ่อย
- ขอให้ agent «ตรวจอย่างละเอียดกว่านี้» บริบทยังเหมือนเดิม การประเมินจะยังถูกประเมินเกินจริง
- ถือว่าการครอบคลุม 100% คือการันตี การครอบคลุมด้วยยูนิตเทสต์ไม่ได้ครอบคลุมจุดเชื่อมต่อ
- รีแฟกเตอร์ก่อนยืนยันฟีเจอร์ ลำดับเข้มงวด: ความถูกต้อง → ประสิทธิภาพ → สไตล์
- mock ใน E2E ในสิ่งที่พังบ่อยที่สุด ฐานข้อมูล migration และการตั้งค่าในเทสต์ปลายทางถึงปลายทางต้องรันจริง บริการ API ภายนอกที่มีค่าใช้จ่ายสามารถแทนด้วยเซิร์ฟเวอร์ทดสอบได้
สรุป
เมื่อ agent รายงานว่า «เสร็จแล้ว» แต่ฟีเจอร์ไม่ทำงาน ปัญหามักอยู่ที่กระบวนการตรวจสอบ ไม่ใช่ที่โมเดล วัด verification gap เขียน Definition of Done ลงใน AGENTS.md ทำให้ E2E เป็นข้อบังคับ มอบสถานะความพร้อมให้สคริปต์ และเพิ่มผู้ประเมินอิสระ วิธีนี้ถูกอธิบายในทางปฏิบัติในหลักสูตร harness engineering
คำถามที่พบบ่อย
สามารถให้ agent รัน E2E เองได้ไหม?
ได้ หากคำสั่งถูกเขียนใน AGENTS.md เป็นส่วนบังคับของ Definition of Done และ agent มีเครื่องมือที่จำเป็น เพื่อความน่าเชื่อถือมากขึ้น ควรรันเพิ่มด้วย hook หรือใน CI เพื่อไม่ต้องพึ่งความจำของ agent
E2E ช้า ต้องรันทุกครั้งที่แก้ไขหรือไม่?
ไม่ต้อง lint ชนิดข้อมูล และยูนิตเทสต์รันเสมอ ส่วน E2E รันเมื่อการเปลี่ยนแปลงกระทบมากกว่าหนึ่งชั้น และต้องรันก่อนประกาศว่าฟีเจอร์เสร็จ
จะตรวจสอบส่วนหน้าที่ agent ทำได้อย่างไร?
ด้วยเทสต์ปลายทางถึงปลายทางผ่านเบราว์เซอร์ (เช่น Playwright) ที่คลิกบนอินเทอร์เฟซจริง ส่วนหน้าตาของอินเทอร์เฟซตรวจด้วยมือตามรายการ หรือผ่านภาพหน้าจอให้ผู้ประเมินดู
การรีวิวโค้ดโดยคนใช้แทนผู้ประเมินได้ไหม?
การรีวิวโดยคนมีประโยชน์ แต่มีค่าใช้จ่ายสูงและขยายไม่ได้สำหรับการทำงานอัตโนมัติหลายชั่วโมง ผู้ประเมินที่มี rubric จะกรองปัญหาที่ชัดเจนก่อนถึงคน และคนตัดสินคำถามที่ถกเถียงกัน
หัวข้อนี้สอนอย่างละเอียดพร้อมฝึกปฏิบัติในคอร์ส “Harness Engineering: инженерия надёжности AI-агентов”
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента