Loop คือของจริง ไม่ใช่ตัว generate: ทำไม AI ที่เขียนโค้ดได้ถึงไร้ค่าถ้าไม่มี loop ที่ตรวจตัวเอง
ผมสร้างระบบให้ AI agent หลายตัวทำงานวิศวกรรมเองเป็น loop — และบทเรียนที่แพงที่สุดคือ "โค้ดที่ AI เขียน" ไม่ใช่ของมีค่า สิ่งที่มีค่าคือ closed loop ที่ตรวจ+แก้จนผ่าน gate จริง กับกับดักที่ทำให้ loop ไม่มีวันบรรจบ
อ่าน ~9 นาที
Thesis: ตัว generate ถูกที่สุดใน pipeline — loop ที่ตรวจตัวเองต่างหากที่แพง
ตอนผมเริ่มสร้างระบบให้ AI agent หลายตัวทำงานวิศวกรรมแทนผม (วางแผน → เขียน → รีวิว → แก้ → ทดสอบ) ผมนึกว่าของยากคือ "ทำให้ AI เขียนโค้ดเก่ง". ผิดถนัด. โมเดลสมัยนี้เขียนโค้ดที่ดูดีได้ในไม่กี่วินาที — ตัว generate คือส่วนที่ถูกและง่ายที่สุดของทั้งระบบ.
ของที่แพงจริงคือ loop — วงจรที่เอา output ของ AI ไปตรวจว่าถูกจริงไหม ถ้าไม่ถูกก็ป้อนกลับให้แก้ แล้ววนจนกว่าจะผ่านเกณฑ์. บทเรียนหลักของบทความนี้: "AI เขียนโค้ดได้" ไม่ใช่คุณค่า. loop ที่ทำให้โค้ดนั้นถูกเองต่างหากคือคุณค่า. และ loop ที่ออกแบบผิดจะเผาเวลา/เงินไปเรื่อยๆ โดยไม่เข้าใกล้ความถูกต้องเลย.
Open-loop vs closed-loop: gate จริง ไม่ใช่ "generate แล้วหวัง"
ระบบ AI ส่วนใหญ่ที่ผมเห็นเป็น open loop: prompt → generate → เอาไปใช้เลย. มันเหมือนยิงปืนแล้วหลับตา — ไม่มีอะไรบอกว่าโดนเป้าหรือเปล่า. closed loop เพิ่มสิ่งเดียวที่เปลี่ยนทุกอย่าง: exit gate ที่วัดผลจริง แล้ววนกลับถ้าไม่ผ่าน.
// open loop — อันตราย
const code = await ai.generate(task);
apply(code); // หวังว่าถูก 🙏
// closed loop — วนจน gate ผ่านจริง
let attempt = 0;
while (attempt++ < MAX_ROUNDS) {
const code = await ai.generate(task, feedback);
const gate = await verify(code); // รัน test/build/review จริง
if (gate.passed) return code; // ออกเฉพาะเมื่อพิสูจน์ได้
feedback = gate.report; // ป้อนความผิดกลับไปให้แก้
}
throw new Error("ไม่บรรจบ — ส่งต่อให้คน");
หัวใจคือคำว่า verify() ต้องรันของจริง — test ที่ผ่าน, build ที่เขียว, browser ที่เปิดได้ — ไม่ใช่ให้ AI ตอบว่า "น่าจะถูกแล้วครับ". กติกาที่ผมยึดคือ "proof or it's not done": ถ้ายังไม่มีหลักฐานว่าพฤติกรรมถูก ก็ยังไม่ถือว่าเสร็จ ต่อให้โค้ดดูดีแค่ไหน.
บทเรียนที่แพงที่สุด: แยก "งานพัง" ออกจาก "ระบบพัง"
นี่คือบั๊กเชิงออกแบบที่กัดผมหนักที่สุด. loop มี budget จำกัด (เช่นแก้ได้ 3 รอบ ไม่ผ่านค่อยเรียกคน). ปัญหาคือตอนแรกผมนับความล้มเหลวทุกชนิดเข้า budget เดียวกัน. ผลคือ: งานที่ implementation ผ่านหมดแล้ว แต่ตัว reviewer ดันสตาร์ทไม่ขึ้น (network flake) — ระบบนับเป็น "แก้ไม่ผ่าน 1 รอบ" ทั้งที่งานไม่ได้ผิดเลย. พอ flake 3 ครั้ง งานที่ถูกต้องก็ถูกโยนทิ้งเพราะ budget หมด.
บทเรียน: ความล้มเหลวมีสองพันธุ์ที่ต้องแยกขาดกัน
- work failure — งานผิดจริง (test แดง, build พัง, reviewer เจอบั๊ก) → นับเข้า revision budget, ป้อน feedback ให้แก้
- system failure — โครงสร้างพัง (worker สตาร์ทไม่ขึ้น, timeout, quota เต็ม, provider ล่ม) → retry ที่ระดับ infra ห้ามแตะ budget ของงาน และห้ามเปลี่ยนสถานะงานที่ผ่านแล้ว
const gate = await verify(code);
if (gate.kind === "work_failure") {
budget.revisions--; // งานผิดจริง → กินโควตาแก้
feedback = gate.report;
} else if (gate.kind === "system_failure") {
await backoffRetryInfra(); // infra พัง → ลองใหม่ ไม่กินโควตางาน
// สถานะงานคงเดิม (ยังเป็น IMPLEMENTED/PASSED อยู่)
}
ถ้า loop ของคุณลงโทษงานเพราะ "เครือข่ายกระตุก" นั่นคือ loop ที่ปลอมผลลัพธ์. infra flake ต้องไม่ทำลาย evidence ที่ผ่านไปแล้ว.
reviewer ล่ม ≠ งานไม่ผ่าน: แยก "verdict" ออกจาก "ไม่มี verdict"
ต่อยอดจากข้อบน — นี่คือกรณีที่ subtle ที่สุด. ในระบบผม reviewer เป็น AI อีกตัวที่ตรวจงานของ implementer. วันหนึ่ง reviewer เปิด session ขึ้นมาแล้ว process ตายก่อนได้อ่านโจทย์ด้วยซ้ำ. โค้ด orchestration ตีความว่า "reviewer ไม่ผ่าน" เลยเปลี่ยนงานเป็น NEEDS_REVISION ทั้งที่ reviewer ยังไม่เคยตรวจเลย.
ผลลัพธ์สามสถานะที่ต้องแยกให้เด็ดขาด — อย่ายุบเหลือ boolean "ผ่าน/ไม่ผ่าน":
- PASS — ตรวจแล้ว ผ่าน
- FAIL — ตรวจแล้ว เจอปัญหา (นี่คือ work failure จริง)
- NO_VERDICT — ตรวจไม่สำเร็จ (crash/timeout ก่อนได้ผล) → เป็น system failure ต้อง retry ไม่ใช่ตัดสินว่างานแย่
"ไม่ได้คำตอบ" กับ "คำตอบว่าไม่ผ่าน" เป็นคนละเรื่องกันโดยสิ้นเชิง. ระบบที่รวมสองอย่างนี้เข้าด้วยกันจะกล่าวโทษงานดีเพราะเครื่องมือตรวจมันพังเอง.
loop ต้องบรรจบ: bounded iteration + เกณฑ์ออก + เส้นชนคน
closed loop ที่ไม่มีเงื่อนไขหยุดคือ infinite loop ที่เผาเงิน. loop ที่ดีต้องมีสามอย่าง:
- เพดานรอบ — เช่นแก้ได้สูงสุด 3 รอบ (เฉพาะ work failure) เกินนั้นถือว่าไม่บรรจบ
- เกณฑ์ออกที่วัดได้ — ไม่ใช่ "รู้สึกว่าดีแล้ว" แต่เป็นเช่น "0 blocker + 0 major + confidence ≥ 9/10 + test เขียวครบ"
- เส้นชนคน (escalation) — พอไม่บรรจบ ต้องหยุดแล้วส่งต่อให้มนุษย์พร้อม context ไม่ใช่วนต่อเงียบๆ หรือ apply ของที่ยังไม่ผ่าน
function shouldExit(gate, round) {
if (gate.blockers === 0 && gate.majors === 0 &&
gate.confidence >= 0.9 && gate.testsGreen) {
return { done: true, ok: true }; // บรรจบ ผ่าน
}
if (round >= MAX_ROUNDS) {
return { done: true, ok: false, escalate: true }; // ยอมแพ้ ส่งต่อคน
}
return { done: false }; // วนต่อ
}
จุดสำคัญที่คนมองข้าม: "ยอมแพ้แล้วเรียกคน" คือฟีเจอร์ ไม่ใช่ความล้มเหลว. loop ที่รู้ตัวว่าตันแล้วขอความช่วยเหลือ ดีกว่า loop ที่มั่นใจส่งของพังออกไป.
ความหลากหลายใน loop: คนตรวจต้องคนละมุมกับคนทำ
ถ้า implementer กับ reviewer เป็นโมเดลตัวเดียวกัน มันจะ "เห็นชอบงานตัวเอง" — bug แบบเดียวกับที่มันสร้าง มันก็มองไม่เห็น. loop ที่ตรวจด้วยสายตาชุดเดิมคือ loop ที่รับรองความผิดของตัวเอง.
สิ่งที่ได้ผลจริงคือ บังคับความต่าง: ให้ reviewer เป็นคนละ model family กับ implementer และให้ prompt reviewer ในเชิง "หาเรื่องให้เจอ" (adversarial) ไม่ใช่ "ช่วยดูหน่อย". ยิ่งถ้ามีหลาย lens (correctness, security, performance) แยกกันตรวจ ยิ่งจับ failure mode ที่ lens เดียวมองข้าม. หลักการนี้ generalize ได้แม้ไม่มี AI: code review ที่ผู้เขียน approve เองก็ไม่ต่างอะไรกับไม่มี review.
สรุป
- ตัว generate คือส่วนที่ถูกที่สุด — คุณค่าอยู่ที่ loop ที่ตรวจ+แก้จนผ่าน gate จริง
- closed loop ต้องมี exit gate ที่วัดของจริง (test/build/review) ไม่ใช่ให้ AI เคลมว่าเสร็จ
- แยก work failure ออกจาก system failure — infra กระตุกห้ามกิน revision budget หรือทำลาย evidence ที่ผ่านแล้ว
- reviewer ล่ม ≠ งานไม่ผ่าน — แยก PASS / FAIL / NO_VERDICT อย่ายุบเหลือ boolean
- loop ต้องบรรจบ — เพดานรอบ + เกณฑ์ออกที่วัดได้ + ยอมแพ้แล้วส่งต่อคนเมื่อตัน
- คนตรวจต้องคนละมุม/คนละ model กับคนทำ ไม่งั้น loop แค่รับรองความผิดตัวเอง




