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 เลย คุณเป็นคนคุมเส้นทางเองทุกก้าว
บทนี้ต่อยอด 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
workflow สองแบบที่ถูกกว่า loop — และมาก่อน loop
หัวข้อที่มีชื่อว่า “workflow สองแบบที่ถูกกว่า loop — และมาก่อน loop”ก่อนจะเอื้อมหา 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
Prompt chaining — ลำดับตายตัว + gate คั่นกลาง
หัวข้อที่มีชื่อว่า “Prompt chaining — ลำดับตายตัว + gate คั่นกลาง”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 เลือกว่าจะทำอะไรต่อ
Routing — จำแนกก่อน แล้ว dispatch
หัวข้อที่มีชื่อว่า “Routing — จำแนกก่อน แล้ว dispatch”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 promptIChatClient 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 คืนเงินไม่ต้องแบกคำสั่งเรื่องเดลิเวอรี และกลับกัน
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
ทำไม2 pattern นี้มาก่อน loop เสมอ
หัวข้อที่มีชื่อว่า “ทำไม2 pattern นี้มาก่อน 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, overloadGetResponseAsync<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/ overloadGetResponseAsync<T>ที่ทั้ง chaining และ routingตั้งอยู่บน (GA ไม่ใช่ beta ต่างจากสะพาน Anthropic)
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3'gate' ใน prompt chaining คืออะไร และใครเป็นเจ้าของ?