---
title: "เทคนิคการไม่อ่าน ตอนที่ 1: เทคนิคการไม่อ่านผลการทดสอบ"
author: garplab
publisher: TypingTube
license: CC BY 4.0
license_url: https://creativecommons.org/licenses/by/4.0/
license_scope: 「CC BY 4.0」の印から始まる節（仕組み・検証手順・コード）。印の無い本文は著作権を留保
canonical: https://typing-tube.net/articles/th/cd93e0c5cac832
series: "バイブコーディングにおける読まない技術"
language: th
---


> บทความนี้เป็นตอนที่ 1 ของซีรีส์ "เทคนิคการไม่อ่าน" เรียงอาการที่ไปได้ไม่ดีกับวิธีแก้ไว้ทีละอย่าง แต่ละตอนจบได้ด้วยการวางไฟล์หรือสคริปต์หนึ่งอย่าง ทำไมกลไกนั้นถึงจำเป็น อ่านคำอธิบายหลังวางแล้วก็จะเข้าใจ ภาพรวมของซีรีส์และรายการตอนทั้งหมดอยู่ใน[บทนำ](https://typing-tube.net/articles/th/34b02627c718fa)

คราวนี้ว่าด้วยผลลัพธ์ของ test ทุกครั้งที่ AI รัน test ผลลัพธ์หลายหมื่นบรรทัดจะไหลเข้าไปใน context window นั้น AI เลือกผลลัพธ์ที่ส่งมาถึงไม่ได้ เพราะทั้งหมดเข้าไปอยู่ใน context window ยิ่งเพิ่มก็ยิ่งเจือจางแบบที่บทนำพูดถึงจึงเกิดขึ้นที่นี่เหมือนกัน ที่ต่างออกไปคือสิ่งที่กำลังเพิ่มขึ้นไม่ใช่คำสั่ง แต่เป็นผลลัพธ์ของ tool

เริ่มจากการตรวจสอบเพียงอย่างเดียว ใน session ล่าสุดกับ AI coding agent ให้มองหาผลลัพธ์ตรงจุดที่รัน test มีกี่บรรทัด แล้วมีใครอ่านผลลัพธ์นั้นจนจบบ้างไหม

คงไม่มีใครอ่าน แต่ AI เป็นอีกเรื่องหนึ่ง AI ข้ามผลลัพธ์ของ tool ไปไม่ได้ ทุกบรรทัดที่ส่งมาถึงจะเข้าไปอยู่ใน context window กลายเป็นวัตถุดิบของคำตอบถัดไป และกลายเป็นค่าใช้จ่ายด้วย รันไปแก้ไป 5 ครั้งก็เข้าไป 5 ครั้ง ยิ่ง context window แน่นขึ้น คุณภาพของคำตอบก็ยิ่งตก จึงเป็นโครงสร้างที่ยิ่งรัน test ความสามารถในการแก้ test ก็ยิ่งลดลง

ใน production project ที่อยู่ในมือของผมเอง ผลลัพธ์ของ test ทั้งหมดมี 26,147 บรรทัด (2.1MB) คือ [typingtube](https://typingtube.net) web service ที่ฝึกพิมพ์ดีดไปกับมิวสิกวิดีโอบน YouTube ได้ ซึ่งดูแลเองอยู่คนเดียว พอรัน test 13,950 รายการก็ได้ปริมาณเท่านี้ สิ่งที่ **ทำให้เหลือ 3 บรรทัด** คือ wrapper (สคริปต์ที่ครอบ command เดิมไว้แล้วรันแทน)

สิ่งที่เข้าใจแล้วเขียนเป็นประโยคเดียวได้

> **AI เลือกผลลัพธ์ที่ส่งมาถึงไม่ได้ ดังนั้นฝั่งที่ส่งออกต้องบีบให้เหลือแค่ความล้มเหลวกับจำนวนรายการ**

## ทำไมยิ่งรันก็ยิ่งแก้ไม่ได้

ประวัติบทสนทนาจะส่งใหม่ทั้งก้อนทุกครั้งที่ตอบ ผลลัพธ์ที่รันไปแล้วไม่ได้หายไปตรงนั้น หลังรันไป 5 ครั้ง ของทั้ง 5 ครั้งก็ขึ้นมาทุกครั้ง

แต่ไม่ใช่ว่าทั้งหมดจะส่งมาถึง ทาง Claude Code วาง[ขีดจำกัดของผลลัพธ์จาก command](https://code.claude.com/docs/en/tools-reference) ไว้ ส่วนที่เกินจะตัดออก ใน 26,147 บรรทัดนั้น สิ่งที่ AI ได้รับคือส่วนที่คัดมาจากหัวและท้าย ถ้าความล้มเหลวอยู่ตรงกลาง AI ก็อ่านว่า "ผ่าน" ทั้งที่ไม่ได้เห็น

ในการวัดที่ให้ค้นหาบรรทัดที่ต้องการจาก input ที่ยาว แค่มีบรรทัดที่ไม่เกี่ยวข้องปนเข้ามา 1 บรรทัด ความแม่นยำก็ลดลง ผลลัพธ์ของ test นั้น เมื่อมองจาก 1 รายการที่กำลังแก้อยู่ ก็อยู่ฝั่งที่เกี่ยวข้องน้อยที่สุด

## กลไก: เปลี่ยนทางเข้าของผลลัพธ์ให้เหลือทางเดียว

เปลี่ยนทางเข้าของการรัน test ให้เหลือ wrapper เพียงทางเดียว ข้อความเต็มไปที่ log ส่วนบทสนทนาได้แค่สรุป สคริปต์ต่อไปนี้คือโค้ดที่อยู่ในมือย่อให้สั้นลง (ตัวอย่างเป็น Rails / Minitest แต่แค่เปลี่ยนบรรทัดที่ `grep` ให้เข้ากับผลลัพธ์ของ test runner ของตัวเอง ก็ใช้กับอะไรก็ได้)

```bash
#!/bin/bash
# scripts/test.sh — เก็บผลลัพธ์ทั้งหมดไว้ใน log และคืนกลับไปที่บทสนทนาแค่สรุป
log=tmp/test_last.log
mkdir -p tmp
bin/rails test "$@" >"$log" 2>&1
status=$?

echo "--- summary (full log: ${log}) ---"
grep -E "^Finished in " "$log" | tail -1
grep -E "^[0-9]+ runs, " "$log" | tail -1   # บรรทัดจำนวนรายการ ⚠️ ห้ามตัดออก (อธิบายทีหลัง)

failures=$(grep -cE "^(Failure|Error):" "$log" || true)
if [ "${failures:-0}" -gt 0 ]; then
  echo "--- ${failures} failure(s)/error(s) (first excerpt) ---"
  grep -A 3 -E "^(Failure|Error):" "$log" | head -40
fi
exit $status
```

สิ่งที่ออกมามีแค่นี้

```
--- summary (full log: tmp/test_last.log) ---
Finished in 277.851362s, 50.2067 runs/s, 318.7712 assertions/s.
13950 runs, 88571 assertions, 0 failures, 0 errors, 228 skips
```

สรุปที่อยู่ในมือของผมมี 29 บรรทัด และรูปแบบเล็กที่สุดข้างบนคือแกน 3 บรรทัด บรรทัดที่เหลือเป็นรายการของส่วนที่ยังไม่ได้รัน เป็นต้น ข้อความเต็มจะเหลืออยู่ที่ `tmp/test_last.log` ทุกครั้ง และบรรทัดแรกของสรุปจะบอกตำแหน่งนั้น การลบทิ้งก็ไม่ดีเหมือนกัน ต้องให้ AI กลับไปอ่านตรงนั้นซ้ำได้

ที่สำคัญคือไม่ได้เพิ่มคำสั่งให้ AI แม้แต่บรรทัดเดียว จะเขียนลงในไฟล์คำสั่งว่า "ให้ย่อผลลัพธ์ก่อนอ่าน" ก็ทำได้ แต่อย่างที่เห็นใน[บทนำ](https://typing-tube.net/articles/th/34b02627c718fa) นั่นคือคำตอบฝั่งยิ่งเพิ่มก็ยิ่งเจือจาง สิ่งที่ผมเปลี่ยนไม่ใช่คำสั่ง แต่เป็นทางเข้า กฎนั้นฝ่าฝืนได้ แต่ทางเข้ามีอยู่แค่ตรงนั้น

แต่แค่วางไว้ AI ก็ยังไม่ยอมเดินผ่านทางเข้านี้ command มาตรฐานอย่าง `rails test` เป็นคำที่ฝังลึกอยู่ใน AI มาจากการเรียนรู้ ส่วน `scripts/test.sh` ที่เพิ่งวางไว้เป็นคำที่ AI เห็นเป็นครั้งแรกในวันนี้ ต่อให้เขียนลงในไฟล์คำสั่งว่า "ให้ใช้ wrapper" สิ่งที่โผล่ขึ้นมาในวินาทีที่จะรัน test ก็ยังเป็นฝั่ง command มาตรฐานที่เห็นมาแล้วหลายร้อยล้านครั้ง หนึ่งบรรทัดในไฟล์คำสั่งต้องไปแข่งลำดับความสำคัญกับความเคยชินที่ฝังอยู่ใน AI และส่วนใหญ่ก็แพ้

[เอกสารทางการ](https://code.claude.com/docs/en/best-practices) ก็ขีดเส้นเดียวกัน สิ่งที่เขียนคือ "Bash command ที่ AI เดาไม่ได้" สิ่งที่ไม่เขียนคือ "ธรรมเนียมมาตรฐานของภาษาที่ AI รู้อยู่แล้ว" `rails test` คืออย่างหลัง ส่วน `scripts/test.sh` คืออย่างแรก สิ่งที่ไฟล์คำสั่งถือไว้ได้มีแค่ฝั่งที่ AI ไม่รู้ ผมจึงปิดฝั่ง command มาตรฐานไว้ การปิดจนสนิทก็ไม่ดีเหมือนกัน จึงเปิดทางเลี่ยงไว้หนึ่งทางแบบระบุชัด

ถ้าเป็น Claude Code การเติม hook หนึ่งตัวที่หยุด `rails test` ตรง ๆ แล้วพาไปหา wrapper จะทำให้ทางเข้าเหลือทางเดียวจริง ๆ hook คือสคริปต์ที่แทรกเข้ามาก่อนที่ command จะรัน และลงทะเบียนไว้ใน `.claude/settings.json` แบบนี้

```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/use_test_wrapper.sh"
      }]
    }]
  }
}
```

เนื้อในมีแค่ "ถ้า command เป็น `rails test` ตรง ๆ ก็แสดงวิธีใช้ wrapper แล้วจบโดยไม่ให้รัน"

```bash
#!/bin/bash
# .claude/hooks/use_test_wrapper.sh
command=$(cat | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
case "$command" in
  *scripts/test.sh*|*SKIP_TEST_GUARD=*) exit 0 ;;   # ให้ผ่านทั้งทางที่ผ่าน wrapper และทางเลี่ยงที่ระบุชัด
esac
if printf '%s' "$command" | grep -qE '(^|[;&|[:space:]])(bin/)?rails[[:space:]]+test([[:space:]]|$)'; then
  echo "Run tests with scripts/test.sh <same args> (full output stays in tmp/test_last.log)" >&2
  exit 2   # exit 2 = ไม่ให้รัน และให้ AI อ่านคำแนะนำใน stderr
fi
exit 0
```

AI ขยับด้วย command ตรง ๆ ไม่ได้ และไปต่อได้ก็ต่อเมื่ออ่านคำแนะนำแล้วเท่านั้น สิ่งที่ได้ผลตรงนี้ไม่ใช่ถ้อยคำของคำแนะนำ แต่เป็นตำแหน่งที่คำแนะนำไปถึง คำแนะนำของ hook จะกลับมาในรูปของการปฏิเสธการรัน ตรงจุดที่ AI พิมพ์ command ลงไป ถ้าคนเข้ามาแทรกในบทสนทนาว่า "ใช้ wrapper สิ" อันนั้นจะไปเรียงต่อท้ายในฐานะการโต้ตอบอีกอันหนึ่ง ต่างกันตรงที่ตำแหน่งหนึ่งเลือกได้ว่าจะอ่านหรือไม่อ่าน กับอีกตำแหน่งที่ถ้าไม่อ่านก็ไปต่อไม่ได้

## สิ่งที่เลิกอ่าน

คือผลลัพธ์ดิบของ test ทั้งคนและ AI ก็ไม่อ่าน ไม่ได้ทิ้งไป จึงเปิด log เฉพาะเมื่อการสืบหาสาเหตุต้องใช้ข้อความเต็มจริง ๆ

## ข้อควรระวัง: ความผิดปกติที่ไม่ใช่ความล้มเหลวจะมองไม่เห็น

จุดบอดของกลไกนี้คือความผิดปกติที่ไม่ได้อยู่ในรูปของความล้มเหลวจะซ่อนตัวอยู่ จำนวน skip ที่พุ่งขึ้น คำเตือน จำนวนรายการที่รันลดลง แม้ test ทั้งก้อนจะไม่ได้โหลดขึ้นมา ถ้าที่เหลือผ่านทั้งหมดก็เป็น "0 failures"

ในมือของผมเองเคยมีครั้งที่การรัน test ไม่ขึ้นมาตั้งแต่แรก ทั้งที่ไม่ได้รันแม้แต่รายการเดียว กลับเหลือไว้แค่บันทึกว่ารันไปแล้ว เมื่อเป็นแบบนั้นความล้มเหลวมี 0 รายการ และสรุปก็เงียบอยู่อย่างนั้น

ดังนั้นเฉพาะบรรทัดจำนวนรายการเท่านั้นที่ไม่ตัดออกจากสรุป ที่ว่านั้นคือ `13950 runs, 88571 assertions, 0 failures, 0 errors, 228 skips` ในตัวอย่างการรันข้างบน การย่อมากเกินไปก็ไม่ดีเหมือนกัน ไม่ใส่แค่ตัวเลข แต่ต้องเรียงชื่อรายการคู่กับตัวเลขเสมอ

## วิธีการตรวจสอบ: ยืนยันเพียงครั้งเดียวว่าความล้มเหลวส่งมาถึง

**เงื่อนไขเบื้องต้น**

- วาง wrapper ของตอนนี้ (`scripts/test.sh`) แล้ว และลงทะเบียน hook ที่หยุด command รัน test ตรง ๆ เรียบร้อยแล้ว
- มี test อย่างน้อย 1 รายการ และรันได้ในมือ

**เวลาที่ใช้**: 5 นาที

**ขั้นตอน**

1. คุณทำให้ test พังหนึ่งรายการโดยตั้งใจ เปิดไฟล์ test ที่รันจบเร็วขึ้นมา 1 ไฟล์ แล้วแก้ค่าที่คาดหวังเพียง 1 ตัวอักษร (เปลี่ยน `assert_equal "abc", ...` เป็น `"abd"`) เพราะจะคืนสภาพเดิมในขั้นเก็บกวาด จึงจดชื่อไฟล์กับหมายเลขบรรทัดเอาไว้
2. ขอให้ AI ว่า "รัน test ให้หน่อย" ไม่ใช่ให้คุณรัน wrapper เองโดยตรง แต่ให้ขอ AI เพราะสิ่งที่อยากตรวจสอบคือเมื่อ AI เดินผ่านทางเข้าแล้วจะมีอะไรส่งมาถึง

**เกณฑ์ผ่าน** (สรุปที่ส่งกลับมาต้องเข้าเกณฑ์ครบทุกข้อ)

- มีตำแหน่งของ log ข้อความเต็มออกมา (บรรทัดแบบ `tmp/test_last.log`)
- มีชื่อ test ที่พัง กับส่วนต่างระหว่างค่าที่คาดหวังกับค่าที่ได้จริงออกมา (บรรทัดที่ตรงกับ `Expected: "abc"` และ `Actual: "abd"` ชื่อไฟล์ที่จดไว้ในขั้นตอนที่ 1 จะออกมาตรงนี้)
- `failures` ในบรรทัดจำนวนรายการเป็น 1 ขึ้นไป (บรรทัดแบบ `13950 runs, 1 failures`)

**เมื่อไม่ผ่าน**

- ออกมาเป็น `0 failures` แปลว่า test ที่พังไม่ได้รันตั้งแต่แรก ทบทวนว่าไฟล์ที่เลือกในขั้นตอนที่ 1 อยู่ในขอบเขตของการรันหรือไม่ แล้วทำขั้นตอนที่ 2 ใหม่
- ล้มเหลวแล้วแต่ชื่อ test หรือส่วนต่างไม่ออกมา แปลว่าบรรทัดที่สรุปเก็บมายังไม่พอ ให้ขยายขอบเขตของ `grep` ใน wrapper

**เก็บกวาด**

- คืนตัวอักษร 1 ตัวที่แก้ไปให้เป็นเหมือนเดิม แล้วให้ AI รันอีกครั้ง ถ้าบรรทัดจำนวนรายการกลับไปเป็น `0 failures` ก็จบ
- ถ้าลืมคืนสภาพเดิม งานหลังจากนี้จะล้มเหลวต่อเนื่องไปเรื่อย ๆ

สิ่งที่ตรวจสอบไปคือฝั่ง "ล้มเหลวแล้วต้องส่งมาถึงแน่นอน" เท่านั้น ถ้าเริ่มใช้งานจริงโดยไม่ได้เห็นตรงนี้ ก็จะใช้ต่อไปโดยแยกไม่ออกว่าที่สรุปเงียบนั้นเป็นเพราะ "ไม่มีความล้มเหลว" หรือเป็นเพราะ "ต่อกันแบบที่ความล้มเหลวส่งมาไม่ถึง"

---

ตอนหน้าคือ "เทคนิคการไม่อ่าน memory" **memory ที่ AI เขียนสะสมไว้ ผมไม่ได้กลับไปอ่านซ้ำ** ถึงอย่างนั้นก็ไม่เคยเดือดร้อนเพราะ memory เพิ่มขึ้น

---

**ซีรีส์ "เทคนิคการไม่อ่าน"**

- ← ตอนที่แล้ว: [บทนำ แทบไม่ได้อ่านผลลัพธ์ของ AI อีกต่อไป](https://typing-tube.net/articles/th/34b02627c718fa)
- → ตอนหน้า: [ตอนที่ 2 เทคนิคการไม่อ่าน memory](https://typing-tube.net/articles/384477896fec22)
- รายการตอนทั้งหมด: [บทนำ แทบไม่ได้อ่านผลลัพธ์ของ AI อีกต่อไป](https://typing-tube.net/articles/th/34b02627c718fa)
