ข้าม​ไป​ยัง​เนื้อหา

Prompt chaining และ routing

บท​ที่​แล้ว​ลาก​เส้น​แบ่ง​ใหญ่​ไว้ — workflow คือ​ระบบ​ที่ LLM กับ tool เดิน​ตาม​เส้นทาง code ที่​วาง​ไว้​ล่วงหน้า ส่วน agent คือ​ระบบ​ที่ model กำหนด​ทิศทาง​ของ​ตัวเอง​แบบ dynamic และ​หลักการ​กำกับ​ทั้ง​คอร์ส​คือ “finding the simplest solution possible, and only increasing complexity when needed” บท​นี้​เริ่ม​ไต่​บันได workflow จาก​ขั้น​ล่าง​สุด — 2 pattern ที่​เรียบ​ง่าย​ที่สุด เขียน​ด้วย C# ธรรมดา และ ไม่​ต้อง​ใช้ loop อัตโนมัติ​ของ agent เลย คุณ​เป็น​คน​คุม​เส้นทาง​เอง​ทุก​ก้าว

📦 code ตัวอย่าง

บท​นี้​ต่อยอด agent Order เดิม​จาก​คอร์ส #15 (พร้อม context layer จาก #16) — repo kaen-food-ordering (กำลัง​จัด​ทำ) สอง​รูปแบบ​ใน​บท​นี้​ไม่​ได้​ใช้ tool ใหม่​หรือ store ใหม่ แต่​เอา IChatClient ที่​ต่อ​สะพาน​ไว้​ใน​บท1 มา เรียง​เป็น​ลำดับ code ที่​คุณ​คุม​เอง กติกา API ฝั่ง​แชต​ทุก​ข้อ​จาก​บท1 ยัง​ยึด​เดิม​ทุก​ประการ: id เปล่า claude-opus-4-8 (ตัวอย่าง ณ 2026-07) · header x-api-key · anthropic-version: 2023-06-01 · ห้าม​ส่ง temperature · อ่าน​คำ​ตอบ​จาก Contents[] ไม่ใช่ .text ระดับ​บน · Claude อยู่​หลัง NuGet Anthropic (beta) ผ่าน AsIChatClient

ก่อน​จะ​เอื้อมหา harness เต็ม​รูป​ของ​บท8 ถาม​ตัวเอง​ก่อน​ว่า​งาน​ตรง​หน้า ต้องการ ให้ model กำหนด​ทิศทาง​เอง​จริง​ไหม ถ้า​งาน​หั่น​เป็น​ขั้นตอน​ตายตัว​ได้ หรือ​มัน​แค่​ต้อง​เลือก​เส้นทาง​จาก​ไม่​กี่​หมวด คุณ​ไม่​ต้องการ autonomous loop เลย — คุณ​ต้องการ code ที่​เรียก model เป็น​ลำดับ​ที่​คุณ​กำหนด นั่น​คือ workflow และ​มัน​คาด​เดา​ได้ test ได้ และ​ถูก​กว่า​มาก

2 pattern นี้​คือ​ขั้น​ล่าง​สุด​ของ​บันได:

  • prompt chainingPrompt Chainingworkflow ตาม BEA: 'decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one' — หั่น​งาน​เป็น​ลำดับ​ตายตัว แต่ละ call ประมวล​ผล output ของ call ก่อนหน้า คั่น​ด้วย gate (ตรวจ​ระหว่าง​ขั้น​แล้ว bail/escalate ได้) เหมาะ​เมื่อ​งาน​หั่น​เป็น subtask ตายตัว​ได้​อย่าง​สะอาดProcess — หั่น​งาน​เป็น​ลำดับ แล้ว​คั่น​ด้วย gate
  • routingRoutingworkflow ตาม BEA: 'classifies an input and directs it to a specialized followup task' — จัด​หมวด input (มัก​ด้วย structured output ราคา​ถูก1 call) แล้ว dispatch ไป​ยัง handler/prompt เฉพาะ​ทาง (เช่น Refund / Delivery / General) เหมาะ​กับ​งาน​ที่​มี​หมวด​หมู่​ชัดเจน​ซึ่ง​จัดการ​แยก​กัน​ได้​ดี​กว่าProcess — จำแนก input แล้ว​ส่ง​ต่อ​ไป path เฉพาะ​ทาง

ทั้ง​คู่​คือ workflow ใน​ความหมาย​ที่​บท2 นิยาม — เส้นทาง​ถูกวาง​ไว้​ใน code ไม่ใช่​ให้ model ตัดสิน​ว่า​จะ​ทำ​อะไร​ต่อ พูด​อีก​อย่าง: while ที่​บท1 สอน ไม่​ปรากฏ​ใน​บท​นี้​เลย คุณ​คือ orchestrator

Anthropic นิยาม pattern นี้ตรงๆ — “Prompt chaining decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one.” หัวใจ​ไม่ใช่​แค่ “เรียก model สอง​ครั้ง​ต่อ​กัน” แต่​คือ​การ​ที่ output ของ call แรก​กลาย​เป็น input ของ call ถัด​ไป — และ​ตรง​รอย​ต่อ​ระหว่าง​ขั้น คุณ​แทรก gate ได้: code ตรวจ​เงื่อนไข​หนึ่ง​บรรทัด ที่​ถ้า​ไม่​ผ่าน​ก็​ตัด​จบ​หรือ escalate ก่อน​จะ​เสีย token ไป​กับ​ขั้น​ถัด​ไป

มัน​เหมาะ​กับ​งาน​แบบ​ไหน? “This workflow is ideal for situations where the task can be easily and cleanly decomposed into fixed subtasks.” — คำ​สำคัญ​คือ fixed หาก​ลำดับ subtask รู้​ล่วงหน้า​และ​ไม่​เปลี่ยน​ตาม input นี่​คือ​รูป​ที่​ใช่

ยก​งาน​จริง​บน agent Order: ลูกค้า​ขอ “อธิบาย​ว่า​ทำไม​ถึง​ได้​คืน​เงิน ออเดอร์ A-1002” งาน​นี้​หั่น​สะอาด​เป็น​สอง​ขั้น — (1) ร่าง โน้ต​คืน​เงิน​จาก record ออเดอร์ แล้ว (2) เกลา ให้​กระชับ​และ​สุภาพ​ก่อน​ส่ง​ลูกค้า และ​คั่น​กลาง​ด้วย gate ตรวจ​ว่า​ร่าง​นั้น​สอดคล้อง​นโยบาย​คืน​เงิน​ไหม ถ้า​ไม่​ผ่าน — ส่ง​ต่อ​ให้​คน ไม่​ปล่อย​ผ่าน​ไป​ขั้น​เกลา

// ทั้งสองขั้นใช้ IChatClient ตัวเดียวกันจากบท1 (anthropic.AsIChatClient("claude-opus-4-8"))
// ChatResponse.Text คือ convenience concatenation ของข้อความ assistant — เหมาะกับการส่งข้อความต่อระหว่างขั้น
ChatResponse draft = await client.GetResponseAsync(
$"ร่างโน้ตอธิบายการคืนเงินสำหรับออเดอร์ {orderId}", cancellationToken: ct);
if (!PassesPolicyGate(draft.Text)) // gate: ตรวจระหว่างขั้น — ไม่ผ่านก็ bail ก่อนเสีย call ถัดไป
return Escalate(draft); // ส่งต่อให้คน แทนที่จะปล่อยผ่านไปขั้นเกลา
ChatResponse final = await client.GetResponseAsync(
$"เกลาให้กระชับและสุภาพ พร้อมส่งลูกค้า:\n{draft.Text}", cancellationToken: ct);

สังเกต​สาม​อย่าง: หนึ่ง ไม่มี while — ลำดับ ตายตัว สอง​ขั้น เพราะ​เรา​รู้​ล่วงหน้า​ว่า​มี​สอง​ขั้น สอง gate เป็น C# ธรรมดา (PassesPolicyGate จะ​เป็น rule เช็ก​คำ​ต้อง​ห้าม หรือ​จะ​เป็น​อีก call หนึ่ง​ที่​จำแนก​ว่า “ผ่าน/ไม่​ผ่าน” ก็ได้) — มัน​คือ​จุด​คุม​คุณภาพ​ที่ code เป็น​เจ้าของ ไม่ใช่ model สาม เรา​อ่าน​ผ่าน draft.Text ได้​ก็​เพราะ​ที่​นี่​เรา​ต้องการ ข้อความ assistant ทั้ง​ก้อน เพื่อ​ส่ง​ต่อ — ต่าง​จาก​ตอน​ไล่ FunctionCallContent ในบท1 ที่​ต้อง​เข้า Contents[] เพราะ​เรา​มอง​หา tool call ไม่ใช่​ข้อความ

flowchart LR
  IN["คำขอ<br/>อธิบายการคืนเงิน A-1002"] --> S1["call ที่ 1<br/>ร่างโน้ตคืนเงิน"]
  S1 --> G{"gate<br/>ผ่านนโยบายไหม"}
  G -->|"ไม่ผ่าน"| ESC["escalate<br/>ส่งต่อให้คน"]
  G -->|"ผ่าน"| S2["call ที่ 2<br/>เกลา + ปิดจบ"]
  S2 --> OUT["โน้ตสุดท้าย<br/>ส่งลูกค้า"]
  classDef gate fill:#0891b2,stroke:#064e5b,color:#f8fafc;
  class G gate;

คำ​บรรยาย​ภาพ: prompt chaining เดิน​เป็น​เส้นตรง​จาก​ซ้าย​ไป​ขวา — output ของ call ที่ 1 (ร่าง) กลาย​เป็น input ของ call ที่ 2 (เกลา) โดย​มี gate สี​ฟ้า​คั่น​กลาง เป็น​จุด​ที่ code ตัดสิน​ว่า​จะ​เดิน​ต่อ​หรือ escalate ก่อน​จะ​เสีย call ถัด​ไป ลำดับ​นี้ ตายตัว — ไม่มี while ไม่มี​การ​ให้ model เลือก​ว่า​จะ​ทำ​อะไร​ต่อ

Anthropic นิยาม routing ว่า “Routing classifies an input and directs it to a specialized followup task.” — จำแนก input หนึ่ง​ครั้ง แล้ว​ส่ง​ต่อ​ไป​ยัง handler ที่ เชี่ยวชาญ​หมวด​นั้น แทนที่​จะ​ยัด prompt เดียว​ให้​ครอบ​ทุก​กรณี คุณ​แยก path ให้​แต่ละ​หมวด​ได้ prompt/tool/แม้​กระทั่ง model ของ​ตัวเอง

เหมาะ​เมื่อไร? “Routing works well for complex tasks where there are distinct categories that are better handled separately.” — เงื่อนไข​คือ​หมวด​หมู่ ชัดเจน​และ​แยก​จาก​กัน ทิก​เก็ต support ของ Order เข้า​เกณฑ์​นี้​พอดี: คำขอ​ที่​เข้า​มา​แทบ​ทั้งหมด​ตกลง​ใน​สาม​หมวด — Refund (ขอ​คืน​เงิน), Delivery (ถาม​สถานะ​ส่ง), General (ทั่วไป) แต่ละ​หมวด​อยาก​ได้ system prompt คนละ​แบบ

ขั้น​แรก​คือ classify ด้วย call ราคา​ถูก​หนึ่ง​ครั้ง — ใช้ overload GetResponseAsync<T> ที่​คืน​ผล​เป็น type มี​โครงสร้าง (structured output) แทน​ข้อความ​อิสระ:

enum TicketKind { Refund, Delivery, General }
// call จำแนก: overload <T> คืน structured output ไม่ใช่ข้อความอิสระ
var kind = await router.GetResponseAsync<TicketKind>(
$"จัดหมวดคำขอนี้: {input}", cancellationToken: ct);
// dispatch: เลือก handler เฉพาะทางตามหมวด — แต่ละตัวคือ IChatClient คนละ system prompt
IChatClient handler = kind.Result switch {
TicketKind.Refund => refundAgent,
TicketKind.Delivery => deliveryAgent,
_ => generalAgent,
};
var answer = await handler.GetResponseAsync(input, options, ct);

refundAgent / deliveryAgent / generalAgent คือ IChatClient สาม​ตัว​ที่​ต่อ​จาก​สะพาน​เดิม​ของ​บท1 ต่าง​กัน​แค่ system prompt (และ​อาจ​ต่าง​ชุด tool ที่​เปิด​ให้) การ​แยก​แบบ​นี้​ทำให้ prompt ของ​แต่ละ path สั้น​และ​คม — handler คืน​เงิน​ไม่​ต้อง​แบก​คำ​สั่ง​เรื่อง​เดลิ​เวอรี และ​กลับ​กัน

ข้อ​ควร​ระวัง​ของ structured-output overload

GetResponseAsync<T> แม็​ปลง​เป็น structured output ของ Anthropic ผ่าน​สะพาน beta (Anthropic 12.8.0 via AsIChatClient) — ยืนยัน​ว่า kind.Result ถูก​เติม​ค่า​จริง​บน version ที่ pin ไว้ ก่อน ship อย่า​เชื่อ​ว่า mapping ทำงาน​เงียบๆ ให้ ถ้า path นี้​ยัง​ไม่​นิ่ง fallback ที่​ปลอดภัย​คือ classify ด้วย​ข้อความ​อิสระ​แล้ว parse เอง (เช่น​ให้ model ตอบ​คำ​เดียว Refund/Delivery/General แล้ว switch บน string) กติกา​ฝั่ง​แชตอื่นๆ ยึด​เดิม — id เปล่า, ไม่มี temperature, อ่าน​ผ่าน​รูป​ที่​ถูกต้อง​ของ IChatClient ไม่ใช่ .text ระดับ​บน

flowchart TD
  IN["ทิกเก็ตจากลูกค้า"] --> CLS["call จำแนก<br/>คืน structured output"]
  CLS --> K{"หมวดไหน"}
  K -->|"Refund"| RA["refundAgent<br/>prompt คืนเงิน"]
  K -->|"Delivery"| DA["deliveryAgent<br/>prompt สถานะส่ง"]
  K -->|"General"| GA["generalAgent<br/>prompt ทั่วไป"]
  RA --> ANS["คำตอบเฉพาะทาง"]
  DA --> ANS
  GA --> ANS
  classDef pick fill:#0891b2,stroke:#064e5b,color:#f8fafc;
  class K pick;

คำ​บรรยาย​ภาพ: routing แยก​เป็น​สอง​จังหวะ — call จำแนก​ราคา​ถูก​หนึ่ง​ครั้ง​ตัดสิน​หมวด (ช่อง​สี​ฟ้า) แล้ว switch ใน code จึง dispatch ทิก​เก็ต​ไป​ยัง handler ที่​เชี่ยวชาญ​หมวด​นั้น แต่ละ path มี system prompt ของ​ตัวเอง​จึง​สั้น​และ​คม​กว่า prompt เดียว​ที่​ครอบ​ทุก​กรณี การ​เลือก path เป็น​ของ code ไม่ใช่​ของ loop

จุด​ที่​ต้อง​ขีด​เส้น​ใต้: ทั้ง chaining และ routing เป็น workflow — เส้นทาง​ถูก code กำหนด​ล่วงหน้า ไม่มี​จุด​ไหน​ที่ model ตัดสิน​ว่า “จะ​ทำ​ขั้น​ต่อ​ไป​อะไร” chaining มี​ลำดับ​สอง​ขั้น​เพราะ เรา​เขียน​สอง​ขั้น; routing มี​สาม​สาขา​เพราะ เรา​เขียน​สาม case ผล​ที่​ตาม​มา​คือ​มัน คาด​เดา​ได้​และ test ได้ — คุณ unit-test gate ได้ตรงๆ, mock การ​จำแนก​แล้ว​ยืนยัน​ว่า​มัน​ไป path ถูก​ได้ ซึ่ง​เป็น​สิ่ง​ที่ autonomous loop (ที่ model เลือก​ทาง​เอง) ให้​คุณ​ได้​ยาก​กว่า​มาก

นี่​คือ​เหตุผล​ที่​บันได workflow มา​ก่อน harness เต็ม​รูป: ถ้า​งาน​ของ​คุณ ตกลง​ใน​รูป chaining หรือ routing ได้ — หยุด​ตรง​นี้ อย่า​เลื่อน​ขึ้น​ไป​หา loop ที่​แพง​กว่า​และ​คาด​เดา​ยาก​กว่า​โดย​ไม่​จำเป็น 🔁 บท4 จะ​ไต่​ขึ้น​อีก​ขั้น​ไป​หา parallelization (หลาย call พร้อม​กัน​แล้ว​รวม​ผล) และ​เมื่อ​ถึง​ตรง​นั้น​เรา​จะ​เริ่ม​บท​ด้วย​การ​ทวน​หลัก “โครงสร้าง​ต้อง​พิสูจน์​คุณค่า​ตัวเอง​ก่อน” อีก​ครั้ง — เพราะ​ทุก​ขั้น​ที่​ไต่​สูง​ขึ้น ราคา​ก็​สูง​ตาม

2 workflow pattern แรก​ไม่​ใช้ loop ของ agent เลย — prompt chaining หั่น​งาน​เป็น​ลำดับ​ตายตัว​ที่​แต่ละ call ประมวล​ผล output ของ call ก่อนหน้า คั่น​ด้วย gate ที่ code เป็น​เจ้าของ; routing จำแนก input ด้วย structured output ราคา​ถูก1 call แล้ว switch ไป​ยัง handler เฉพาะ​ทาง ทั้ง​คู่​คือ​เส้นทาง​ที่ code วาง​ไว้​ล่วงหน้า จึง​คาด​เดา​ได้ test ได้ และ​ถูก​กว่า loop — เป็น​ค่า​เริ่มต้น​ที่​คุณ​ควร​เอื้อม​ถึง ก่อน harness เต็ม​รูป 🔁 บท4 พาไป parallelization ซึ่ง​เป็น multi-actor pattern ตัว​แรก​ของ​คอร์ส และ​เป็น​จุด​ที่​เรา​ต้อง​เริ่ม​เก็บ​ค่า “ความ​คุ้ม” ให้​ครบ​ก่อน​ใช้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • Anthropic, “Building Effective Agents” (2024-12-19) — นิยาม prompt chaining (“decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one”) + fit (“cleanly decomposed into fixed subtasks”), นิยาม routing (“classifies an input and directs it to a specialized followup task”) + fit (“distinct categories that are better handled separately”), และ​หลัก “finding the simplest solution possible”
  • Microsoft Learn, “Use the IChatClient interface”GetResponseAsync, overload GetResponseAsync<T> (structured output), ChatResponse.Text (convenience concatenation), ChatResponse.Result
  • NuGet, “Anthropic 12.8.0” (official C# SDK, beta)AnthropicClient, AsIChatClient (pin version; mapping ของ structured output อาจ​เปลี่ยน)
  • Microsoft, “Microsoft.Extensions.AI” (10.8.0, GA) — package แกน​ของ IChatClient / ChatResponse / overload GetResponseAsync<T> ที่​ทั้ง chaining และ routingตั้ง​อยู่​บน (GA ไม่ใช่ beta ต่าง​จาก​สะพาน Anthropic)

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

'gate' ใน prompt chaining คืออะไร และใครเป็นเจ้าของ?