บทที่ 5 จาก 12 · Agentic Engineering — คู่มือปฏิบัติเขียนซอฟต์แวร์ระดับ Production ด้วย AI Agent
Loop, Gate และ Red / Green
Gate คือหัวใจของทั้งระบบ ถ้าอ่อน agent จะเรียนรู้ที่จะทำให้เขียวโดยไม่ทำให้โปรแกรมถูก บทนี้มี verify.sh แบบเต็ม สคริปต์กันการโกง gate ลำดับ red-green ที่ต้องบังคับ และตัวขับ loop ที่มีเพดานกับการตรวจจับการไม่คืบหน้า
Gate คือหัวใจของทั้งระบบ ถ้า gate อ่อน agent จะเรียนรู้ที่จะทำให้ gate เขียวโดยไม่ทำให้โปรแกรมถูก ถ้า gate แข็งเกินหรือช้าเกิน loop จะปิดไม่ได้และคนจะเลิกใช้
5.1 คุณสมบัติของ gate ที่ดี
| คุณสมบัติ | ความหมายในทางปฏิบัติ |
|---|---|
| Deterministic | รันสิบครั้งได้ผลเหมือนกัน ไม่มี test ที่ขึ้นกับเวลา เครือข่าย หรือลำดับ |
| Fast enough | ชุดเร็วต่ำกว่า 60 วินาที ชุดเต็มต่ำกว่า 10 นาที ถ้าช้ากว่านี้ loop จะพัง |
| Loud | error บอกว่าอะไรผิด ที่ไหน คาดหวังอะไร ได้อะไรมา ไม่ใช่แค่ exit 1 |
| Layered | แบ่งเป็น stage ที่หยุดทันทีเมื่อเจอ error ไม่ต้องรอผลชุดหลัง |
| Tamper-evident | ไฟล์ gate อยู่นอกขอบเขตที่ agent แก้ได้ และ CI รันชุดเดียวกับ local |
5.2 verify.sh — นิยามของคำว่า “ผ่าน”
#!/usr/bin/env bash
# GATE — แหล่งความจริงเดียวของคำว่า "งานเสร็จ"
# ไฟล์นี้ห้าม agent แก้ (บังคับด้วย PreToolUse hook + CODEOWNERS)
set -uo pipefail
FAILED=0
STAGE_LOG=$(mktemp)
run_stage () {
local name="$1"; shift
printf "\n\033[1m▸ %s\033[0m\n" "$name"
local start=$SECONDS
if "$@" 2>&1 | tee -a "$STAGE_LOG"; then
printf " \033[32mPASS\033[0m (%ss)\n" "$((SECONDS-start))"
else
printf " \033[31mFAIL\033[0m (%ss)\n" "$((SECONDS-start))"
FAILED=1
[ "${FAIL_FAST:-1}" = "1" ] && finish
fi
}
finish () {
echo
if [ $FAILED -eq 0 ]; then
echo "════ ALL GATES PASSED ════"; exit 0
else
echo "════ GATE FAILED ════"; exit 1
fi
}
# ── STAGE 1: รูปแบบ (เร็วที่สุด จับก่อน) ──────────────────
run_stage "format" uv run ruff format --check .
run_stage "lint" uv run ruff check .
run_stage "types" uv run mypy src --strict
# ── STAGE 2: ความถูกต้องเชิงตรรกะ ────────────────────────
run_stage "unit" uv run pytest tests/unit -q --timeout=30
run_stage "property" uv run pytest tests/property -q --hypothesis-seed=0
# ── STAGE 3: การประกอบร่าง ───────────────────────────────
run_stage "integration" uv run pytest tests/integration -q --timeout=120
run_stage "contract" uv run schemathesis run openapi.json --checks all
# ── STAGE 4: คุณภาพเชิงโครงสร้าง ─────────────────────────
run_stage "coverage" uv run pytest --cov=src --cov-fail-under=80 -q
run_stage "arch-rules" uv run pytest tests/architecture -q # กฎ dependency
run_stage "security" uv run bandit -r src -q -ll
run_stage "deps-audit" uv run pip-audit --strict
run_stage "migrations" ./scripts/check_migrations.sh
# ── STAGE 5: กันการโกง gate ──────────────────────────────
run_stage "no-cheating" ./scripts/check_no_cheating.sh
finish
ตัวตรวจว่าไม่มีการ “ทำให้เขียว” ด้วยการปิดปากเครื่องมือตรวจสอบ:
#!/usr/bin/env bash
set -uo pipefail
BASE="${BASE_REF:-origin/main}"
DIFF=$(git diff "$BASE"...HEAD -- 'src/**' 'tests/**')
BAD=0
check () {
local pattern="$1" msg="$2"
# ดูเฉพาะบรรทัดที่ "เพิ่มเข้ามา" ใน diff
if echo "$DIFF" | grep -E "^\+" | grep -Eq "$pattern"; then
echo " ✗ $msg"; BAD=1
fi
}
check '@pytest\.mark\.(skip|xfail)' "เพิ่ม skip/xfail ใน test"
check '#\s*type:\s*ignore' "เพิ่ม type: ignore"
check '#\s*noqa' "เพิ่ม noqa"
check 'assert True' "assertion ที่ไม่ทดสอบอะไร"
check 'except\s*:\s*pass' "กลืน exception เงียบ ๆ"
# จำนวน test ต้องไม่ลดลง
OLD=$(git grep -c "^def test_" "$BASE" -- tests | awk -F: '{s+=$3} END {print s+0}')
NEW=$(git grep -c "^def test_" HEAD -- tests | awk -F: '{s+=$3} END {print s+0}')
if [ "$NEW" -lt "$OLD" ]; then
echo " ✗ จำนวน test ลดลงจาก $OLD เหลือ $NEW"; BAD=1
fi
[ $BAD -eq 0 ] && echo " ไม่พบร่องรอยการเลี่ยง gate"
exit $BAD
ทำไมต้องมี stage “no-cheating”
เมื่อ agent ถูกกดดันด้วยเงื่อนไข “ต้องทำให้ test ผ่าน” มันจะหาทางที่สั้นที่สุด และบางครั้งทางที่สั้นที่สุดคือการทำให้ test หายไป นี่ไม่ใช่ความไม่ซื่อสัตย์ของโมเดล แต่เป็นผลของการตั้งเป้าหมายที่วัดด้วยตัวแทน ทางแก้คือทำให้ตัวแทนนั้นโกงยาก ไม่ใช่การขอร้องให้อย่าโกง
5.3 Red / Green ที่ agent ทำได้จริง
TDD กับ agent ได้ผลดีกว่ากับมนุษย์ในบางแง่ เพราะ test ที่แดงคือ feedback ที่เครื่องอ่านได้ ซึ่งเป็นสิ่งที่ agent ต้องการที่สุด แต่ต้องบังคับลำดับให้ถูก มิฉะนั้น agent จะเขียน implementation ก่อน แล้วเขียน test ที่ยืนยันสิ่งที่มันเพิ่งเขียน ซึ่งไร้ค่า
[RED-1] เขียน test จาก spec เท่านั้น ห้ามอ่านหรือเขียน implementation
[RED-2] รัน test → ต้องแดง และต้องแดงด้วยเหตุผลที่ถูกต้อง
(AssertionError = ดี | ImportError/NameError = ยังไม่นับว่า red ที่ใช้ได้)
[RED-3] แสดง output ที่แดงให้เห็น พร้อมจำนวน test ที่ fail
[GREEN] เขียน implementation ให้น้อยที่สุดที่ทำให้เขียว
ห้ามเพิ่มฟีเจอร์ที่ test ไม่ได้ขอ
[REFACTOR] ปรับโครงสร้างโดย test ต้องเขียวตลอด รันหลังแก้ทุกครั้ง
prompt สำหรับสั่ง agent ทำ red-green:
งาน: implement ตาม docs/specs/coupon.md
ทำตามลำดับนี้อย่างเคร่งครัด และรายงานผลของทุกขั้นด้วย output จริง:
ขั้น 1 (RED)
- อ่าน docs/specs/coupon.md อย่างเดียว ห้ามเปิดไฟล์ใน src/domain/coupon.py
- เขียน test ใน tests/unit/test_coupon.py ให้ครอบทุกกติกาใน spec
รวมถึงทุกเคสที่ spec บอกว่าต้อง reject
- รัน `uv run pytest tests/unit/test_coupon.py -q` แล้ววาง output ที่ได้
- ยืนยันว่าทุกตัวแดงเพราะ AssertionError หรือ business logic ไม่มี
ถ้าแดงเพราะ ImportError ให้สร้าง stub ที่ raise NotImplementedError ก่อน แล้วรันใหม่
- หยุดตรงนี้ รอฉันยืนยันว่า test ครอบคลุมพอ
ขั้น 2 (GREEN) — เริ่มเมื่อฉันบอกว่า "go" เท่านั้น
- เขียน src/domain/coupon.py ให้ test เขียว
- ห้ามแก้ไฟล์ test ในขั้นนี้ แม้แต่บรรทัดเดียว
- ถ้าเชื่อว่า test ผิด ให้หยุดและอธิบาย อย่าแก้เอง
- รัน verify-fast.sh หลังทุกการแก้
ขั้น 3 (REFACTOR)
- ปรับให้อ่านง่ายขึ้น รัน test หลังทุกการเปลี่ยนแปลง
- รายงาน diff สรุปว่าเปลี่ยนโครงสร้างอะไรและทำไม
เทคนิค: การหยุดระหว่าง RED กับ GREEN
จุดที่มนุษย์ให้คุณค่าสูงสุดคือช่วงระหว่าง RED กับ GREEN การอ่าน test ที่ agent เขียนใช้เวลา 3–5 นาที แต่ตัดสินคุณภาพของงานทั้งหมด เพราะ implementation จะวิ่งเข้าหา test เสมอ ถ้า test ผิด โค้ดจะผิดตาม และไม่มี gate ใดจับได้
5.4 การควบคุม loop: เพดานและการตรวจจับการไม่คืบหน้า
#!/usr/bin/env bash
# ตัวขับ loop แบบ headless — ใช้กับ claude -p, codex exec หรือ cursor-agent
set -uo pipefail
TASK="${1:?ใช้: ./agent-loop.sh 'คำอธิบายงาน'}"
MAX_ITER="${MAX_ITER:-6}"
STALL_LIMIT=2 # ถ้าจำนวน test ที่ fail ไม่ลดลง N รอบติด ให้หยุด
prev_fail=999999
stall=0
for i in $(seq 1 "$MAX_ITER"); do
echo "═══ รอบที่ $i / $MAX_ITER ═══"
# --- 1. ให้ agent ทำงานหนึ่งรอบ ---
claude -p "$TASK
บริบท: อ่าน AGENTS.md, memory/pitfalls.md และ output ของรอบก่อนใน /tmp/gate.log
ทำหนึ่งขั้นที่ทำให้ verify ใกล้เขียวขึ้น แล้วหยุด อย่าพยายามแก้ทุกอย่างในรอบเดียว" \
--allowedTools "Read,Edit,Write,Bash(uv run pytest:*),Bash(uv run ruff:*)" \
--output-format stream-json > "/tmp/agent_$i.log"
# --- 2. รัน gate ---
./scripts/verify-fast.sh > /tmp/gate.log 2>&1
if [ $? -eq 0 ]; then
echo "GREEN ที่รอบ $i"
./scripts/verify.sh # ชุดเต็มเพื่อยืนยัน
exit $?
fi
# --- 3. ตรวจว่าคืบหน้าไหม ---
cur_fail=$(grep -oE '[0-9]+ failed' /tmp/gate.log | head -1 | grep -oE '[0-9]+' || echo 999999)
echo " test ที่ยัง fail: $cur_fail (รอบก่อน: $prev_fail)"
if [ "$cur_fail" -ge "$prev_fail" ]; then
stall=$((stall+1))
if [ "$stall" -ge "$STALL_LIMIT" ]; then
echo "หยุด: ไม่คืบหน้า $STALL_LIMIT รอบติดกัน — ปัญหาน่าจะอยู่ที่สเปกหรืองานใหญ่เกินไป"
git stash push -m "agent-stalled-$(date +%s)" # เก็บไว้ให้คนดู ไม่ทิ้ง
exit 3
fi
else
stall=0
fi
prev_fail=$cur_fail
# --- 4. safety: โค้ดไม่ควรบวมผิดปกติ ---
ADDED=$(git diff --numstat | awk '{s+=$1} END {print s+0}')
if [ "$ADDED" -gt 800 ]; then
echo "หยุด: เพิ่มโค้ด $ADDED บรรทัด เกินขนาดที่ควรรีวิวได้ในหนึ่ง PR"
exit 4
fi
done
echo "หยุด: ครบ $MAX_ITER รอบแล้วยังไม่เขียว"
echo "ให้มนุษย์อ่าน /tmp/gate.log แล้วตัดสินใจว่าจะซอยงานหรือแก้สเปก"
exit 2
เงื่อนไขหยุดทั้งหมดที่ควรมี
| เงื่อนไข | สัญญาณ | สิ่งที่ควรทำต่อ |
|---|---|---|
| สำเร็จ | gate เขียวทั้งชุด | เปิด PR |
| ชนเพดานรอบ | ครบ N รอบยังแดง | คนอ่าน log แล้วซอยงาน |
| ไม่คืบหน้า | จำนวน fail ไม่ลดลง 2 รอบติด | สเปกน่าจะกำกวม กลับไปแก้ spec |
| โค้ดบวม | diff เกิน 800 บรรทัด | งานใหญ่เกินหนึ่ง loop ให้แตกงาน |
| แตะของต้องห้าม | hook บล็อก 3 ครั้งขึ้นไป | agent เข้าใจขอบเขตผิด แก้ AGENTS.md |
| วนกลับที่เดิม | diff ซ้ำกับรอบก่อนหน้า | ติดใน local minimum ให้ reset แล้วเปลี่ยนวิธี |
| งบหมด | token หรือเวลาเกินที่ตั้ง | หยุดแล้วรายงานต้นทุนต่อผลลัพธ์ |
กฎเชิงประสบการณ์
ถ้า agent แก้ไม่ผ่านภายใน 3 รอบ ปัญหามักไม่ได้อยู่ที่ agent แต่อยู่ที่สเปกกำกวมหรืองานใหญ่เกินไป การเพิ่มเพดานเป็น 15 รอบแทบไม่เคยช่วย มีแต่จะทำให้โค้ดเละกว่าเดิม ทางแก้ที่ถูกคือกลับไปแก้สเปก ไม่ใช่ขยายเพดาน
สรุปบทนี้
gate คือสิ่งที่นิยามคำว่าเสร็จ ต้อง deterministic เร็วพอ พูดเก่ง แบ่งชั้น และ agent แก้ไม่ได้ ต้องมี stage ที่ตรวจการโกงโดยเฉพาะ เพราะเป้าหมายที่วัดด้วยตัวแทนย่อมถูกหาทางลัด บังคับลำดับ red ก่อน green เสมอ และหยุด loop เมื่อไม่คืบหน้าแทนที่จะขยายเพดาน
บทถัดไปว่าด้วยชั้นของการทดสอบ การทำ A/B ทั้งสองความหมาย และการวัดว่าเราทำร้ายผู้ใช้หรือยัง