← Harness Engineering Handbook
LEVEL 4 · แงะของจริง
ผ่า Claude Code — case study เต็มตัว
สิบเอ็ดบทที่ผ่านมาคือทฤษฎี — บทนี้เอาของจริงที่พี่ใช้อยู่ทุกวันขึ้นโต๊ะผ่า แล้วชี้ว่าอวัยวะแต่ละชิ้นคือบทไหนของเล่มนี้ · เป้าหมายไม่ใช่รู้จัก Claude Code ดีขึ้น (พี่ใช้เป็นอยู่แล้ว) แต่คือเห็นการตัดสินใจออกแบบว่าแต่ละอย่างตอบ trade-off ที่เราเรียนมายังไง
12.1 การตัดสินใจที่น่าผ่าที่สุดสามเรื่อง
เรื่องแรก: ทำไม tools มีไม่กี่ตัว และไม่มี tool เฉพาะภาษา · ไม่มี refactor_python ไม่มี fix_css — มีแต่ Read/Edit/Bash/Glob/Grep ที่ compose กันได้ครอบงานแทบทุกแบบ · นี่คือหลัก "จำนวนพอดี" ของบทที่ 4 แบบสุดทาง: เครื่องมือ general ไม่กี่ตัว = schema ไม่ท่วม context ไม่มีตัวเลือกให้สับสน · ราคาที่จ่าย: Bash กลายเป็นประตูที่เปิดได้ทุกอย่าง — จึงเป็นจุดที่ permission ต้องละเอียดที่สุด (บทที่ 6) · เห็นไหมครับ การตัดสินใจฝั่ง tools ลากการตัดสินใจฝั่ง guardrails ตามมา — ชิ้นส่วน 5 ชิ้นไม่เคยออกแบบแยกกันได้
เรื่องที่สอง: ทำไมไม่ index repo ล่วงหน้า · หลายระบบยุคก่อนสร้าง index/embedding ของทั้ง repo ไว้ค้น — Claude Code เลือกทางตรงข้าม: ให้ agent ใช้ Glob/Grep ค้นสด ๆ เอาตอนต้องใช้ · คือ just-in-time retrieval (บทที่ 3) เต็มรูปแบบ: ไม่มี index ที่ stale ไม่ต้อง sync และ agent ตัดสินใจเองว่าค้นอะไร — แลกกับการเสียเวลาไม่กี่วินาทีต่อการค้น ซึ่งถูกกว่า context ที่เสียไปกับของไม่เกี่ยวมาก
เรื่องที่สาม: hooks — จุดเสียบ guardrail ที่เป็นโค้ด · กติกาบางอย่างสำคัญเกินกว่าจะ "ขอร้อง" ผ่าน prompt (โมเดลอาจลืม/ถูกหลอก — บทที่ 6 สอนแล้ว) · hooks ให้ผู้ใช้เขียนเช็กของตัวเองที่รันก่อน/หลังทุก tool call: ห้ามแตะไฟล์นี้ รัน formatter หลังแก้ทุกครั้ง เตือนเมื่อจะรันคำสั่งอันตราย — เบรกที่เป็นโค้ดบังคับได้ 100% ไม่ใช่คำขอร้องที่หวังว่าโมเดลจะฟัง · นี่คือหลัก "ชั้นแข็ง" ของบทที่ 6 ที่เปิดให้ผู้ใช้ต่อเอง
12.2 สิ่งที่ Claude Code สอนเรื่องการออกแบบ harness
- default ปลอดภัย แต่ไม่รำคาญ — เริ่มด้วยการถาม แล้ว "จำ" คำอนุมัติ (allowlist) ให้ความถี่การถามลดลงเอง — ตรงตำราสมดุลของบทที่ 6
- ยืดหยุ่นกับคุมได้ ไม่ใช่ขั้วตรงข้าม — Bash เปิดโลกเต็มที่ + permission/hooks คุมเข้ม = ได้ทั้งสอง
- ทุกจุดขยายเปิดให้ผู้ใช้ — MCP (เพิ่ม tools), hooks (เพิ่ม guardrails), skills + CLAUDE.md (เพิ่มความรู้), subagents (ขยายสเกล) — ผู้ใช้ต่อเติมได้โดยไม่ต้องรอ vendor · harness ที่ดีจึงไม่ใช่ product ปิด แต่เป็น platform ที่คนอื่นสร้างต่อได้
12.3 เคสจริง: ตามรอย transcript ทีละขั้น
ทฤษฎีข้างบนอาจยังรู้สึกลอย ๆ — ตัวอย่างต่อไปนี้ประกอบจากรูปแบบงานที่เจอกันจริงบ่อย ๆ (คล้ายเคส config.py vs config_prod.py ของบทที่ 7) ไล่ทีละขั้นว่าแต่ละจังหวะคือกลไกจากบทไหนของเล่มนี้กำลังทำงานอยู่จริง พี่คนหนึ่งเจอบั๊กแล้วโยนงานให้ Claude Code ประโยคเดียว:
▸ transcript ช่วงที่ 1 — รับงาน แล้วค้นแบบ pull ไม่ใช่โหลดทั้ง repo
พี่: มีรายงานว่าลูกค้าสมัครสมาชิกซ้ำได้ ถ้าใช้อีเมลตัวพิมพ์ใหญ่เล็กต่างกัน
(User@mail.com กับ user@mail.com ควรนับเป็นคนเดียวกัน) ช่วยแก้ให้หน่อย
กำลังหา logic การสมัครสมาชิกในโปรเจกต์…
▸ Grep(pattern: "def.*(signup|register)", glob: "**/*.py")
⎿ 2 matches — services/signup_service.py, admin/user_admin.py
▸ Read(services/signup_service.py)
⎿ 38 linesจุดแรกที่เห็นชัด: agent ไม่ได้โหลดทั้ง repo มาอ่าน — มันเดาคำค้นจากคำในงาน (signup, register) แล้ว grep หาก่อน นี่คือ agentic search แบบ pull ที่บทที่ 3 สอนไว้ ผลลัพธ์ที่น่าสนใจ: grep เจอสองไฟล์ ไม่ใช่ไฟล์เดียว — เก็บไว้ในหัวก่อน จะกลับมาสำคัญอีกที
▸ transcript ช่วงที่ 2 — แก้ เพิ่มเทสต์ แล้วส่งให้ verifier ตัดสิน
เจอบรรทัดที่ตรวจซ้ำ: if User.query.filter_by(email=email).first(): raise DuplicateEmailError()
กำลังเช็ค test เดิมของไฟล์นี้ก่อนแก้…
▸ Grep(pattern: "def test_.*signup", path: "tests/test_signup.py")
⎿ 4 matches — ไม่มีเคสตัวพิมพ์ใหญ่เล็กเลย
▸ Edit(services/signup_service.py)
⎿ เพิ่ม email = email.strip().lower() ก่อนตรวจซ้ำและก่อนบันทึก
▸ Edit(tests/test_signup.py) — เพิ่มเคส test_signup_rejects_case_variant_email
▸ Bash(pytest tests/test_signup.py -q)
⎿ 5 passedถูกต้องตามตำรา: เช็กเทสต์เดิมก่อนแก้ (กัน regression) → แก้โค้ด → เพิ่มเทสต์ใหม่ให้ตรงกับบั๊กที่เพิ่งเจอ → รันเพื่อดูผล — closed loop เต็มรูปแบบของบทที่ 5 ผ่าน Bash เป็นประสาทสัมผัส ถ้าจบตรงนี้ก็ดูเรียบร้อยดี แต่ agent ที่ดีไม่หยุดแค่ไฟล์ที่แก้ — มันจำได้ว่าตอน grep เจอสองไฟล์ ไม่ใช่ไฟล์เดียว
▸ transcript ช่วงที่ 3 — เจอของนอกขอบเขต รายงานแทนที่จะลงมือเงียบ ๆ
เมื่อกี้ grep เจอ user_admin.py ด้วย — เช็คว่าใช้ logic เดียวกันไหม…
▸ Read(admin/user_admin.py)
⎿ พบฟังก์ชัน admin_create_user() — สร้าง user ตรง ๆ ไม่ผ่าน signup_service เลย
ไม่มีการ .lower() เหมือนกัน — บั๊กเดียวกัน ช่องทางที่สอง
[แจ้งพี่] เจอช่องโหว่เดียวกันอีกจุดที่ admin_create_user() — นอกขอบเขตงานเดิม
จะแก้ให้ด้วยเลย หรือแค่รายงานไว้ก่อน?จุดนี้คือที่บทที่ 8 เตือนไว้เรื่อง scope: เจอ "ของที่ควรแก้เพิ่ม" ระหว่างทาง หลักที่ดีคือทำน้อยที่สุดที่แก้ปัญหา แล้วรายงานส่วนเกิน ไม่ใช่ลงมือเงียบ ๆ — ในเซสชันจริง กติกานี้มักตั้งไว้ล่วงหน้าใน CLAUDE.md ว่า "เจอบั๊กที่เกี่ยวข้องแต่นอกขอบเขต ให้ถามก่อน" (ความรู้ถาวรจากบทที่ 9 เสียบเข้ามาตรงจุดตัดสินใจนี้พอดี) สมมติพี่ตอบว่า "แก้ให้เลย" agent เดินหน้าต่อ:
▸ transcript ช่วงที่ 4 — แตะข้อมูลจริง เส้นบนแผนที่บทที่ 6 ถูกข้าม
▸ Edit(admin/user_admin.py) — เพิ่ม .lower() ให้ตรงกับ signup_service.py
▸ Edit(tests/test_user_admin.py) — เพิ่มเคสเดียวกันสำหรับ admin_create_user
▸ Bash(pytest tests/ -q)
⎿ 48 passed
ข้อมูลเก่าที่ซ้ำกันตั้งแต่ก่อนแก้ อาจยังนอนอยู่ในฐานข้อมูลจริง — ต้องเช็ค
ต้องรัน migration ทำความสะอาดอีเมลซ้ำของเก่า — คำสั่งนี้แก้ข้อมูลจริงใน production
⚠ ขออนุญาต: รัน scripts/normalize_existing_emails.py กับฐานข้อมูล production
[อนุมัติครั้งนี้] [อนุมัติเสมอสำหรับคำสั่งนี้] [ปฏิเสธ]blast radius × reversibility ของบทที่ 6 ทำงานตรงหน้าต่อหน้า: แก้โค้ดใน branch ย้อนได้ด้วย git จึงรันเทสต์ไปเรื่อย ๆ โดยไม่ถามทุกครั้ง (allowlist จำคำตอบไว้แล้ว) แต่พอคำสั่งขยับไปแตะข้อมูลจริงในฐานการผลิต — วงกว้างขึ้น ย้อนยากขึ้น — เส้นที่ FIG 6.1 วาดไว้ถูกข้าม ระบบจึงเลื่อนอำนาจตัดสินใจกลับมาที่คนทันที ไม่ใช่เพราะ agent "ไม่มั่นใจ" แต่เพราะกรงกำหนดไว้แบบนั้น
▸ transcript ช่วงที่ 5 — ประกาศเสร็จ ก็ต่อเมื่อ verifier ผ่านแล้วเท่านั้น
พี่: อนุมัติ
▸ Bash(python scripts/normalize_existing_emails.py --env=production)
⎿ พบอีเมลซ้ำ 3 คู่ — รวมบัญชีเรียบร้อย
▸ Bash(pytest tests/ -q)
⎿ 48 passed, 0 failed
สรุปให้พี่: แก้จุดตรวจซ้ำอีเมลใน signup_service.py และ user_admin.py ให้ไม่สนตัวพิมพ์เล็ก-ใหญ่
เพิ่มเทสต์คุมทั้งสองจุด และรวมบัญชีซ้ำเก่าที่มีอยู่แล้ว 3 คู่ในฐานข้อมูลจริง — เทสต์ทั้งหมดผ่าน 48/48ประโยคสรุปมาหลังเทสต์ผ่านเท่านั้น — ตรงกฎของบทที่ 8: "ห้ามบอกว่าเสร็จ จนกว่า verifier จะผ่าน" ไล่ทั้งห้าช่วงย้อนกลับไป จะเห็นว่าไม่มีจังหวะไหนเป็น "ความฉลาด" ล้วน ๆ ของโมเดล — ทุกจังหวะคือจุดที่ harness ออกแบบไว้ล่วงหน้า: ให้มันค้นแทนยัด context (บท 3), ให้มันมีเครื่องมือที่คืนผลตรงพอจะตัดสินใจต่อ (บท 4), ให้มันตรวจตัวเองด้วยเทสต์ก่อนพูดว่าเสร็จ (บท 5, 8), ขีดเส้นว่าอะไรถามก่อนอะไรทำเลย (บท 6), มี CLAUDE.md กำกับพฤติกรรมตอนเจอทางแยก (บท 9) — ทุกกล่องใน FIG 12.1 ไม่ใช่แค่รายการฟีเจอร์ มันคือสิ่งที่กำลังทำงานจริงในทุกวินาทีของเซสชันข้างบน โมเดลตัวเดียวกันเป๊ะ ถ้าไม่มีรั้วพวกนี้ ผลลัพธ์จะไม่ใช่แบบนี้
✅ สรุปบทที่ 12 — Claude Code ลงโครง 5 ชิ้นของบทที่ 1 ได้พอดี: CLAUDE.md/compaction/agentic search = context · Read/Edit/Bash + MCP = tools · เทสต์/lint ผ่าน Bash = feedback · permission/allowlist/hooks/sandbox = guardrails · narration/transcript/usage = observability · transcript 5 ช่วงข้างบนคือทุกกลไกนี้ทำงานร่วมกันจริงในงานเดียว · การตัดสินใจเด่น: tools น้อยแบบ general (Bash เปิดโลก + permission เข้มชดเชย), ค้นสดแทน index ล่วงหน้า, hooks = เบรกที่เป็นโค้ด · บทเรียนใหญ่: ชิ้นส่วนทั้งห้าออกแบบแยกกันไม่ได้ และ harness ที่ดีคือ platform