จาก raw ไป Microsoft.Extensions.AI
บทที่ 4 คุณต่อท่อ tool use ด้วยมือทุก byte — ส่ง tools เข้าไป อ่าน stop_reason แตก branch เมื่อเจอ tool_use รัน getOrder ใน C# ต่อ tool_result กลับเข้า list แล้วส่งทั้งก้อนไปใหม่ ท่อนั้นทำงานถูก และคุณควรเห็นมันสักครั้งเพื่อรู้ว่าใต้ฝากระโปรงมีอะไร แต่ใน project จริงคุณคงไม่อยากเขียน JsonDocument.Parse กับไล่ index content[] เองทุกครั้ง บทนี้เอาการเรียก ตัวเดิมเป๊ะๆ มาเขียนใหม่ด้วยสำนวน .NET ผ่าน abstraction กลางของ Microsoft — แล้วปล่อยให้ middleware วน loop ของ tool ให้เราแทน
คอร์สนี้ค่อยๆ ประกอบ agent สำหรับ support ลูกค้าบน domain Order ของบริการฟู้ดเดลิเวอรี จาก repo kaen-food-ordering (กำลังจัดทำ) — บทนี้ยกการเรียก getOrder จากบท 4 มาเขียนใหม่ด้วย IChatClient โครง wiring ที่เห็นเป็นชิ้นที่ capstone (บท 8) จะหยิบไปประกอบต่อจริง
abstraction กลาง: 1 interface ต่อได้ทุก model
หัวข้อที่มีชื่อว่า “abstraction กลาง: 1 interface ต่อได้ทุก model”Microsoft.Extensions.AIMicrosoft.Extensions.AIlibrary ทางการของ .NET (package `Microsoft.Extensions.AI` สาย 10.x GA) ที่ให้ abstraction กลางอย่าง IChatClient สำหรับต่อกับ model AI แบบไม่ผูกค่าย พร้อม automatic function invocation และเข้ากับ DI ได้ — ⚠️ ความจริงเรื่อง connector: Microsoft มี connector ทางการเฉพาะ OpenAI/Azure OpenAI สำหรับ Claude ยังไม่มี first-party ต้องใช้ package `Anthropic` ทางการ (v10+ สถานะ beta) ผ่าน `AsIChatClient(...)` และห้ามลากผ่าน connector ของ OpenAIArchitecture (ย่อ MEAI) คือชุด abstraction กลางของ .NET สำหรับคุยกับ generative AI — วางตัวแบบเดียวกับที่ ILogger วางตัวเหนือ logger ทุกเจ้า คุณเขียน code พิงหน้า interface กลาง แล้วสลับ provider ข้างล่างได้โดยไม่รื้อ logic หัวใจของมันคือ 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 — interface เดียวที่แทน “ปลายทางที่รับข้อความแล้วตอบกลับ” ไม่ว่าเบื้องหลังจะเป็น Claude, model อื่น หรือตัว mock ใน test
method ที่มือใหม่แตะจริงมีแค่สองตัว:
Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage>, ChatOptions? = null, CancellationToken = default)— ส่งประวัติข้อความเข้า รับคำตอบหนึ่งก้อนกลับIAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(...)— version stream ทีละชิ้น (เก็บไว้ก่อน บทนี้ใช้ตัวบน)
package หลักคือ Microsoft.Extensions.AI version 10.8.0 (GA, เผยแพร่ 2026-07-14) — เป็นสาย GA แล้ว ไม่ใช่สาย 9.x-preview เก่า อย่าอ้าง version preview เพราะชื่อ type บางตัวขยับ
message เปลี่ยนหน้าตา แต่เป็นแนวคิดเดิม
หัวข้อที่มีชื่อว่า “message เปลี่ยนหน้าตา แต่เป็นแนวคิดเดิม”ในบท 2–4 MessageMessageหน่วยหนึ่งเทิร์นของบทสนทนา ประกอบด้วย role (`user` หรือ `assistant`) กับ content — Messages API รับ `messages[]` ที่เรียงสลับกันไป และตัว API เป็นแบบ stateless คือต้องส่งประวัติทั้งชุดกลับไปใหม่ทุกครั้ง หมายเหตุ: ใน API ดิบไม่มี role ชื่อ `system` (มันเป็น field ระดับบนสุดแยกต่างหาก) และไม่มี role `tool` — ผลของ tool ถูกส่งกลับในข้อความ role `user`Architecture คือ object ดิบใน JSON ที่มี role เป็น string ("user" / "assistant") ฝั่ง MEAI ห่อสิ่งเดียวกันนี้ด้วย type ของ .NET: 1 message คือ new ChatMessage(ChatRole.User, "...") โดย ChatRole เป็น enum ที่มีสมาชิก System, User, Assistant, Tool แนวคิดไม่ต่างจากเดิมเลย — แค่ได้ type safety กับ IntelliSense มาแทน string ดิบ
ในโลก C# มีหน้าตา message อยู่ สองสำนวนที่ต่างกัน และห้ามจับมาปนกัน:
- official Anthropic SDK (สำนวน native):
new MessageCreateParams { System = "...", Messages = [ new() { Role = Role.User, Content = "..." } ] }— persona อยู่ใน fieldSystemแยกต่างหาก ใช้Role.User - MEAI (สำนวน
IChatClient):new ChatMessage(ChatRole.User, "...")รวมอยู่ใน listChatMessageเดียว และ persona กลายเป็น message ที่มีChatRole.Systemนำหน้า list ใช้ChatOptionsคุม parameter
ทั้งคู่ “ถูก” แต่คนละ API — code ชิ้นเดียวเลือกได้แค่สำนวนเดียว บทนี้ทั้งบทอยู่ในสำนวน MEAI ส่วนสำนวน native ของ SDK คือที่คุณเห็นในเอกสาร prompting ของบท 3
ความจริงเรื่อง connector ของ Claude: ยังเป็น beta
หัวข้อที่มีชื่อว่า “ความจริงเรื่อง connector ของ Claude: ยังเป็น beta”ก่อนต่อ Claude เข้ากับ MEAI ต้องพูดกันตรงๆ ข้อหนึ่ง เพราะมันต่างจากที่คุณอาจคาดจากฝั่ง OpenAI:
Microsoft ปล่อย connector แบบ first-party ให้เฉพาะ OpenAI / Azure OpenAI เท่านั้น — ไม่มี package Microsoft.Extensions.AI.Anthropic ที่เป็นทางการ ทางที่แนะนำคือใช้ package Anthropic ตัวทางการ (v10+ ซึ่งติดป้าย beta) แล้วเรียก AsIChatClient(...) เพื่อสวมมันเข้าหน้า IChatClient
พูดให้ชัด: มันคือ พื้นที่ beta — pin version ไว้ให้แน่นและลงวันที่กำกับ เพราะ minor/patch อาจมี breaking change ได้ ต่างจาก connector OpenAI ที่นิ่งกว่า และ อย่า ลาก Claude ผ่าน connector ของ OpenAI (เคยมีปัญหาเรื่อยมา ดู dotnet/extensions issue #7058) — ต่อผ่าน package Anthropic ของมันเองเท่านั้น
(ทางเลือกฝั่ง community ก็มี เช่น Anthropic.SDK ของ tghamm แต่คอร์สนี้ยึด package Anthropic ตัวทางการเป็นหลัก)
การเรียกเดิม เขียนใหม่ด้วย IChatClient
หัวข้อที่มีชื่อว่า “การเรียกเดิม เขียนใหม่ด้วย IChatClient”wiring ขั้นต่ำ: สร้าง client จาก package Anthropic แล้วสวมเข้าหน้า IChatClient
using Anthropic;using Microsoft.Extensions.AI;
AnthropicClient client = new(); // อ่าน ANTHROPIC_API_KEY จาก env เองIChatClient chatClient = client .AsIChatClient("claude-opus-4-8") // model id: ตัวอย่าง ณ 2026-07 — ไม่ต่อ date suffix .AsBuilder() .UseFunctionInvocation() // เปิด middleware ที่วน loop ของ tool ให้เอง .Build();สังเกตว่า header สามตัวจากบท 2 (x-api-key, anthropic-version: 2023-06-01, content-type) หายไปจากสายตา — ไม่ใช่หายไปจริง แต่ AnthropicClient ใส่ให้เองใต้ฝากระโปรง คุณจึงไม่ต้องจำ และไม่มีทางลืม
function invocation: middleware วน loop ของ tool ให้เอง
หัวข้อที่มีชื่อว่า “function invocation: middleware วน loop ของ tool ให้เอง”นี่คือจุดที่ MEAI จ่ายค่าตัวคืน ในบท 4 คุณเขียน branch if stop_reason == "tool_use" เอง รัน tool เอง ต่อ tool_result เอง แล้ววนเอง — MEAI ยุบทั้งหมดนั้นเหลือ await เดียว
กลไกมีสามชิ้นที่ต้องรู้จัก:
AIFunctionFactory.Create(delegate, name, description)— ห่อ method C# ธรรมดาให้กลายเป็นAIFunction(เทียบบท 4 คือตัวสร้าง element ใน arraytoolsให้ พร้อมสังเคราะห์ schema จาก signature ของ delegate)ChatOptions.Tools— ที่วาง listAIFunctionเหล่านั้น.UseFunctionInvocation()— เปิด middleware ชื่อFunctionInvokingChatClientที่คอย: เห็น model ขอเรียก function → รัน delegate นั้นให้ → ยัดผลกลับเป็น message → เรียก model ต่อ — วนจน model ได้คำตอบสุดท้าย ทั้งหมดในการawaitครั้งเดียวของคุณ
var chatOptions = new ChatOptions{ Tools = [ AIFunctionFactory.Create( (string orderId) => /* ไปดึงออเดอร์จริง */ "{\"status\":\"OutForDelivery\"}", "getOrder", "Retrieve a food-delivery order by its ID.") ] // NOTE: อย่าตั้ง chatOptions.Temperature กับ model Claude รุ่นล่าสุด — ได้ HTTP 400};
List<ChatMessage> history =[ new(ChatRole.System, "You are a customer-support agent for a food-delivery service.")];history.Add(new(ChatRole.User, "What's the status of order ORD-1042?"));
ChatResponse response = await chatClient.GetResponseAsync(history, chatOptions);Console.WriteLine(response.Text); // อ่านคำตอบตรง ๆ — MEAI รวม content[] ให้แล้ว
history.AddMessages(response); // เก็บ turn ทั้งหมด (รวมรอบ tool) กลับเข้าประวัติรอบ tool_use ↔ tool_result ทั้งชุดจากบท 4 เกิดขึ้น ข้างใน GetResponseAsync บรรทัดเดียวนี้ ถ้า model ตัดสินใจเรียก getOrder middleware รัน delegate ให้แล้ววนต่อโดยที่คุณไม่ต้องเขียน branch เอง เมื่อจบ response.Text คือคำตอบสุดท้าย — ไม่มี content[] ให้ไล่ index อีก
ทุกอย่างที่ middleware ทำมี counterpart ตรงๆ ใน API ดิบ: AIFunction = element ใน array tools, การที่ model ขอเรียก function = block tool_use, message ผลลัพธ์ที่ middleware ยัดกลับ = block tool_result MEAI ไม่ได้เปลี่ยน protocol — มันแค่เขียน loop ที่คุณต่อมือในบท 4 ให้แทน นี่คือ loopAgent Loopวงจร observe → decide → act → repeat ที่ทำซ้ำจนถึงเงื่อนไขหยุด — model สังเกตผลจากสภาพแวดล้อม (ผลของ tool) แล้วตัดสินใจก้าวถัดไป วนไปจนกว่าจะได้คำตอบสุดท้าย (ไม่มี `tool_use` แล้ว), ชนเพดานจำนวนรอบ, ต้องขออนุมัติจากคน หรือเกินงบ token/ต้นทุน — ต้องรักษาให้มีเงื่อนไขหยุดเสมอเพื่อคุมไม่ให้วนไม่จบ ใน Microsoft.Extensions.AI ตัว `.UseFunctionInvocation()` ทำวงนี้ให้อัตโนมัติArchitecture ตัวเดียวกับที่บท 6 จะกางออกให้เห็นเต็มๆ
flowchart TB
subgraph RAW["version ดิบ — ต่อท่อเอง (บท 4)"]
R1["ส่ง messages บวก tools"] --> R2{"อ่าน stop_reason"}
R2 -->|tool_use| R3["แตก branch เอง<br/>รัน getOrder ใน C#"]
R3 --> R4["ต่อ tool_result<br/>ส่งทั้ง list ใหม่ทั้งก้อน"]
R4 --> R2
R2 -->|end_turn| R5["ไล่ content[] อ่านคำตอบ"]
end
subgraph MEAI["IChatClient บวก function-invocation middleware"]
M1["GetResponseAsync ครั้งเดียว"] --> MW["middleware วนเรียก tool ให้เอง"]
MW --> M2["response.Text"]
end
classDef mid fill:#ea580c,stroke:#7c2d12,color:#f8fafc;
class MW mid;
คำบรรยายภาพ: บน — version ดิบจากบท 4 คุณต่อ loop ด้วยมือ: อ่าน stop_reason เอง แตก branch เอง รัน getOrder เอง ต่อ tool_result แล้วส่งทั้ง list กลับเข้าไปใหม่ วนจน end_turn แล้วไล่ content[] อ่านคำตอบ ล่าง — ฝั่ง IChatClient การ await GetResponseAsync ครั้งเดียวพอ จุดสีส้มคือ function-invocation middleware ที่วนเรียก tool ให้แทนคุณทั้งวง แล้วคืน response.Text เป็นคำตอบสุดท้าย protocol เบื้องหลังเหมือนกันเป๊ะ ต่างแค่ใครเป็นคนเขียน loop
ต่อเข้ากับ DI
หัวข้อที่มีชื่อว่า “ต่อเข้ากับ DI”เพราะ IChatClient เป็น interface ตามสำนวน .NET มันจึงเข้า container ของ dependency injection ได้ตรงๆ ลำดับที่ chain มีความหมาย — middleware ประกอบกันจากนอกเข้าใน:
builder.Services.AddChatClient(innerClient) .UseFunctionInvocation() .UseDistributedCache() .UseOpenTelemetry();
// ที่อื่นใน app: ขอ IChatClient จาก DI ได้เลย// var chat = host.Services.GetRequiredService<IChatClient>();พอ inject IChatClient เข้า service ของ domain Order ได้ ก็หมายความว่า test สลับตัว fake เข้าแทน Claude ได้ทันที และย้าย cross-cutting concern อย่าง caching กับ telemetry ไปเป็น middleware แทนที่จะปนใน logic ธุรกิจ — นี่คือเหตุผลจริงที่ยอมแลก abstraction เพิ่มมาหนึ่งชั้น
type ที่ควรจำหน้าตาไว้: ChatRole.{System, User, Assistant, Tool}, ChatResponse.{Text, Messages, ConversationId}, และ ChatOptions.{Tools, ModelId, ConversationId, Temperature, AdditionalProperties} — ข้อควรระวังเดิมย้ำอีกครั้ง: ChatOptions มี field Temperature ให้ตั้งก็จริง แต่ อย่าตั้งกับ model Claude รุ่นล่าสุด เพราะได้ HTTP 400 เหมือนตอนส่งใน JSON ดิบ กำกับพฤติกรรมผ่าน prompt แทน
สรุปบทนี้
หัวข้อที่มีชื่อว่า “สรุปบทนี้”การเรียกไม่ได้เปลี่ยนเลย — ยังเป็น Messages API ตัวเดิม header เดิม protocol tool เดิม — เปลี่ยนแค่ ผิวสัมผัส ที่คุณเขียน: ChatMessage/ChatRole แทน JSON ดิบ, GetResponseAsync แทน HttpClient ที่ต่อมือ, และ .UseFunctionInvocation() ที่เขียน loop ของ tool ให้แทนคุณ สิ่งที่ยังต้องระวังเองมีสองข้อ: connector ของ Claude ยังเป็น beta (pin version) และห้ามตั้ง Temperature บทหน้าเราจะกาง loop ที่ middleware ซ่อนไว้ออกมาดูเต็มๆ — ทั้งเงื่อนไขหยุด, max-iterations guard และจุดที่ต้องหยุดรอมนุษย์อนุมัติก่อนลงมือ
บทนี้สังเคราะห์จากห้าแหล่งหลัก อ่านต่อได้ที่ต้นทางโดยตรง:
- Microsoft Learn, “Use the IChatClient interface” —
IChatClient,GetResponseAsync/GetStreamingResponseAsync,ChatMessage/ChatRole/ChatOptionsและการต่อ DI (ms.date 2026-03-13) - Microsoft Learn, “Quickstart: local function calling” —
AIFunctionFactory.Create,ChatOptions.Toolsและ.UseFunctionInvocation()ที่วนเรียก tool ให้เอง (ms.date 2026-03-04) - NuGet, “Microsoft.Extensions.AI 10.8.0” — version GA เผยแพร่ 2026-07-14 (สาย 10.x ไม่ใช่ 9.x-preview)
- Anthropic, “C# SDK” — package
Anthropicตัวทางการ (v10+, beta) และAsIChatClient(...)เข้าถึง 2026-07-19 - dotnet/extensions issue #7058 — ข้อควรระวังเรื่องต่อ Anthropic ผ่าน
IChatClient(เหตุผลที่ไม่ลาก Claude ผ่าน connector OpenAI)
เช็กความเข้าใจ — บทที่ 5
ข้อ 1 / 3เทียบกับการต่อ tool use ด้วยมือในบท 4 อะไรคือสิ่งที่ .UseFunctionInvocation() ทำให้?