The Loop — ร้อยทุกอย่างเป็นวง observe → decide → act
ห้าบทที่ผ่านมาคุณสะสมชิ้นส่วนครบแล้ว — การเรียกดิบ (บทที่ 2), prompt ที่ตั้งบทบาท (บทที่ 3), รอบ tool_use ↔ tool_result (บทที่ 4) และ IChatClient ที่ทำให้ทุกอย่างเป็นสำนวน .NET (บทที่ 5) แต่ทั้งหมดนั้นยังเป็นการเรียก model ทีละก้าว ผู้ใช้ถาม model ขอ tool คุณรัน แล้วป้อนผลกลับ — จบหนึ่งรอบ บทนี้คือชิ้นสุดท้ายที่บทที่ 1 สัญญาไว้ว่าจะจ่ายคืน: เอาก้าวเดี่ยวๆ พวกนั้นมา วน จนกลายเป็น agent ที่เดินงานหลายก้าวได้เอง คำนิยามของ Anthropic ตรงเป๊ะ — agent “are typically just LLMs using tools based on environmental feedback in a loop.” หัวใจอยู่ที่คำสุดท้าย: in a loop
บทนี้ประกอบ loop บน domain Order ของบริการฟู้ดเดลิเวอรี จาก repo kaen-food-ordering (กำลังจัดทำ) — ตัวอย่างเดินเรื่องคือคำขอเดียว “ออเดอร์ส่งช้ามาก ขอเช็คสถานะแล้วคืนเงินให้ด้วย” ที่ agent ต้องเดินสามก้าว getOrder → getDeliveryStatus → issueRefund กว่าจะจบ code ที่เห็นอิงรูป API จริงจากบทที่ 4–5 ทุกจุด แต่เดินเรื่องเพื่อสอนกลไก loop เป็นหลัก
the loop คืออะไร: observe → decide → act → repeat
หัวข้อที่มีชื่อว่า “the loop คืออะไร: observe → decide → act → repeat”Agent LoopAgent Loopวงจร observe → decide → act → repeat ที่ทำซ้ำจนถึงเงื่อนไขหยุด — model สังเกตผลจากสภาพแวดล้อม (ผลของ tool) แล้วตัดสินใจก้าวถัดไป วนไปจนกว่าจะได้คำตอบสุดท้าย (ไม่มี `tool_use` แล้ว), ชนเพดานจำนวนรอบ, ต้องขออนุมัติจากคน หรือเกินงบ token/ต้นทุน — ต้องรักษาให้มีเงื่อนไขหยุดเสมอเพื่อคุมไม่ให้วนไม่จบ ใน Microsoft.Extensions.AI ตัว `.UseFunctionInvocation()` ทำวงนี้ให้อัตโนมัติArchitecture คือวงเดียวสี่จังหวะที่วนจนกว่าจะถึงเงื่อนไขหยุด:
- decide — model ดูบริบททั้งหมดที่มีในตอนนี้ แล้วตัดสินก้าวถัดไป: จะเรียก tool ตัวไหน หรือได้คำตอบสุดท้ายแล้ว
- act — code C# ของคุณรัน tool ที่ model ขอจริงๆ (
getOrder,issueRefund, …) - observe — เอาผลของ tool ป้อนกลับเข้า model เป็น “สิ่งที่เห็นจากสภาพแวดล้อม” Anthropic ย้ำว่าจังหวะนี้สำคัญ: “it’s crucial for the agents to gain ‘ground truth’ from the environment at each step (such as tool call results or code execution) to assess its progress.” — model ไม่ได้เดาว่าออเดอร์สถานะอะไร มันได้ ground truth จากผล tool จริง
- repeat — วนกลับไป decide ด้วยบริบทที่เพิ่งโตขึ้น
สิ่งที่คุณสร้างในบทที่ 4 คือ หนึ่งรอบ ของวงนี้ที่หมุนด้วยมือ บทนี้แค่เอามันมาใส่ while — และเพิ่มสมองส่วนที่คอยดูว่าเมื่อไรควรหยุดหมุน
building block: LLM ที่ถูกเสริม (augmented)
หัวข้อที่มีชื่อว่า “building block: LLM ที่ถูกเสริม (augmented)”ก่อนจะดู loop วนจริง ต้องเห็นว่ามันวนอยู่บน อะไร Anthropic เรียกหน่วยตั้งต้นของระบบ agent ว่า Augmented LLMAugmented LLMหน่วยประกอบพื้นฐานที่สุดของระบบ agentic ตามนิยามของ Anthropic — LLM หนึ่งตัวที่ถูกเสริม (augment) ด้วย retrieval, tools และ memory เช่น `getOrder`/`getDeliveryStatus`/`issueRefund` คือ tools ที่เสริมให้ model ลงมือกับ domain Order ได้จริง เป็นก้อนที่ Agent Loop เอามาวนซ้ำArchitecture — “The basic building block of agentic systems is an LLM enhanced with augmentations such as retrieval, tools, and memory.” แกะเป็นภาษาของคอร์สนี้:
- tools —
getOrder/getDeliveryStatus/issueRefundคือ “มือ” ที่ให้ model ไปแตะ domainOrderจริง (นี่คือ tools จากบทที่ 4 ตรงๆ) - memory —
messageslist ที่โตขึ้นทุกรอบ เก็บทั้งคำถาม คำขอ tool และผล tool ไว้ให้ model อ้างอิงได้ในก้าวถัดไป (ในบทที่ 5 คือhistoryที่คุณAddMessages(response)เข้าไป)
Augmented LLM คือ กล่อง ที่ loop หมุนรอบมัน — LLM ตัวเดิม ที่มีมือ (tools) และความจำ (memory) ต่อพ่วง ไม่ใช่ model พันธุ์ใหม่
เดินจริงสามก้าว: getOrder → getDeliveryStatus → issueRefund
หัวข้อที่มีชื่อว่า “เดินจริงสามก้าว: getOrder → getDeliveryStatus → issueRefund”คำขอเดียว “ออเดอร์ ORD-1042 ส่งช้ามาก ขอเช็คสถานะแล้วคืนเงินให้ด้วย” ไม่มีทางจบในก้าวเดียว model ต้องรู้ก่อนว่าออเดอร์มีจริงไหม (getOrder) แล้วส่งถึงไหนแล้ว (getDeliveryStatus) ถึงจะตัดสินได้ว่าควรคืนเงินเท่าไร (issueRefund) แต่ละก้าวคือหนึ่งรอบของ loop และ ผลของก้าวก่อนคือ input ของ decide ก้าวถัดไป
flowchart TB
START["คำขอลูกค้า"] --> DECIDE["model ตัดสินใจ (decide)"]
DECIDE -->|ขอเรียก tool| ACT["รัน tool ใน C# (act)<br/>getOrder / getDeliveryStatus / issueRefund"]
ACT --> OBSERVE["ป้อนผลกลับเป็น tool_result (observe)"]
OBSERVE --> CHECK{"ถึงเงื่อนไขหยุดหรือยัง"}
CHECK -->|ยัง วนต่อ| DECIDE
CHECK -->|เกินเพดานรอบ หรือ เกินงบ หรือ รออนุมัติ| STOPCTRL["หยุดโดยตัวคุม"]
DECIDE -->|ได้คำตอบสุดท้าย ไม่เรียก tool| DONE["ตอบลูกค้า จบ loop"]
classDef loop fill:#ea580c,stroke:#7c2d12,color:#f8fafc;
classDef stop fill:#dc2626,stroke:#7f1d1d,color:#f8fafc;
class DECIDE loop;
class STOPCTRL stop;
คำบรรยายภาพ: วง observe → decide → act → repeat ที่มีด่านตรวจ (สีส้มคือหัวใจที่วน — model ตัดสินใจ) ทุกรอบ model ตัดสินใจ ถ้ายังขอ tool ก็ไปรันใน C# แล้วป้อนผลกลับเป็น tool_result จากนั้นเข้าด่านเช็กเงื่อนไขหยุด — ยังไม่ถึงก็วนกลับไป decide ถ้าถึง (เกินเพดานรอบ เกินงบ หรือ tool ต้องรออนุมัติ) ตัวคุมสั่งหยุด (สีแดง) ส่วนทางลัดขวาคือ done signal: รอบไหน model ตอบโดยไม่ขอ tool เลย แปลว่างานจบ ออกจาก loop ตามธรรมชาติ
แบบหมุนด้วยมือ: while รอบการเรียกดิบ
หัวข้อที่มีชื่อว่า “แบบหมุนด้วยมือ: while รอบการเรียกดิบ”ก่อนจะปล่อยให้ framework หมุนให้ ดูให้เห็นก่อนว่ามันหมุนยังไง นี่คือรอบเดียวจากบทที่ 4 ที่เอามาใส่ while — ทุกบรรทัดรักษา invariant เดิม (x-api-key, anthropic-version: 2023-06-01, ไม่มี temperature, อ่านคำตอบจาก content[] ไม่มี .text ระดับบนสุด):
var messages = new List<object> { new { role = "user", content = "ออเดอร์ ORD-1042 ส่งช้ามาก ขอเช็คสถานะแล้วคืนเงินให้ด้วย" }};var tools = new[] { /* getOrder, getDeliveryStatus, issueRefund — schema เดิมจากบทที่ 4 */ };
const int maxIterations = 8; // (b) เพดานจำนวนรอบ — กันวนไม่รู้จบfor (int step = 0; step < maxIterations; step++){ var body = new { model = "claude-opus-4-8", // ตัวอย่าง ณ 2026-07 — id เลื่อนไหวตามเวลา max_tokens = 1024, tools, messages }; // ไม่มี temperature — 400 บน model รุ่นล่าสุด
using var resp = await http.PostAsJsonAsync("v1/messages", body); resp.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); var root = doc.RootElement; string? stopReason = root.GetProperty("stop_reason").GetString();
// (a) done signal: stop_reason ไม่ใช่ "tool_use" แล้ว = model ได้คำตอบสุดท้าย // ระวัง: มีแต่ end_turn ที่แปลว่างานสำเร็จ — ค่าอื่นที่ไม่ใช่ tool_use เช่น // max_tokens หรือ refusal แปลว่าเทิร์นหยุดด้วยเหตุอื่น ไม่ใช่ว่างานสำเร็จ if (stopReason != "tool_use") { PrintFinalText(root); // อ่านจาก content[] เหมือนบทที่ 2 break; }
// จำคำตอบของ assistant ทั้งก้อน verbatim (memory โตขึ้นหนึ่งเทิร์น) messages.Add(new { role = "assistant", content = CloneContent(root) });
// act + observe: รัน tool ที่ model ขอ แล้วสร้าง tool_result ป้อนกลับ var results = new List<object>(); foreach (var block in root.GetProperty("content").EnumerateArray()) { if (block.GetProperty("type").GetString() != "tool_use") continue; string name = block.GetProperty("name").GetString()!; string id = block.GetProperty("id").GetString()!; // tool_use_id ต้องตรงกับ id นี้ var input = block.GetProperty("input");
// (c) human checkpoint: issueRefund ย้ายเงินจริง — หยุดขออนุมัติก่อนลงมือ if (name == "issueRefund" && !ApprovedByHuman(input)) { results.Add(new { type = "tool_result", tool_use_id = id, content = "ยังไม่คืนเงิน — รอเจ้าหน้าที่อนุมัติก่อน", is_error = true }); continue; }
string toolOutput = name switch { "getOrder" => GetOrder(input), "getDeliveryStatus" => GetDeliveryStatus(input), "issueRefund" => IssueRefund(input), _ => "{\"error\":\"unknown tool\"}" }; results.Add(new { type = "tool_result", tool_use_id = id, content = toolOutput }); } // tool_result ต้องมาก่อนใน content ของ user message เสมอ (ไม่งั้น 400) messages.Add(new { role = "user", content = results });}สังเกตว่าไม่มีเวทมนตร์ในนี้เลย — มีแต่ for ครอบการเรียกเดิม บวกด่านเช็ก stop_reason Stop ReasonStop Reasonfield `stop_reason` ในทุกคำตอบที่บอกว่า model หยุดเพราะอะไร — `end_turn` = ตอบจบเองตามธรรมชาติ, `tool_use` = model ขอเรียก tool (สัญญาณให้แตกกิ่งไปรัน tool), `max_tokens` = ชนเพดานที่ตั้งไว้ (คำตอบอาจถูกตัด) — เป็นสัญญาณควบคุมหลักของ Agent Loop ว่าจะวนต่อหรือหยุดArchitecture เพื่อรู้ว่าเมื่อไรหยุด นี่คือทั้งหมดที่ทำให้การเรียกครั้งเดียวกลายเป็น agent
แบบ framework หมุนให้: .UseFunctionInvocation() คือ loop
หัวข้อที่มีชื่อว่า “แบบ framework หมุนให้: .UseFunctionInvocation() คือ loop”พอเข้าใจว่า while ข้างบนทำอะไร ก็จะเห็นว่า 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 ในบทที่ 5 ซ่อนมันไว้ให้ทั้งวง — .UseFunctionInvocation() ไม่ใช่แค่ “เรียก tool ให้” แต่มันคือ loop ทั้งอัน: มันวน decide → act → observe ให้เอง จน model ไม่ขอ tool อีก แล้วค่อยคืน ChatResponse ให้คุณ คุณ await ครั้งเดียว ได้คำตอบสุดท้ายที่ผ่านสามก้าวมาแล้ว:
IChatClient chatClient = client.AsIChatClient("claude-opus-4-8") // ตัวอย่าง ณ 2026-07 (beta) .AsBuilder() .UseFunctionInvocation() // ← บรรทัดนี้คือ loop ทั้งวง .Build();
var options = new ChatOptions { Tools = [ AIFunctionFactory.Create(GetOrder, "getOrder", "ดึงออเดอร์ตาม id"), AIFunctionFactory.Create(GetDeliveryStatus, "getDeliveryStatus", "ดูสถานะการจัดส่งของออเดอร์"), AIFunctionFactory.Create(IssueRefund, "issueRefund", "คืนเงินออเดอร์ตามจำนวนที่ระบุ"), ] // ⚠️ ห้ามตั้ง options.Temperature — 400 บน model Claude รุ่นล่าสุด};
List<ChatMessage> history = [ new(ChatRole.System, "You are a customer-support agent for a food-delivery service."), new(ChatRole.User, "ออเดอร์ ORD-1042 ส่งช้ามาก ขอเช็คสถานะแล้วคืนเงินให้ด้วย")];
ChatResponse response = await chatClient.GetResponseAsync(history, options);Console.WriteLine(response.Text); // getOrder → getDeliveryStatus → issueRefund วนครบแล้วในสำนวน MEAI สัญญาณ done ก็เปลี่ยนหน้าตาไปด้วย: แทนที่จะเช็ก stop_reason == "tool_use" เอง middleware เช็กให้ — มันวนต่อตราบใดที่ model ยังขอ tool (FinishReason เป็น ToolCalls) และหยุดเมื่อได้คำตอบสุดท้าย (FinishReason เป็น Stop) รูปสองก้อนนี้ — ผล tool_result ป้อนกลับเป็น Tool ResultTool Resultblock `tool_result` ที่เราส่งกลับหลังรัน tool เสร็จ ต้องอ้าง `tool_use_id` ให้ตรงกับ id ของ block `tool_use` ที่ model ส่งมา — กติกาที่ผิดแล้ว 400: ต้องวางเป็น block แรกใน content ของข้อความ role `user`, ค่าจะเป็น string หรือ list ก็ได้ และถ้า tool พังให้ใส่ `is_error: true` แทนการโยน exception เพื่อให้ loop เห็นความล้มเหลวแล้วกู้ต่อได้Architecture ในราว HTTP กับ ChatResponse.FinishReason ใน MEAI — คือหน้าเดียวกันของเหรียญ อย่าสับสนสองสำนวนเข้าหากัน
เงื่อนไขหยุด: หัวใจที่ทำให้ loop ไม่หลุดมือ
หัวข้อที่มีชื่อว่า “เงื่อนไขหยุด: หัวใจที่ทำให้ loop ไม่หลุดมือ”loop ที่ไม่มีเงื่อนไขหยุดคือ while(true) ที่เผาเงิน Anthropic เขียนไว้ตรงๆ ว่า “The task often terminates upon completion, but it’s also common to include stopping conditions (such as a maximum number of iterations) to maintain control.” — คำว่า maintain control คือประเด็น loop ต้องหยุดได้ด้วยเงื่อนไขที่ คุณ กำหนด ไม่ใช่รอให้ model ตัดสินใจหยุดเอง มีสี่แบบที่ต้องรักษาไว้เสมอ:
- (a) done signal — งานเสร็จ รอบไหน model ตอบโดยไม่ขอ tool (
stop_reasonไม่ใช่tool_use/FinishReasonเป็นStop) แปลว่าจบ ออกจาก loop นี่คือทางออกปกติ - (b) เพดานจำนวนรอบ (max iterations)
for (step < maxIterations)— ด่านนิรภัยเผื่อ model วนไม่จบ เช่น เรียก tool เดิมซ้ำเพราะเข้าใจผล tool ผิด ถ้าไม่มีเพดานนี้ bug หนึ่งตัวกลายเป็นบิลค่า API ที่บานปลาย - (c) human checkpoint — รออนุมัติ
issueRefundย้ายเงินจริงใน domainOrderมันคือจุดที่ควร พัก loop ให้คนตรวจก่อนลงมือ Anthropic แนะตรงนี้ว่า “Agents can then pause for human feedback at checkpoints or when encountering blockers.” — action ที่ผลย้อนกลับไม่ได้ ควรมีด่านคนเสมอ (บทที่ 8 จะทำด่านนี้ให้แน่นใน code C# ไม่ใช่ฝากไว้กับ prompt) - (d) เพดานงบ (cost ceiling) ทุกรอบคือการยิง Messages หนึ่งครั้งที่คิดเงิน และเพราะ API ไม่มี state คุณต้องส่ง
messagesทั้งก้อน +toolsซ้ำทุกรอบ — Token CostToken Costต้นทุนที่คิดตามจำนวน token — ทุกคำตอบรายงานการใช้งานใน field `usage` (ขนาด prompt จริง = `input_tokens + cache_creation_input_tokens + cache_read_input_tokens` ส่วน `input_tokens` เดี่ยว ๆ เป็นแค่ส่วนที่ไม่ถูก cache) — ใน Agent Loop แต่ละรอบต้องส่งประวัติทั้งชุด + tools กลับไปใหม่ token จึงพอกขึ้นทุกรอบ งบต้นทุน/เวลาจึงเป็นเงื่อนไขหยุดวงจรอย่างหนึ่งProcess จึง โตขึ้นทุกเทิร์น Anthropic ยอมรับ trade-off นี้ตรงๆ ว่า “Agentic systems often trade latency and cost for better task performance.” งบ token หรือจำนวนเงินต่อคำขอ จึงเป็นเงื่อนไขหยุดในตัวมันเอง
เช็ก list ทางออกที่ควรพก: (a) ได้คำตอบสุดท้าย ไม่มี tool call → หยุด; (b) ชนเพดานรอบ → หยุด; (c) tool ต้องขออนุมัติ → พัก; (d) เกินงบ → หยุด สี่ข้อนี้คือสิ่งที่แยก “agent ที่คุมได้” ออกจาก “loop ที่หลุดมือ”
workflow กับ agent: เส้นแบ่งแบบสั้น
หัวข้อที่มีชื่อว่า “workflow กับ agent: เส้นแบ่งแบบสั้น”คำถามที่มักโผล่มาตรงนี้คือ “แล้วนี่มัน workflow หรือ agent” Anthropic ลากเส้นไว้ชัด: “Workflows are systems where LLMs and tools are orchestrated through predefined code paths. Agents… are systems where LLMs dynamically direct their own processes and tool usage.” — ต่างกันที่ ใครเป็นคนกำหนดลำดับก้าว ถ้าคุณเขียน code ตายตัวว่า “เรียก getOrder ก่อน แล้ว getDeliveryStatus แล้วค่อย issueRefund” นั่นคือ workflow แต่ใน code ข้างบน model เป็นคนเลือกเองว่าจะเรียก tool ไหน เมื่อไร ตามผลที่มันสังเกตได้ — agents “dynamically direct their own processes and tool usage” — นั่นทำให้มันเป็น agent
พอแค่นี้ก่อน เส้นแบ่งนี้ลึกกว่าที่เห็น และคำถามที่ตามมา — เมื่อไรควรเป็น agent เต็มตัว เมื่อไรพอแค่ workflow และขอบเขตของ agent ควรอยู่ตรงไหน — เป็นคำถามเรื่อง ขอบเขต ที่คอร์ส #12 (agent-as-bounded-context) เจาะเต็มๆ บทนี้จบที่กลไก: คุณสร้าง loop ที่ ง่ายที่สุดเท่าที่ควร ได้แล้ว — 1 agent 1 loop ตามที่บทที่ 1 ปักธงไว้ ส่วนคำถามว่าเมื่อไรมันควรกลายเป็น1 bounded context และเมื่อไร (ซึ่งน้อยครั้ง) ควรมี agent ตัวที่สอง ยกไปให้คอร์สถัดไป
บทหน้าเราจะทำให้ loop นี้ เชื่อถือได้ — จัดการ error ของ tool คุมต้นทุน และวาง guardrail รอบๆ ธรรมชาติที่ไม่ deterministic ของมัน
บทนี้สังเคราะห์จากสองแหล่งหลัก อ่านต่อได้ที่ต้นทางโดยตรง:
- Anthropic, “Building Effective Agents” (2024-12-19) — นิยาม agent ว่าคือ LLM ใช้ tools ตาม feedback วนเป็น loop, building block ที่เป็น augmented LLM (tools + memory), จังหวะ observe ที่ต้องได้ ground truth จากสภาพแวดล้อม, เส้นแบ่ง workflow vs agent, เงื่อนไขหยุด (เพดานรอบ + human checkpoint) และ trade-off ด้าน latency/cost
- Anthropic, Tool use — handle tool calls — รอบ
tool_use↔tool_resultที่เอามาวน: API ไม่มี state ต้องส่ง messages + tools ทั้งก้อนซ้ำทุกรอบ,tool_resultมาก่อนใน content, และtool_use_idต้องตรงกับ id ของtool_useเข้าถึง 2026-07-19
เช็กความเข้าใจ — บทที่ 6
ข้อ 1 / 3ในวง observe → decide → act → repeat จังหวะ 'observe' หมายถึงอะไร?