Tool use — ให้ model เรียก code C# ของคุณ
จนถึงบทที่แล้ว model ตอบได้แค่จาก “ความจำ” ของมัน — ถ้าลูกค้าถามว่า “ออเดอร์ ORD-1042 ไปถึงไหนแล้ว” มันก็ได้แต่เดา เพราะสถานะจริงของออเดอร์อยู่ในฐานข้อมูลของคุณ ไม่ใช่ในน้ำหนักของ model บทนี้เติมสิ่งที่ขาด: Tool UseTool Useความสามารถที่ให้ model เรียก code ของเราได้ — เราส่งรายการ tool ไปพร้อมคำถาม model ตอบกลับด้วย block `tool_use` (พร้อม `stop_reason: tool_use`) เรารัน function จริงใน C# แล้วส่งผลกลับเป็น Tool Result model จึงทำงานต่อจนได้คำตอบ — ทั้งหมดวิ่งอยู่บนโครงสร้าง message ธรรมดา ไม่มี role พิเศษ นี่คือสิ่งที่ยกระดับการเรียก model ธรรมดาให้กลายเป็น AgentArchitecture — กลไกที่ให้ model ขอ ให้คุณรัน code C# แล้วเอาผลกลับไปใช้ต่อ นี่คือครึ่งแรกของสองอย่างที่ยกการเรียกครั้งเดียวขึ้นเป็น agent (อีกครึ่งคือ loop ในบทที่ 6)
เราจะทำมันด้วยมือบน Messages API ดิบเหมือนบทที่ 2 อีกครั้ง — ยังไม่มี SDK ช่วย — เพราะจุดประสงค์ของบทนี้คือให้คุณเห็น ทุก byte ของรอบเดินทาง model ไม่ได้รัน code ของคุณเอง และไม่มี “tool runtime” ลับๆ อยู่ฝั่ง Anthropic ทั้งหมดคือการส่ง JSON ไปกลับบนโครงสร้าง user/assistant ที่คุณรู้จักอยู่แล้ว
บทนี้ต่อยอด agent support ลูกค้าบน domain Order จาก repo kaen-food-ordering (กำลังจัดทำ) — tool แรกที่เราจะให้ model เรียกคือ getOrder(orderId) ซึ่งไปดึงสถานะออเดอร์จริง JSON ที่เห็นในบทนี้คือรูปแบบที่ถูกต้องตามเอกสาร Anthropic ล่าสุด ก๊อปไปใช้ได้ตรงๆ (ระวังกติกา header/version เดิมจากบทที่ 2 ยังใช้ทุกข้อ)
รอบเดินทางมีห้าก้าว
หัวข้อที่มีชื่อว่า “รอบเดินทางมีห้าก้าว”tool use ทั้งหมดคือรอบ request/response ที่มีห้าก้าวชัดเจน จำโครงนี้ไว้ก่อน แล้วค่อยลงรายละเอียดทีละก้าว:
- คุณส่ง
tools— รายการเครื่องมือที่ model เรียกได้ แต่ละตัวคือ Tool SchemaTool Schemaนิยามของ tool หนึ่งตัวที่ส่งให้ model ประกอบด้วย `name`, `description` และ `input_schema` (เป็น JSON Schema บอกรูป parameter) — `name` ต้องเข้าแพทเทิร์น `^[a-zA-Z0-9_-]{1,64}$` และ `description` ที่ดีคือ prompt ชั้นหนึ่งเลย ควรใส่ใจเท่ากับ prompt หลัก เพราะเป็นสิ่งที่ model ใช้ตัดสินว่าจะเรียก tool ตัวไหนและใส่ค่าอะไรArchitecture ที่บอกว่าเครื่องมือชื่ออะไร ทำอะไร และรับ input หน้าตาไหน - model ขอเรียก tool — มันตอบกลับด้วย content block ชนิด
tool_use(มีid,name,input) และstop_reasonกลายเป็น"tool_use" - คุณรัน code เอง — เห็น
stop_reasonเป็นtool_useก็แตกกิ่งไปเรียกgetOrder("ORD-1042")ใน C# จริง - คุณส่งผลกลับ — เป็น 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 block ที่อ้าง
idของtool_useก้อนเดิม ห่อในข้อความuserใหม่ - model ทำงานต่อ — เอาผลนั้นไปเรียบเรียงเป็นคำตอบสุดท้ายให้ลูกค้า
จุดที่คนใหม่มักเข้าใจผิด: ไม่มี tool role ใน API ดิบ ผลของ tool เดินทางกลับในข้อความ role: "user" เสมอ และ API ไม่มีสถานะ (stateless) — ทุกครั้งที่คุณเรียก คุณต้องส่งประวัติข้อความ ทั้งหมด บวก tools array ชุดเดิมไปด้วยใหม่ ไม่มีการจำฝั่ง server
sequenceDiagram participant C as code C# ของคุณ participant M as model C->>M: 1. ส่ง tools บวกคำถามลูกค้า M->>C: 2. tool_use: getOrder(ORD-1042)<br/>stop_reason = tool_use Note over C: 3. รัน getOrder ใน C# จริง C->>M: 4. tool_result อ้าง id เดิม<br/>บวกประวัติทั้งหมดใหม่ M->>C: 5. คำตอบสุดท้าย<br/>stop_reason = end_turn
คำบรรยายภาพ: วงจร tool_use ↔ tool_result หนึ่งรอบของ getOrder model ไม่ได้รัน code เอง มันแค่ ขอ ผ่าน block tool_use (ก้าว 2) แล้วหยุดรอด้วย stop_reason = tool_use code C# ของคุณเป็นคนรันจริง (ก้าว 3) แล้วป้อนผลกลับเป็น tool_result ที่อ้าง id เดิม (ก้าว 4) model จึงเรียบเรียงคำตอบสุดท้ายและจบด้วย end_turn (ก้าว 5) ทุกลูกศรคือ HTTP round-trip หนึ่งครั้งที่ส่งประวัติทั้งหมดไปใหม่
ก้าวที่ 1 — นิยาม tool ด้วย schema
หัวข้อที่มีชื่อว่า “ก้าวที่ 1 — นิยาม tool ด้วย schema”tool หนึ่งตัวคือ object 3 field: name, description, และ input_schema โดย input_schema เขียนเป็น JSON Schema ธรรมดา (โครงเดียวกับที่คุณคง validate request body ในเว็บ API มาแล้ว) — มันบอก model ว่า input มี property อะไร ชนิดไหน อันไหน required
{ "model": "claude-opus-4-8", "max_tokens": 1024, "tools": [{ "name": "getOrder", "description": "Retrieve a food-delivery order by its ID. Returns status, line items, and total. Use this when the customer asks about a specific existing order and gives an order ID.", "input_schema": { "type": "object", "properties": { "orderId": { "type": "string", "description": "The order identifier, e.g. ORD-1042" } }, "required": ["orderId"] } }], "messages": [ { "role": "user", "content": "What's the status of order ORD-1042?" } ]}กติกาสำคัญของ schema ที่ต้องรักษา ไม่งั้นเจอ HTTP 400 หรือ model เรียกพลาด:
nameต้องแมตช์^[a-zA-Z0-9_-]{1,64}$— ตัวอักษร ตัวเลข ขีดล่าง ขีดกลาง เท่านั้นdescriptionคือส่วนที่ สำคัญที่สุด ของ tool — มันคือ prompt ที่บอก model ว่าเมื่อไรควรเรียกและ input หมายถึงอะไร เขียนให้ละเอียดเหมือนอธิบายให้เพื่อนร่วมงานใหม่ฟัง (แนวคิดเดียวกับบทที่ 3)descriptionที่คลุมเครือคือสาเหตุอันดับหนึ่งที่ model เรียก tool ผิดจังหวะ- เมื่อมี
toolsอยู่ในคำขอ ค่าเริ่มต้นของtool_choiceคือauto— model ตัดสินใจเองว่าจะเรียกหรือไม่เรียกก็ได้ - ใส่
strict: trueได้ถ้าต้องการให้ Anthropic การันตีว่าinputตรงกับ schema เป๊ะ (ปิดช่องที่ required param หายไป)
ก้าวที่ 2 — model ตอบกลับด้วย block tool_use
หัวข้อที่มีชื่อว่า “ก้าวที่ 2 — model ตอบกลับด้วย block tool_use”พอ model ตัดสินใจว่าต้องใช้ getOrder มันจะไม่ตอบเป็นข้อความล้วน แต่ตอบด้วย content array ที่มี block ชนิด tool_use และตั้ง stop_reason เป็น "tool_use" — สัญญาณที่ code คุณใช้แตกกิ่ง:
{ "id": "msg_01Aq9w938a90dw8q", "model": "claude-opus-4-8", "role": "assistant", "stop_reason": "tool_use", "content": [ { "type": "text", "text": "Let me look up that order for you." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "getOrder", "input": { "orderId": "ORD-1042" } } ]}สังเกตสามอย่าง อย่างแรก content มีได้หลาย block — model มักพูดนำ1 block text ก่อน แล้วตามด้วย block tool_use (จำหลักจากบทที่ 2 ไว้ว่าไม่มี .text ระดับบนสุด คุณต้องวน content แล้วเช็ค type เสมอ) อย่างที่2 block tool_use มี id เฉพาะตัว (toolu_...) — จด id นี้ไว้ เดี๋ยวก้าวที่ 4 ต้องอ้างกลับ อย่างที่สาม input มาเป็น JSON ที่ parse แล้ว ไม่ใช่ string — อ่านมันเป็น object อย่าเอาไป string-match เอง
stop_reason: "tool_use" แปลตรงตัวว่า “Claude กำลังเรียก tool” — model หยุดรอ มันจะไม่ทำอะไรต่อจนกว่าคุณจะป้อนผลกลับไป
ก้าวที่ 3 — รัน getOrder ใน C#
หัวข้อที่มีชื่อว่า “ก้าวที่ 3 — รัน getOrder ใน C#”ก้าวนี้เป็น code C# ล้วน ไม่มีอะไรเกี่ยวกับ AI — คุณอ่าน input.orderId แล้วไปดึงออเดอร์จริงจาก domain Order ที่คุณคุ้นมือ:
// เห็น stop_reason == "tool_use" แล้ว วน content หา block tool_useforeach (var block in root.GetProperty("content").EnumerateArray()){ if (block.GetProperty("type").GetString() != "tool_use") continue;
string toolUseId = block.GetProperty("id").GetString()!; string name = block.GetProperty("name").GetString()!; JsonElement input = block.GetProperty("input"); // JSON ที่ parse แล้ว — ไม่ใช่ string
if (name == "getOrder") { string orderId = input.GetProperty("orderId").GetString()!; Order order = await _orders.GetAsync(orderId); // code C# จริงของคุณ // ก้าวที่ 4 ใช้ toolUseId บวกผลนี้สร้าง tool_result }}การแตกกิ่งด้วย name ทำให้คุณรองรับหลาย tool ได้ใน loop เดียว (บทที่ 8 จะมีสามตัว) จุดที่ต้องรักษาคือ จำ toolUseId ของ block นี้ไว้ เพราะ tool_result ในก้าวถัดไปต้องอ้างค่านี้เป๊ะ
ก้าวที่ 4 — ส่ง tool_result กลับ
หัวข้อที่มีชื่อว่า “ก้าวที่ 4 — ส่ง tool_result กลับ”นี่คือก้าวที่คนใหม่พลาดบ่อยที่สุด เพราะมีกติกาการจัดเรียงที่ผิดแล้ว 400 ทันที คุณต้องส่ง request ใหม่ทั้งก้อน ที่ประกอบด้วย: ประวัติเดิม + ข้อความ assistant ที่ model เพิ่งตอบ (ก๊อป content มา ทั้งดุ้น) + ข้อความ user ใหม่ที่ขึ้นต้นด้วย block tool_result
{ "model": "claude-opus-4-8", "max_tokens": 1024, "tools": [ /* tools array ชุดเดิม — ต้องส่งมาใหม่ทุกครั้ง */ ], "messages": [ { "role": "user", "content": "What's the status of order ORD-1042?" }, { "role": "assistant", "content": [ { "type": "text", "text": "Let me look up that order for you." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "getOrder", "input": { "orderId": "ORD-1042" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "{\"orderId\":\"ORD-1042\",\"status\":\"OutForDelivery\",\"total\":18.50,\"items\":[\"Pad Thai\",\"Thai Iced Tea\"]}" } ] } ]}กติกาที่ 400 ถ้าทำผิด:
tool_use_idต้องเท่ากับidของ blocktool_useเป๊ะ — นี่คือเส้นที่ผูกผลกลับเข้ากับคำขอ ถ้าไม่ตรง model ไม่รู้ว่าผลนี้ตอบ tool ไหน- block
tool_resultต้องมาก่อน (first) ในcontentarray ของข้อความuserนั้น - ต้องก๊อปข้อความ
assistantเดิมกลับมาทั้งดุ้น ก่อนต่อด้วยuserที่มีtool_result— เพราะ API ไม่มีสถานะ ถ้าไม่ส่งtool_useเดิมกลับไปด้วย มันจะหาคู่ของtool_resultไม่เจอ - ส่ง
toolsarray ชุดเดิมมาใหม่ทุกครั้ง — stateless แปลว่าไม่มีอะไรถูกจำไว้ระหว่างคำขอ
tool_result.content เป็นได้ทั้ง string ธรรมดา (อย่างในตัวอย่าง) หรือ list ของ block และจะเว้นก็ได้ ในทางปฏิบัติ การส่ง JSON string ของผลลัพธ์กลับไปตรงๆ ก็เพียงพอสำหรับ getOrder
ถ้า tool ของคุณ ล้มเหลว — เช่นหาออเดอร์ไม่เจอ — อย่า throw ให้ loop พัง แต่รายงานกลับด้วย is_error: true เพื่อให้ model กู้สถานการณ์เองได้ (เช่น ขอ id ใหม่จากลูกค้า):
{ "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: no order found with ID ORD-1042.", "is_error": true}ก้าวที่ 5 — model ทำงานต่อ
หัวข้อที่มีชื่อว่า “ก้าวที่ 5 — model ทำงานต่อ”พอได้ tool_result model ก็มี “ความจริงจากสภาพแวดล้อม” ที่มันขาดไปแล้ว มันจะตอบรอบใหม่ คราวนี้เป็นข้อความ text ล้วนที่เรียบเรียงสถานะออเดอร์ให้ลูกค้าอ่าน และ stop_reason กลับมาเป็น end_turn — สัญญาณว่าจบตาแล้ว ไม่มี tool ให้เรียกต่อ
สังเกตว่ารอบเดียวของ getOrder กิน2 HTTP round-trip: หนึ่งครั้งให้ model ขอเรียก tool อีกครั้งให้มันเรียบเรียงคำตอบหลังได้ผล ถ้าลูกค้าถามอะไรที่ต้องเรียกหลาย tool ต่อเนื่อง (เช็คสถานะ แล้วคืนเงิน) round-trip ก็ยิ่งเพิ่ม — และนั่นคือคือสิ่งที่ loop ในบทที่ 6 จะมาห่อให้เป็นวงอัตโนมัติ บทนี้คุณเพิ่งทำ หนึ่งรอบด้วยมือ เพื่อเห็นเครื่องจักรข้างในก่อน ในบทที่ 5 เราจะเห็นว่า Microsoft.Extensions.AI ยุบห้าก้าวนี้ให้เหลือการเรียกเดียวได้อย่างไร — แต่ตอนนี้คุณรู้แล้วว่าใต้ abstraction นั้นเกิดอะไรขึ้นจริง
บทนี้อิงรูปแบบ tool use จากเอกสาร Anthropic โดยตรงสามหน้า (ปรับตัวอย่างจาก get_weather ในเอกสารมาเป็น getOrder):
- Anthropic, “Tool use — overview” — ภาพรวมรอบเดินทาง และรูปแบบ response ที่มี block
tool_useเข้าถึง 2026-07-19 - Anthropic, “Tool use — define tools” — โครง
{ name, description, input_schema }, กติกาname(^[a-zA-Z0-9_-]{1,64}$),tool_choiceค่าเริ่มต้นautoและstrictเข้าถึง 2026-07-19 - Anthropic, “Tool use — handle tool calls” — กติกาส่ง
tool_resultกลับ:tool_use_idต้องตรง, block ต้องมาก่อน, resend ประวัติทั้งหมดบวกtools, และis_errorเข้าถึง 2026-07-19
เช็กความเข้าใจ — บทที่ 4
ข้อ 1 / 3เมื่อ model ตัดสินใจว่าต้องใช้ getOrder มันตอบกลับมาอย่างไร?