เลิกต่อ agent เป็นเส้นตรง — Graph Engineering กับ Claude
คนส่วนใหญ่ที่ลองสร้าง agent หลายขั้น จบลงที่เส้นตรง — แต่ละขั้นยืนรอขั้นก่อนหน้าจบอย่างสุภาพ ทั้งที่ครึ่งหนึ่งไม่ได้เกี่ยวกันเลย บทความนี้พาไปดูว่างานจริงมันเป็นกราฟ ไม่ใช่เส้น
ทำไมทุกคนเริ่มด้วยเส้นตรง
เพราะเราคิดเป็นลำดับ เราเขียนสิ่งที่อยากได้เป็นข้อ ๆ แล้วแปลงเป็นโค้ดตรง ๆ:
// วิธีที่เกือบทุกคนเริ่ม const a = await agent("อ่าน schema ทั้งหมด") const b = await agent("หา bug ฝั่ง backend") const c = await agent("หา bug ฝั่ง frontend") const d = await agent("หา bug เรื่อง security") const e = await agent("สรุปทั้งหมด")
โค้ดนี้อ่านง่ายมาก และผิดโดยสิ้นเชิงในแง่เวลา — b, c, d ไม่ได้ใช้ผลของกันและกันเลย
แต่ await ทำให้มันต่อคิว ถ้าแต่ละตัวใช้ 3 นาที คุณเสียไป 9 นาที ทั้งที่ควรใช้ 3
Node กับ Edge — ศัพท์แค่สองคำ
หน่วยงาน 1 ชิ้น = subagent 1 ตัว ที่ได้ prompt ไป แล้วคืนผลลัพธ์กลับมา มันมีบริบทของตัวเอง ไม่เห็นบริบทของ node อื่น
ข้อมูลที่ไหลจาก node หนึ่งไปอีก node — ในโค้ดคือ “ผลของอันนี้ ถูกส่งเป็น input ของอันนั้น” ไม่มีข้อมูลไหล = ไม่มี edge
คนมักคิดว่า edge คือ “ลำดับ” แต่ไม่ใช่ — edge คือ การพึ่งพาข้อมูล ถ้า node B ไม่ได้ใช้อะไรจาก node A เลย แต่คุณเขียนให้มันรอ A นั่นคือ edge ปลอมที่คุณเผลอสร้างขึ้นมาเอง และมันคือต้นเหตุของเวลาที่หายไปเกือบทั้งหมด
กราฟอยู่ในโค้ด ไม่ได้อยู่ใน prompt
นี่คือจุดที่ทำให้ graph engineering ต่างจาก “สั่ง AI ให้จัดการเอง”
โครงของกราฟ — จะแตกกี่แขนง วนกี่รอบ ถ้าเงื่อนไขนี้แล้วไปทางไหน — เขียนเป็น
JavaScript ธรรมดา for, if, map, filter
ส่วนโมเดลคิดแค่ “เนื้องานใน node นี้” ไม่ต้องคิดว่าต่อไปทำอะไร
- เผา token ไปกับการวางแผน
- รันสองครั้งได้ผลคนละอย่าง
- debug ไม่ได้ ไม่รู้ว่าทำไมมันเลือกทางนั้น
- token ไปอยู่กับงานจริง 100%
- รันซ้ำได้ผลเหมือนเดิม (deterministic)
- อ่านโค้ดก็รู้เลยว่ากราฟหน้าตายังไง
ตัวอย่างโครงของ workflow หนึ่งตัว — ส่วนหัวบอกว่ามีกี่เฟส ส่วนล่างคือกราฟจริง:
export const meta = { name: 'audit-pos', description: 'หา bug ในระบบ POS แล้วยืนยันทีละข้อ', phases: [{ title: 'Find' }, { title: 'Verify' }], } phase('Find') // ...กราฟเริ่มตรงนี้
Fan-out — แตกออกให้กว้าง
ท่าพื้นฐานที่สุด: งานหนึ่งชิ้น แตกเป็น N ชิ้นที่ทำพร้อมกันได้
const DIMS = ['ราคา', 'สต็อก', 'สิทธิ์', 'offline'] const results = await parallel( DIMS.map(d => () => agent(`หา bug เรื่อง ${d} ในระบบ POS`)) )
() =>
parallel() รับ “ฟังก์ชันที่ยังไม่ถูกเรียก” ไม่ใช่ promise ที่รันไปแล้ว
ลืม () => คือ bug อันดับหนึ่งของมือใหม่ข้อควรรู้: มันไม่ได้ยิงพร้อมกันทั้งร้อยตัวจริง ๆ ระบบมีเพดาน concurrency (ราว 10–16 ตัวพร้อมกัน) ที่เหลือเข้าคิวรอช่องว่าง — คุณส่ง 100 ชิ้นได้ ทุกชิ้นเสร็จครบ แค่ไม่ได้วิ่งพร้อมกันหมด
Fan-in — และคำว่า barrier
ขากลับคือ N → 1: เอาผลทั้งหมดมารวมเป็นชิ้นเดียว จุดนี้เรียกว่า barrier (กำแพง) เพราะทุกสายต้องมาถึงก่อน ถึงจะผ่านไปได้
const all = results.filter(Boolean).flatMap(r => r.bugs) const report = await agent(`สรุป bug ${all.length} ข้อนี้เป็นรายงานเดียว`)
parallel() ไม่เคยโยน error
thunk ที่พังจะกลายเป็น null ในอาร์เรย์ผลลัพธ์ ไม่ใช่ exception
ต้อง .filter(Boolean) เสมอ ไม่งั้นเจอ Cannot read property of null ทีหลังทรงเพชร — fan-out แล้ว fan-in
เอา 03 + 04 มาต่อกันได้ทรงที่ใช้บ่อยที่สุดในงานจริง เรียกว่า diamond
ทรงนี้ถูกต้องเมื่อ “รวมผล” จำเป็นต้องเห็นผลของทั้งสามสายพร้อมกันจริง ๆ แต่ถ้ามันไม่จำเป็น เส้นประนั้นคือเวลาที่คุณทิ้งเปล่า — ซึ่งพาเราไปสู่หัวข้อสำคัญที่สุดของบทความ
pipeline vs barrier — จุดที่คนพลาดมากที่สุด
สมมติงาน 5 ชิ้น แต่ละชิ้นต้องผ่าน 3 ขั้น ลองกดสลับดูว่าต่างกันแค่ไหน
ต่างกันตรงไหน
ทุกชิ้นต้องจบขั้นที่ 1 ให้ครบก่อน ถึงจะมีใครได้เริ่มขั้นที่ 2 → เวลารวม = ผลบวกของ “ตัวช้าสุดในแต่ละขั้น”
แต่ละชิ้นไหลไปตามทางของตัวเอง ชิ้น A อยู่ขั้น 3 ได้ ขณะที่ชิ้น B ยังอยู่ขั้น 1 → เวลารวม = “สายที่ยาวที่สุดสายเดียว”
// pipeline — ค่าเริ่มต้นที่ควรใช้ const out = await pipeline( DIMS, d => agent(`หา bug เรื่อง ${d}`, { schema: FINDINGS }), res => parallel(res.findings.map(f => () => agent(`ยืนยันว่าจริงไหม: ${f.title}`, { schema: VERDICT }))) ) // bug ของ 'ราคา' เริ่มถูกยืนยัน ขณะที่ 'offline' ยังหาไม่เสร็จ
barrier ถูกต้องเมื่อไหร่
เมื่อขั้นถัดไปจำเป็นต้องเห็นผลของทุกชิ้นพร้อมกันเท่านั้น มีอยู่สามกรณีจริง ๆ:
- ต้องตัดของซ้ำข้ามทุกชิ้นก่อน ไม่งั้นงานขั้นถัดไปจะทำซ้ำโดยเปล่าประโยชน์
- ต้องเช็กยอดรวมเพื่อออกก่อนเวลา — เช่น “ถ้าไม่เจอ bug เลยสักข้อ ข้ามขั้นยืนยันทั้งหมด”
- prompt ของขั้นถัดไปอ้างถึงผลของชิ้นอื่น เช่น “เทียบข้อนี้กับที่เหลือแล้วจัดอันดับ”
• “มันคนละ stage กันอยู่แล้ว” → คนละ stage ≠ ต้องรอพร้อมกัน
• “เขียนแบบนี้โค้ดสวยกว่า” → ความสวยแลกด้วยเวลารอจริง ๆ
parallel → transform → parallel แล้ว transform ตรงกลางไม่ได้ยุ่งกับข้อมูลข้ามชิ้นเลย
แปลว่า barrier นั้นไม่จำเป็น → เขียนใหม่เป็น pipelineRouting — ให้กราฟเลือกทางเองตอนรัน
ไม่ต้องมีของวิเศษ ใช้ if ธรรมดา สิ่งที่ทำให้มันเป็น “routing” คือคุณตัดสินใจจาก
ผลของ node ก่อนหน้า ไม่ใช่จากที่เดาไว้ล่วงหน้า
const triage = await agent('จัดหมวดปัญหานี้', { schema: TRIAGE }) if (triage.kind === 'security') { return await agent('ตรวจเชิงลึกด้านความปลอดภัย', { effort: 'high' }) } if (triage.kind === 'typo') { return await agent('แก้คำผิด', { effort: 'low' }) } return await agent('ตรวจแบบมาตรฐาน')
ประโยชน์จริงคือไม่จ่ายค่าเส้นทางที่ไม่ได้เดิน — งานง่ายไม่ต้องผ่านด่านตรวจ 5 ชั้น
Node ตรวจสอบ — อย่าเชื่อ node เดียว
agent ตัวเดียวที่พูดจาน่าเชื่อถือ ยังผิดได้ วิธีแก้ไม่ใช่ prompt ให้ดีขึ้น แต่คือเพิ่ม node ที่พยายามหักล้าง
const votes = await parallel(['ถูกต้อง', 'reproduce ได้', 'กระทบผู้ใช้จริง'].map(lens => () => agent(`ลองหักล้างข้อนี้ในมุม ${lens}: ${claim} — ถ้าไม่แน่ใจให้ตอบว่าหักล้างได้`, { schema: VERDICT }) )) const real = votes.filter(Boolean).filter(v => !v.refuted).length >= 2
เลือกโมเดล/ความพยายาม เป็นราย node
ไม่ใช่ทุก node ที่ต้องใช้ของแพง node กวาดข้อมูลกับ node ตัดสินใจ ควรคนละระดับ
| ชนิดของ node | ความพยายาม | เหตุผล |
|---|---|---|
| กวาด/รวบรวมข้อมูล | low | งานกลไก ไม่ต้องคิดลึก |
| วิเคราะห์/หา bug | medium | ค่าเริ่มต้นของงานส่วนใหญ่ |
| ตรวจสอบ/ตัดสิน | high | ผิดตรงนี้ = ของเสียหลุดออกไปทั้งชุด |
| สังเคราะห์ครั้งสุดท้าย | high | ต้องถือภาพรวมทั้งหมดพร้อมกัน |
agent('ลิสต์ไฟล์ที่แก้ใน branch นี้', { effort: 'low' }) agent('ตัดสินว่า bug นี้จริงหรือไม่', { effort: 'high' })
วนจนหมดเกลี้ยง — เมื่อไม่รู้ว่ามีของกี่ชิ้น
ถ้าคุณกำลังหา bug แล้วบอกว่า “หาให้ครบ 10 ข้อ” คุณกำลังเดา — อาจมี 3 หรือ 40 ท่าที่ถูกคือวนจน ไม่เจอของใหม่ติดกัน K รอบ
const seen = new Set(), confirmed = [] let dry = 0 while (dry < 2) { const found = (await parallel(FINDERS.map(f => () => agent(f.prompt, { schema: BUGS })))) .filter(Boolean).flatMap(r => r.bugs) const fresh = found.filter(b => !seen.has(key(b))) // ← ตัดกับ seen if (!fresh.length) { dry++; continue } dry = 0 fresh.forEach(b => seen.add(key(b))) confirmed.push(...(await verifyAll(fresh))) }
seen (ทุกอย่างที่เคยเจอ) ไม่ใช่ confirmed (ที่ผ่านการตรวจ)
ไม่งั้นของที่ผู้ตรวจปัดตกจะโผล่กลับมาใหม่ทุกรอบตลอดกาลกราฟที่สร้างรูปร่างตัวเอง
คุณไม่จำเป็นต้องรู้รูปร่างกราฟตั้งแต่ก่อนเริ่มงาน — รู้ก่อนตอนสั่งแตกแขนงก็พอ ท่าที่ใช้จริงคือ ลูกผสม: สำรวจก่อนด้วยตัวเอง แล้วค่อยเอาผลไปกางเป็นกราฟ
// 1) สำรวจเองก่อน — ยังไม่ต้องแตกแขนง const files = await agent('ลิสต์ router ทั้งหมดในแอปนี้', { schema: FILES }) // 2) รูปร่างกราฟเพิ่งจะรู้ตรงนี้ const audits = await pipeline( files.paths, p => agent(`ตรวจ ${p}`, { schema: FINDINGS }), r => verifyAll(r.findings) )
อีกท่าคือให้ขนาดของกราฟผูกกับงบที่มี:
while (budget.total && budget.remaining() > 50_000) { // เดินอีกรอบเท่าที่งบยังเหลือ }
log() บอกว่าตัดอะไรทิ้ง ไม่งั้นรายงานจะอ่านเหมือน “ตรวจครบแล้ว” ทั้งที่ไม่ครบ6 เทมเพลตที่หยิบไปใช้ได้เลย
ทำความเข้าใจ
N → 1ปล่อยผู้อ่านหลายตัวลงคนละส่วนของระบบ แล้วรวมเป็นแผนที่เดียว
ออกแบบ
N ⇄ N → 1ให้ N แนวทางแยกกันคิด แล้วให้กรรมการให้คะแนน สังเคราะห์จากตัวชนะ
ตรวจงาน
pipeline 2 ขั้นแตกตามมิติ → หา → ยืนยันแบบหักล้าง ทีละข้อทันทีที่เจอ
ค้นคว้า
หลายช่องทาง → 1ค้นคนละวิธี (ตามหมวด/ตามเนื้อหา/ตามช่วงเวลา) แล้วอ่านลึก แล้วสรุป
ย้ายระบบ
pipeline + แยกพื้นที่หาจุดที่ต้องแก้ → แก้ทีละจุดในพื้นที่แยก (worktree) → ตรวจว่ายังทำงานได้
ล่าจนหมด
วนซ้ำ + ตัดซ้ำวนหาจนไม่เจอของใหม่ K รอบ ตัดซ้ำกับทุกอย่างที่เคยเจอ
ตัวอย่างจริง — ตรวจระบบ POS ทั้งแอป
รวมทุกอย่างข้างบนเป็นกราฟเดียวที่ใช้งานได้จริง
export const meta = { name: 'audit-pos', description: 'หา bug ระบบ POS แยกตามมิติ แล้วยืนยันทีละข้อ', phases: [{ title: 'Find' }, { title: 'Verify' }], } const DIMS = [ { key: 'money', effort: 'high', p: 'ตรวจการคำนวณเงิน ส่วนลด ภาษี การปัดเศษ' }, { key: 'stock', effort: 'medium', p: 'ตรวจการตัดสต็อก คลังรวม การขายแตกหน่วย' }, { key: 'perm', effort: 'high', p: 'ตรวจสิทธิ์ role และการรั่วข้ามร้าน (org_id)' }, { key: 'offline', effort: 'medium', p: 'ตรวจคิว offline การ sync ซ้ำ idempotency' }, ] const LENSES = ['ถูกต้องเชิงตรรกะ', 'สร้างซ้ำได้จริง', 'กระทบผู้ใช้จริง'] const out = await pipeline( DIMS, // ── ขั้น 1: หา (แต่ละมิติเริ่มพร้อมกัน) d => agent(d.p, { label: `find:${d.key}`, phase: 'Find', effort: d.effort, schema: FINDINGS }), // ── ขั้น 2: ยืนยัน (เริ่มทันทีที่มิตินั้นเสร็จ ไม่รอมิติอื่น) (res, d) => parallel((res?.findings ?? []).map(f => () => parallel(LENSES.map(lens => () => agent(`ลองหักล้างในมุม ${lens}: ${f.title}\n${f.detail}`, { label: `verify:${d.key}`, phase: 'Verify', effort: 'high', schema: VERDICT }))) .then(vs => ({ ...f, dim: d.key, real: vs.filter(Boolean).filter(v => !v.refuted).length >= 2, })))) ) const confirmed = out.flat().filter(Boolean).filter(f => f.real) log(`ยืนยันแล้ว ${confirmed.length} ข้อ จากที่เจอทั้งหมด ${out.flat().length}`) return { confirmed }
สังเกตว่ามิติ money ที่เจอ bug ก่อน จะเริ่มถูกยืนยันทันที
ขณะที่ offline ยังหาไม่เสร็จด้วยซ้ำ — นั่นคือ pipeline ทำงาน
กับดัก 5 อย่าง + เช็กลิสต์
| กับดัก | อาการ | ทางแก้ |
|---|---|---|
ลืม () => | ทุกอย่างรันเรียงกันเหมือนเดิม | ส่งฟังก์ชัน ไม่ใช่ promise |
ไม่ .filter(Boolean) | พังตอน map ผลลัพธ์ | node ที่ล้ม = null เสมอ |
| barrier เกินจำเป็น | ช้ากว่าที่ควร 30–50% | ใช้ pipeline เป็นค่าเริ่มต้น |
| ตัดซ้ำผิดตัว | ลูปไม่มีวันจบ | ตัดกับ seen ไม่ใช่ confirmed |
| ตัดขอบเขตเงียบ ๆ | รายงานดูครบทั้งที่ไม่ครบ | log() ทุกครั้งที่ตัดทิ้ง |
เช็กลิสต์ก่อนปล่อยกราฟลงสนาม
- วาดกราฟก่อน แล้วค่อยเขียนโค้ด — ถ้าวาดไม่ออก แปลว่ายังไม่เข้าใจงาน
- ทุก edge ตอบได้ว่า “ข้อมูลอะไรไหลผ่านเส้นนี้”
- ลบ edge ที่ตอบไม่ได้ทิ้ง มันคือ edge ปลอม
- เลือก pipeline เป็นค่าเริ่มต้น ใช้ barrier เมื่อพิสูจน์ได้ว่าจำเป็น
- ทุก
parallel()มี.filter(Boolean) - node ที่แตกแขนง ใช้
schemaเพื่อให้ผลกลับมาเป็นข้อมูล ไม่ใช่ข้อความ - node ตัดสินใจสำคัญ ผ่านด่านตรวจอย่างน้อย 1 ชั้น
- ผู้ตรวจถูกสั่งให้หักล้าง และไม่แน่ใจ = หักล้าง
- ผู้ตรวจหลายตัว มองคนละมุม ไม่ใช่ถามซ้ำคำถามเดิม
- ระดับ effort เหมาะกับความยากของแต่ละ node
- ลูปมีเงื่อนไขจบที่ชัดเจน และตัดซ้ำกับ
seen - ทุกจุดที่จำกัดขอบเขต มี
log()กำกับ - รันซ้ำแล้วได้โครงเดิม (ไม่ใช้
Date.now()/Math.random()ในสคริปต์) - ขนาดกราฟสมกับงาน — “เช็กเร็ว ๆ” ไม่ต้องมี 40 node
- Codez (@0xCodez). Graph Engineering with Claude: 14-Step roadmap from 0 to graph architect. X Article, 2026 —
โพสต์ต้นทาง
โครงหัวข้อ 14 ขั้นและคำว่า fan-out / barrier / diamond มาจากบทความนี้ - Anthropic. Claude Code — Workflow & subagents — สเปกของ
agent(),parallel(),pipeline(),phase(),log()ที่ใช้ในตัวอย่างทั้งหมด - Anthropic Engineering. Building effective agents — ที่มาของหลัก “เริ่มจากโครงที่ง่ายที่สุดที่ใช้ได้”
- Anthropic Engineering. How we built our multi-agent research system — ที่มาของท่ากวาดหลายช่องทางแล้วสังเคราะห์