在昨天的文章中,我們建立了Tool Calling的核心認知——LLM 本身不執行程式,而是透過Tool Schema充當「決策與參數生成大腦」,再由外部Python環境完成執行並將結果回傳閉環
今天我們進入實戰篇:在實際開發中,我們該如何使用LangChain / Python 定義自訂工具、處理多工具平行調用(Parallel Tool Calling),並建立強健的工具異常容錯機制?
假設我們要為客服Agent配備兩個核心工具:
query_inventory(查詢庫存):輸入商品名稱,查詢當前各倉庫庫存量calculate_shipping_fee(計算運費):輸入會員等級與配送區域,計算運費當使用者詢問:「我是 VIP 會員,想寄台北,另外幫我查一下降噪耳機跟機械鍵盤有沒有現貨?」模型應該能一次平行發起多個工具調用,並整合回傳
@tool 裝飾器定義型別安全工具在LangChain中,定義工具的最佳實踐是結合Python Type Hints與Docstring
框架會自動將其轉譯為標準的JSON Schema
from langchain_core.tools import tool
from typing import Literal
@tool
def query_inventory(item_name: str) -> str:
"""根據商品名稱查詢倉庫目前的即時庫存量。
Args:
item_name: 商品完整名稱或關鍵字,例如 '降噪耳機' 或 '機械鍵盤'
"""
# 模擬資料庫庫存查詢邏輯
inventory_db = {
"降噪耳機": 12,
"機械鍵盤": 0,
"無線滑鼠": 45
}
stock = inventory_db.get(item_name)
if stock is not None:
return f"商品【{item_name}】目前庫存量為: {stock} 件。"
return f"查無商品【{item_name}】的庫存記錄。"
@tool
def calculate_shipping_fee(
destination: Literal["台北", "台中", "高雄", "離島"],
membership_tier: Literal["NORMAL", "VIP", "VVIP"]
) -> str:
"""計算特定目的地與會員等級的配送運費。
Args:
destination: 配送目的地城市
membership_tier: 用戶會員等級
"""
if membership_tier in ["VIP", "VVIP"]:
return f"會員等級為 {membership_tier},享有全台免運優惠,運費為 0 元。"
fee_map = {"台北": 60, "台中": 80, "高雄": 80, "離島": 150}
fee = fee_map.get(destination, 100)
return f"配送至【{destination}】,一般會員運費為 {fee} 元。"
# 註冊可用工具清單
tools = [query_inventory, calculate_shipping_fee]
現代主流模型(如 GPT-4o 系列)支援平行工具調用
當單次提問包含多個查詢需求時,模型會在單一回應中一次輸出多組 tool_calls
import json
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
# 1. 初始化模型並綁定工具清單
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0)
llm_with_tools = llm.bind_tools(tools)
# 2. 用戶提出複合型問題
messages = [
HumanMessage(content="我是 VIP 會員,想寄台北,另外幫我查一下降噪耳機跟機械鍵盤有沒有現貨?")
]
# 3. LLM 進行推理並發出工具調用決策
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# 印出模型生成的工具調用清單 (將看到 3 個平行呼叫)
print(f"模型生成的工具調用數量: {len(ai_msg.tool_calls)}")
for call in ai_msg.tool_calls:
print(f"- 預計調用: {call['name']} | 參數: {call['args']}")
輸出範例:
模型生成的工具調用數量: 3
- 預計調用: calculate_shipping_fee | 參數: {'destination': '台北', 'membership_tier': 'VIP'}
- 預計調用: query_inventory | 參數: {'item_name': '降噪耳機'}
- 預計調用: query_inventory | 參數: {'item_name': '機械鍵盤'}
在執行工具時,API逾時、參數型別異常或資料庫斷線是常態
千萬不要讓Python直接拋出Exception中斷程式,應將錯誤轉化為 ToolMessage 餵回模型,讓模型具備自我修正或委婉告知使用者的機會
# 建立工具名稱對應字典
tools_by_name = {tool.name: tool for tool in tools}
# 4. 本地環境依序/並行執行工具,並進行異常攔截
for tool_call in ai_msg.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
tool_id = tool_call["id"]
try:
# 動態查找並執行工具
selected_tool = tools_by_name.get(tool_name)
if not selected_tool:
raise ValueError(f"未知的工具名稱: {tool_name}")
tool_output = selected_tool.invoke(tool_args)
except Exception as e:
# 異常攔截:將錯誤格式化為工具回傳訊息,防止系統崩潰
tool_output = f"工具執行失敗 (錯誤原因: {str(e)}),請嘗試調整參數或提示用戶。"
# 5. 包裝成 ToolMessage 寫回上下文
messages.append(
ToolMessage(
content=str(tool_output),
tool_call_id=tool_id
)
)
# 6. 將工具執行結果餵回 LLM,產生最終自然語言回覆
final_response = llm_with_tools.invoke(messages)
print("\n最終回答:")
print(final_response.content)
最終模型整合回傳範例:
您好!為您查詢到相關資訊如下:
- 運費資訊:您身為 VIP 會員,寄送至台北享有免運優惠(運費 0 元)
- 商品庫存:
- 降噪耳機:目前現貨剩餘 12 件
- 機械鍵盤:目前已無現貨(庫存為 0 件)
在將Tool Calling上線至生產環境前,請務必完成以下檢查:
| 檢驗維度 | 潛在風險 | 最佳工程實踐 |
|---|---|---|
| Docstring 描述 | 模型不知道何時觸發工具 | 明確撰寫「在何種條件下應調用」,並給出典型參數範例 |
| 參數嚴格驗證 | 模型傳入非法型別或自創字串 | 使用 Pydantic 或 Literal 限制枚舉值,啟用型別驗證 |
| 超時與重試 | 外部 API 延遲拖垮整條對話鏈 | 為每個 Tool 呼叫設定 Strict Timeout(如 3 秒),並限制重試次數 |
| 敏感權限確認 | Agent 未經確認直接執行危險操作(如扣款、刪除) | 引入 Human-in-the-Loop(人機協同審查) 機制攔截關鍵操作 |
透過自訂Tool Schema、平行調用處理與異常防禦管線,我們成功讓LLM擁有了與外部世界精準互動的能力
然而,當系統需求變得更加複雜——例如需要先查詢資料、經由人工審批、再依據結果分流到不同子任務時,單純的線性Tool Calling迴圈將難以應付。我們需要更具表達力的工作流狀態機(State Machine)
明天 【Day 10】甚麼是 LangGraph,我們將正式邁向進階Agent框架,探討現代AI工作流如何利用「圖(Graph)結構」來精確控制多步驟與分支決策!