ข้ามไปยังเนื้อหา
KoishiAI
EN
← สารบัญ

บทที่ 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 จะพัง
Louderror บอกว่าอะไรผิด ที่ไหน คาดหวังอะไร ได้อะไรมา ไม่ใช่แค่ 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 ทั้งสองความหมาย และการวัดว่าเราทำร้ายผู้ใช้หรือยัง