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

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 ที่​คุณ​รู้จัก​อยู่​แล้ว

📦 code ตัวอย่าง

บท​นี้​ต่อยอด agent support ลูกค้า​บน domain Order จาก repo kaen-food-ordering (กำลัง​จัด​ทำ) — tool แรก​ที่​เรา​จะ​ให้ model เรียก​คือ getOrder(orderId) ซึ่ง​ไป​ดึงสถานะ​ออเดอร์​จริง JSON ที่​เห็น​ใน​บท​นี้​คือ​รูปแบบ​ที่​ถูกต้อง​ตาม​เอกสาร Anthropic ล่าสุด ก๊อปไปใช้ได้ตรงๆ (ระวัง​กติกา header/version เดิม​จาก​บท​ที่ 2 ยัง​ใช้​ทุก​ข้อ)

tool use ทั้งหมด​คือ​รอบ request/response ที่​มี​ห้า​ก้าว​ชัดเจน จำ​โครง​นี้​ไว้​ก่อน แล้ว​ค่อย​ลง​รายละเอียด​ที​ละ​ก้าว:

  1. คุณ​ส่ง 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 หน้าตา​ไหน
  2. model ขอ​เรียก tool — มัน​ตอบ​กลับ​ด้วย content block ชนิด tool_use (มี id, name, input) และ stop_reason กลาย​เป็น "tool_use"
  3. คุณ​รัน code เอง — เห็น stop_reason เป็น tool_use ก็​แตก​กิ่ง​ไป​เรียก getOrder("ORD-1042") ใน C# จริง
  4. คุณ​ส่ง​ผลกลับ — เป็น 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 ใหม่
  5. 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_usetool_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 หนึ่ง​ครั้ง​ที่​ส่ง​ประวัติ​ทั้งหมด​ไป​ใหม่

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 หาย​ไป)

พอ 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 หยุด​รอ มัน​จะ​ไม่​ทำ​อะไร​ต่อ​จนกว่า​คุณ​จะ​ป้อน​ผลกลับ​ไป

ก้าว​นี้​เป็น code C# ล้วน ไม่มี​อะไร​เกี่ยว​กับ AI — คุณ​อ่าน input.orderId แล้วไป​ดึง​ออเดอร์​จริง​จาก domain Order ที่​คุณ​คุ้น​มือ:

// เห็น stop_reason == "tool_use" แล้ว วน content หา block tool_use
foreach (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 ใน​ก้าว​ถัด​ไป​ต้อง​อ้าง​ค่า​นี้​เป๊ะ

นี่​คือ​ก้าว​ที่​คน​ใหม่​พลาด​บ่อย​ที่สุด เพราะ​มี​กติกา​การ​จัด​เรียง​ที่​ผิด​แล้ว 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 ของ block tool_use เป๊ะ — นี่​คือ​เส้น​ที่​ผูก​ผลกลับ​เข้า​กับ​คำขอ ถ้า​ไม่​ตรง model ไม่รู้​ว่า​ผล​นี้​ตอบ tool ไหน
  • block tool_result ต้อง​มา​ก่อน (first) ใน content array ของ​ข้อความ user นั้น
  • ต้องก๊อป​ข้อความ assistant เดิม​กลับ​มา​ทั้งดุ้น ก่อน​ต่อ​ด้วย user ที่​มี tool_result — เพราะ API ไม่มี​สถานะ ถ้า​ไม่​ส่ง tool_use เดิม​กลับ​ไป​ด้วย มัน​จะ​หา​คู่​ของ tool_result ไม่​เจอ
  • ส่ง tools array ชุด​เดิม​มา​ใหม่​ทุก​ครั้ง — 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
}

พอได้ 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 มันตอบกลับมาอย่างไร?