# ดีบัก AI Agent: การมองเห็นภายในที่เราอยากมีตั้งแต่วันแรก

> Agent ผลาญไป 4 ดอลลาร์และเรียกใช้เครื่องมือ 90 ครั้งกับงานที่ควรใช้แค่สามครั้ง ส่วนเราเองก็ได้แต่จ้องเทอร์มินัลเปล่า ๆ นี่คือเครื่องมือวัดที่เปลี่ยนการเดาสุ่มให้กลายเป็นการวินิจฉัยภายในสองนาที — /info, /report และ /context ของ Octomind, บันทึกเซสชันแบบ zstd, การ trace ด้วย RUST_LOG, --format jsonl และ OctoHub ที่วางไว้ด้านหน้าเพื่อจับทุกคำขอ upstream

Agent รันมาหกนาทีแล้ว สปินเนอร์ยังหมุนอยู่ พอผมสั่งหยุดมัน `/info` สรุปบิลไว้ที่ **$4.18** และ `/report` นับการเรียกใช้เครื่องมือได้แปดสิบเก้าครั้ง กับงานที่ผมประเมินไว้ว่าแค่สามครั้งกับไม่กี่เซนต์ มันอ่านไฟล์เดิมไปสิบเอ็ดรอบ มัน grep หาสัญลักษณ์ตัวหนึ่ง ได้ผลลัพธ์ว่าง เรียบเรียง grep ใหม่ ได้ผลลัพธ์ว่างอีก แล้วก็ทำแบบนี้ไปเรื่อย ๆ — อย่างสุภาพ อย่างมั่นใจ อย่างแพง — เพราะไม่มีอะไรในลูปบอกให้มันหยุด

ส่วนที่แย่ที่สุดไม่ใช่สี่ดอลลาร์ แต่คือผมไม่รู้เลยว่า _ทำไม_ จนกว่าจะลงไปหา Agent ไม่โยน stack trace ออกมาเวลามันออกนอกลู่ มันไม่แครช มันแค่เงียบ ๆ ทำสิ่งที่ผิด รายงานว่าสำเร็จ แล้วยื่นบิลให้คุณ จากภายนอก รันที่ยอดเยี่ยมกับรันที่หายนะหน้าตาเหมือนกันเป๊ะ: ข้อความไหลผ่าน เครื่องมือทำงาน คำตอบสุดท้ายหนึ่งคำตอบ

โพสต์นี้คือเครื่องมือวัดที่ผมหวังว่าได้ต่อไว้ก่อนรันนั้น ไม่ใช่หลังจากนั้น ทั้งหมดนี้มาพร้อม [Octomind](https://github.com/Muvon/octomind) รันไทม์ agent ภาษา Rust แบบโอเพนซอร์สของเรา และส่วนสุดท้ายเพิ่ม [OctoHub](https://github.com/Muvon/octohub) ไว้ด้านหน้า เพื่อจับสิ่งเดียวที่ตัว agent เองแสดงให้ดูไม่ได้: ไบต์ดิบที่ส่งถึงโมเดลจริง ๆ

---

## ทำไม Agent ถึงล้มเหลวอย่างเงียบ ๆ

ซอฟต์แวร์แบบดั้งเดิมล้มเหลวเสียงดัง null deref, 500, assertion ที่พัง — ความล้มเหลวมีรูปร่าง มีตำแหน่ง มีหมายเลขบรรทัด ความล้มเหลวของ agent ไม่มีสิ่งเหล่านั้นเลย เพราะในมุมมองของรันไทม์ ไม่มีอะไรล้มเหลว ทุกการเรียก API คืน 200 ทุกเครื่องมือออกด้วยโค้ด 0 โมเดลผลิตข้อความที่ถูกไวยากรณ์และดูสมเหตุสมผลในทุกขั้น พฤติกรรม _โดยรวม_ ผิด แต่ไม่มีปฏิบัติการเดี่ยว ๆ ตัวไหนผิดเลย

ในทางปฏิบัติ ความล้มเหลวรวมกลุ่มเป็นสี่รูปแบบ และแต่ละแบบมองไม่เห็นพอดีในจังหวะที่คุณอยากจะจับมันได้:

- **ผิดเครื่องมือ แต่มั่นใจถูก** โมเดลคว้า `grep` ทั้งที่ควรอ่านไฟล์ หรือยิงไปที่ shell ทั่วไปทั้งที่มีเครื่องมือเฉพาะวางอยู่ตรงนั้น มันไม่ลังเลเลย — การเลือกเครื่องมือผิดจากภายนอกหน้าตาเหมือนการเลือกถูกเป๊ะ (เราเขียนบทความเต็ม ๆ เรื่องการลดพื้นที่เครื่องมือเพื่อสู้กับเรื่องนี้: [MCP ที่ปรับแต่งเองควรอยู่ในรีโปของคุณ](/blog/custom-mcps-belong-in-your-repo))
- **การตัดทอนคอนเท็กซ์** บทสนทนาโตเกินหน้าต่าง การบีบอัดทำงาน และข้อเท็จจริงที่ agent ต้องใช้ก็ถูกย่อจนหายไป ตอนนี้มันให้เหตุผลจากความจำที่สูญเสียข้อมูล และคุณก็มองไม่เห็นรอยต่อ
- **ลูปที่ควบคุมไม่อยู่** ผลลัพธ์ว่าง → เรียบเรียงใหม่ → ผลลัพธ์ว่าง → เรียบเรียงใหม่ แต่ละเทิร์นดูสมเหตุสมผลเดี่ยว ๆ ตัวแพตเทิร์นเองคือบั๊ก และคุณสังเกตได้แค่จากจำนวนการเรียกเครื่องมือ
- **โทเคนระเบิด** เครื่องมือตัวหนึ่งเทไฟล์ขนาด 200KB ลงคอนเท็กซ์ หรืออาร์กิวเมนต์ของการเรียกเครื่องมือตัวหนึ่งบวมขึ้น และตอนนี้ทุกเทิร์นถัดไปก็ส่งทั้งหมดนั้นซ้ำ ต้นทุนกลายเป็นซูเปอร์ลิเนียร์ และอาการเดียวคือใบแจ้งหนี้

คุณดีบักสิ่งที่มองไม่เห็นไม่ได้ ดังนั้นงานแรกคือทำให้รันสังเกตได้ — ทั้งหลังเกิดเหตุและระหว่างทาง

---

## ระดับ 0: `/info` — เงินหายไปไหน

คำถามที่ตอบได้เร็วที่สุดคือ "เซสชันนี้ใช้เงินจริง ๆ เท่าไร และในรูปแบบไหน" ภายในเซสชันแบบโต้ตอบใด ๆ `/info`:

```
╭ /info
│ session  my-feature-x
│ model       openrouter:anthropic/claude-sonnet-5
│ tokens      214,883 total
│ breakdown   18,402 in · 9,114 out · 184,201 cache rd · 2,890 cache wr · 276 reasoning
│ cost        $4.18661
│ throughput  41.3 tok/s
╰ /info  my-feature-x
```

ทุกฟิลด์ที่นี่คือการวินิจฉัย ตัวที่แกะเคสลูปควบคุมไม่อยู่ของผมออกคือแถว **breakdown** `184,201 cache rd` เทียบกับ `18,402 in` หมายความว่าคอนเท็กซ์เดิมถูกอ่านซ้ำเกือบทุกเทิร์น — ลายเซ็นของลูปที่เอาแต่ต่อท้ายโดยไม่เคยจบ ส่วนจำนวนการเรียกใช้เครื่องมือที่ยืนยันเรื่องนี้อยู่ลึกลงไปอีกระดับ ใน `/report`

`/info` ยังแจกแจงค่าใช้จ่ายนอกเหนือจากลูปหลักด้วย — โทเคนและต้นทุนของโมเดลบีบอัดเอง การวางมาร์กเกอร์แคชพร้อมยอดอ่าน/เขียน ซับเอเจนต์ใด ๆ และ supervisor ต่างมีเซกชันของตัวเอง — ดังนั้นเมื่อบิลสูง คุณจะเห็นว่าบทสนทนาหลักหรือกระบวนการเบื้องหลังเป็นตัวกิน

ถ้าเซสชันเคยบีบอัดแม้แต่ครั้งเดียว `/info` จะแสดงบล็อก **compression**: บีบอัดไปกี่ครั้ง ลบข้อความไปเท่าไร ประหยัดโทเคนไปเท่าไร อัตราส่วนเฉลี่ย นั่นคือสัญญาณเตือนล่วงหน้าเรื่องการตัดทอน ถ้าคุณเห็นการบีบอัดระดับโปรเจกต์สามครั้งในเซสชันสั้น ๆ แสดงว่า agent ลืมอะไรไปแล้ว และควรรู้ก่อนจะไว้ใจข้อสรุปของมัน

---

## ระดับ 1: `/report` — ต้นทุนต่อคำขอ ไม่ใช่ต่อเซสชัน

`/info` คือยอดรวมของเซสชัน `/report` คือใบเสร็จแยกรายการ — หนึ่งแถวต่อหนึ่งคำขอของผู้ใช้ สร้างใหม่จากบันทึกเซสชัน:

```
╭ /report
│ #   request                          cost      tools  task     ai       proc
│ ──  ───────────────────────────────  ────────  ─────  ───────  ───────  ───────
│  1  add a health-check endpoint      $0.04120      3   18s      12s      1s
│  2  wire it into the router          $0.02980      2   11s      9s       1s
│  3  why is the test flaky            $3.98120     84   5m 31s   3m 40s   20s
│ ──  ───────────────────────────────  ────────  ─────  ───────  ───────  ───────
│  Σ  3 request(s)                     $4.05220     89   6m 00s   4m 01s   22s
╰ /report  3 request(s) · $4.05220
```

นั่นไง คำขอที่ 3 — "why is the test flaky" — คือบิลทั้งหมด การเรียกเครื่องมือแปดสิบสี่ครั้งสำหรับคำถามเดียว คำขอที่ประพฤติดีสองอันขนาบหายนะหนึ่งอันไว้ และถ้าไม่มีการแยกต่อคำขอ ทั้งสามก็เบลอรวมเป็นยอดเซสชันเดียวกัน `/report` คือวิธีที่คุณหาว่า _พรอมต์ไหน_ ที่ส่ง agent ออกนอกราง ซึ่งเป็นคำถามที่คุณต้องตอบจริง ๆ ก่อนจะแก้อะไรได้

คอลัมน์แยกเวลา `task` (ทั้งคำขอ ตั้งแต่ต้นจนจบ) ออกจากเวลา `ai` (เฉพาะการเรียกโมเดล) เมื่อ `task` เกิน `ai` ไปมาก แสดงว่าเครื่องมือของคุณช้า เมื่อทั้งสองวิ่งไล่กัน แสดงว่าโมเดลกำลังวิ่งไปกลับเยอะ — ลายเซ็นลูปอีกครั้ง

---

## ระดับ 2: `/context` — อ่านว่าจริง ๆ แล้ว agent กำลังคิดเรื่องอะไร

ต้นทุนและตัวนับบอกคุณว่า _มี_ บางอย่างผิด การจะเห็นว่า _อะไร_ คุณต้องอ่านข้อความ `/context` ดัมป์บทสนทนาสด ๆ เป็น JSON ที่มีโครงสร้าง พร้อมตัวกรอง:

```
/context              # ทั้งหมด
/context large        # เฉพาะข้อความเกิน 1000 ตัวอักษร — หาตัวกินคอนเท็กซ์
/context tool         # เฉพาะผลลัพธ์เครื่องมือ — ดูว่า agent ได้อะไรกลับมาจริง ๆ
/context assistant    # เฉพาะเทิร์นของโมเดลเอง
```

`/context large` คือตัวที่ผมคว้าเมื่อโทเคนระเบิด มันดึงข้อความที่ทำให้หน้าต่างบวมขึ้นมาให้เห็นเป๊ะ ๆ — มักเป็นผลลัพธ์เครื่องมือตัวหนึ่งที่คืนไฟล์ยักษ์หรือ JSON blob ก้อนใหญ่ — และตอนนี้คุณรู้แล้วว่าต้องสอนเครื่องมือตัวไหนให้แบ่งหน้าหรือตัดทอน ในหายนะ flaky-test ของผม `/context tool` แสดงผลลัพธ์ `grep` แปดสิบกว่าอันที่ว่างเปล่าทั้งหมด โมเดลไม่เคยปรับกลยุทธ์เลยเพราะไม่มีอะไรบอกมันว่ากลยุทธ์ไม่ได้ผล การอ่านผลลัพธ์เครื่องมือทำให้ลูปชัดเจนในราวสิบห้าวินาที

---

## ระดับ 3: บันทึกเซสชัน — บันทึกครบถ้วนที่เล่นซ้ำได้

ทุกอย่างข้างบนอ่านจากสิ่งประดิษฐ์ชิ้นเดียว: ไฟล์เซสชัน Octomind เขียนทุกเซสชันลงที่

```
~/.local/share/octomind/sessions/<name>.jsonl.zst
```

มันคือ JSONL ที่บีบอัดด้วย zstd — หนึ่งอ็อบเจกต์ JSON ต่อบรรทัด บรรทัดส่วนใหญ่คือข้อความบทสนทนาดิบ (`role: user|assistant|tool|system`) แทรกอยู่ระหว่างนั้นคือมาร์กเกอร์ที่มีไทป์ ซึ่งรันไทม์ต้องใช้สร้างสถานะใหม่ตอน resume:

| มาร์กเกอร์                            | บันทึกอะไร                                                      |
| ------------------------------------- | --------------------------------------------------------------- |
| `STATS`                               | ยอดรวมที่กำลังวิ่ง — ต้นทุน เวลา API เวลาเครื่องมือ — ณ จุดนั้น |
| `COMPRESSION_POINT`                   | มีการบีบอัด: ชนิด ข้อความที่ลบ โทเคนที่ประหยัด                  |
| `RESTORATION_POINT`                   | เช็กพอยต์ `/done` — ข้อความก่อนหน้ายุบเมื่อโหลดใหม่             |
| `KNOWLEDGE_ENTRY`                     | ข้อเท็จจริงที่สกัดระหว่างบีบอัด ฉีดกลับเข้าไปตอน resume         |
| `COMMAND`                             | คำสั่งรันไทม์ (`/model`, `/role`, `/effort`…) เล่นซ้ำตอน resume |
| `PLAN_SNAPSHOT` / `SCHEDULE_SNAPSHOT` | แผน / ตารางเวลาที่กำลังใช้งาน เพื่อให้รอดจากการรีสตาร์ท         |

ไฟล์นี้คือความจริงพื้นฐาน `/report` ถูกสร้างจากการคลายบีบอัดไฟล์นี้แล้วเดินผ่านรายการ `STATS` และ `USER`/`COMMAND` เพื่อแบ่งต้นทุนระหว่างคำขอตามตัวอักษรเลย คุณทำแบบเดียวกันเองได้:

```bash
zstd -dc ~/.local/share/octomind/sessions/my-feature-x.jsonl.zst \
  | jq -r 'select(.role == "tool") | "\(.content | length)\t\(.name)"' \
  | sort -n | tail
```

one-liner นั้นจัดอันดับผลลัพธ์เครื่องมือตามขนาดตรงจากบันทึก — ตัวกินโทเคนของคุณ โดยไม่ต้องเปิดเซสชันด้วยซ้ำ เพราะบันทึกเป็น JSONL แบบ append-only มันจึงเป็นสิ่งประดิษฐ์สำหรับเล่นซ้ำที่สมบูรณ์แบบด้วย: resume เซสชันเดิมเป๊ะ ๆ ด้วย `octomind run --resume my-feature-x` หรือหยิบอันล่าสุดของไดเรกทอรีปัจจุบันด้วย `--resume-recent` แล้วรันไทม์จะสร้างสถานะใหม่จากบรรทัดเหล่านี้เอง

---

## ระดับ 4: `RUST_LOG` — เมื่อต้องมองเข้าไปในรันไทม์

บันทึกเซสชันแสดงว่า _บทสนทนาคืออะไร_ เมื่อคุณต้องเห็นว่า _รันไทม์ทำอะไร_ — ทำไมเซิร์ฟเวอร์ MCP ไม่โหลด ทำไมเครื่องมือถูกข้าม โพรไวเดอร์คืนอะไรมาจริง ๆ — ให้เปิด trace Octomind สร้างบน crate `tracing` หลังตัวแปรสภาพแวดล้อมมาตรฐาน `RUST_LOG` ในโหมด CLI **จงใจ** ไม่มี subscriber ของ trace จนกว่าคุณจะขอ — ผู้ใช้ได้เอาต์พุตสีสะอาด ไม่ใช่สายดับเพลิง ตั้ง `RUST_LOG` แล้วสายดับเพลิงก็เปิด:

```bash
# ทุกอย่างที่ระดับ debug
RUST_LOG=debug octomind run

# จำกัดให้แค่โมดูลเดียว — มีประโยชน์กว่ามากในทางปฏิบัติ
RUST_LOG=octomind::mcp=debug octomind run

# หลายขอบเขต ระดับผสม
RUST_LOG=octomind::session=debug,octomind::mcp=trace octomind run
```

ขอบเขตมีความหมาย `RUST_LOG=debug` ในเซสชันที่วุ่นวายอ่านไม่รู้เรื่อง ส่วน `RUST_LOG=octomind::mcp=debug` เมื่อเครื่องมือไม่โผล่จะบอกคุณเป๊ะ ๆ ว่าผู้สมัครตัวไหนถูกรับหรือปฏิเสธและเพราะอะไร นี่คือระดับที่ "agent มองไม่เห็นเครื่องมือของผม" เลิกเป็นปริศนา

ในโหมดเอาต์พุตมีโครงสร้าง — ACP และ WebSocket — stdout และ stderr ถูกสงวนไว้สำหรับโปรโตคอล ดังนั้น trace จึงไปลงไฟล์แทน:

```
~/.local/share/octomind/logs/acp-debug.log        ← trace ของ ACP
~/.local/share/octomind/logs/acp-errors.jsonl     ← ข้อผิดพลาด ACP แบบมีโครงสร้าง
~/.local/share/octomind/logs/websocket-debug.log  ← trace ของ WebSocket
```

ถ้าคุณรัน Octomind หลังเอดิเตอร์ผ่าน ACP แล้วมีบางอย่างผิด หลักฐานก็อยู่ในไฟล์เหล่านั้น

---

## ระดับ 5: `--format jsonl` — ส่งรันเข้าทูลของคุณเอง

ทุกอย่างที่ผ่านมาเป็นของมนุษย์ที่อ่านเทอร์มินัล พอคุณเอา agent ใส่ CI หรือไปป์ไลน์ คุณต้องการรันเป็นสตรีมของอีเวนต์มีโครงสร้างที่คุณ assert ได้ `octomind run --format jsonl` ปล่อยอ็อบเจกต์ JSON หนึ่งตัวต่อบรรทัด — สตรีมอีเวนต์ภายในตัวเดียวกับที่เซิร์ฟเวอร์ WebSocket ใช้ — แท็กด้วย `type`:

```bash
echo "audit the auth module" | octomind run developer:general --format jsonl
```

แต่ละบรรทัดเป็นอีเวนต์แยกกัน ตัวแปรที่คุณจะสนใจ:

| `type`        | บรรจุอะไร                                                                                                                        |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `assistant`   | ชิ้นข้อความตอบของโมเดล                                                                                                           |
| `thinking`    | เนื้อหาการให้เหตุผล แยกจากคำตอบ                                                                                                  |
| `tool_use`    | `tool`, `tool_id`, `server`, `params` — agent กำลังจะลงมือ                                                                       |
| `tool_result` | `tool`, `content`, `success` — สิ่งที่กลับมา                                                                                     |
| `cost`        | `session_tokens`, `session_cost`, `input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`, `reasoning_tokens` |
| `error`       | ข้อความความล้มเหลว                                                                                                               |
| `injected`    | เทิร์นที่ไม่ใช่ผู้ใช้ — ตัวจับเวลาตามกำหนด, agent เบื้องหลัง, skill — พร้อม `source_kind`                                        |
| `skill`       | skill ถูกเปิดใช้ ใช้งาน หรือลืม                                                                                                  |

ตอนนี้โหมดความล้มเหลวกลายเป็น assertion นับอีเวนต์ `tool_use` แล้วทำให้ build พังถ้าคำขอเดียวเกินเกณฑ์ — นั่นคือสายสะดุดกันลูปควบคุมไม่อยู่ของคุณ เฝ้าดู `session_cost` ในอีเวนต์ `cost` แล้วแจ้งเตือนเมื่อเกินงบ กรอง `tool_result` หา `success: false` agent ที่เคยล้มเหลวเงียบ ๆ ตอนนี้ล้มเหลวในแบบที่ `jq` จับได้:

```bash
echo "run the migration check" \
  | octomind run --format jsonl \
  | jq -c 'select(.type == "tool_use") | .tool' \
  | sort | uniq -c | sort -rn
```

นั่นพิมพ์ฮิสโทแกรมการเรียกเครื่องมือของทั้งรัน ถ้า `shell` อยู่บนสุดด้วยจำนวน 60 คุณก็เจอลูปก่อนที่มันจะเจอกระเป๋าเงินของคุณ — และมันเป็น one-liner ในสเต็ป CI ไม่ใช่คนนั่งจ้องสปินเนอร์

---

## ระดับ 6: OctoHub — จับทุกคำขอ upstream

มีสิ่งหนึ่งที่อะไรข้างบนทั้งหมดแสดงให้ดูไม่ได้ เพราะมันเกิดใต้ตัว agent: **ไบต์ที่แน่นอน** ที่ Octomind ส่งให้โพรไวเดอร์และไบต์ที่แน่นอนที่กลับมา มุมมองของ agent คือข้อความของมันเอง มันแสดงคำขอระดับสายไฟไม่ได้ — โมเดลที่ resolve แล้ว เพย์โหลดที่ serialize เต็ม ๆ การตอบดิบของโพรไวเดอร์ เวลาแฝงจริง เมื่อคุณสงสัยว่าปัญหาอยู่ที่ชั้นแปล (system prompt ที่ไม่ใช่อย่างที่คุณคิด, สคีมาเครื่องมือที่โพรไวเดอร์ทำพัง, โมเดลที่ไม่ใช่ตัวที่คุณตั้งค่า) คุณต้องเห็นสายไฟ

[OctoHub](https://github.com/Muvon/octohub) คือพร็อกซี LLM ของเรา และการบันทึกคำขอ/การตอบแบบครบถ้วนคือเหตุผลที่มันมีอยู่ ชี้ Octomind มาที่มันแทนโพรไวเดอร์ แล้วทุก completion จะลงในฐานข้อมูลพร้อมอินพุต เอาต์พุต และเมตริกแนบมา รันมัน:

```bash
./octohub   # ฟังที่ 127.0.0.1:8080 โดยปริยาย
```

มันพูดทั้ง `POST /v1/completions` (รูปแบบดั้งเดิมของมัน) และ `POST /v1/chat/completions` (OpenAI คลาสสิก เสียบใช้แทนได้กับไคลเอนต์ที่เข้ากันได้กับ OpenAI ตัวใดก็ตาม) และทั้งคู่กระทบเอนจินเดียวกันและเขียนลงตาราง `completions` เดียวกันด้วยรหัสเรกคอร์ด `id` เดียวกัน หลังรัน ดึงเรกคอร์ดดิบกลับมาด้วย admin API:

```bash
curl "http://127.0.0.1:8080/v1/admin/completions?limit=50" \
  -H "Authorization: Bearer <master-key>"
```

แต่ละเรกคอร์ดบรรจุภาพครบที่ agent ให้ไม่ได้:

```json
{
  "id": "cmpl_<uuid>",
  "session_id": "<uuid>",
  "input_model": "my-model",
  "resolved_model": "gpt-5.5",
  "provider": "openai",
  "usage": {
    "input_tokens": 10,
    "output_tokens": 5,
    "total_tokens": 15,
    "cost": 0.0001,
    "request_time_ms": 320
  },
  "input": [...],
  "output": [...],
  "created_at": 1700000000
}
```

`input_model` เทียบกับ `resolved_model` เพียงอย่างเดียวก็จับบั๊กทั้งกลุ่ม "ทำไมมันทำตัวต่างไป" ได้ — คุณขอโมเดลหนึ่ง แต่ alias resolve ไปเป็นอีกตัว อาเรย์ `input` และ `output` คือเพย์โหลดตามตัวอักษร ดังนั้นสคีมาเครื่องมือที่โพรไวเดอร์สำลักก็อยู่ตรงนั้นให้อ่านได้ `request_time_ms` คือเวลาแฝงจริงของโพรไวเดอร์ แยกจากอะไรก็ตามที่ Octomind เพิ่มเข้าไป และ `GET /v1/admin/usage` รวมทั้งหมดตามคีย์ API และช่วงเวลา ซึ่งคือวิธีที่คุณก้าวจาก "agent แพง" ไปสู่ "คีย์นี้ โมเดลนี้ ชั่วโมงนี้" โดยไม่ต้องเดา (ถ้าคุณรัน agent กับหลายโพรไวเดอร์พร้อมกัน พร็อกซีก็เป็นที่ที่ทำให้เรื่องนี้สมเหตุสมผลด้วย — ดู [agent เดียวข้ามหลายโมเดล](/blog/running-one-ai-agent-across-many-models))

พร็อกซีเปลี่ยนขอบเขตของโมเดลจากขอบทึบ ๆ ให้กลายเป็นพื้นผิวที่บันทึกและสืบค้นได้ ที่จัดเก็บโดยปริยายคือ SQLite — `db_url = "sqlite://octohub.db"` หรือตั้ง `OCTOHUB_DB_URL` — จึงไม่มีโครงสร้างพื้นฐานที่ต้องตั้งก่อนคุณจะเริ่มอ่านคำขอ

---

## เมื่อ agent ทำ X ให้ดู Y

จุดประสงค์ทั้งหมดคือเปลี่ยน "agent ทำอะไรงง ๆ" ให้เป็นการค้นหาที่รู้จัก นี่คือตารางที่ติดอยู่เหนือโต๊ะของผม:

| อาการ                               | ดูที่แรก                                                | กำลังหาอะไร                                |
| ----------------------------------- | ------------------------------------------------------- | ------------------------------------------ |
| ต้นทุนเซสชันโผล่มาจากไหนไม่รู้      | `/info`                                                 | `cache rd` ≫ `in` ในแถว breakdown          |
| พรอมต์เดียวกินงบหมด                 | `/report`                                               | แถวเดียวที่แบกบิล                          |
| หน้าต่างคอนเท็กซ์เต็ม / โมเดล "ลืม" | บล็อก compression ของ `/info` แล้ว `/context large`     | จำนวนการบีบอัด และข้อความที่ใหญ่เกิน       |
| ลูป / การเรียกเครื่องมือซ้ำ         | คอลัมน์ tools ของ `/report` หรือ `jsonl` + `uniq -c`    | เครื่องมือเดิม อาร์กิวเมนต์เดิม ไม่คืบหน้า |
| เครื่องมือไม่พร้อมใช้สำหรับ agent   | `RUST_LOG=octomind::mcp=debug`                          | ทำไมผู้สมัครถูกข้าม                        |
| ผลลัพธ์เครื่องมือดูผิด              | `/context tool`                                         | ไบต์จริงที่โมเดลได้รับ                     |
| ทำตัวเหมือนโมเดลอื่น                | OctoHub `GET /v1/admin/completions`                     | `input_model` เทียบ `resolved_model`       |
| โพรไวเดอร์ error หรือเวลาแฝงแปลก    | `input`/`output` ของเรกคอร์ด OctoHub, `request_time_ms` | เพย์โหลดดิบและเวลาจริง                     |
| ต้องการสายสะดุดใน CI                | `--format jsonl`                                        | นับ `tool_use` เฝ้า `cost` จับ `error`     |
| เล่นซ้ำรันเดิมเป๊ะ                  | `octomind run --resume <name>`                          | เล่นซ้ำจาก `<name>.jsonl.zst`              |

---

## การวินิจฉัยในสองนาที

นี่คือสิ่งที่หายนะ flaky-test จะเป็นไป ถ้าต่อทั้งหมดนี้ไว้ตั้งแต่ต้น แทนปริศนาหกนาทีที่มันเป็นจริง ๆ

สปินเนอร์รันนาน `/report` — คำขอที่ 3 คือ $3.98 และเรียกเครื่องมือ 84 ครั้ง อีกสองอันปกติ งั้นก็พรอมต์นั้นแหละ `/context tool` — ผลลัพธ์ `grep` ว่างแปดสิบอัน โมเดลไม่เคยเปลี่ยนกลยุทธ์ นั่นไงลูป และนั่นไงเหตุผล เวลารวมจนถึงสาเหตุราก: ราวเก้าสิบวินาที ไม่ต้องเสียสี่ดอลลาร์ เพราะในไปป์ไลน์จริง ฮิสโทแกรมการเรียกเครื่องมือจาก `jsonl` คงสะดุดเกณฑ์และฆ่ารันตั้งแต่การเรียกเครื่องมือครั้งที่ยี่สิบ

ไม่มีอะไรในนี้แปลกใหม่ มันคือสัญชาตญาณเดียวกับการ logging, metrics และ tracing ในระบบใด ๆ ที่คุณจะเอาขึ้นโปรดักชัน — นำมาใช้กับระบบที่ความล้มเหลวเงียบโดยธรรมชาติ agent จะไม่บอกคุณว่ามันหลงทาง แต่รันสังเกตได้ครบถ้วนถ้าคุณถามมันให้ถูกวิธี: `/info` สำหรับรูปร่าง `/report` สำหรับตัวการ `/context` สำหรับการให้เหตุผล บันทึกเซสชันสำหรับบันทึก `RUST_LOG` สำหรับรันไทม์ `--format jsonl` สำหรับเครื่องจักร และ OctoHub สำหรับสายไฟ

ต่อมันไว้ก่อนรันที่แพง ไม่ใช่หลังจากนั้น

— Don

---

_[Octomind](https://github.com/Muvon/octomind) และ [OctoHub](https://github.com/Muvon/octohub) เป็นโอเพนซอร์สภายใต้ Apache-2.0 ถ้าพื้นผิวดีบักที่คุณต้องการยังขาดอยู่ ก็ [เปิด issue](https://github.com/Muvon/octomind/issues) — การมองเห็นภายในในโพสต์นี้มีอยู่ส่วนใหญ่เพราะ agent ของเราเองทำเราประหลาดใจไม่หยุด_
