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) ขั้นตอนคือ:
- ส่ง
tools=[{name, description, input_schema}]มากับคำขอ - ถ้าโมเดลต้องการใช้เครื่องมือ คำตอบจะมี block แบบ
tool_use(id,name,input) และstop_reason == "tool_use" - โค้ดเราเรียกเครื่องมือจริง แล้วส่งข้อความ
userที่มี blocktool_result(tool_use_id,content) กลับไป - วนซ้ำจนโมเดลตอบเป็นข้อความสุดท้าย
ในบทนี้เราฝึกส่วนที่เป็นโค้ดของเรา — 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
get_order(1) และ count_orders(675) (ลูกค้าที่มีออเดอร์มากสุด) ได้ 14
dispatcher และ error เป็น observation
รันคำขอที่ถูกและผิดผ่าน dispatch
คำขอที่ถูกต้องได้ ok และผลลัพธ์; ชื่อเครื่องมือไม่มีจริงได้ error ที่ชัดเจน
เติมช่องว่าง
แทน ___ ด้วยโค้ดที่ถูกต้อง แล้วกดตรวจคำตอบ
ตรวจว่าชื่อเครื่องมือมีอยู่ใน 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:
ใช้ 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
สรุป
- 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 ให้ตรง และให้ผลลัพธ์กลับมาสั้นชัดเจน จากนั้นทดสอบกับคำขอจริงหลายแบบเพื่อดูว่าโมเดลเลือกใช้ถูกไหม