AI Engineer › Agents และ tool use

Tool use: schema, dispatcher และ error เป็น observation

ประมาณ 40 นาที

เป้าหมายของบท

  • อธิบายกลไก tool use: โมเดลขอเรียกเครื่องมือ โค้ดของเราตรวจและรัน แล้วส่งผลกลับ
  • เขียน tool schema และ dispatcher ที่ตรวจชื่อ อาร์กิวเมนต์ ชนิด และจับ exception
  • ส่งผลลัพธ์/ข้อผิดพลาดกลับให้โมเดลในรูปแบบที่ปลอดภัยและกระชับ

Agent = โมเดล + เครื่องมือ + ลูป

LLM ล้วนๆ รู้แค่ข้อความที่ให้ ทำอะไรกับโลกภายนอกไม่ได้ (ดูฐานข้อมูล เรียก API คำนวณ) tool use แก้ด้วยการให้โมเดล “ขอ” เรียกฟังก์ชันที่เราเตรียมไว้ — โมเดลไม่ได้รันโค้ดเอง มันแค่ ส่งคำขอเรียกเครื่องมือ (ชื่อ + อาร์กิวเมนต์) ให้โค้ดของเราตรวจและรัน แล้วส่งผลกลับไปเป็นข้อความ

ใน API จริง (เช่น Anthropic Messages API) ขั้นตอนคือ:

  1. ส่ง tools=[{name, description, input_schema}] มากับคำขอ
  2. ถ้าโมเดลต้องการใช้เครื่องมือ คำตอบจะมี block แบบ tool_use (id, name, input) และ stop_reason == "tool_use"
  3. โค้ดเราเรียกเครื่องมือจริง แล้วส่งข้อความ user ที่มี block tool_result (tool_use_id, content) กลับไป
  4. วนซ้ำจนโมเดลตอบเป็นข้อความสุดท้าย

ในบทนี้เราฝึกส่วนที่เป็นโค้ดของเรา — schema, dispatcher, การตรวจอาร์กิวเมนต์ และรูปแบบของผลลัพธ์ — บนตาราง orders / products จริงของร้านค้า ส่วน โมเดลตัดสินใจเรียกเครื่องมือ ถูกจำลองด้วยสคริปต์ในบทถัดไป

1) เครื่องมือ = ฟังก์ชัน + schema

def get_order(order_id: int) -> dict:
    """Look up an order by id"""
    ...

TOOL_SCHEMAS = [
    {"name": "get_order", "description": "Look up an order by id",
     "input_schema": {"type": "object",
                      "properties": {"order_id": {"type": "integer"}},
                      "required": ["order_id"]}},
]

description สำคัญมาก — เป็นสิ่งเดียวที่โมเดลใช้ตัดสินใจว่าเมื่อไรควรเรียก ตั้งชื่อชัดเจน อธิบายว่าทำอะไรและรับอะไร (สร้าง schema จากฟังก์ชันอัตโนมัติได้ด้วย inspect — ดูแบบฝึกหัด)

2) Dispatcher: ด่านตรวจก่อนรันจริง

อย่ารัน REGISTRY[name](**args) ตรงๆ — อาร์กิวเมนต์มาจากโมเดล (อาจผิด/ขาด/ถูกชักจูงจากข้อความภายนอก) ต้องตรวจ: ชื่อเครื่องมือมีจริงไหม, อาร์กิวเมนต์ที่ต้องมีครบไหม, ชนิดถูกไหม, มีอาร์กิวเมนต์แปลกปลอมไหม, แล้วจับ exception

def dispatch(call):
    name, args = call["name"], call.get("input", {})
    if name not in REGISTRY:
        return {"ok": False, "error": "unknown tool: " + name}
    ...
    try:
        return {"ok": True, "result": REGISTRY[name](**args)}
    except Exception as e:
        return {"ok": False, "error": f"{type(e).__name__}: {e}"}

3) ข้อผิดพลาดคือ “observation” ไม่ใช่การล้ม

เมื่อเครื่องมือล้มเหลว ให้ส่ง ข้อความ error ที่ชัดเจนกลับให้โมเดล ("missing argument: order_id") แทนที่จะให้โปรแกรมล้ม — โมเดลมักแก้อาร์กิวเมนต์แล้วลองใหม่ได้เอง

4) ผลลัพธ์ที่ส่งกลับต้องเป็นข้อความที่ปลอดภัยและกระชับ

  • ส่งเฉพาะฟิลด์ที่จำเป็น (ไม่ใช่ทั้งแถว) เพื่อลด token และไม่เปิดเผยข้อมูลเกินเหตุ
  • ค่า NaN ของ pandas (เช่น discount_pct ที่ว่างในตาราง orders) ทำให้ json.dumps ได้ NaN ซึ่ง ไม่ใช่ JSON ที่ถูกต้อง — แปลงเป็น None (→ null) ก่อน
  • จำกัดความยาว (ตัดข้อความยาว) เพราะทุกตัวอักษรที่ส่งกลับคือ input token ของรอบถัดไป

ข้อมูลจริงที่ใช้ในบทนี้: ออเดอร์ #1 ของลูกค้า 8 สถานะ completed วันที่ 2024-01-03; ลูกค้า 675 มีออเดอร์มากสุด 14 รายการ

ตัวอย่างแก้โจทย์

กด "ขั้นต่อไป" ทีละขั้น ลองรันและแก้โค้ดในแต่ละขั้นได้

เรียกเครื่องมือของร้านค้า

เรียกฟังก์ชันที่ลงทะเบียนไว้ตรงๆ และอ่าน schema

  1. get_order(1) และ count_orders(675) (ลูกค้าที่มีออเดอร์มากสุด) ได้ 14

    Python

dispatcher และ error เป็น observation

รันคำขอที่ถูกและผิดผ่าน dispatch

  1. คำขอที่ถูกต้องได้ ok และผลลัพธ์; ชื่อเครื่องมือไม่มีจริงได้ error ที่ชัดเจน

    Python

เติมช่องว่าง

แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ

ยังไม่ได้ทำ

ตรวจว่าชื่อเครื่องมือมีอยู่ใน REGISTRY หรือไม่ เก็บ True/False ไว้ในตัวแปร known เติม ___ ให้ครบ

แบบฝึกท้ายบท

บทนี้จะนับว่าเสร็จเมื่อผ่านข้อพื้นฐานและประยุกต์ครบ ข้อท้าทายได้ดาวเพิ่ม

🟢 พื้นฐาน

🟢 พื้นฐานยังไม่ได้ทำ

เรียก get_order(1) เก็บสถานะไว้ใน status_1 และวันที่สั่งซื้อไว้ใน date_1

🟢 พื้นฐานยังไม่ได้ทำ

ลูกค้าหมายเลข 675 มีออเดอร์ทั้งหมดกี่รายการ (n_675) และเป็น cancelled กี่รายการ (n_675_cancelled) ใช้ count_orders

🟢 พื้นฐานยังไม่ได้ทำ

TOOL_SCHEMAS มีกี่เครื่องมือ (n_tools) และชื่อของทั้งหมดเรียงตามลำดับเป็น list (tool_names)

🟢 พื้นฐานยังไม่ได้ทำ

ฟิลด์ที่ต้องมี (required) ของเครื่องมือ count_orders ตาม schema เก็บเป็น list ไว้ในตัวแปร required_count และชื่อฟิลด์ทั้งหมดใน properties (เรียงตามตัวอักษร) ไว้ใน all_params

🟡 ประยุกต์

🟡 ประยุกต์ยังไม่ได้ทำ

เขียนฟังก์ชัน dispatch(call) แบบย่อ: ถ้าชื่อไม่อยู่ใน REGISTRY คืน {"ok": False, "error": "unknown tool: "}; ถ้าขาดอาร์กิวเมนต์ที่ต้องมีตาม schema คืน {"ok": False, "error": "missing argument: "}; ไม่งั้นเรียก REGISTRYname แล้วคืน {"ok": True, "result": ผลลัพธ์} — ใช้กับ calls ที่ให้ไว้ เก็บ list ของค่า ok ไว้ใน ok_flags และ error ของตัวที่ล้มเหลวเป็น list ไว้ใน error_texts

🟡 ประยุกต์ยังไม่ได้ทำ

ใช้ dispatch ที่ให้ไว้ (ตรวจชนิดด้วย): ส่ง get_order ที่ order_id เป็นสตริง "1" เก็บข้อความ error ไว้ใน type_error และส่ง get_order ที่มีอาร์กิวเมนต์แปลกปลอม {"order_id": 1, "admin": True} เก็บข้อความ error ไว้ใน unexpected_error

🟡 ประยุกต์ยังไม่ได้ทำ

แปลงผลของเครื่องมือ raw (ให้ไว้ มี NaN ใน discount_pct) เป็นข้อความที่ส่งกลับให้โมเดล: แทนค่า NaN ด้วย None (ใช้ pd.isna) แล้ว json.dumps(..., ensure_ascii=False) เก็บข้อความไว้ในตัวแปร observation และความยาวไว้ใน observation_len

🔴 ท้าทาย

🔴 ท้าทายยังไม่ได้ทำ

สร้าง schema จากลายเซ็นของฟังก์ชันด้วย inspect: เขียน types_of(fn) คืน dict ชื่อพารามิเตอร์ → ชนิดใน JSON schema (int → "integer", str → "string") และ required_of(fn) คืน list ของพารามิเตอร์ที่ไม่มีค่าเริ่มต้น — ใช้กับ count_orders เก็บไว้ใน types_count และ required_count_fn

🔴 ท้าทายยังไม่ได้ทำ

รัน calls ที่ให้ไว้ (5 คำขอ) ผ่าน dispatch ที่ให้ไว้ เก็บจำนวนที่สำเร็จไว้ใน n_ok, ที่ล้มเหลวไว้ใน n_err และ error ของตัวที่ล้มเหลวทั้งหมดเป็น list (ตามลำดับ) ไว้ใน errors

Quiz

1. ใน tool use ใครเป็นผู้รันฟังก์ชันจริง
2. ทำไม dispatcher ต้องตรวจอาร์กิวเมนต์ก่อนรันเครื่องมือ
3. เมื่อเครื่องมือล้มเหลว ควรทำอย่างไร
4. ทำไมต้องแปลง NaN เป็น None ก่อน json.dumps ผลของเครื่องมือ

สรุป

  • Tool use: โมเดลส่งคำขอ (ชื่อ + input) โค้ดเราตรวจและรันจริง แล้วส่ง tool_result กลับ — โมเดลไม่ได้รันโค้ดเอง
  • Dispatcher ตรวจชื่อ ฟิลด์ที่ต้องมี ชนิด อาร์กิวเมนต์แปลกปลอม และจับ exception; ข้อผิดพลาดส่งกลับเป็น observation ที่ชัดเจน
  • ผลลัพธ์ควรกระชับและเป็นข้อความปลอดภัย: ส่งเฉพาะฟิลด์จำเป็น แปลง NaN เป็น None ก่อน JSON (บทนี้ใช้ข้อมูล orders/products จริง ส่วนโมเดลจำลองในบทถัดไป)

คำถามสัมภาษณ์

อธิบายลำดับการทำงานของ tool use กับ LLM API

ส่งรายการ tools (name, description, input_schema) มาพร้อมคำขอ ถ้าโมเดลต้องการเครื่องมือ มันตอบด้วยคำขอเรียกเครื่องมือ (tool_use: id, name, input) แทนข้อความสุดท้าย โค้ดของเราตรวจอาร์กิวเมนต์ รันฟังก์ชันจริง แล้วส่ง tool_result (พร้อม id เดิม) กลับเป็นข้อความถัดไป วนจนโมเดลตอบข้อความสุดท้าย

คุณจะออกแบบ description และ schema ของเครื่องมืออย่างไรให้โมเดลใช้ถูก

ตั้งชื่อสื่อความหมาย เขียน description ว่าเครื่องมือทำอะไร เมื่อไรควรใช้ และไม่ควรใช้เมื่อไร ระบุชนิดและความหมายของแต่ละพารามิเตอร์ ใช้ enum เมื่อมีค่าที่กำหนดได้ ระบุ required ให้ตรง และให้ผลลัพธ์กลับมาสั้นชัดเจน จากนั้นทดสอบกับคำขอจริงหลายแบบเพื่อดูว่าโมเดลเลือกใช้ถูกไหม