อยากรู้YAKROO
⬡ Hub

หน้าแรก · 🧠 AI & เทคโนโลยี

เลิกต่อ agent เป็นเส้นตรง — Graph Engineering กับ Claude

คนส่วนใหญ่ที่ลองสร้าง agent หลายขั้น จบลงที่เส้นตรง — แต่ละขั้นยืนรอขั้นก่อนหน้าจบอย่างสุภาพ ทั้งที่ครึ่งหนึ่งไม่ได้เกี่ยวกันเลย บทความนี้พาไปดูว่างานจริงมันเป็นกราฟ ไม่ใช่เส้น

อ่าน ~18 นาที 23/07/2026 โดย Cosmixon
แบบเส้นตรง — 5 ขั้น รอกันเป็นทอด อ่าน ค้น A ค้น B ค้น C สรุป รวมเวลา = 1+3+3+3+1 = 11 หน่วย แบบกราฟ — สามอันที่ไม่เกี่ยวกัน แตกออกพร้อมกัน อ่าน ค้น A ค้น B ค้น C สรุป รวม = 1+3+1 = 5 หน่วย
งานชิ้นเดียวกัน จำนวน node เท่ากัน ต่างกันแค่ว่า “อะไรต้องรออะไร” — เวลาลดลงกว่าครึ่ง
00

ทำไมทุกคนเริ่มด้วยเส้นตรง

เพราะเราคิดเป็นลำดับ เราเขียนสิ่งที่อยากได้เป็นข้อ ๆ แล้วแปลงเป็นโค้ดตรง ๆ:

// วิธีที่เกือบทุกคนเริ่ม
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 ที่ไม่มีเส้นเชื่อมถึงกัน = รันพร้อมกันได้ หน้าที่ของคุณคือหาให้เจอว่าเส้นไหนไม่มีอยู่จริง
01

Node กับ Edge — ศัพท์แค่สองคำ

Node

หน่วยงาน 1 ชิ้น = subagent 1 ตัว ที่ได้ prompt ไป แล้วคืนผลลัพธ์กลับมา มันมีบริบทของตัวเอง ไม่เห็นบริบทของ node อื่น

Edge

ข้อมูลที่ไหลจาก node หนึ่งไปอีก node — ในโค้ดคือ “ผลของอันนี้ ถูกส่งเป็น input ของอันนั้น” ไม่มีข้อมูลไหล = ไม่มี edge

คนมักคิดว่า edge คือ “ลำดับ” แต่ไม่ใช่ — edge คือ การพึ่งพาข้อมูล ถ้า node B ไม่ได้ใช้อะไรจาก node A เลย แต่คุณเขียนให้มันรอ A นั่นคือ edge ปลอมที่คุณเผลอสร้างขึ้นมาเอง และมันคือต้นเหตุของเวลาที่หายไปเกือบทั้งหมด

edge ปลอมที่เจอบ่อย “ต้องอ่านไฟล์ให้ครบก่อนค่อยเริ่มวิเคราะห์” — จริงหรือ? ถ้าแต่ละ node อ่านไฟล์ของตัวเองได้ ก็ไม่ต้องมีขั้นอ่านรวม
02

กราฟอยู่ในโค้ด ไม่ได้อยู่ใน 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')
// ...กราฟเริ่มตรงนี้
03

Fan-out — แตกออกให้กว้าง

ท่าพื้นฐานที่สุด: งานหนึ่งชิ้น แตกเป็น N ชิ้นที่ทำพร้อมกันได้

รายการงาน agent · ราคา agent · สต็อก agent · สิทธิ์ agent · offline ทั้งสี่เริ่มพร้อมกัน
const DIMS = ['ราคา', 'สต็อก', 'สิทธิ์', 'offline']

const results = await parallel(
  DIMS.map(d => () => agent(`หา bug เรื่อง ${d} ในระบบ POS`))
)
สังเกตให้ดี: () => parallel() รับ “ฟังก์ชันที่ยังไม่ถูกเรียก” ไม่ใช่ promise ที่รันไปแล้ว ลืม () => คือ bug อันดับหนึ่งของมือใหม่

ข้อควรรู้: มันไม่ได้ยิงพร้อมกันทั้งร้อยตัวจริง ๆ ระบบมีเพดาน concurrency (ราว 10–16 ตัวพร้อมกัน) ที่เหลือเข้าคิวรอช่องว่าง — คุณส่ง 100 ชิ้นได้ ทุกชิ้นเสร็จครบ แค่ไม่ได้วิ่งพร้อมกันหมด

04

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 ทีหลัง
05

ทรงเพชร — fan-out แล้ว fan-in

เอา 03 + 04 มาต่อกันได้ทรงที่ใช้บ่อยที่สุดในงานจริง เรียกว่า diamond

เริ่ม งาน 1 งาน 2 งาน 3 รวมผล barrier
เส้นประแดงคือ barrier — ทุกสายต้องข้ามเส้นนี้พร้อมกัน

ทรงนี้ถูกต้องเมื่อ “รวมผล” จำเป็นต้องเห็นผลของทั้งสามสายพร้อมกันจริง ๆ แต่ถ้ามันไม่จำเป็น เส้นประนั้นคือเวลาที่คุณทิ้งเปล่า — ซึ่งพาเราไปสู่หัวข้อสำคัญที่สุดของบทความ

06

pipeline vs barrier — จุดที่คนพลาดมากที่สุด

สมมติงาน 5 ชิ้น แต่ละชิ้นต้องผ่าน 3 ขั้น ลองกดสลับดูว่าต่างกันแค่ไหน

เวลารวม 0
กำลังทำงาน รอเปล่า ๆ
ตัวเลขในตัวอย่างนี้: barrier ใช้ 14 หน่วย · pipeline ใช้ 10 หน่วย — งานเท่ากันทุกอย่าง

ต่างกันตรงไหน

barrier

ทุกชิ้นต้องจบขั้นที่ 1 ให้ครบก่อน ถึงจะมีใครได้เริ่มขั้นที่ 2 → เวลารวม = ผลบวกของ “ตัวช้าสุดในแต่ละขั้น”

pipeline

แต่ละชิ้นไหลไปตามทางของตัวเอง ชิ้น 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 ถูกต้องเมื่อไหร่

เมื่อขั้นถัดไปจำเป็นต้องเห็นผลของทุกชิ้นพร้อมกันเท่านั้น มีอยู่สามกรณีจริง ๆ:

  1. ต้องตัดของซ้ำข้ามทุกชิ้นก่อน ไม่งั้นงานขั้นถัดไปจะทำซ้ำโดยเปล่าประโยชน์
  2. ต้องเช็กยอดรวมเพื่อออกก่อนเวลา — เช่น “ถ้าไม่เจอ bug เลยสักข้อ ข้ามขั้นยืนยันทั้งหมด”
  3. prompt ของขั้นถัดไปอ้างถึงผลของชิ้นอื่น เช่น “เทียบข้อนี้กับที่เหลือแล้วจัดอันดับ”
เหตุผลที่ฟังดูดีแต่ไม่ใช่ • “ขอ flatten/map/filter ก่อน” → ทำใน stage ได้เลย ไม่ต้องมี barrier
• “มันคนละ stage กันอยู่แล้ว” → คนละ stage ≠ ต้องรอพร้อมกัน
• “เขียนแบบนี้โค้ดสวยกว่า” → ความสวยแลกด้วยเวลารอจริง ๆ
ทดสอบง่าย ๆ ถ้าโค้ดคุณหน้าตาแบบ parallel → transform → parallel แล้ว transform ตรงกลางไม่ได้ยุ่งกับข้อมูลข้ามชิ้นเลย แปลว่า barrier นั้นไม่จำเป็น → เขียนใหม่เป็น pipeline
07

Routing — ให้กราฟเลือกทางเองตอนรัน

ไม่ต้องมีของวิเศษ ใช้ 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 ชั้น

08

Node ตรวจสอบ — อย่าเชื่อ node เดียว

agent ตัวเดียวที่พูดจาน่าเชื่อถือ ยังผิดได้ วิธีแก้ไม่ใช่ prompt ให้ดีขึ้น แต่คือเพิ่ม node ที่พยายามหักล้าง

ข้อค้นพบ ถูกต้องไหม? เกิดขึ้นจริงไหม? อันตรายจริงไหม? ผ่าน 2 ใน 3 = จริง
ให้ผู้ตรวจแต่ละตัวมองคนละมุม ดีกว่าให้ 3 ตัวถามคำถามเดียวกัน
const votes = await parallel(['ถูกต้อง', 'reproduce ได้', 'กระทบผู้ใช้จริง'].map(lens => () =>
  agent(`ลองหักล้างข้อนี้ในมุม ${lens}: ${claim} — ถ้าไม่แน่ใจให้ตอบว่าหักล้างได้`,
        { schema: VERDICT })
))
const real = votes.filter(Boolean).filter(v => !v.refuted).length >= 2
เคล็ดลับที่ได้ผลจริง สั่งให้ผู้ตรวจพยายามหักล้าง และ “ถ้าไม่แน่ใจ ให้ถือว่าหักล้างได้” การตั้ง default เป็นฝั่งไม่เชื่อ ทำให้ของที่ฟังดูดีแต่ผิดตกรอบ
09

เลือกโมเดล/ความพยายาม เป็นราย node

ไม่ใช่ทุก node ที่ต้องใช้ของแพง node กวาดข้อมูลกับ node ตัดสินใจ ควรคนละระดับ

ชนิดของ nodeความพยายามเหตุผล
กวาด/รวบรวมข้อมูลlowงานกลไก ไม่ต้องคิดลึก
วิเคราะห์/หา bugmediumค่าเริ่มต้นของงานส่วนใหญ่
ตรวจสอบ/ตัดสินhighผิดตรงนี้ = ของเสียหลุดออกไปทั้งชุด
สังเคราะห์ครั้งสุดท้ายhighต้องถือภาพรวมทั้งหมดพร้อมกัน
agent('ลิสต์ไฟล์ที่แก้ใน branch นี้',  { effort: 'low' })
agent('ตัดสินว่า bug นี้จริงหรือไม่', { effort: 'high' })
ระวังการ override โมเดล ถ้าไม่มั่นใจ ให้ไม่ต้องระบุ — node จะใช้โมเดลเดียวกับ session ซึ่งถูกต้องเกือบทุกครั้ง การกดลงเป็นโมเดลเล็กใน node ที่ต้องคิด ทำให้ผลลัพธ์ทั้งกราฟพัง
10

วนจนหมดเกลี้ยง — เมื่อไม่รู้ว่ามีของกี่ชิ้น

ถ้าคุณกำลังหา bug แล้วบอกว่า “หาให้ครบ 10 ข้อ” คุณกำลังเดา — อาจมี 3 หรือ 40 ท่าที่ถูกคือวนจน ไม่เจอของใหม่ติดกัน K รอบ

รอบใหม่ ตัดของที่เคยเจอ เจอของใหม่? เจอ → วนอีกรอบ · ไม่เจอ 2 รอบติด → จบ
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 (ที่ผ่านการตรวจ) ไม่งั้นของที่ผู้ตรวจปัดตกจะโผล่กลับมาใหม่ทุกรอบตลอดกาล
11

กราฟที่สร้างรูปร่างตัวเอง

คุณไม่จำเป็นต้องรู้รูปร่างกราฟตั้งแต่ก่อนเริ่มงาน — รู้ก่อนตอนสั่งแตกแขนงก็พอ ท่าที่ใช้จริงคือ ลูกผสม: สำรวจก่อนด้วยตัวเอง แล้วค่อยเอาผลไปกางเป็นกราฟ

// 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) {
  // เดินอีกรอบเท่าที่งบยังเหลือ
}
อย่าตัดเงียบ ๆ ถ้ากราฟจำกัดขอบเขต (เอาแค่ 10 อันดับแรก, ไม่ลองใหม่เมื่อพลาด, สุ่มตัวอย่าง) ต้อง log() บอกว่าตัดอะไรทิ้ง ไม่งั้นรายงานจะอ่านเหมือน “ตรวจครบแล้ว” ทั้งที่ไม่ครบ
12

6 เทมเพลตที่หยิบไปใช้ได้เลย

ทำความเข้าใจ

N → 1

ปล่อยผู้อ่านหลายตัวลงคนละส่วนของระบบ แล้วรวมเป็นแผนที่เดียว

ออกแบบ

N ⇄ N → 1

ให้ N แนวทางแยกกันคิด แล้วให้กรรมการให้คะแนน สังเคราะห์จากตัวชนะ

ตรวจงาน

pipeline 2 ขั้น

แตกตามมิติ → หา → ยืนยันแบบหักล้าง ทีละข้อทันทีที่เจอ

ค้นคว้า

หลายช่องทาง → 1

ค้นคนละวิธี (ตามหมวด/ตามเนื้อหา/ตามช่วงเวลา) แล้วอ่านลึก แล้วสรุป

ย้ายระบบ

pipeline + แยกพื้นที่

หาจุดที่ต้องแก้ → แก้ทีละจุดในพื้นที่แยก (worktree) → ตรวจว่ายังทำงานได้

ล่าจนหมด

วนซ้ำ + ตัดซ้ำ

วนหาจนไม่เจอของใหม่ K รอบ ตัดซ้ำกับทุกอย่างที่เคยเจอ

13

ตัวอย่างจริง — ตรวจระบบ 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 ทำงาน

14

กับดัก 5 อย่าง + เช็กลิสต์

กับดักอาการทางแก้
ลืม () =>ทุกอย่างรันเรียงกันเหมือนเดิมส่งฟังก์ชัน ไม่ใช่ promise
ไม่ .filter(Boolean)พังตอน map ผลลัพธ์node ที่ล้ม = null เสมอ
barrier เกินจำเป็นช้ากว่าที่ควร 30–50%ใช้ pipeline เป็นค่าเริ่มต้น
ตัดซ้ำผิดตัวลูปไม่มีวันจบตัดกับ seen ไม่ใช่ confirmed
ตัดขอบเขตเงียบ ๆรายงานดูครบทั้งที่ไม่ครบlog() ทุกครั้งที่ตัดทิ้ง

เช็กลิสต์ก่อนปล่อยกราฟลงสนาม

  1. วาดกราฟก่อน แล้วค่อยเขียนโค้ด — ถ้าวาดไม่ออก แปลว่ายังไม่เข้าใจงาน
  2. ทุก edge ตอบได้ว่า “ข้อมูลอะไรไหลผ่านเส้นนี้”
  3. ลบ edge ที่ตอบไม่ได้ทิ้ง มันคือ edge ปลอม
  4. เลือก pipeline เป็นค่าเริ่มต้น ใช้ barrier เมื่อพิสูจน์ได้ว่าจำเป็น
  5. ทุก parallel() มี .filter(Boolean)
  6. node ที่แตกแขนง ใช้ schema เพื่อให้ผลกลับมาเป็นข้อมูล ไม่ใช่ข้อความ
  7. node ตัดสินใจสำคัญ ผ่านด่านตรวจอย่างน้อย 1 ชั้น
  8. ผู้ตรวจถูกสั่งให้หักล้าง และไม่แน่ใจ = หักล้าง
  9. ผู้ตรวจหลายตัว มองคนละมุม ไม่ใช่ถามซ้ำคำถามเดิม
  10. ระดับ effort เหมาะกับความยากของแต่ละ node
  11. ลูปมีเงื่อนไขจบที่ชัดเจน และตัดซ้ำกับ seen
  12. ทุกจุดที่จำกัดขอบเขต มี log() กำกับ
  13. รันซ้ำแล้วได้โครงเดิม (ไม่ใช้ Date.now()/Math.random() ในสคริปต์)
  14. ขนาดกราฟสมกับงาน — “เช็กเร็ว ๆ” ไม่ต้องมี 40 node
สรุปเป็นประโยคเดียว โครงงานอยู่ในโค้ด ความคิดอยู่ใน node — ทำสองอย่างนี้ให้แยกกัน แล้วที่เหลือจะง่ายขึ้นเอง
อ้างอิง
  1. Codez (@0xCodez). Graph Engineering with Claude: 14-Step roadmap from 0 to graph architect. X Article, 2026 — โพสต์ต้นทาง
    โครงหัวข้อ 14 ขั้นและคำว่า fan-out / barrier / diamond มาจากบทความนี้
  2. Anthropic. Claude Code — Workflow & subagents — สเปกของ agent(), parallel(), pipeline(), phase(), log() ที่ใช้ในตัวอย่างทั้งหมด
  3. Anthropic Engineering. Building effective agents — ที่มาของหลัก “เริ่มจากโครงที่ง่ายที่สุดที่ใช้ได้”
  4. Anthropic Engineering. How we built our multi-agent research system — ที่มาของท่ากวาดหลายช่องทางแล้วสังเคราะห์
หมายเหตุเรื่องที่มา บทความต้นทางเป็น X Article ที่ต้องล็อกอินอ่าน หน้านี้จึงเขียนขึ้นใหม่ทั้งหมดจากโครงหัวข้อของผู้เขียน บวกกับสเปกทางการของเครื่องมือ และตัวอย่างจากงานจริงของเราเอง ไม่ได้คัดลอกเนื้อหาต้นฉบับ