從零打造一個最基礎的 Agent:LLM + 工具 + 迴圈

用最少的程式碼,從零實作一個能接收問題、呼叫工具、生成回答的最基礎 Agent,理解其核心其實非常簡單。

從零打造一個最基礎的 Agent:LLM + 工具 + 迴圈

前三篇我們談了 Agent 的概念、核心循環與工具呼叫。
但你可能會想:這些聽起來都很合理,但我到底要怎麼「真的做出一個 Agent」?

這一篇,我們不談複雜的框架,也不依賴 LangChain 或 AutoGPT。
我們要用最少的程式碼,從零打造一個能運作的最基礎 Agent。
你會看到它的核心其實非常簡單:LLM + 工具 + 一個迴圈

LLM + Tools + Loop = Minimal Agent

一、先把問題縮到最小

在開始寫程式之前,我們先定義清楚:一個「最基礎的 Agent」應該具備什麼能力?

它不需要多 Agent 協作,不需要長期記憶,不需要複雜的規劃。
它只需要:

  1. 能接收使用者的問題
  2. 能決定是否呼叫工具
  3. 能執行工具並取得結果
  4. 能根據結果決定下一步
  5. 能重複直到任務完成

這樣就夠了。
其他的能力——記憶、反思、多 Agent、RAG——都是在此之上的擴展。

所以我們的架構會是:

使用者輸入

┌────────────────────────────────────────────┐
│   Agent 主迴圈                             │
│                                            │
│  1. 呼叫 LLM                               │
│  2. 解析輸出                                │
│  3. 如果要用工具 → 執行工具 → 把結果餵回 LLM  │
│  4. 如果是最終答案 → 回傳給使用者            │
│                                            │
└────────────────────────────────────────────┘

最終回答

就是這麼簡單。

二、定義工具

我們先定義兩個最簡單的工具:一個查天氣,一個查時間。

import json
from datetime import datetime

# ========== 工具定義 ==========

def get_weather(city: str, day: str = "今天") -> dict:
    """查詢指定城市的天氣(模擬)"""
    fake_weather = {
        ("台北", "今天"): {"condition": "多雲", "rain": "60%", "temp": "22-28°C"},
        ("台北", "明天"): {"condition": "晴時多雲", "rain": "20%", "temp": "24-30°C"},
        ("台中", "今天"): {"condition": "晴", "rain": "10%", "temp": "25-32°C"},
        ("高雄", "今天"): {"condition": "晴時多雲", "rain": "20%", "temp": "26-33°C"},
    }
    result = fake_weather.get((city, day))
    if result:
        return {"city": city, "day": day, **result}
    return {"error": f"查無 {city} {day} 的天氣資料"}

def get_current_time(city: str) -> dict:
    """查詢指定城市的當前時間(模擬)"""
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return {"city": city, "time": now}

# ========== 工具註冊表 ==========

TOOL_FUNCTIONS = {
    "get_weather": get_weather,
    "get_current_time": get_current_time,
}

這裡有兩個關鍵設計:

  • 每個工具都是一個普通的 Python 函式,接收參數、回傳結果。
  • 工具註冊表把工具名稱映射到實際的函式,方便後續查詢與執行。

注意:工具回傳的是結構化的 dict,而不是一段文字。
這讓 LLM 更容易解析與使用。

三、定義工具 Schema

接下來,我們要定義給 LLM 看的工具描述。
這些 Schema 會隨著每次請求一起送給 LLM,讓它知道有哪些工具可用。

TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查詢指定城市與日期的天氣資訊,回傳天氣狀況、降雨機率與溫度",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名稱,例如:台北、台中、高雄"
                    },
                    "day": {
                        "type": "string",
                        "enum": ["今天", "明天", "後天"],
                        "description": "日期,預設為今天"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "查詢指定城市的當前時間",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名稱,例如:台北、台中、高雄"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

Schema 的設計要點我們在上一篇已經詳細談過,這裡不再重複。
重點是:名稱清楚、描述具體、參數明確、用 enum 限制選項

四、建立 LLM 客戶端

我們用 OpenAI 的 API 作為範例。
如果你用的是其他模型,只需要替換這一段。

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

def call_llm(messages: list, tools: list = None) -> object:
    """呼叫 LLM,回傳完整的 response 物件"""
    kwargs = {
        "model": "gpt-4",
        "messages": messages,
        "temperature": 0.0,  # 降低隨機性,讓工具呼叫更穩定
    }
    if tools:
        kwargs["tools"] = tools
        kwargs["tool_choice"] = "auto"

    return client.chat.completions.create(**kwargs)

這裡有幾個關鍵設定:

  • temperature=0.0:降低隨機性,讓工具呼叫的決策更穩定。Agent 通常不需要創造力,需要的是可靠性。
  • tool_choice="auto":讓模型自行決定是否呼叫工具。也可以設為 "required" 強制呼叫,或指定特定工具。
  • tools:傳入工具 Schema,讓模型知道有哪些工具可用。

五、實作工具執行器

在執行工具之前,我們需要一個安全執行器,處理各種可能的錯誤。

import inspect

def execute_tool(function_name: str, arguments: dict) -> str:
    """
    安全執行工具,處理常見錯誤。
    回傳字串結果,方便直接放入訊息中。
    """
    # 1. 檢查工具是否存在
    if function_name not in TOOL_FUNCTIONS:
        return f"錯誤:找不到工具 {function_name}"

    func = TOOL_FUNCTIONS[function_name]

    # 2. 檢查必填參數
    sig = inspect.signature(func)
    required_params = [
        name for name, param in sig.parameters.items()
        if param.default is inspect.Parameter.empty
    ]
    missing = [p for p in required_params if p not in arguments]
    if missing:
        return f"錯誤:缺少必填參數 {missing}"

    # 3. 執行工具
    try:
        result = func(**arguments)
        # 把 dict 轉成 JSON 字串,方便 LLM 解析
        return json.dumps(result, ensure_ascii=False)
    except TypeError as e:
        return f"參數錯誤:{e}"
    except Exception as e:
        return f"執行錯誤:{e}"

這個執行器做了三件事:

  1. 檢查工具是否存在:防止 LLM 幻覺出不存在的工具。
  2. 檢查必填參數:防止 LLM 漏傳參數。
  3. 捕捉所有例外:把錯誤轉成字串回傳,而不是讓程式崩潰。

六、實作 Agent 主迴圈

現在來寫最核心的部分:Agent 主迴圈。

def run_agent(user_input: str, max_iterations: int = 5, verbose: bool = True) -> str:
    """
    最基礎的 Agent 主迴圈。

    參數:
        user_input: 使用者的問題
        max_iterations: 最大迭代次數,防止無限迴圈
        verbose: 是否印出中間過程

    回傳:
        最終回答字串
    """
    # 初始化對話歷史
    messages = [
        {
            "role": "system",
            "content": "你是一個樂於助人的助理,會使用工具來回答問題。"
                       "如果需要即時資訊或精確計算,請呼叫對應的工具。"
                       "當你已經有足夠資訊時,直接生成最終回答。"
        },
        {"role": "user", "content": user_input},
    ]

    for iteration in range(max_iterations):
        if verbose:
            print(f"\n{'='*50}")
            print(f"Iteration {iteration + 1}")
            print(f"{'='*50}")

        # 1. 呼叫 LLM
        response = call_llm(messages, tools=TOOLS_SCHEMA)
        message = response.choices[0].message
        messages.append(message)

        # 2. 檢查是否有工具呼叫
        if not message.tool_calls:
            # 沒有工具呼叫,代表 LLM 已經生成最終回答
            if verbose:
                print(f"\n[最終回答]\n{message.content}")
            return message.content

        # 3. 執行所有工具呼叫
        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            try:
                arguments = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError:
                arguments = {}

            if verbose:
                print(f"\n[工具呼叫] {function_name}({arguments})")

            # 執行工具
            result = execute_tool(function_name, arguments)

            if verbose:
                print(f"[工具結果] {result}")

            # 4. 把結果回傳給 LLM
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

    # 達到最大迭代次數
    return "已達到最大迭代次數,任務未完成。"

這就是一個最基礎的 Agent。
它的核心邏輯只有五步:

  1. 呼叫 LLM:把對話歷史與工具 Schema 一起送出。
  2. 檢查是否有工具呼叫:如果沒有,代表 LLM 已生成最終回答。
  3. 執行工具:遍歷所有 tool_calls,逐一執行。
  4. 把結果回傳給 LLM:把每個工具結果包成 tool 訊息。
  5. 重複:回到步驟 1,直到 LLM 生成最終回答或達到最大迭代次數。

七、實際執行

現在我們來跑幾個例子。

if __name__ == "__main__":
    print("\n" + "="*60)
    print("範例 1:查詢天氣")
    print("="*60)
    answer = run_agent("台北今天天氣如何?適合爬山嗎?")
    print(f"\n最終回答:{answer}")

    print("\n" + "="*60)
    print("範例 2:查詢時間")
    print("="*60)
    answer = run_agent("台中現在幾點?")
    print(f"\n最終回答:{answer}")

    print("\n" + "="*60)
    print("範例 3:不需要工具的問題")
    print("="*60)
    answer = run_agent("你好,你是誰?")
    print(f"\n最終回答:{answer}")

執行結果

============================================================
範例 1:查詢天氣
============================================================

==================================================
Iteration 1
==================================================

[工具呼叫] get_weather({'city': '台北', 'day': '今天'})
[工具結果] {"city": "台北", "day": "今天", "condition": "多雲", "rain": "60%", "temp": "22-28°C"}

==================================================
Iteration 2
==================================================

[最終回答]
台北今天多雲,降雨機率 60%,氣溫 22-28°C。
降雨機率偏高,山區午後可能會有陣雨,
整體來說不太適合爬山。如果一定要去,
建議攜帶雨具、穿著防滑鞋,並避免午後時段上山。

最終回答:台北今天多雲,降雨機率 60%,氣溫 22-28°C。
降雨機率偏高,山區午後可能會有陣雨,
整體來說不太適合爬山。如果一定要去,
建議攜帶雨具、穿著防滑鞋,並避免午後時段上山。

============================================================
範例 3:不需要工具的問題
============================================================

==================================================
Iteration 1
==================================================

[最終回答]
你好,我是一個能夠使用工具的 AI 助理。
我可以幫你查天氣、查時間,或回答各種問題。
請告訴我你想查詢什麼。

最終回答:你好,我是一個能夠使用工具的 AI 助理。
我可以幫你查天氣、查時間,或回答各種問題。
請告訴我你想查詢什麼。

你可以看到,Agent 的運作非常直觀:

  • 需要工具時,它會呼叫工具
  • 不需要工具時,它直接回答
  • 工具結果會影響下一步的推理

八、完整程式碼

把上面所有片段整合起來,就是一個完整的 Agent:

import json
import inspect
from datetime import datetime
from openai import OpenAI

# ========== 1. 初始化 LLM 客戶端 ==========
client = OpenAI(api_key="your-api-key")

# ========== 2. 定義工具 ==========
def get_weather(city: str, day: str = "今天") -> dict:
    fake_weather = {
        ("台北", "今天"): {"condition": "多雲", "rain": "60%", "temp": "22-28°C"},
        ("台北", "明天"): {"condition": "晴時多雲", "rain": "20%", "temp": "24-30°C"},
        ("台中", "今天"): {"condition": "晴", "rain": "10%", "temp": "25-32°C"},
        ("高雄", "今天"): {"condition": "晴時多雲", "rain": "20%", "temp": "26-33°C"},
    }
    result = fake_weather.get((city, day))
    if result:
        return {"city": city, "day": day, **result}
    return {"error": f"查無 {city} {day} 的天氣資料"}

def get_current_time(city: str) -> dict:
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return {"city": city, "time": now}

TOOL_FUNCTIONS = {
    "get_weather": get_weather,
    "get_current_time": get_current_time,
}

# ========== 3. 定義工具 Schema ==========
TOOLS_SCHEMA = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查詢指定城市與日期的天氣資訊,回傳天氣狀況、降雨機率與溫度",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名稱"},
                    "day": {
                        "type": "string",
                        "enum": ["今天", "明天", "後天"],
                        "description": "日期,預設為今天",
                    },
                },
                "required": ["city"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "查詢指定城市的當前時間",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名稱"},
                },
                "required": ["city"],
            },
        },
    },
]

# ========== 4. 工具執行器 ==========
def execute_tool(function_name: str, arguments: dict) -> str:
    if function_name not in TOOL_FUNCTIONS:
        return f"錯誤:找不到工具 {function_name}"

    func = TOOL_FUNCTIONS[function_name]
    sig = inspect.signature(func)
    required_params = [
        name for name, param in sig.parameters.items()
        if param.default is inspect.Parameter.empty
    ]
    missing = [p for p in required_params if p not in arguments]
    if missing:
        return f"錯誤:缺少必填參數 {missing}"

    try:
        result = func(**arguments)
        return json.dumps(result, ensure_ascii=False)
    except TypeError as e:
        return f"參數錯誤:{e}"
    except Exception as e:
        return f"執行錯誤:{e}"

# ========== 5. Agent 主迴圈 ==========
def run_agent(user_input: str, max_iterations: int = 5, verbose: bool = True) -> str:
    messages = [
        {
            "role": "system",
            "content": "你是一個樂於助人的助理,會使用工具來回答問題。"
                       "如果需要即時資訊或精確計算,請呼叫對應的工具。"
                       "當你已經有足夠資訊時,直接生成最終回答。",
        },
        {"role": "user", "content": user_input},
    ]

    for iteration in range(max_iterations):
        if verbose:
            print(f"\n{'='*50}")
            print(f"Iteration {iteration + 1}")
            print(f"{'='*50}")

        response = client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=TOOLS_SCHEMA,
            tool_choice="auto",
            temperature=0.0,
        )

        message = response.choices[0].message
        messages.append(message)

        if not message.tool_calls:
            if verbose:
                print(f"\n[最終回答]\n{message.content}")
            return message.content

        for tool_call in message.tool_calls:
            function_name = tool_call.function.name
            try:
                arguments = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError:
                arguments = {}

            if verbose:
                print(f"\n[工具呼叫] {function_name}({arguments})")

            result = execute_tool(function_name, arguments)

            if verbose:
                print(f"[工具結果] {result}")

            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": result,
            })

    return "已達到最大迭代次數,任務未完成。"

# ========== 6. 執行 ==========
if __name__ == "__main__":
    answer = run_agent("台北今天天氣如何?適合爬山嗎?")
    print(f"\n最終回答:{answer}")

這就是一個完整的 Agent。
你可以把 your-api-key 換成自己的 API key,直接進行測試。

九、這個基礎 Agent 的局限

我們用不到 150 行程式碼,就做出了一個能運作的 Agent。
但它有很多局限:

  1. 沒有記憶
    每次對話都是獨立的。
    Agent 不記得你上次問過什麼、喜歡什麼、之前查過什麼。

  2. 沒有長期規劃
    它只會根據當下的對話歷史決定下一步,沒有全局規劃能力。

  3. 沒有反思
    它不會檢視自己的錯誤,也不會在失敗時調整策略。

  4. 容易陷入迴圈
    如果 LLM 一直重複呼叫同一個工具卻無法成功,它會不斷重試直到達到最大步數。

  5. 錯誤處理很陽春
    工具執行失敗時,只是把錯誤訊息回傳給 LLM,沒有更細緻的重試或降級機制。

  6. 沒有安全護欄
    高風險操作沒有加入人類審核,敏感資訊沒有脫敏。

這些局限,正是後續文章要一一解決的問題。

十、逐步擴展的路線圖

我們的基礎 Agent 是一個起點。
接下來,我們會在這個基礎上逐步擴展:

擴展方向對應文章
讓 Agent 記住過去Agent 的記憶機制
讓 Agent 檢索外部知識RAG 完整解析
讓 Agent 自我修正Reflection(已在第二篇介紹)
讓多個 Agent 協作多 Agent 協作
讓 Agent 更安全安全與護欄
讓 Agent 能被評估評估與可觀測性

每一項擴展,都是在你現在看到的這個核心迴圈上,加上新的能力。

十一、總結:Agent 的核心比你想像的簡單

讓我們回顧這一篇的核心:

  • 最基礎的 Agent = LLM + 工具 + 一個迴圈
  • 核心流程:呼叫 LLM → 檢查工具呼叫 → 執行工具 → 把結果餵回 LLM → 重複
  • 工具定義:用 Python 函式定義工具,用 Schema 描述給 LLM 看
  • 工具執行器:檢查工具存在性、必填參數,並捕捉所有例外
  • 最大迭代次數:防止 Agent 陷入無限迴圈
  • 可擴展性:記憶、規劃、反思、多 Agent、安全、評估,都是在這個基礎上疊加

你現在已經有能力從零打造一個 Agent 了。
它雖然陽春,但五臟俱全。
理解了這個核心,你再去看 LangChain、AutoGPT、CrewAI 等框架,就會發現它們本質上都在做同一件事:用更精緻的方式,實作這個 LLM + 工具 + 迴圈 的組合。

下一篇,我們要給這個 Agent 加上第一個重要能力:記憶
它要如何記住使用者的偏好、過去的互動、學到的經驗?

下一篇預告

《Agent 的記憶機制:短期、長期與向量資料庫》

我們會解釋 Agent 為什麼需要記憶、短期記憶與長期記憶的差異、向量資料庫如何運作,以及如何用 Python 實作一個帶有記憶的 Agent。