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

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 ตัวอย่าง

code ใน​บท​นี้​ประกอบ​จาก​ชิ้น​ส่วน​ที่​คุณ​เห็น​มา​ตลอด​คอร์ส บน domain Order ของบริการฟู้ด​เดลิ​เวอรี​จาก repo kaen-food-ordering (กำลัง​จัด​ทำ) — โครง wiring และ tool ทุก​ตัว​ยึด​ตาม API invariant เดิม (x-api-key, anthropic-version: 2023-06-01, ไม่​ส่ง temperature, อ่าน​คำ​ตอบ​จาก content[]) เพื่อ​ให้​ต่อยอด​เป็น project จริง​ได้​ทันที

ก่อน​ประกอบ ขอ​ปัก​หมุด​ว่า​แต่ละ​ขั้น​ให้​ชิ้น​ส่วน​อะไร​มา เพราะ capstone คือ​การ​วาง​หมุด​เหล่า​นี้​ต่อ​กัน:

  1. บท​ที่ 1 — LLM คือ function text → token → text ที่​ไม่ deterministic ทุก​อย่าง​สร้าง​อยู่​บน​กรอบ​นี้
  2. บท​ที่ 2 — การ​เรียก​ดิบ เห็น Messages API ทุก byte ผ่าน HttpClient คำ​ตอบ​อยู่​ใน content[] ไม่มี .text ที่​ระดับ​บน​สุด
  3. บท​ที่ 3 — prompt ตั้ง​บทบาท​และ​กติกา​ด้วย​โครงสร้าง เกิด support persona กับ​กติกา​คืน​เงิน​ที่​บท​นี้​เอา​มา​ใช้​ซ้ำ
  4. บท​ที่ 4 — tool use รอบ tool_usetool_result ที่​ยก​การ​เรียก​ครั้ง​เดียว​ให้​เรียก code C# ได้
  5. บท​ที่ 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. บท​ที่ 6 — the loop ร้อย​ทุก​อย่าง​เป็น​วง observe → decide → act → repeat พร้อม​เงื่อนไข​หยุด
  7. บท​ที่ 7 — ทำให้​เชื่อถือ​ได้ วิศวกรรม​รอบ non-determinism ทั้ง​ต้นทุน error และ guardrail

capstone นี้​หยิบ​ขั้น 3 (persona + กติกา), ขั้น 4 (tool), ขั้น 5 (IChatClient), ขั้น 6 (loop) และ​ขั้น 7 (guardrail) มา​ต่อ​กัน​ใน file เดียว

ทั้ง 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 และ​กั้น​ด้วย​กติกา​ก่อน​เสมอ

จาก​บท​ที่ 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

จาก​บท​ที่ 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 อยู่​ข้าง​ใน ซึ่ง​เป็น​ชิ้น​ถัด​ไป

นี่​คือ​หัวใจ​ที่​แยก​เดโม​ออก​จาก​ของ​จริง บท​ที่ 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

เหลือ​แค่​ร้อย​ทุก​อย่าง​เข้า​ด้วย​กัน จาก​บท​ที่ 5 เรา​รู้​ว่า .UseFunctionInvocation() คือ the loop — มัน​รัน​รอบ tool_usetool_result ทั้งหมด​ให้​เอง คุณ​แค่ await การ​เรียก​ครั้ง​เดียว แล้ว middleware วน​เรียก tool และ​ป้อน​ผลกลับ​จน model ได้​คำ​ตอบ​สุดท้าย agent ทั้ง​ตัว​จึง​ย่อ​ลง​เหลือ​เท่า​นี้:

using Anthropic;
using Microsoft.Extensions.AI;
// wiring จากบทที่ 5 — connector ทางการของ Anthropic (v10+, ระบุ beta), model id เป็นตัวอย่าง ณ 2026-07
AnthropicClient anthropic = new(); // อ่าน ANTHROPIC_API_KEY จาก environment
IChatClient 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 แล้วอาจเสนอ issueRefund
ChatResponse response = await agent.GetResponseAsync(history, chatOptions); // chatOptions = ชิ้นที่ 2
Console.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 ตัว​นี้

⚠️ 2 message surface อย่า​ปน​กัน

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 ตัว​เต็ม — 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?