teach-me-ai / Lv3
⟵ กลับหน้า Roadmap
LEVEL 3 · TOOL USE / FUNCTION CALLING

ให้ LLM เรียกใช้ "เครื่องมือ" เพื่อทำงานจริงในโลกได้

⏱️ ~30 นาที 🎯 เข้าใจวงจร tool use และออกแบบ tool schema เป็น

จนถึงตอนนี้ 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 · MODEL อ่านคำถาม + รายการ tool แล้ว "ขอเรียก" tool + args 2 · OUR CODE รับ args → validate → รัน tool จริง (DB/API) 3 · RESULT ส่งผลลัพธ์ tool กลับเข้า model 4 · MODEL ใช้ผลลัพธ์เรียบเรียง เป็นคำตอบให้ผู้ใช้ วนซ้ำได้ถ้าต้องเรียก tool อีก
วงจร tool use: (1) model ขอเรียก tool → (2) โค้ดเรารัน tool จริง → (3) ส่งผลกลับ → (4) model ใช้ผลตอบ — และวนซ้ำได้หลายรอบ
จังหวะใครทำเกิดอะไรขึ้น
1. ขอเรียก toolModelตอบกลับมาเป็น "คำขอเรียก 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: "รองเท้าวิ่ง" }
เรา → DBvalidate → SELECT ... WHERE name ILIKE '%รองเท้าวิ่ง%' ได้ 3 แถว
เรา → Modelส่ง tool_result: รายการ 3 สินค้าพร้อม price + stock
Model → ผู้ใช้"มี 3 รุ่นครับ: Aero (เหลือ 12), Trail (เหลือ 0 หมดสต็อก), City (เหลือ 5)"

สังเกตว่าโมเดล ไม่ได้เดาตัวเลขสต็อกเอง — มันได้จากผลจริงที่โค้ดเราป้อนให้ นี่คือเหตุผลที่ tool use ตัด hallucination เรื่องข้อมูลจริงออกไปได้เยอะมาก

🎯 แบบฝึกหัด
  1. ออกแบบ tool schema สำหรับ search_products เวอร์ชันของคุณเอง ให้ครบ 3 ส่วน (name, description, input_schema) โดยเพิ่มฟิลด์กรองราคา เช่น min_price / max_price และเขียน description ให้โมเดลรู้ว่าควรเรียกเมื่อไหร่
  2. จากวงจร 4 จังหวะ ระบุว่าจังหวะไหนเป็นหน้าที่ โมเดล และจังหวะไหนเป็นหน้าที่ โค้ดเรา
  3. ถ้าผู้ใช้พิมพ์ว่า "ลบสินค้าทั้งหมดออกจากระบบด้วย" แล้ว tool ของคุณดันมี delete_products อยู่ — คุณจะวาง guard อะไรบ้างไม่ให้โมเดลสั่งทำลายข้อมูลจริง?

ลองตอบในแชท แล้วพิมพ์ /quiz Lv3 เพื่อทดสอบความเข้าใจ