capstone: agent Order support ตัวเต็ม
เจ็ดบทที่ผ่านมาคุณไต่บันไดมาทีละขั้น — จาก function text → token → text ที่ไม่ deterministic ในบทที่ 1 มาจนถึง the loop ในบทที่ 6 และ guardrail ในบทที่ 7 แต่ละขั้นแยกกันสอนทีละชิ้น บทนี้เป็น capstone: เอาทุกชิ้นมาต่อกันเป็น AgentAgentตามกรอบของ Anthropic: agent ก็คือ 'LLM ที่ใช้ tools ตาม feedback จากสภาพแวดล้อมวนเป็น loop' — พูดง่าย ๆ คือการเรียก model ธรรมดาบวก tools บวก Agent Loop ไม่มีอะไรวิเศษไปกว่านั้น หลักที่คอร์สนี้ยึดคือสร้างสิ่งที่ 'ง่ายที่สุดเท่าที่จำเป็น' แล้วค่อยเพิ่มความซับซ้อนเมื่อจำเป็นจริง ๆ — อย่าเอื้อมไปหา agent เป็นค่าเริ่มต้นArchitecture ตัวเดียวที่ทำงานได้จริง — agent support ลูกค้าบน domain Order ที่ตอบคำถามออเดอร์ เช็กสถานะจัดส่ง และคืนเงินได้ ไม่มีแนวคิดใหม่ในบทนี้ มีแต่การประกอบ และเมื่อประกอบเสร็จคุณจะได้เห็นชัดว่า “agent” ไม่ใช่ของวิเศษ มันคือชิ้นส่วนที่คุณรู้จักหมดแล้ว วางต่อกันอย่างมีวินัย
code ในบทนี้ประกอบจากชิ้นส่วนที่คุณเห็นมาตลอดคอร์ส บน domain Order ของบริการฟู้ดเดลิเวอรีจาก repo kaen-food-ordering (กำลังจัดทำ) — โครง wiring และ tool ทุกตัวยึดตาม API invariant เดิม (x-api-key, anthropic-version: 2023-06-01, ไม่ส่ง temperature, อ่านคำตอบจาก content[]) เพื่อให้ต่อยอดเป็น project จริงได้ทันที
ทบทวนบันไดทั้งเจ็ดขั้น
หัวข้อที่มีชื่อว่า “ทบทวนบันไดทั้งเจ็ดขั้น”ก่อนประกอบ ขอปักหมุดว่าแต่ละขั้นให้ชิ้นส่วนอะไรมา เพราะ capstone คือการวางหมุดเหล่านี้ต่อกัน:
- บทที่ 1 — LLM คือ function
text → token → textที่ไม่ deterministic ทุกอย่างสร้างอยู่บนกรอบนี้ - บทที่ 2 — การเรียกดิบ เห็น Messages API ทุก byte ผ่าน
HttpClientคำตอบอยู่ในcontent[]ไม่มี.textที่ระดับบนสุด - บทที่ 3 — prompt ตั้งบทบาทและกติกาด้วยโครงสร้าง เกิด support persona กับกติกาคืนเงินที่บทนี้เอามาใช้ซ้ำ
- บทที่ 4 — tool use รอบ
tool_use↔tool_resultที่ยกการเรียกครั้งเดียวให้เรียก code C# ได้ - บทที่ 5 — Microsoft.Extensions.AI เขียนการเรียกเดิมด้วยสำนวน .NET ผ่าน IChatClientIChatClientinterface กลางใน Microsoft.Extensions.AI สำหรับคุยกับ model แบบ provider-agnostic — method หลักที่มือใหม่ใช้คือ `GetResponseAsync(messages, options)` (และ version streaming) รับ `ChatMessage`/`ChatRole` คืน `ChatResponse` ต่อ middleware เพิ่มได้ผ่าน `.AsBuilder()` เช่น `.UseFunctionInvocation()` — เป็นการเรียกแบบเดียวกับ API ดิบในบทก่อน แต่เขียนด้วยสำนวน .NET ⚠️ อย่าตั้ง `ChatOptions.Temperature` กับ Claude รุ่นล่าสุด (400)Architecture และ
.UseFunctionInvocation() - บทที่ 6 — the loop ร้อยทุกอย่างเป็นวง observe → decide → act → repeat พร้อมเงื่อนไขหยุด
- บทที่ 7 — ทำให้เชื่อถือได้ วิศวกรรมรอบ non-determinism ทั้งต้นทุน error และ guardrail
capstone นี้หยิบขั้น 3 (persona + กติกา), ขั้น 4 (tool), ขั้น 5 (IChatClient), ขั้น 6 (loop) และขั้น 7 (guardrail) มาต่อกันใน file เดียว
สถาปัตยกรรมของ agent ตัวเต็ม
หัวข้อที่มีชื่อว่า “สถาปัตยกรรมของ agent ตัวเต็ม”ทั้ง agent มีแค่สามส่วนที่ล้อม model ตัวเดิม — system prompt ที่กำหนดบทบาท, 3 tool ที่พา model ไปแตะ domain Order จริง และ Agent LoopAgent Loopวงจร observe → decide → act → repeat ที่ทำซ้ำจนถึงเงื่อนไขหยุด — model สังเกตผลจากสภาพแวดล้อม (ผลของ tool) แล้วตัดสินใจก้าวถัดไป วนไปจนกว่าจะได้คำตอบสุดท้าย (ไม่มี `tool_use` แล้ว), ชนเพดานจำนวนรอบ, ต้องขออนุมัติจากคน หรือเกินงบ token/ต้นทุน — ต้องรักษาให้มีเงื่อนไขหยุดเสมอเพื่อคุมไม่ให้วนไม่จบ ใน Microsoft.Extensions.AI ตัว `.UseFunctionInvocation()` ทำวงนี้ให้อัตโนมัติArchitecture ที่เอาผลของ tool ป้อนกลับเข้า model จนได้คำตอบ โดยมี guardrail คั่นตรงจุดที่ tool จะไปแตะเงินจริง
flowchart TB
USER["ลูกค้า: ขอสถานะ/ขอคืนเงิน"] --> HOST
subgraph HOST["host C# — the loop"]
LOOP["observe → decide → act<br/>วนจนได้คำตอบ"]
GATE["guardrail: ตรวจ input<br/>กั้นก่อนคืนเงิน"]
end
LOOP -->|system prompt + ประวัติ| MODEL["LLM ตัดสินใจ"]
MODEL -->|ขอเรียก tool| LOOP
LOOP --> T1["getOrder"]
LOOP --> T2["getDeliveryStatus"]
LOOP --> GATE
GATE --> T3["issueRefund"]
T1 --> DOMAIN["domain Order"]
T2 --> DOMAIN
T3 --> DOMAIN
DOMAIN -->|ผลจริง = สิ่งที่สังเกตเห็น| LOOP
LOOP -->|ได้คำตอบแล้ว| USER
classDef gate fill:#ea580c,stroke:#7c2d12,color:#f8fafc;
class GATE gate;
คำบรรยายภาพ: host C# รัน the loop เป็นแกนกลาง model ตัดสินใจว่าจะเรียก tool ไหน host รัน tool นั้นบน domain Order จริงแล้วป้อนผลกลับเป็นสิ่งที่ model สังเกตเห็น วนจน model ตอบลูกค้า getOrder กับ getDeliveryStatus เป็นการอ่านล้วน จึงต่อตรงถึง domain ได้ แต่ issueRefund แตะเงินจริง จึงถูกบังคับให้ผ่าน guardrail (จุดสีส้ม) ที่ตรวจ input และกั้นด้วยกติกาก่อนเสมอ
ชิ้นที่ 1 — system prompt: บทบาทและกติกาคืนเงิน
หัวข้อที่มีชื่อว่า “ชิ้นที่ 1 — system prompt: บทบาทและกติกาคืนเงิน”จากบทที่ 3 เรารู้ว่าโครงสร้างสำคัญกว่าความฉลาดของถ้อยคำ และการใส่ เหตุผล หลังกติกาช่วยให้ model สรุปเองได้ตอนเจอเคสที่กติกาไม่ได้ครอบเป๊ะ ใน MEAI เราวาง persona นี้เป็น ChatMessage ตัวแรกด้วย ChatRole.System:
const string SupportPersona = """ You are a customer-support agent for a food-delivery service. You help customers check order status, delivery status, and issue refunds.
Refund rules (and the reason behind each, so you can generalize): - Refund only orders that are late or have a quality problem — the point is to make good on a broken promise, not to give money away on request. - Never invent an order or a refund amount. If you are unsure, look it up first. - You may propose a refund, but the system approves and executes it, not you.
Use the tools to get ground truth. Do not guess an order's state from memory. """;สังเกตว่ากติกาข้อสุดท้ายพูดตรงๆ ว่า model เสนอได้ แต่ระบบเป็นคนอนุมัติและลงมือ — ประโยคนี้คือ guardrail ที่เขียนไว้ใน prompt แต่บทที่ 7 สอนไว้ชัดว่า prompt อย่างเดียวไม่พอ เราจะบังคับกติกาเดียวกันนี้อีกชั้นด้วย code C# ในชิ้นที่ 3
ชิ้นที่ 2 — 3 tool บน domain Order
หัวข้อที่มีชื่อว่า “ชิ้นที่ 2 — 3 tool บน domain Order”จากบทที่ 4 และ 5 เรารู้ว่า AIFunctionFactory.Create(delegate, name, description) แปลง delegate C# ให้เป็น tool ที่ model เรียกได้ และ description ของ tool ต้องได้รับความใส่ใจระดับเดียวกับ prompt agent นี้มี3 tool — สองตัวแรกอ่านล้วน ตัวที่สามแตะเงิน:
var chatOptions = new ChatOptions { Tools = [ AIFunctionFactory.Create( (string orderId) => orderService.GetOrder(orderId), // อ่านล้วน "getOrder", "Retrieve a food-delivery order by its ID. Returns status, line items, and total."),
AIFunctionFactory.Create( (string orderId) => orderService.GetDeliveryStatus(orderId), // อ่านล้วน "getDeliveryStatus", "Get the live delivery status and ETA for an order by its ID."),
AIFunctionFactory.Create( (string orderId, decimal amount, string reason) => refundService.IssueRefund(orderId, amount, reason), // แตะเงินจริง — มี guardrail ข้างใน "issueRefund", "Issue a refund for an order. Provide the order ID, amount, and reason."), ]};getOrder กับ getDeliveryStatus เรียก service จริงใน domain ได้ตรงๆ เพราะมันแค่อ่าน แต่ issueRefund เปลี่ยนสถานะเงินจริง — delegate ของมันไม่ได้ลงมือทันที มันเรียก refundService.IssueRefund(...) ที่มี guardrail อยู่ข้างใน ซึ่งเป็นชิ้นถัดไป
ชิ้นที่ 3 — guardrail: กั้นก่อนแตะเงิน
หัวข้อที่มีชื่อว่า “ชิ้นที่ 3 — guardrail: กั้นก่อนแตะเงิน”นี่คือหัวใจที่แยกเดโมออกจากของจริง บทที่ 7 วางหลักไว้ว่าต้องปฏิบัติกับผลจาก model แบบ ไม่ไว้ใจ (zero-trust) — model อาจถูก input ของลูกค้าชักจูงให้เสนอคืนเงินผิดๆ ได้ ดังนั้น issueRefund จึงต้องไม่ลงมือตาม amount ที่ model ส่งมาแบบดิบๆ กติกาเดียวกับใน system prompt ถูกบังคับซ้ำใน code — คราวนี้บังคับจริง ไม่ใช่แค่ขอ:
public RefundResult IssueRefund(string orderId, decimal amount, string reason){ // 1) input จาก model ถือเป็นไม่น่าเชื่อถือ — ตรวจกับ domain จริงก่อนเสมอ var order = _orders.Find(orderId) ?? throw new InvalidOperationException($"ไม่พบออเดอร์ {orderId}");
// 2) บังคับกติกาด้วย code ไม่พึ่ง prompt: คืนได้ไม่เกินยอดจริง และเฉพาะเคสที่เข้าเงื่อนไข if (amount <= 0 || amount > order.Total) return RefundResult.Rejected("จำนวนเงินคืนไม่ถูกต้องเทียบกับยอดออเดอร์จริง"); if (!order.IsRefundEligible) return RefundResult.Rejected("ออเดอร์นี้ไม่เข้าเงื่อนไขคืนเงิน");
// 3) การกระทำที่แตะเงินจริง = human-in-the-loop ก่อนลงมือ return _approvals.RequestApproval(order, amount, reason);}จุดสำคัญสามข้อ: (1) เราไม่เชื่อ orderId ที่ model ส่งมา ไปหาออเดอร์จริงใน domain ก่อน (2) กติกา “คืนไม่เกินยอดจริง และเฉพาะเคสที่เข้าเงื่อนไข” บังคับด้วย code ต่อให้ model ถูกชักจูงให้เสนอคืน ฿9,999 บนออเดอร์ ฿185 code ก็ปฏิเสธเอง (3) การกระทำที่แตะเงินไม่ลงมือเงียบๆ แต่ขออนุมัติจากคนก่อน — issueRefund เป็นประตูอนุมัติโดยธรรมชาติเพราะมันเปลี่ยนสถานะและเกี่ยวกับเงิน ถ้า refundService throw หรือคืน Rejected ผลนั้นจะกลับเข้า loop เป็น tool_result (แนวคิด is_error จากบทที่ 4) แล้ว model อธิบายให้ลูกค้าฟังได้อย่างสุภาพ แทนที่จะพังทั้ง agent
ชิ้นที่ 4 — IChatClient กับ the loop ที่ประกอบครบ
หัวข้อที่มีชื่อว่า “ชิ้นที่ 4 — IChatClient กับ the loop ที่ประกอบครบ”เหลือแค่ร้อยทุกอย่างเข้าด้วยกัน จากบทที่ 5 เรารู้ว่า .UseFunctionInvocation() คือ the loop — มันรันรอบ tool_use ↔ tool_result ทั้งหมดให้เอง คุณแค่ await การเรียกครั้งเดียว แล้ว middleware วนเรียก tool และป้อนผลกลับจน model ได้คำตอบสุดท้าย agent ทั้งตัวจึงย่อลงเหลือเท่านี้:
using Anthropic;using Microsoft.Extensions.AI;
// wiring จากบทที่ 5 — connector ทางการของ Anthropic (v10+, ระบุ beta), model id เป็นตัวอย่าง ณ 2026-07AnthropicClient anthropic = new(); // อ่าน ANTHROPIC_API_KEY จาก environmentIChatClient agent = anthropic.AsIChatClient("claude-opus-4-8") .AsBuilder() .UseFunctionInvocation() // <- .UseFunctionInvocation() คือ the loop .Build();
// ชิ้นที่ 1: persona + กติกา เป็น ChatMessage ตัวแรกList<ChatMessage> history = [ new(ChatRole.System, SupportPersona) ];history.Add(new(ChatRole.User, "ออเดอร์ ORD-1042 มาช้ามาก ขอเช็กสถานะและถ้าช้าจริงขอคืนเงินด้วย"));
// เรียกครั้งเดียว — ข้างในคือ the loop เต็ม ๆ: getDeliveryStatus แล้วอาจเสนอ issueRefundChatResponse response = await agent.GetResponseAsync(history, chatOptions); // chatOptions = ชิ้นที่ 2Console.WriteLine(response.Text);history.AddMessages(response); // เก็บ turn กลับเข้า history ไว้คุยต่อในการเรียกครั้งเดียวนี้ the loop อาจเดินหลายรอบ — model เรียก getDeliveryStatus("ORD-1042") เห็นว่าช้าจริง แล้วเสนอ issueRefund(...) ซึ่งวิ่งชน guardrail ในชิ้นที่ 3 ก่อนเสมอ ทุกรอบคือการเรียก Messages หนึ่งครั้งที่คิดเงิน (บทที่ 7) และ .UseFunctionInvocation() มีเงื่อนไขหยุดของมันเอง เมื่อ model ตอบโดยไม่ขอเรียก tool อีก (FinishReason เป็น Stop ไม่ใช่ ToolCalls) — นี่คือ the loop จากบทที่ 6 ทำงานอยู่ใต้ middleware ตัวนี้
code ข้างบนใช้ทรง MEAI — new ChatMessage(ChatRole.System, ...) กับ ChatOptions ต่างจากทรง native ของ Anthropic SDK (MessageCreateParams { System = ..., Messages = [ new() { Role = Role.User, ... } ] }) ที่คุณเห็นตอนพูดถึง prompt ในบทที่ 3 ทั้งสองทรงเรียก model ตัวเดียวกันแต่คนละ API — อย่าเอา Role.User ไปปนกับ ChatRole.User และอย่าตั้ง ChatOptions.Temperature กับ Claude รุ่นล่าสุด (ได้ HTTP 400 — invariant เดิมจากบทที่ 1)
agent ที่ง่ายที่สุดเท่าที่ควรสร้าง
หัวข้อที่มีชื่อว่า “agent ที่ง่ายที่สุดเท่าที่ควรสร้าง”นี่แหละ agent ตัวเต็ม — 1 persona, 3 tool, 1 loop, guardrail หนึ่งจุดตรงที่แตะเงิน ไม่มี agent ตัวที่สอง ไม่มี orchestration หลายชั้น และนั่นคือเจตนา หลักของ Anthropic ที่เราปักไว้ตั้งแต่บทที่ 1 คือ “finding the simplest solution possible, and only increasing complexity when needed.” — หาทางที่ง่ายที่สุดก่อน เพิ่มความซับซ้อนเมื่อมันคุ้มที่จะเพิ่มเท่านั้น agent นี้คือ สิ่งที่ง่ายที่สุดที่ Anthropic แนะนำให้เริ่ม ไม่ใช่เพราะเราสร้างได้แค่นี้ แต่เพราะเริ่มตรงนี้แล้วค่อยเติมเมื่อมันพิสูจน์ว่าจำเป็น เชื่อถือได้กว่าเริ่มจากของซับซ้อน Anthropic ปิดท้ายเรื่องนี้ไว้คมมาก:
“Success in the LLM space isn’t about building the most sophisticated system. It’s about building the right system for your needs.”
ความสำเร็จไม่ได้อยู่ที่ระบบซับซ้อนที่สุด แต่อยู่ที่ระบบที่ ถูก กับงานของคุณ สำหรับ support Order 1 agent 1 loop คือระบบที่ถูก
ส่งไม้ต่อ — คอร์สนี้คือประตู ไม่ใช่ปลายทาง
หัวข้อที่มีชื่อว่า “ส่งไม้ต่อ — คอร์สนี้คือประตู ไม่ใช่ปลายทาง”คุณสร้าง agent ตัวแรกเสร็จแล้ว แต่ทันทีที่มันทำงานได้ คำถามที่ลึกกว่าก็โผล่มา และแต่ละคำถามมีคอร์สของมันรออยู่ในสาย AI-engineering pillar:
- ขอบเขต — เมื่อไร1 agent ควรเป็น1 bounded context และเมื่อไร (ซึ่งน้อยครั้ง) ที่ควรมี agent ตัวที่สอง → คอร์ส #12 agent-as-bounded-context คอร์สนี้สร้างของที่ง่ายที่สุด (1 agent 1 loop) #12 ตอบว่าเมื่อไรจึงควรแยก
- การวัดความเชื่อถือได้ — บทที่ 7 แนะนำเรื่อง non-determinism แล้ว วัด มันยังไง → คอร์ส #13 evals-for-ai-agents สอน eval ที่วัดผลลัพธ์ข้ามหลายรอบ ไม่ใช่ test รอบเดียว
- tool ให้เป็น server ใช้ซ้ำได้ — 3 tool ที่เราต่อด้วยมือ ทำเป็นบริการที่หลาย agent เรียกได้ยังไง → คอร์ส #14 designing-mcp-servers โดยมี injection surface จากบทที่ 7/8 เป็นเส้นเรื่องเดียวกัน
- การประกอบ context — retrieval, memory และการจัดของเข้า context window อย่างมีระบบ → คอร์ส context-engineering ในอนาคต ที่ต่อยอดจากงบ context window ที่เราคุยไว้ตั้งแต่บทที่ 1
บันไดเจ็ดขั้นในคอร์สนี้พาคุณจาก function text → text มาถึง agent ที่ทำงานได้จริง จากนี้ไม่ใช่การไต่บันไดขั้นเดิมให้สูงขึ้น แต่เป็นการเดินเข้าประตูสี่บานข้างบน — และคุณมีชิ้นส่วนพื้นฐานครบแล้วที่จะเดินเข้าไป
บทนี้ประกอบจากชิ้นส่วนตลอดคอร์ส สองแหล่งที่อ้างตรงในบทนี้:
- Anthropic, “Building Effective Agents” (2024-12-19) — หลัก “หาทางที่ง่ายที่สุดก่อน” และประโยคปิดว่าความสำเร็จอยู่ที่ระบบที่ถูกกับงาน ไม่ใช่ระบบที่ซับซ้อนที่สุด
- Anthropic, C# SDK docs — connector ทางการของ Anthropic (
AnthropicClient,AsIChatClient(...), v10+ ระบุ beta) ที่ใช้ต่อIChatClientเข้ากับ Claude เข้าถึง 2026-07-19
เช็กความเข้าใจ — บทที่ 8
ข้อ 1 / 3ใน agent capstone นี้ ทำไมกติกาคืนเงินจึงถูกบังคับทั้งใน system prompt และใน code C# ของ issueRefund?