ให้ LLM เรียกใช้ "เครื่องมือ" เพื่อทำงานจริงในโลกได้
จนถึงตอนนี้ LLM ที่เราคุยด้วยทำได้แค่ "พูด" — มันสร้างข้อความออกมาเท่านั้น มันเรียก database ไม่ได้ ยิง API ไม่ได้ ดูเวลาปัจจุบันก็ไม่ได้ (รู้แค่ข้อมูลตอนที่ถูกเทรน) บทนี้จะปลดล็อกสิ่งที่เปลี่ยนเกม: การให้ LLM ขอเรียกเครื่องมือ (tool) ที่เราเขียนไว้ แล้วเราเอาผลจริงป้อนกลับให้มัน
ปัญหา: LLM เก่งภาษา แต่แตะโลกจริงไม่ได้
ลองนึกถึง endpoint ที่ผู้ใช้ถามว่า "สต็อกสินค้า SKU-1024 เหลือเท่าไร" LLM ตอบเองไม่ได้ เพราะคำตอบอยู่ใน Postgres ของคุณ ไม่ได้อยู่ในหัวมัน ถ้าให้เดา มันจะ hallucinate (มั่วตัวเลขหน้าตาเฉย) ทันที
ทางออกคือ Tool Use (การใช้เครื่องมือ) หรืออีกชื่อคือ Function Calling (การเรียกฟังก์ชัน) — เราบอกโมเดลว่า "นายมีเครื่องมือพวกนี้นะ" แล้วให้มันเป็นคน ตัดสินใจ ว่าเมื่อไหร่ควรเรียกอันไหน ส่วนการรันจริง เป็นหน้าที่ของโค้ดเรา
จุดสำคัญที่คนพลาดบ่อย: LLM ไม่ได้รันเครื่องมือเอง มันแค่ "ขอ" ให้เรารันให้ (ส่ง JSON บอกว่าอยากเรียก tool ไหน ด้วย argument อะไร) แล้วเราค่อยรันและส่งผลกลับ
วงจร Tool Use — 4 จังหวะที่ต้องจำ
หัวใจของทั้งบทคือลูปนี้ อ่านตามภาพ แล้วจำให้ขึ้นใจ เพราะทุกอย่างต่อจากนี้คือรายละเอียดของ 4 จังหวะนี้
| จังหวะ | ใครทำ | เกิดอะไรขึ้น |
|---|---|---|
| 1. ขอเรียก tool | Model | ตอบกลับมาเป็น "คำขอเรียก tool" (tool_use) พร้อมชื่อ tool + argument เป็น JSON |
| 2. รัน tool | โค้ดเรา | validate argument แล้วรันฟังก์ชันจริง เช่น query Postgres, ยิง REST API |
| 3. ส่งผลกลับ | โค้ดเรา | แนบผลลัพธ์ (tool_result) ต่อท้ายบทสนทนา แล้วเรียกโมเดลอีกครั้ง |
| 4. ตอบต่อ | Model | อ่านผลลัพธ์ แล้วเรียบเรียงเป็นภาษาคนให้ผู้ใช้ (หรือขอเรียก tool ตัวต่อไป) |
ออกแบบ Tool Schema — description คือหัวใจ
ก่อนโมเดลจะเรียก tool ได้ เราต้อง อธิบาย tool ให้มันฟัง ผ่าน schema 3 ส่วน:
- name — ชื่อ tool สั้น ชัด เป็น snake_case เช่น
search_products - description (คำอธิบาย) — สำคัญที่สุด โมเดลใช้ตรงนี้ตัดสินใจว่าจะเรียกเมื่อไหร่ เขียนให้ละเอียดเหมือนเขียน docstring ให้ junior dev อ่าน
- input schema — โครงสร้าง argument เขียนเป็น JSON Schema ระบุ type, ฟิลด์ไหน required, คำอธิบายแต่ละฟิลด์
ตัวอย่าง schema ของ tool ค้นหาสินค้า (รูปแบบตามที่ Claude API ใช้):
const searchProductsTool = {
name: "search_products",
description:
"ค้นหาสินค้าในแคตตาล็อกด้วยคีย์เวิร์ด ใช้เมื่อผู้ใช้ถามหา" +
"สินค้า ราคา หรือสต็อก. คืนรายการสินค้าที่ตรงพร้อมราคาและจำนวนคงเหลือ.",
input_schema: {
type: "object",
properties: {
keyword: {
type: "string",
description: "คำค้น เช่นชื่อสินค้าหรือหมวดหมู่"
},
max_results: {
type: "integer",
description: "จำนวนผลลัพธ์สูงสุด (ค่าเริ่มต้น 5)",
minimum: 1,
maximum: 20
}
},
required: ["keyword"]
}
};
เขียน description ให้ดีเหมือนลงทุน: ยิ่งอธิบายชัดว่า tool ทำอะไร ใช้เมื่อไหร่
แต่ละ argument หมายถึงอะไร โมเดลยิ่งเรียกถูกจังหวะและส่ง argument ถูกต้อง
description ห่วย = โมเดลเรียกมั่ว
โค้ดเราคือคนรัน tool จริง
เมื่อโมเดลส่ง tool_use กลับมา (บอกว่าอยากเรียก search_products
ด้วย keyword: "รองเท้าวิ่ง") หน้าที่เราคือ: จับคู่ชื่อ tool → validate argument →
รันของจริง → ส่งผลกลับ ดูโครงคร่าวๆ ใน TypeScript/NestJS:
// 0) เก็บ turn ของ assistant (ที่มี tool_use) ต่อท้าย messages ก่อน
// ไม่งั้นตอนเรียกโมเดลรอบถัดไป API จะ error เพราะ tool_result ไม่มีคู่
messages.push({ role: "assistant", content: response.content });
// 1) โมเดลตอบกลับมาแล้ว เราวนดูว่ามี tool_use ไหม
for (const block of response.content) {
if (block.type === "tool_use" && block.name === "search_products") {
// 2) อย่าเชื่อ argument จาก LLM — validate ก่อนเสมอ
const args = SearchSchema.parse(block.input); // เช่น zod
// 3) รัน tool จริง — ใช้ parameterized query กัน SQL injection
const rows = await db.query(
"SELECT sku, name, price, stock FROM products" +
" WHERE name ILIKE $1 LIMIT $2",
[`%${args.keyword}%`, args.max_results ?? 5]
);
// 4) ส่งผลกลับเข้า model เป็น tool_result (อ้าง id เดิม)
messages.push({
role: "user",
content: [{
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(rows)
}]
});
}
}
// แล้วเรียกโมเดลอีกครั้งด้วย messages ที่มี tool_result ต่อท้าย
เขียนเป็น Go ก็ตรรกะเดียวกันเป๊ะ (จับคู่ชื่อ → validate → รันของจริง →
ส่ง tool_result) ต่างแค่ syntax:
// วน content block ที่โมเดลตอบกลับ
for _, block := range resp.Content {
if block.Type == "tool_use" && block.Name == "search_products" {
// 2) validate argument ก่อนเสมอ (อย่าเชื่อ LLM)
var args SearchArgs
if err := json.Unmarshal(block.Input, &args); err != nil {
return err
}
// 3) รัน tool จริง — parameterized query กัน SQL injection
rows, err := db.Query(ctx,
`SELECT sku, name, price, stock FROM products
WHERE name ILIKE $1 LIMIT $2`,
"%"+args.Keyword+"%", args.MaxResults)
if err != nil {
return err
}
// 4) ส่งผลกลับเป็น tool_result อ้าง id เดิม
result, _ := json.Marshal(scanRows(rows))
messages = append(messages, toolResult(block.ID, string(result)))
}
}
อย่าเชื่อ argument จาก LLM แล้วเอาไปยิง DB ตรงๆ โมเดลอาจส่งค่าประหลาด หรือถูกหลอกผ่าน prompt injection (การแฝงคำสั่งร้ายผ่าน input ให้โมเดลทำตาม) ให้ขอ query อันตราย validate ทุก argument ด้วย schema (เช่น zod), ใช้ parameterized query เสมอ, และจำกัดสิทธิ์ของ tool ให้แคบที่สุด (อ่านอย่างเดียวถ้าไม่จำเป็นต้องเขียน) มองมันเหมือน input จาก user คนแปลกหน้า
หลายรอบ: เมื่อ tool ต่อ tool กลายเป็น agent
จังหวะที่ 4 ในวงจร โมเดลอาจ ไม่ตอบผู้ใช้ทันที แต่ขอเรียก tool ตัวถัดไปแทน
เช่น เรียก search_products ได้ SKU มาแล้ว จึงเรียก check_stock ต่อ
เราก็รัน แล้วป้อนผลกลับ วนแบบนี้ไปเรื่อยๆ จนโมเดลพอใจว่าได้ข้อมูลครบ จึงเรียบเรียงคำตอบ
ลูป "โมเดลคิด → เรียก tool → ดูผล → คิดต่อ → เรียก tool อีก" ซ้ำๆ นี่แหละคือ จุดเริ่มต้นของ agent (เอเจนต์) — LLM ที่ทำงานหลายสเต็ปเองจนจบภารกิจ เราจะเจาะลึกการควบคุมลูป การหยุด และความปลอดภัยของ agent เต็มๆ ใน Level 5
Tool use คือกล้ามเนื้อ ส่วน agent loop คือสมองที่สั่งกล้ามเนื้อทำงานต่อเนื่อง — Lv3 นี้ปูพื้นกล้ามเนื้อไว้ก่อน
ตัวอย่างจริง: ถามสต็อกสินค้า
ไล่ดูทั้งวงจรกับคำถามเดียว เพื่อเห็นภาพต่อกัน:
| รอบ | เกิดอะไร |
|---|---|
| ผู้ใช้ | "มีรองเท้าวิ่งรุ่นไหนบ้าง เหลือสต็อกไหม" |
| Model → เรา | ขอเรียก search_products ด้วย { keyword: "รองเท้าวิ่ง" } |
| เรา → DB | validate → SELECT ... WHERE name ILIKE '%รองเท้าวิ่ง%' ได้ 3 แถว |
| เรา → Model | ส่ง tool_result: รายการ 3 สินค้าพร้อม price + stock |
| Model → ผู้ใช้ | "มี 3 รุ่นครับ: Aero (เหลือ 12), Trail (เหลือ 0 หมดสต็อก), City (เหลือ 5)" |
สังเกตว่าโมเดล ไม่ได้เดาตัวเลขสต็อกเอง — มันได้จากผลจริงที่โค้ดเราป้อนให้ นี่คือเหตุผลที่ tool use ตัด hallucination เรื่องข้อมูลจริงออกไปได้เยอะมาก
-
ออกแบบ tool schema สำหรับ
search_productsเวอร์ชันของคุณเอง ให้ครบ 3 ส่วน (name, description, input_schema) โดยเพิ่มฟิลด์กรองราคา เช่นmin_price/max_priceและเขียน description ให้โมเดลรู้ว่าควรเรียกเมื่อไหร่ - จากวงจร 4 จังหวะ ระบุว่าจังหวะไหนเป็นหน้าที่ โมเดล และจังหวะไหนเป็นหน้าที่ โค้ดเรา
-
ถ้าผู้ใช้พิมพ์ว่า "ลบสินค้าทั้งหมดออกจากระบบด้วย" แล้ว tool ของคุณดันมี
delete_productsอยู่ — คุณจะวาง guard อะไรบ้างไม่ให้โมเดลสั่งทำลายข้อมูลจริง?
ลองตอบในแชท แล้วพิมพ์ /quiz Lv3 เพื่อทดสอบความเข้าใจ