← Phase 1 Phase 1 • Day 3 Reboot

Local SE Agent Masterclass

MacBook M3 16GB • Complete Implementation Standard
📍 ส่วนที่ 1: หมุดตั้งโจทย์ & วัตถุประสงค์หลัก (Title Connection)

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 1: Setup & Ollama API ✓ Day 2: JSON Mode & Structured Output Day 3: Tool Calling & Dispatcher Day 4-5: Base Tools (File & Maven) Day 6+: Autonomous ReAct Loop
📂 ส่วนที่ 2: ผังโครงสร้างโปรเจกต์จริง & ตำแหน่งไฟล์ (Project File Structure)

โค้ดของ Day 3 จะต่อยอดจากโฟลเดอร์ se_agent/ โดยเพิ่มโมดูล core/dispatcher.py และสคริปต์ test_tool_dispatch.py เพื่อทดสอบวงจรการเลือกและเรียกใช้ฟังก์ชันจริง:

se_agent/
├── requirements.txt                       # ollama>=0.4.0
├── test_connection.py                    # สคริปต์ Day 1
├── test_structured_output.py             # สคริปต์ Day 2
├── test_tool_dispatch.py                 # [ไฟล์ใหม่ Day 3] ทดสอบส่งคำสั่งมนุษย์ -> AI เลือก Tool -> Python รันฟังก์ชันจริง
├── core/
│   ├── __init__.py
│   ├── client.py                          # ควบคุมการยิงคุยกับ Ollama
│   ├── parser.py                          # ถอดรหัส JSON และคลีน Markdown code fences
│   └── dispatcher.py                      # [ไฟล์ใหม่ Day 3] ทะเบียนฟังก์ชัน (Registry) และตัวรันคำสั่ง (Dispatcher)
└── main.py
📍 core/dispatcher.py ทำหน้าที่อะไร?

ทำหน้าที่เป็น Execution Gateway จดทะเบียนฟังก์ชัน Python จริง (Callable functions) และผูกชื่อเข้ากับสตริงคำสั่ง เมื่อ LLM ตัดสินใจเลือก {"tool": "calculate", "params": {...}} Dispatcher จะแกะพารามิเตอร์ไปสั่งรันฟังก์ชันจริง และดักจับ TypeError หากส่งค่ามาไม่ครบ

📍 test_tool_dispatch.py ทำหน้าที่อะไร?

เป็นสคริปต์รันบน Terminal เพื่อทดลองป้อนคำถามมนุษย์ เช่น "ช่วยคำนวณ 15 * 8 ให้หน่อย" ให้โมเดลเลือกใช้ Tool และส่งผลลัพธ์กลับมาให้โปรแกรม Python คำนวณจริงแบบ End-to-End

🏛️ ส่วนที่ 3: สถาปัตยกรรม Tool Calling & Function Dispatching (Mental Model)
1️⃣
Tool Declaration
อธิบายชื่อ + พารามิเตอร์ใน System Prompt
➔
2️⃣
Model Decisions
LLM ตอบ JSON ขอใช้ Tool + ใส่ arguments
➔
3️⃣
Python Dispatcher
จับคู่ฟังก์ชันจริงในเครื่อง แล้ว Execute
ทำไมโมเดลถึงไม่รันโค้ดเอง ทำไมต้องมี Dispatcher ฝั่ง Python?
  • LLM คือ Neural Network ไม่ใช่ CPU: โมเดลภาษาเป็นเพียงระบบคำนวณเมทริกซ์ที่ทำนายคำถัดไป (Next Token Predictor) มันไม่สามารถเปิดไฟล์ หรือต่อสายอินเทอร์เน็ตได้ด้วยตัวเอง
  • โมเดลมีหน้าที่แค่ "ส่งเจตนา (Intent)": มันจะบอกเราว่า "ถ้าอยากได้คำตอบนี้ ต้องรันฟังก์ชันชื่ออะไรและป้อนตัวเลขไหนเข้าไป"
  • Python คือ "มือและเท้า (Executor)": โปรแกรม Python ของเรามีหน้าที่นำพารามิเตอร์นั้นไปประมวลผลจริงในโลกความจริง แล้วนำคำตอบ (Observation) ส่งกลับมาให้โมเดลอ่านต่อ
💻 ส่วนที่ 4: ซอร์สโค้ดจริง & คำอธิบายทีละบรรทัด (Line-by-Line Implementation)
📄 1. core/dispatcher.py (ไฟล์ใหม่ประจำ Day 3) หัวใจของการผูกฟังก์ชันและการรันคำสั่ง
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)

📄 2. test_tool_dispatch.py (สคริปต์ทดสอบผลลัพธ์จริง) ทดสอบวงจรการเลือก Tool ➔ Dispatch ➔ Execute
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()
⚡ ส่วนที่ 5: ปฏิบัติการจริงบน Terminal Mac M3 (Step-by-Step Lab)

เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อทดสอบ Tool Dispatcher ทันที:

ขั้นตอนที่ 1: เข้าโฟลเดอร์โปรเจกต์เดิมและเปิด Virtual Environment
cd ~/se_agent
source .venv/bin/activate
ขั้นตอนที่ 2: สร้างไฟล์ใหม่สำหรับ Day 3
touch core/dispatcher.py test_tool_dispatch.py
ขั้นตอนที่ 3: วางโค้ดลงในไฟล์

- นำโค้ด core/dispatcher.py ใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/dispatcher.py
- นำโค้ด test_tool_dispatch.py ใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน test_tool_dispatch.py

ขั้นตอนที่ 4: รันสคริปต์ทดสอบผลลัพธ์
python test_tool_dispatch.py

คุณจะเห็นว่าโมเดลตัดสินใจเลือกฟังก์ชัน calculate_metric พร้อมส่งพารามิเตอร์ {"a": 14.5, "b": 32.0, "operation": "multiply"} มาให้ Dispatcher ใน Python รันผลลัพธ์ได้ 464.0 อย่างแม่นยำ 100%!

📝 ส่วนที่ 6: ชุดควิซประเมินความรู้เชิงวิศวกรรม 15 ข้อ (ตอบปุ๊บรู้ผลทันที)

ข้อที่ 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 เพื่อให้โมเดลเริ่มเปิดอ่านไฟล์ซอร์สโค้ดจริงในโปรเจกต์ได้แล้วครับ!