Chapter 3 — Concept of Tool Calling & Function Dispatcher
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 1 เรามีท่อส่งข้อมูล ใน Day 2 เราบังคับให้ LLM ตอบกลับเป็น JSON Dictionary ได้แล้ว แต่โมเดลก็ยังเป็นได้แค่ "สมองกลที่ถูกขังอยู่ในห้องกระจก" ไม่สามารถรับรู้สภาพแวดล้อมจริงในเครื่อง ไม่สามารถรันคำสั่ง OS หรือแตะต้องไฟล์ได้เลย
Day 3 จึงเป็นการสร้างสะพานเชื่อมสองมิติ: "Tool Calling & Function Dispatcher" เรียนรู้ว่าทำไมเราถึงต้องเขียน Schema ประกาศความสามารถให้โมเดลเลือกใช้ และเขียน Dispatcher Engine ใน Python เพื่อดักจับว่าเมื่อโมเดลตอบกลับมาว่า "ฉันต้องการเรียกฟังก์ชัน X ด้วยพารามิเตอร์ Y" โค้ด Python ฝั่งเราจะหยิบคำสั่งนั้นไปรันฟังก์ชันจริงได้อย่างไร ปูรากฐาน 100% สู่การสร้าง File Reader ใน Day 4
โค้ดของ Day 3 จะต่อยอดจากโฟลเดอร์ se_agent/ โดยเพิ่มโมดูล core/dispatcher.py และสคริปต์ test_tool_dispatch.py เพื่อทดสอบวงจรการเลือกและเรียกใช้ฟังก์ชันจริง:
ทำหน้าที่เป็น Execution Gateway จดทะเบียนฟังก์ชัน Python จริง (Callable functions) และผูกชื่อเข้ากับสตริงคำสั่ง เมื่อ LLM ตัดสินใจเลือก {"tool": "calculate", "params": {...}} Dispatcher จะแกะพารามิเตอร์ไปสั่งรันฟังก์ชันจริง และดักจับ TypeError หากส่งค่ามาไม่ครบ
เป็นสคริปต์รันบน Terminal เพื่อทดลองป้อนคำถามมนุษย์ เช่น "ช่วยคำนวณ 15 * 8 ให้หน่อย" ให้โมเดลเลือกใช้ Tool และส่งผลลัพธ์กลับมาให้โปรแกรม Python คำนวณจริงแบบ End-to-End
- LLM คือ Neural Network ไม่ใช่ CPU: โมเดลภาษาเป็นเพียงระบบคำนวณเมทริกซ์ที่ทำนายคำถัดไป (Next Token Predictor) มันไม่สามารถเปิดไฟล์ หรือต่อสายอินเทอร์เน็ตได้ด้วยตัวเอง
- โมเดลมีหน้าที่แค่ "ส่งเจตนา (Intent)": มันจะบอกเราว่า "ถ้าอยากได้คำตอบนี้ ต้องรันฟังก์ชันชื่ออะไรและป้อนตัวเลขไหนเข้าไป"
- Python คือ "มือและเท้า (Executor)": โปรแกรม Python ของเรามีหน้าที่นำพารามิเตอร์นั้นไปประมวลผลจริงในโลกความจริง แล้วนำคำตอบ (Observation) ส่งกลับมาให้โมเดลอ่านต่อ
from typing import Callable, Dict, Any
class ToolExecutionError(Exception):
"""Custom exception สำหรับข้อผิดพลาดระหว่างรัน Tool จริง"""
pass
class ToolDispatcher:
"""
ศูนย์จดทะเบียนฟังก์ชันและตัวกลางสั่งรันคำสั่งจาก LLM (Function Dispatcher)
"""
def __init__(self):
# เก็บฟังก์ชัน Python จริงในรูป: {"tool_name": callable_function}
self.registry: Dict[str, Callable[..., Any]] = {}
def register(self, name: str, func: Callable[..., Any]) -> None:
"""
จดทะเบียนฟังก์ชันเข้าสู่ระบบ
:param name: ชื่อ Tool ที่โมเดลจะใช้อ้างอิง (เช่น 'get_system_info')
:param func: ฟังก์ชัน Python จริง
"""
self.registry[name] = func
def dispatch(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
"""
เรียกใช้ฟังก์ชันตามที่โมเดลขอมา พร้อมระบบดักจับ Error
:param tool_name: ชื่อฟังก์ชันที่ขอรัน
:param arguments: พารามิเตอร์ที่แกะมาจาก JSON ของโมเดล
"""
if tool_name not in self.registry:
raise ToolExecutionError(f"ไม่พบ Tool ชื่อ '{tool_name}' ในระบบ Registry")
func = self.registry[tool_name]
try:
# Unpack dictionary arguments เข้าไปในฟังก์ชัน Python จริง
return func(**arguments)
except TypeError as err:
raise ToolExecutionError(
f"โมเดลส่งพารามิเตอร์ให้ฟังก์ชัน '{tool_name}' ไม่ถูกต้อง: {str(err)}"
)
except Exception as err:
raise ToolExecutionError(
f"เกิดข้อผิดพลาดขณะรันฟังก์ชัน '{tool_name}': {str(err)}"
)
ชำแหละไวยากรณ์สำคัญใน core/dispatcher.py
self.registry: Dict[str, Callable[..., Any]]
ในภาษา Python ฟังก์ชันคือ First-Class Citizen เราสามารถเก็บฟังก์ชันตัวเป็นๆ ลงใน Dictionary ได้โดยไม่ต้องใส่ () ทำให้เราสามารถค้นหาและเรียกใช้งานได้ตามสตริงชื่อฟังก์ชัน
return func(**arguments)
**arguments คือ Keyword Argument Unpacking แปลง Dictionary เช่น {"a": 10, "b": 20} ให้กลายเป็นการเรียก func(a=10, b=20) แบบไดนามิก
except TypeError as err:
ดักจับกรณีที่โมเดลส่งชื่อพารามิเตอร์ผิดตัว หรือส่งจำนวนพารามิเตอร์ไม่ครบตามที่ฟังก์ชันต้องการ เพื่อส่งข้อความเตือนให้โมเดลแก้ไขตัวเอง (Self-Correction)
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser
from core.dispatcher import ToolDispatcher, ToolExecutionError
import platform
# 1. ประกาศฟังก์ชันตัวอย่างที่โปรแกรมเตรียมไว้ให้ Agent ใช้งาน
def get_system_specs() -> str:
"""ดึงข้อมูลสถาปัตยกรรมของเครื่อง Mac"""
return f"OS: {platform.system()} | Machine: {platform.machine()} | Architecture: Apple Silicon"
def calculate_metric(a: float, b: float, operation: str) -> float:
"""คำนวณตัวเลขทางคณิตศาสตร์อย่างแม่นยำ"""
if operation == "multiply":
return a * b
elif operation == "add":
return a + b
elif operation == "divide":
return a / b if b != 0 else 0.0
return 0.0
def main():
print("🚀 เริ่มทดสอบ Day 3: Tool Calling & Function Dispatcher บน Mac M3...")
# 2. ติดตั้ง Dispatcher และจดทะเบียนฟังก์ชัน
dispatcher = ToolDispatcher()
dispatcher.register("get_system_specs", get_system_specs)
dispatcher.register("calculate_metric", calculate_metric)
client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
if not client.check_health():
print("❌ ข้อผิดพลาด: ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b")
return
# 3. กำหนด System Prompt บอกว่าเรามี Tool อะไรบ้าง และบังคับตอบเป็น JSON สั่งงาน
system_prompt = (
"You are an AI Agent with access to tools. If you need external data or calculations,\n"
"respond ONLY in JSON format to request tool execution.\n\n"
"Available Tools:\n"
"1. get_system_specs(): No parameters. Returns OS and hardware information.\n"
"2. calculate_metric(a: number, b: number, operation: string): Operations are 'add', 'multiply', 'divide'.\n\n"
"JSON Response Schema:\n"
"{\n"
' "tool": string,\n'
' "arguments": object\n'
"}"
)
user_query = "ช่วยคำนวณพื้นที่รูปสี่เหลี่ยมที่มีความกว้าง 14.5 เมตร และความยาว 32.0 เมตร ให้หน่อย"
print(f"💬 User Prompt: {user_query}")
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
print("\n📡 กำลังส่ง Prompt ให้โมเดลเลือก Tool...")
raw_response = client.chat(messages, temperature=0.1, json_mode=True)
print(f"📥 โมเดลตอบกลับมาเป็น JSON:\n{raw_response}")
# 4. แปลงคำตอบและส่งให้ Dispatcher รันฟังก์ชันจริง
try:
parsed_action = StructuredOutputParser.parse(raw_response)
tool_name = parsed_action.get("tool")
arguments = parsed_action.get("arguments", {})
print(f"\n⚙️ Dispatcher กำลังประมวลผล...")
print(f" 👉 สั่งรันฟังก์ชัน: {tool_name}")
print(f" 👉 ป้อนพารามิเตอร์: {arguments}")
# รันคำสั่งจริงใน Python
result = dispatcher.dispatch(tool_name, arguments)
print(f"\n🎯 ผลลัพธ์จากการคำนวณจริงในเครื่อง Mac: {result}")
print(f"🎉 สำเร็จ 100%! เราได้ติดแขนติดขาให้ LLM สั่งงานโค้ดภายนอกได้แล้ว!")
except ToolExecutionError as err:
print(f"❌ Dispatcher Error: {err}")
except Exception as err:
print(f"❌ Error: {err}")
if __name__ == "__main__":
main()
เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อทดสอบ Tool Dispatcher ทันที:
cd ~/se_agent
source .venv/bin/activate
touch core/dispatcher.py test_tool_dispatch.py
- นำโค้ด core/dispatcher.py ใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/dispatcher.py
- นำโค้ด test_tool_dispatch.py ใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน test_tool_dispatch.py
python test_tool_dispatch.py
คุณจะเห็นว่าโมเดลตัดสินใจเลือกฟังก์ชัน calculate_metric พร้อมส่งพารามิเตอร์ {"a": 14.5, "b": 32.0, "operation": "multiply"} มาให้ Dispatcher ใน Python รันผลลัพธ์ได้ 464.0 อย่างแม่นยำ 100%!
ข้อที่ 1: หน้าที่หลักของ "Function Dispatcher" ในสถาปัตยกรรม AI Agent คืออะไร?
ข้อที่ 2: เหตุใดตัวโมเดลภาษา (LLM) ถึงไม่สามารถรันคำสั่งเปิดไฟล์หรือคำนวณโค้ดด้วยตัวเองได้โดยตรง?
ข้อที่ 3: ในภาษา Python ไวยากรณ์ `func(**arguments)` มีหน้าที่ทางเทคนิคอย่างไร?
ข้อที่ 4: หากโมเดลส่งชื่อพารามิเตอร์มาผิดตัว หรือส่งมาไม่ครบตาม Signature ของฟังก์ชัน Python จะเกิด Exception ใดขึ้น?
ข้อที่ 5: การออกแบบให้ฟังก์ชันถูกเก็บในรูป `self.registry[name] = func` ใช้คุณสมบัติใดของภาษา Python?
ข้อที่ 6: ใน System Prompt เหตุใดเราจึงต้องระบุทั้ง "ชื่อฟังก์ชัน" และ "คำอธิบายหน้าที่ (Description)" ของ Tool?
ข้อที่ 7: ข้อใดคือความแตกต่างระหว่าง "Direct Chat Completion" กับ "Tool Calling Request"?
ข้อที่ 8: เมื่อ Dispatcher สั่งรันฟังก์ชันและได้ผลลัพธ์กลับมา ขั้นตอนต่อไปของ ReAct Agent คืออะไร?
ข้อที่ 9: หากโมเดลส่งชื่อ Tool ที่ไม่มีอยู่ในระบบ (เช่น `format_disk`) โค้ด `ToolDispatcher` ควรตอบสนองอย่างไร?
ข้อที่ 10: การใช้ Type Hinting ในฟังก์ชัน Python (เช่น `a: float, b: float`) ช่วยลดความผิดพลาดของ Agent อย่างไร?
ข้อที่ 11: การที่โมเดล `qwen2.5-coder:7b` สามารถเลือก Tool และส่ง Arguments ได้ถูกต้องตั้งแต่รอบแรก เกิดจากปัจจัยใด?
ข้อที่ 12: การออกแบบคลาส `ToolDispatcher` แยกต่างหากจาก `OllamaLLMClient` เป็นไปตามหลักการออกแบบซอฟต์แวร์ข้อใด?
ข้อที่ 13: เมื่อเราทำ Tool Dispatcher สำเร็จใน Day 3 สะพานนี้จะทำให้เราสร้างสิ่งใดต่อใน **Day 4 (Building Tool 1: File System Reader)**?
ข้อที่ 14: ทำไมการรัน Tool Calling ในเครื่อง Local (MacBook M3) ถึงปลอดภัยกว่าการใช้ Agent บน Cloud สำหรับงานระดับ Enterprise?
ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อคุณเห็นว่าโมเดลสามารถสั่งรันฟังก์ชันใน Python และส่งค่ากลับมาคำนวณได้จริง รู้สึกอย่างไร?
🏁 สรุปหมุดเชื่อมโยง Day 3 (Chapter Wrap-up)
ใน Day 3 — Concept of Tool Calling & Function Dispatcher คุณได้สร้างโมดูล core/dispatcher.py ซึ่งเป็นศูนย์รวมทะเบียนฟังก์ชัน (Registry) และตัวรันคำสั่ง (Dispatcher) พร้อมทั้งทดสอบรัน test_tool_dispatch.py บนชิป Apple Silicon M3 ได้สำเร็จจริง ตอนนี้โมเดลของคุณไม่ได้แค่นั่งคิดอยู่ในกะลาแล้ว แต่สามารถสั่งการฟังก์ชัน Python ในเครื่องให้ทำงานแทนได้แล้ว พร้อมก้าวสู่ Day 4: Building Tool 1 — File System Reader เพื่อให้โมเดลเริ่มเปิดอ่านไฟล์ซอร์สโค้ดจริงในโปรเจกต์ได้แล้วครับ!