如何讓模型能呼叫外部工具,並親手實作「模型選工具 → 執行 → 回輸 → 答覆」的完整迴圈 核心原則
Tool Calling 是 Agent 的底層——Agent 不過是「會自己決定何時呼叫哪個工具的迴圈」

模型本身只會產生文字,不會真的算數、查資料、打 API。Tool Calling 的機制是:

07187-i14olny57p.png

1. 把工具的 JSON Schema 連同問題一起送給模型
2. 模型回答「我要呼叫 calculator,參數 {expression:'...'}」
3. 而程式(不是模型)真的去執行該函式
4. 再把執行結果回輸給模型
5. 模型用結果產出最終自然語言答覆

執行工具的是你的程式碼,不是模型。模型只負責「決定呼叫什麼、用什麼參數」。
理解這個執行的迴圈,下個模組 Agent(ReAct)其實就是把這個迴圈自動跑很多輪

工具定義與 JSON Schema

印出三個工具(calculator / get_weather / search_kb)的完整 Schema,這是模型實際看到的東西

python tool_calling_lab.py tools

# ─────────────────────────────────────────────────────────────────────────────
# 工具實作:每個工具是一個普通 Python 函式(帶型別標註與 docstring)
# ─────────────────────────────────────────────────────────────────────────────
def calculator(expression: str) -> str:
    """計算一個算術運算式,例如 (23 + 19) * 3。只支援 + - * / ( ) 與數字。"""
    if not re.fullmatch(r"[\d\s+\-*/().]+", expression):
        return "錯誤:運算式含不支援的字元"
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))  # 已用白名單字元限制,安全
    except Exception as e:
        return f"錯誤:{e}"


def get_weather(city: str) -> str:
    """查詢城市目前天氣。city 為城市名稱,例如 台北。"""
    fake = {"台北": "28°C 多雲", "東京": "22°C 晴", "高雄": "31°C 晴時多雲"}
    return fake.get(city, f"{city}:查無資料(mock)")


def search_kb(keyword: str) -> str:
    """在知識庫搜尋關鍵字並回傳摘要。"""
    return f"關於「{keyword}」的知識庫摘要(mock):這是一段示範用的檢索結果。"

TOOLS = {fn.__name__: fn for fn in (calculator, get_weather, search_kb)}

def build_schema(fn):
    sig = inspect.signature(fn)
    props, required = {}, []
    for name, param in sig.parameters.items():
        jtype = _PY2JSON.get(param.annotation, "string")
        props[name] = {"type": jtype, "description": f"{name} 參數"}
        if param.default is inspect.Parameter.empty:
            required.append(name)
    return {
        "type": "function",
        "function": {
            "name": fn.__name__,
            "description": (fn.__doc__ or "").strip().split("\n")[0],
            "parameters": {"type": "object", "properties": props, "required": required},
        },
    }

def all_schemas():
    return [build_schema(fn) for fn in TOOLS.values()]
def cmd_tools(_args):
    print("已註冊工具(模型實際看到的 JSON Schema):\n")
    print(json.dumps(all_schemas(), ensure_ascii=False, indent=2))
    print("\n工具設計原則:名稱動詞化、描述寫清楚『何時用』、參數最小化、回傳要可被模型理解。")

執行結果:

 註冊工具(模型實際看到的 JSON Schema):

[
  {
    "type": "function",
    "function": {
      "name": "calculator",
      "description": "計算一個算術運算式,例如 (23 + 19) * 3。只支援 + - * / ( ) 與數字。",
      "parameters": {
        "type": "object",
        "properties": {
          "expression": {
            "type": "string",
            "description": "expression 參數"
          }
        },
        "required": [
          "expression"
        ]
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "查詢城市目前天氣。city 為城市名稱,例如 台北。",
      "parameters": {
        "type": "object",
        "properties": {
          "city": {
            "type": "string",
            "description": "city 參數"
          }
        },
        "required": [
          "city"
        ]
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "search_kb",
      "description": "在知識庫搜尋關鍵字並回傳摘要。",
      "parameters": {
        "type": "object",
        "properties": {
          "keyword": {
            "type": "string",
            "description": "keyword 參數"
          }
        },
        "required": [
          "keyword"
        ]
      }
    }
  }
]

工具設計原則:名稱動詞化、描述寫清楚『何時用』、參數最小化、回傳要可被模型理解。

示範 build_schema() 如何從 Python 函式的型別標註 + docstring 自動推導出 Schema

python tool_calling_lab.py schema --fn calculator

def cmd_schema(args):
    fn = TOOLS.get(args.fn)
    if not fn:
        print(f"無此工具:{args.fn},可用:{list(TOOLS)}"); return
    print(f"從函式 `{args.fn}{inspect.signature(fn)}` 推導出的工具定義:\n")
    print(json.dumps(build_schema(fn), ensure_ascii=False, indent=2))

執行結果:

{
  "type": "function",
  "function": {
    "name": "calculator",
    "description": "計算一個算術運算式,例如 (23 + 19) * 3。只支援 + - * / ( ) 與數字。",
    "parameters": {
      "type": "object",
      "properties": {
        "expression": {
          "type": "string",
          "description": "expression 參數"
        }
      },
      "required": [
        "expression"
      ]
    }
  }
}

這裏有幾個點需要注意

  • 工具設計原則:名稱動詞化、description 寫清楚「何時該用這個工具」
  • 參數最小化、回傳要簡潔且可被模型理解。描述寫不好,模型就會選錯工具。

完整 Tool-Calling 迴圈

python tool_calling_lab.py loop --query "(23 + 19) * 3 等於多少?" --offline

# ─────────────────────────────────────────────────────────────────────────────
# 真實 API(OpenAI 相容 function calling;Kimi 同此介面)
# ─────────────────────────────────────────────────────────────────────────────
def _client():
    if os.environ.get("KIMI_API_KEY"):
        from openai import OpenAI
        return OpenAI(api_key=os.environ["KIMI_API_KEY"],
                      base_url=os.environ.get("KIMI_BASE_URL", "https://api.moonshot.cn/v1")), \
               os.environ.get("KIMI_MODEL", "kimi-k2.6")
    p = Path(__file__).resolve().parent.parent / "kimi.json"
    if p.exists():
        c = json.loads(p.read_text(encoding="utf-8"))
        if c.get("KIMI_API_KEY"):
            from openai import OpenAI
            return OpenAI(api_key=c["KIMI_API_KEY"],
                          base_url=c.get("KIMI_BASE_URL", "https://api.moonshot.cn/v1")), \
                   c.get("KIMI_MODEL", "kimi-k2.6")
    if os.environ.get("OPENAI_API_KEY"):
        from openai import OpenAI
        return OpenAI(), "gpt-4o-mini"
    return None, None


def real_loop(query):
    cli, model = _client()
    if cli is None:
        raise RuntimeError("無 OpenAI 相容憑證")
    messages = [{"role": "user", "content": query}]
    for _ in range(5):  # 限制最多 5 輪,避免無限工具循環
        resp = cli.chat.completions.create(model=model, messages=messages, tools=all_schemas())
        msg = resp.choices[0].message
        if not msg.tool_calls:
            return msg.content
        messages.append(msg)
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            result = TOOLS[tc.function.name](**args)
            print(f"  [工具] {tc.function.name}({args}) → {result}")
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
    return "(達到工具呼叫上限)"

def mock_decide_tools(query):
    """回傳 [(tool_name, args_dict), ...];空 list 代表不需工具、直接回答。"""
    calls = []
    if re.search(r"[\d]+\s*[+\-*/]", query):  # 含算式
        expr = re.search(r"[\d\s+\-*/().]+", query.replace("等於", "")).group().strip()
        calls.append(("calculator", {"expression": expr}))
    for city in ("台北", "東京", "高雄"):       # 並行:多城市各發一個 weather 呼叫
        if city in query and "天氣" in query or (city in query and ("度" in query or "幾度" in query)):
            calls.append(("get_weather", {"city": city}))
    if not calls and ("查" in query or "什麼是" in query):
        kw = query.replace("查", "").replace("什麼是", "").strip("??")
        calls.append(("search_kb", {"keyword": kw}))
    return calls

def mock_final_answer(query, results):
    joined = ";".join(f"{name} → {out}" for name, out in results)
    return f"根據工具結果({joined}),回答你的問題「{query}」。" if results \
        else f"這題不需要工具,直接回答:{query}"

def _run_loop(query, offline, parallel=False):
    use_offline = offline or _client()[0] is None
    if not use_offline:
        print("→ 走真實 API function calling\n")
        print("最終答覆:", real_loop(query)); return

    print("→ offline mock 模式\n")
    calls = mock_decide_tools(query)
    if parallel and len(calls) > 1:
        print(f"模型決定**並行**呼叫 {len(calls)} 個工具:")
    results = []
    for name, args in calls:
        out = TOOLS[name](**args)
        print(f"  [工具] {name}({args}) → {out}")
        results.append((name, out))
    if not calls:
        print("  (模型判斷不需要工具)")
    print("\n最終答覆:", mock_final_answer(query, results))

輸出結果:
(1)離綫模式

→ offline mock 模式

  [工具] get_weather({'city': '台北'}) → 28°C 多雲

最終答覆: 根據工具結果(get_weather → 28°C 多雲),回答你的問題「台北今天天氣如何?」。

(2)API模式

→ 走真實 API function calling

  [工具] get_weather({'city': '台北'}) → 28°C 多雲
最終答覆: 台北今天的天氣是 **28°C**,**多雲**。天氣還算舒適,但外出記得帶件薄外套以防室內冷氣較強喔!

(3)測試另外case

python tool_calling_lab.py loop --query "什麼是RAG?
→ 走真實 API function calling

  [工具] search_kb({'keyword': 'RAG'}) → 關於「RAG」的知識庫摘要(mock):這是一段示範用的檢索結果。
最終答覆: **RAG** 的全名是 **Retrieval-Augmented Generation(檢索增強生成)**,是一種結合「資訊檢索」與「文本生成」的 AI 技術架構,主要用來提升大型語言模型(LLM)回答的準確性與時效性。

---

### 核心概念

傳統的語言模型只能依賴訓練時學到的「參數記憶」來回答問題,這會導致兩個問題:

1. **知識時效性不足**:模型不知道訓練資料截止後發生的事。
2. **幻覺(Hallucination)**:模型會「腦補」出不存在的資訊。

RAG 的做法是:**在生成回答之前,先從外部知識庫(如文件、資料庫、網頁)檢索相關資料,再把這些資料連同原本的問題一起餵給模型,讓模型基於檢索到的證據來生成答案。**

---

### 運作流程

大致可分為三步驟:

1. **檢索(Retrieval)**
   將使用者的問題轉換成向量(Embedding),到向量資料庫或搜尋引擎中找出最相關的若干篇文件或段落。

2. **增強(Augmentation)**
   把檢索到的內容與使用者的問題組合成一個更豐富的提示(Prompt),例如:「請根據以下資料回答問題:[檢索結果];問題是:[使用者問題]」。

3. **生成(Generation)**
   語言模型根據這個提示,引用檢索到的資料來產生最終回答。
...
### 主要優勢

| 優勢 | 說明 |
|------|------|
| **減少幻覺** | 答案有外部資料佐證,可信度更高。 |
| **知識可更新** | 不用重新訓練模型,只需更新外部資料庫即可。 |
| **可解釋性** | 可以回朔模型參考了哪些文件來得出答案。 |
| **成本效益** | 相較於動輒數百萬美元的模型微調,建立向量資料庫的成本低很多。 |

簡單來說,RAG 就像給語言模型配了一位「圖書館管理員」:先幫它找到相關書籍,再請它根據書本內容作答,而不是憑空想像。這也是目前企業導入生成式 AI 最常採用的架構之一。

並行工具調用

python tool_calling_lab.py parallel --query "台北跟東京現在幾度?"
→ 走真實 API function calling

  [工具] get_weather({'city': '台北'}) → 28°C 多雲
  [工具] get_weather({'city': '東京'}) → 22°C 晴
最終答覆: 台北現在 **28°C**,多雲;東京現在 **22°C**,晴天。

無標籤

關注作者:

新增評論