loop คือเครื่องยนต์
จบสองคอร์สที่ผ่านมา คุณถือของจริงมาแล้วสองชั้น คอร์สที่ 15 ให้ agent support ลูกค้าบน domain Order ที่ รันได้ — model claude-opus-4-8 (ใช้เป็นตัวอย่าง ณ 2026-07 — id เลื่อนไหวตามเวลา) บวก tool สามตัว getOrder / getDeliveryStatus / issueRefund บวก loop ที่ป้อนผลของ tool กลับเข้า model จนงานเสร็จ คอร์สที่ 16 ให้ context layer คร่อมมัน — retrieval, memory, การจัดงบ token, compaction #15 สอนให้ ถือ model ถูก #16 สอนว่า จะป้อนอะไร คอร์สนี้ลงลึกอีกชั้นเดียวที่ทั้งสองคอร์สนั้นวางไว้บนโต๊ะแต่ยังไม่ได้เปิดกล่อง — runtime ที่ agent อาศัยอยู่: harness และตัว loop เอง
Anthropic นิยาม agent ไว้ประโยคเดียว — “Agents are typically just LLMs using tools based on environmental feedback in a loop.” — คอร์ส #15 ยึดประโยคนี้เป็นแกน คอร์สนี้จะ เดินเข้าไปข้างใน ประโยคนั้น ลึกกว่าบทเกริ่น “THE LOOP” ของ #15 (บท6) ที่หยุด loop ด้วย predicate เดียว
คอร์สนี้ต่อยอด agent Order ตัวเดิมจากคอร์ส #15 (พร้อม context layer จากคอร์ส #16) — repo kaen-food-ordering (กำลังจัดทำ) #17 ไม่เพิ่ม tool หรือ store ใหม่ แต่ห่อ runtime คร่อมมัน บทนี้เป็นบทวางกรอบความคิด code ที่แสดงคือ รูปทรงของ loop สองแบบ ที่ทุกบทหลังจากนี้จะหยิบไปดัดแปลงทีละ clause ของ stop policy ทุกกติกา API ฝั่งแชตจากคอร์ส #15 ยังยึดเดิมทุกข้อ — บทนี้ตั้ง precedent ของกติกาเหล่านั้นไว้ให้ทั้งคอร์ส
ชั้นล่างสุดที่ยังไม่ได้เปิด: อะตอม → runtime
หัวข้อที่มีชื่อว่า “ชั้นล่างสุดที่ยังไม่ได้เปิด: อะตอม → runtime”ใต้ context ที่เราป้อน (คอร์ส #16) มีอะไรอยู่? มีสองชั้นที่ต้องแยกให้ขาด
ชั้นแรกคือ อะตอม — augmented LLMAugmented LLMอะตอมที่ harness ห่ออยู่: LLM ที่เสริมด้วย retrieval, tools และ memory (BEA — 'the basic building block of agentic systems') ผู้เรียนสร้างมันไว้แล้วใน #15 (tools) + #16 (retrieval/memory) — #17 คือ runtime ที่เรียกอะตอมตัวนี้ซ้ำ ๆ ใน loop; retrieval/memory คือ 'ส่วนเสริม' ส่วน #17 คือ 'ตัว runtime' ที่เรียกใช้มันArchitecture ที่ Anthropic เรียกว่า “The basic building block of agentic systems is an LLM enhanced with augmentations such as retrieval, tools, and memory.” คุณสร้างอะตอมตัวนี้เสร็จแล้ว: tool สามตัวจาก #15 คือ augmentation แบบ tool, retrieval/memory จาก #16 คือ augmentation อีกสองชนิด อะตอมตัวนี้ทำงานได้ หนึ่งครั้ง ต่อการเรียกหนึ่งครั้ง
ชั้นที่สองคือ runtime ที่เรียกอะตอมตัวนั้นซ้ำๆ — และนั่นคือสิ่งที่คอร์สนี้เปิด เราจะเรียกมันด้วยชื่อที่ Anthropic ตั้งไว้ว่า harnessAgent Harnesscode scaffolding ที่ห่อรอบการเรียก model หนึ่งครั้ง — คือส่วนที่จัดการ tool dispatch, การรักษาประวัติข้อความ (message history), ตัว `while` ที่วนซ้ำ และการตรวจเงื่อนไขหยุด (stop check) Anthropic เรียกชื่อ 'agent harness' ไว้ (Claude Agent SDK) แต่การแยกส่วนออกเป็นสี่ชิ้นนี้คือกรอบสอนของคอร์สนี้ ต่างจาก #16 ที่ engineer 'สิ่งที่ป้อน' (context) — harness คือ runtime ที่ตอบว่า 'ป้อนอย่างไร' เทิร์นแล้วเทิร์นเล่าArchitecture Anthropic พูดถึง “the agent harness that powers Claude Code” โดยไม่ได้ให้นิยามเชิงโครงสร้าง — การแยกส่วน harness ออกเป็นสี่ชิ้นต่อไปนี้คือกรอบสอนของคอร์สนี้ ไม่ใช่คำนิยามทางการของ Anthropic เราเป็นคนตั้งกรอบ ยืมแค่ ชื่อ มาจากเขา:
- tool dispatch — รับ tool call ที่ model ขอ แล้วเรียก method C# ตัวจริง
- การรักษาประวัติข้อความ (message history) — สะสม
ChatMessageทุกเทิร์นแล้วส่งกลับเข้าไปซ้ำ - ตัว
while— วงวนที่พา model กลับมาเรียกอีกรอบหลังเห็นผลของ tool - การตรวจเงื่อนไขหยุด (stop check) — ตัดสินว่ารอบนี้ควรจบหรือวนต่อ
harness ไม่ใช่ model ไม่ใช่ tool ไม่ใช่ context มันคือ โครง code ที่ประสานทั้งสามอย่างให้ทำงานเป็นวงจร — engine ที่หมุนอะตอม
loop คือวงจร gather → act → verify → repeat
หัวข้อที่มีชื่อว่า “loop คือวงจร gather → act → verify → repeat”loopAgent Loopวงจร runtime ที่ harness วนซ้ำ: gather context → take action → verify work → repeat (Claude Agent SDK) — เรียก model, dispatch tool ที่ถูกเรียก, ผนวกผลกลับเข้า context, ตรวจเงื่อนไขหยุด แล้ววนใหม่ ลึกกว่า loop ของ #15 บท6 ที่หยุดด้วย predicate เดียว ตรงที่เพิ่มเฟส verify — หลัง write action (เช่น issueRefund) อ่าน ground truth ซ้ำ (getOrder) ก่อนประกาศว่าเสร็จArchitecture คือรูปทรงที่ harness วนซ้ำ Anthropic อธิบายรูปทรงนี้จาก Claude Code ไว้ตรงๆ — “In Claude Code, Claude often operates in a specific feedback loop: gather context, take action, verify work, repeat.” — model รวบรวม context, ลงมือทำ (เรียก tool), ตรวจงานที่ทำ แล้ววนใหม่ โดยวนไปต่อเนื่อง “iteratively until the code succeeds.”
รากทางความคิดของวงจรนี้มีชื่อ: ReActReAct (Reasoning + Acting)รากทางความคิดของ loop (Yao et al., 2022): model reason (thinking trace) แล้ว act (tool call) แล้วผลของ tool คือ observation ที่ป้อนกลับ — `observe → decide → act → repeat` reasoning trace ช่วยวางแผนและจัดการ exception ส่วน action ต่อกับแหล่งภายนอกเพื่อดึงข้อมูลเพิ่ม ⚠️ ผลที่อ้าง (ALFWorld/WebShop เหนือ baseline 34%/10%) เป็นตัวเลขเฉพาะงานและลงวันที่ นำเสนอเป็นผลลัพธ์ ไม่ใช่กฎArchitecture (Yao et al., 2022) ที่จับ reasoning กับ acting มาสานสลับกัน — “we explore the use of LLMs to generate both reasoning traces and task-specific actions in an interleaved manner… reasoning traces help the model induce, track, and update action plans as well as handle exceptions, while actions allow it to interface with external sources, such as knowledge bases or environments, to gather additional information.” กติกาข้อนี้คือแม่แบบของ loop ที่คุณเขียนใน #15: model คิด (reason) → เรียก tool (act) → ผลของ tool คือ observation ที่ป้อนกลับ → คิดต่อ ⚠️ ผลที่ ReAct รายงาน (เหนือ baseline บน ALFWorld/WebShop ที่ “an absolute success rate of 34% and 10% respectively”) เป็นตัวเลข เฉพาะงานและลงวันที่ — นำเสนอเป็นผลลัพธ์ของงานวิจัยหนึ่ง ไม่ใช่กฎที่การันตีทุก domain
เฟส verify คือแกนใหม่ที่ลึกกว่า #15 บท6 loop ของ #15 หยุดด้วย predicate เดียว: ถ้าไม่มี tool call แล้วก็จบ แต่วงจรของ Anthropic เพิ่มขั้น ตรวจงาน เข้ามา — หลัง write action อย่าง issueRefund ควร อ่าน ground truth ซ้ำ ด้วย getOrder ก่อนประกาศว่าเสร็จ ไม่ใช่เชื่อว่า tool คืน success แล้วจบ
เส้นแบ่ง read/write นี้สำคัญ Chip Huyen เขียนไว้ว่า “Actions that allow an agent to perceive the environment are read-only actions, whereas actions that allow an agent to act upon the environment are write actions.” map ลงบน agent ของเรา (เรียบเรียงจาก Huyen): getOrder / getDeliveryStatus คือ sensor (อ่านอย่างเดียว รันซ้ำได้ไม่เจ็บ) ส่วน issueRefund คือ actuator (เขียน มีผลข้างเคียง) — และเพราะมันเป็น write action นี่เอง มันจึงต้องมีเฟส verify และ checkpoint กำกับในบทหลังๆ
เงื่อนไขหยุดคือ นโยบายประกอบ ไม่ใช่ predicate เดียว
หัวข้อที่มีชื่อว่า “เงื่อนไขหยุดคือ นโยบายประกอบ ไม่ใช่ predicate เดียว”นี่คือประโยคที่คอร์สทั้งคอร์สหมุนรอบ stop check ของ #15 ตรวจสิ่งเดียว — stop_reason != "tool_use" (raw) หรือ “ไม่มี FunctionCallContent แล้ว” (IChatClient) งานของ #17 คือแสดงว่า predicate เดียวนั้นคือเคส degenerate ของนโยบายหยุดแบบประกอบ (compound stop policyStopping Conditionเงื่อนไขหยุด loop — เป็น 'นโยบายประกอบ' (compound policy) ไม่ใช่ predicate เดียว: task-complete ∨ max-iterations ∨ token/cost-budget ∨ unrecoverable-error ∨ human-checkpoint (BEA + Huyen) `stop_reason != tool_use` ของ #15 คือเคส degenerate ของนโยบายนี้ ทุกบทลงมือแทนที่ทีละ clause; loop ออกทันทีที่ clause แรก trip และ log เหตุผลที่ออก (ตั้งไว้บท1 จ่ายคืนบท8)Process):
task-complete ∨ เกิน max-iterations ∨ เกินงบ token/cost ∨ เจอ error ที่กู้ไม่ได้ ∨ ถึง human-checkpoint
stop_reason != "tool_use" เป็นแค่ clause แรก (task-complete) ของนโยบายที่มี5 clause ทุกบทลงมือของคอร์สนี้จะไป แทนที่ทีละ clause — บท7 เติม clause “error ที่กู้ไม่ได้”, บท8 ประกอบทั้ง5 clause เข้าเป็น LoopController ตัวเดียวที่ตรวจ ที่หัวของทุกรอบ ก่อนเรียก model ครั้งถัดไป 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.” จำภาพนี้ไว้ แล้วบท8 จะจ่ายคืน
รูปทรงของ loop สองแบบ — อย่าปนกัน
หัวข้อที่มีชื่อว่า “รูปทรงของ loop สองแบบ — อย่าปนกัน”มีสองวิธีเขียน loop ใน .NET และคอร์สนี้จะใช้ ทั้งสอง ตามงาน อย่าปนกัน เริ่มจากเชื่อม agent เดิมเข้ากับ IChatClient — ไม่มี connector first-party ของ Anthropic ใน Microsoft.Extensions.AI ดังนั้น Claude ยังอยู่หลัง NuGet Anthropic ตัวทางการผ่าน AsIChatClient เหมือนที่ทำใน #15/#16:
using Anthropic; // AnthropicClient, .AsIChatClientusing Microsoft.Extensions.AI; // IChatClient, ChatOptions, AIFunctionFactory, FunctionCallContent, ...
AnthropicClient anthropic = new(); // อ่าน ANTHROPIC_API_KEY จาก envIChatClient raw = anthropic.AsIChatClient("claude-opus-4-8"); // bare id, ตัวอย่าง ณ 2026-07 — ไม่ต่อ suffix วันที่
ChatOptions options = new() { Tools = [ AIFunctionFactory.Create(GetOrder), // 3 tool เดิมจาก #15 เป็น method C# ธรรมดา AIFunctionFactory.Create(GetDeliveryStatus), AIFunctionFactory.Create(IssueRefund), ],};สะพานเชื่อมนี้แบก invariant เดิมของ #15 มาครบ ทุก snippet ที่แตะ Messages API / IChatClient ในคอร์สนี้ต้องยึด: header เป็น x-api-key ไม่ใช่ Authorization: Bearer · anthropic-version: 2023-06-01 เสมอ · ห้ามส่ง temperature / top_p / top_k ไปกับ model รุ่นล่าสุด (steer ผ่าน prompt) · model id เป็น alias เปล่า claude-opus-4-8 (ลงวันที่ 2026-07 กำกับ ไม่ต่อ suffix) · คำตอบดิบ ไม่มี .text ระดับบนสุด — มันคือ array content[] ที่ต้องไล่ match type; ใต้ IChatClient ตัวเทียบคือ ChatResponse.Messages[].Contents[] ที่ match ด้วย FunctionCallContent / FunctionResultContent ส่วน ChatResponse.Text เป็นแค่ convenience concatenation ไม่ใช่รูปดิบ
สถานะ package (พูดครั้งเดียวที่นี่): Anthropic 12.8.0 เป็น beta (“breaking changes may occur in minor or patch releases”) — pin version ไว้ และพึงระวังว่ารายละเอียดการ map ของ AsIChatClient (field usage, รูปทรง tool-content) อาจขยับ ส่วน Microsoft.Extensions.AI 10.x เป็น GA — สอนมันในฐานะ layer ที่ทนทาน แล้วสอนสะพาน Anthropic → AsIChatClient ในฐานะ beta-ที่อาจเปลี่ยน
แบบที่ 1 — hand-rolled while (คุณเป็นเจ้าของทุกอย่าง)
หัวข้อที่มีชื่อว่า “แบบที่ 1 — hand-rolled while (คุณเป็นเจ้าของทุกอย่าง)”นี่คือ loop ของ #15 บท6 ที่เขียนออกมาให้เห็นทุกกลไก คุณเรียก model เอง ไล่หา FunctionCallContent เอง ผนวก FunctionResultContent กลับเข้า ChatRole.Tool message เอง และคุมเงื่อนไขหยุดเอง:
List<ChatMessage> messages = [ new(ChatRole.User, "ออเดอร์ A-1002 อยู่ไหน ถ้าส่งช้าให้คืนเงิน") ];
for (int step = 0; step < 10; step++) { // for = while ที่มีขอบ: ตัวนับนี้คือ step budget — clause หนึ่งของ stop policy (จ่ายคืนบท8) ChatResponse resp = await raw.GetResponseAsync(messages, options, ct); messages.AddRange(resp.Messages); // ผนวก assistant turn ตามจริง — resp.Messages เป็นพหูพจน์
var calls = resp.Messages.SelectMany(m => m.Contents) .OfType<FunctionCallContent>().ToList(); if (calls.Count == 0) break; // ไม่มี tool call → จบ (predicate ของ #15; degenerate stop)
List<AIContent> results = []; foreach (var call in calls) { object? output; try { output = await Dispatch(call.Name, call.Arguments); } // registry ของคุณ catch (Exception ex) { output = $"ERROR: {ex.Message}"; } // จุดต่อ tool-error recovery (บท7) results.Add(new FunctionResultContent(call.CallId, output)); } messages.Add(new ChatMessage(ChatRole.Tool, results)); // ผลของ tool อยู่ใน ChatRole.Tool message}สังเกต: FunctionCallContent พก .Name / .CallId / .Arguments; ผลกลับเป็น FunctionResultContent(callId, output); และผลของ tool ต้องอยู่ใน message ที่มี role เป็น ChatRole.Tool การอ่านคำตอบทำผ่าน resp.Messages[].Contents[] ไม่ใช่ resp.Text
แบบที่ 2 — middleware loop (.UseFunctionInvocation() วนให้เอง)
หัวข้อที่มีชื่อว่า “แบบที่ 2 — middleware loop (.UseFunctionInvocation() วนให้เอง)”FunctionInvokingChatClient เป็น IChatClient ตัวหนึ่งที่วน call→execute→append→repeat ให้อัตโนมัติ เสียบเข้าไปใน pipeline ด้วยบรรทัดเดียว แล้ว loop ก็รันเองข้างในการเรียกครั้งเดียว:
IChatClient agent = anthropic.AsIChatClient("claude-opus-4-8") .AsBuilder() .UseFunctionInvocation() // เพิ่ม FunctionInvokingChatClient เข้าไปใน pipeline .Build();
ChatResponse resp = await agent.GetResponseAsync( "ออเดอร์ A-1002 อยู่ไหน ถ้าส่งช้าให้คืนเงิน", options, ct); // loop รันจบแล้ว; tool ถูกเรียกให้แล้วcode สองก้อนนี้ ทำงานเหมือนกัน บนเคสธรรมดา ต่างกันที่ใครเป็นเจ้าของ while แล้วจะเลือกอันไหน?
- middleware = ค่าเริ่มต้น สำหรับ agent ที่ใช้ custom tool ทั่วไป: ให้ compose caching/telemetry/retry เป็น layer ใน pipeline และคุณไม่ได้แทรกงานอื่นกลางวง นี่คือ runtime แบบ middlewareFunction Invocationรูป loop แบบ middleware: `FunctionInvokingChatClient` (ผ่าน `.AsBuilder().UseFunctionInvocation().Build()`) วน call→execute→append→repeat ให้อัตโนมัติ เป็น IChatClient ตัวหนึ่งใน pipeline — คือ runtime ค่าเริ่มต้นของ single-agent (สอดคล้อง #12) knob สำคัญ: `MaximumIterationsPerRequest`, `MaximumConsecutiveErrorsPerRequest` ฯลฯ ⚠️ เป็นพื้นผิวที่ขยับเร็ว pin ให้ตรง version ที่ติดตั้ง; ตรงข้ามกับ loop แบบ hand-rolled `while` ที่จองไว้สำหรับ stop policy ที่ middleware แสดงออกไม่ได้ (บท6, บท8)Architecture ของ single-agent มาตรฐาน (สอดคล้องกับ default “1 agent 1 loop” ของคอร์ส #12)
- hand-roll เมื่อ ต้องการเงื่อนไขหยุดที่ knob ของ middleware แสดงออกไม่ได้ (compound stop policy — บท8), ต้อง mutate ประวัติกลางเทิร์น, ต้องแทรกงานที่ไม่ใช่ tool ระหว่างเทิร์น (reflection — บท6) หรือไม่อยากให้ loop เป็นกล่องดำ
คอร์สนี้จองรูป hand-rolled ไว้ให้บท6 กับบท8 โดยเฉพาะ เพราะ2 stop policy นั้น knob ของ FunctionInvokingChatClient (MaximumIterationsPerRequest, MaximumConsecutiveErrorsPerRequest ฯลฯ) แสดงออกไม่หมด บทอื่นที่เหลือจะเดินบน middleware เป็นหลัก
flowchart TD
U["ข้อความจากผู้ใช้"] --> P{"ตรวจ stop policy ก่อนเริ่มรอบ<br/>เกิน max-iters · เกินงบ token · error · human-checkpoint"}
P -->|"trip clause ใด clause หนึ่ง"| STOP["หยุดแบบมีเหตุผล<br/>escalate หรือคืนคำตอบบางส่วน"]
P -->|"ผ่านทุก clause"| C["เรียก model<br/>raw.GetResponseAsync"]
C --> D{"มี FunctionCallContent ไหม"}
D -->|"ไม่มี — done signal"| DONE["ตอบผู้ใช้ แล้วจบ"]
D -->|"มี"| T["tool dispatch<br/>getOrder · getDeliveryStatus · issueRefund"]
T --> R["ผนวกผลกลับเข้า context<br/>FunctionResultContent ใน ChatRole.Tool"]
R --> P
classDef engine fill:#b91c1c,stroke:#7f1d1d,color:#f8fafc;
class C,T,R engine;
คำบรรยายภาพ: เครื่องยนต์ของ harness หมุนเป็นวงเดียว — เรียก model → ตรวจว่ามี tool call ไหม → dispatch tool → ผนวกผลกลับเข้า context → กลับไปตรวจ stop policy แล้ววนใหม่ สามช่องสีแดงคือ engine ที่ harness หมุน (การเรียก model, tool dispatch, การผนวกผล) ส่วน done signal (“ไม่มี FunctionCallContent”) คือทางออกสำเร็จ เพียง1 clause ของ stop policy ที่หัววง — ที่เหลือ (max-iters, งบ token, error, checkpoint) คือ clause อื่นๆ ที่บทหลังๆ จะเติมเข้ามาทีละอัน
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”agent ของคุณไม่ได้ฉลากเพราะ model ใหญ่ขึ้น มันทำงานได้เพราะ เครื่องยนต์ที่หมุนอะตอมนั้นซ้ำๆ — harness (โครง code สี่ชิ้น), loop (gather → act → verify → repeat แบบ ReAct) และ stop policy ที่เป็น นโยบายประกอบ5 clause ไม่ใช่ predicate เดียว บทหน้าเราถอยออกมาหนึ่งก้าวเพื่อถามคำถามที่ต้องตอบ ก่อน เอื้อมหา harness เลยด้วยซ้ำ — เมื่อไรควรสร้าง agent และเมื่อไร workflow ธรรมดาดีกว่า
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Anthropic, “Building Effective Agents” (2024-12-19) — นิยาม agent (“LLMs using tools based on environmental feedback in a loop”), building block ที่เป็น augmented LLM, และ stopping conditions ที่มีไว้ “to maintain control”
- Anthropic, “Building agents with the Claude Agent SDK” (2025-09-29) — รูปทรง loop
gather context → take action → verify work → repeatและการเรียกชื่อ “agent harness” - Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models” (arXiv:2210.03629, 2022; ICLR 2023) — reasoning traces สานสลับกับ actions, การ “handle exceptions”, และผล 34%/10% บน ALFWorld/WebShop
- Chip Huyen, “Agents” (2025-01-07) — เส้นแบ่ง read-only action (sensor) กับ write action (actuator)
- Microsoft Learn, “Use the IChatClient interface” —
GetResponseAsync,ChatResponse.Messages,FunctionCallContent/FunctionResultContent,ChatRole.Tool - Microsoft Learn, “FunctionInvokingChatClient” (pkg v10.7.0, upd. 2026-06-12) — middleware loop และ knob
MaximumIterationsPerRequest/MaximumConsecutiveErrorsPerRequest - NuGet, “Anthropic 12.8.0” (official C# SDK, beta) —
AnthropicClient,AsIChatClient(pin version; mapping อาจเปลี่ยน) - NuGet, “Microsoft.Extensions.AI 10.8.0” (GA) — layer ที่ทนทาน durable ที่สอนเป็นฐาน
เช็กความเข้าใจ — บทที่ 1
ข้อ 1 / 3ในกรอบสอนของคอร์สนี้ 'agent harness' หมายถึงอะไร?