LLM Tools / Function Calling / Intent Routing with FastAPI
昨天把人設與互動選單做起來後,今天要讓 AI 雲端情人真正「長手腳」:會去查即時資料(股匯金)、跑小計算、叫用你寫好的服務。做法有兩種:
• 做法 A(最通用):Intent Router —— 先讓 LLM 判斷「需求意圖」,再由後端呼叫對應工具。
• 做法 B(可選):Function Calling / JSON 模式 —— 讓 LLM 直接輸出結構化參數,後端據此執行工具。
⸻
🧠 觀念:把能力抽成「工具盒」(Function Box)
把「查金價、股市、天氣、彩券、翻譯、摘要、算式 …」都做成可重用的工具,每個工具只負責一件小事,介面長這樣:
輸入:標準化參數(symbol, city, date, …)
輸出:標準化結果(result, unit, ts, source, …)
前台只要給 LLM 一句話:「幫我看台股大盤」,Intent Router 會決定叫哪個工具,怎麼把回答「包裝成女友口吻」。
⸻
🧱 架構圖
⸻
✅ 做法 A:Intent Router(最穩、相依最少)
步驟 1:讓 LLM 說出「我要哪個工具」與必要參數(JSON)
# intent_router.py
from typing import Literal, Optional, Dict
from openai import OpenAI
import os, json
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url="https://free.v36.cm/v1")
# 你可以把這些 tool 名稱對應到你現有的 my_commands 模組
ToolName = Literal["stock", "forex", "gold", "weather", "lottery", "translate", "summary", "calc", "none"]
SYSTEM = (
"你是意圖分類器,請輸出 JSON,字段固定:"
'{"tool":"stock|forex|gold|weather|lottery|translate|summary|calc|none",'
'"args":{...},"reason":"簡述"}。不要輸出多餘文字。'
)
def classify_intent(utterance: str) -> Dict:
msg = [
{"role":"system","content": SYSTEM},
{"role":"user","content": f"句子:{utterance}。請輸出 JSON。"}
]
resp = client.chat.completions.create(
model="gpt-4o-mini", messages=msg, temperature=0, max_tokens=200
)
raw = resp.choices[0].message.content or "{}"
try:
return json.loads(raw)
except Exception:
return {"tool":"none","args":{},"reason":"parse_error"}
步驟 2:在主流程把結果路由到工具
# 在你的 app_fastapi.py 的某個 util 區域
from intent_router import classify_intent
from my_commands.stock.stock_gpt import stock_gpt
from my_commands.gold_gpt import gold_gpt
from my_commands.money_gpt import money_gpt
from my_commands.weather_gpt import weather_gpt
from my_commands.lottery_gpt import lottery_gpt
def call_tool(tool: str, args: dict) -> str:
try:
if tool == "stock":
sym = args.get("symbol") or args.get("ticker") or "大盤"
return stock_gpt(sym)
if tool == "forex":
code = (args.get("code") or "USD").upper()
return money_gpt(code)
if tool == "gold":
return gold_gpt()
if tool == "weather":
city = args.get("city") or "桃園市"
return weather_gpt(city)
if tool == "lottery":
game = args.get("game") or "大樂透"
return lottery_gpt(game)
if tool == "translate":
text = args.get("text") or ""
return f"【翻譯】\n{text}\n(此處可接你自己的翻譯器)"
if tool == "summary":
text = args.get("text") or ""
return f"【摘要】\n{text[:200]}..."
if tool == "calc":
expr = args.get("expr") or ""
try:
# 安全性:避免 eval,這裡只做簡單四則
import ast, operator as op
return f"結果:{safe_eval(expr)}"
except Exception:
return "算式不合法或過於複雜。"
except Exception as e:
return f"工具執行失敗:{e}"
return "目前還不會這件事。"
📌 好處
• 你的工具是你可控的(權限、速率、快取…都在你這邊),而不是把網址交給 LLM。
• 可以逐步加能力,不會把主對話搞得很複雜。
• Groq / OpenAI 任選,不依賴特定的 Function-Calling 規格。
⸻
🔁 做法 B(可選):Function Calling/JSON 模式
如果你偏好讓 LLM 直接產生「要叫的函式與參數」,可以用「JSON 嚴格模式」或「function-calling 風格的 system」。
重點是一樣:產出結構化參數 → 你來實際叫工具。
(Groq 目前以文字 JSON 最穩;OpenAI 有 tools/JSON mode 可用。)
⸻
💻 串進主流程(與 Day 8 情緒、Day 9 人設相容)
在 handle_message() 的「一般聊天」分支裡,加入這段:
(順序:判斷內建指令 → 否則做 intent classify → 呼叫工具 → 再把結果交給「人設 + 情緒」包裝)
# ……略(判斷內建指令/股票代碼之後)
else:
# 1) 意圖分類
intent = classify_intent(processed_msg)
tool = intent.get("tool","none")
args = intent.get("args",{})
# 2) 真的去執行工具
tool_result = call_tool(tool, args) if tool != "none" else None
if tool_result:
# 3) 注入人設與情緒,生成「女友口吻」回覆
sentiment = await analyze_sentiment(processed_msg)
# 把工具結果與使用者問題一起給 LLM,讓它包裝成對話語氣
messages = conversation_history[user_id][-MAX_HISTORY_LEN:] + [
{"role":"user","content": f"原始問題:{processed_msg}"},
{"role":"assistant","content": f"工具結果(給你參考、請改寫成口語):\n{tool_result}"}
]
reply_text = await get_reply_with_persona_and_sentiment(user_id, messages, sentiment)
else:
# 沒工具命中:走原本聊天
sentiment = await analyze_sentiment(processed_msg)
reply_text = await get_reply_with_persona_and_sentiment(
user_id, conversation_history[user_id][-MAX_HISTORY_LEN:], sentiment
)
⸻
🧪 測試腳本(Smoke Test)
# day10_smoketest.py
from intent_router import classify_intent
tests = [
"幫我看台股大盤",
"今天台北天氣如何?",
"美元對台幣現在多少?",
"明天大樂透開幾點?",
"請把這段變成英文:早安,今天天氣不錯。",
"2*(3+7)-5 等於?",
]
for t in tests:
j = classify_intent(t)
print(f"{t} -> {j}")
看輸出是否長這樣:
{"tool":"stock","args":{"symbol":"大盤"},"reason":"股市行情"}
{"tool":"weather","args":{"city":"台北"},"reason":"天氣查詢"}
{"tool":"forex","args":{"code":"USD"}}
{"tool":"lottery","args":{"game":"大樂透"}}
{"tool":"translate","args":{"text":"早安,今天天氣不錯。"}}
{"tool":"calc","args":{"expr":"2*(3+7)-5"}}
⸻
🧯 實戰細節
• 速率與快取:同一請求(同字串)在 60 秒內不要重查;股票/金價/匯率可加記憶體快取。
• 超時與退級:工具最多 3 秒;失敗就回一版「道歉 + 已改走靜態摘要」;LLM 也要準備 fallback(Groq↔OpenAI)。
• 輸入驗證:特別是 calc 類型,不要用 eval。
• 資料可信度:回答裡清楚標示「資料來源/時間」,避免誤導。
• 長度控制:先讓工具輸出結構化簡報,再由 LLM 用人設重寫成 3–6 句口語。
• 觀測:log 你每次意圖判斷與實際叫到的工具,便於調整 few-shot。
⸻
🎯 成果
到 Day 10,AI 雲端情人具備:
• 會看心情(情感分析)
• 有人格,有互動選單(人設 + Flex Menu)
• 會做事(Intent Router / Tools):看盤、匯率、金價、天氣、彩券、翻譯、小計算…
• 全流程仍口吻一致:先完成事,再讓 LLM 用女友風格說給你聽。