- Function Calling 讓 AI 從「只能回答問題」升級為「能操作真實世界」——查資料庫、呼叫 API、執行計算,AI 判斷何時需要工具、傳什麼參數,你的程式負責執行
- Spring AI 的
@Tool+@ToolParam註解讓工具定義極其簡潔——一個普通的 Kotlin 方法加上註解就是一個 AI 工具,不需要複雜的 JSON Schema 定義 - AI 具備自動選擇能力:即使註冊了多個工具,AI 會根據使用者問題自動判斷是否需要工具、該用哪個工具,不需要的問題完全不會觸發呼叫
- 透過工具鏈(Tool Chain),AI 可以串接多個工具完成複雜任務——例如先查訂單狀態、再查今天日期、最後計算預估到貨時間
為什麼 AI 需要工具?
在前三堂課中,AI 的能力僅限於「根據訓練資料回答問題」。但 AI 有幾個天生的限制[2]:
- 知識截止日 — 不知道今天的天氣、即時股價、最新新聞
- 無法存取外部系統 — 不能查你的資料庫、ERP、CRM
- 無法執行動作 — 不能寄信、下單、更新狀態
- 數學不精確 — 複雜計算可能出錯(它是語言模型,不是計算機)
Function Calling(也叫 Tool Calling)[1]就是讓 AI 突破這些限制的機制。核心流程是:
使用者: "台北現在幾度?"
│
▼
AI 思考: "我需要即時天氣資料,應該使用 getWeather 工具"
│
▼
AI 輸出: 呼叫 getWeather(city="台北") ← AI 決定要呼叫什麼、傳什麼參數
│
▼
你的程式: 執行 getWeather("台北") ← 你的程式負責真正的 API 呼叫
│
▼
回傳結果: { temperature: 28, condition: "晴天" }
│
▼
AI 組合回應: "台北現在 28 度,天氣晴朗,適合出門走走!"
安全設計: AI 不會直接呼叫外部 API——它只是告訴你的程式「我需要這個工具」,由你的程式決定是否執行。這確保了安全性與可控性。
Step 1:環境準備
開啟課程專案的 Lesson4/Lesson4_FunctionCalling.ipynb:
@file:DependsOn("org.springframework.ai:spring-ai-openai:1.0.0")
@file:DependsOn("org.springframework.ai:spring-ai-client-chat:1.0.0")
@file:DependsOn("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
@file:DependsOn("org.slf4j:slf4j-simple:2.0.16")
import org.springframework.ai.openai.OpenAiChatModel
import org.springframework.ai.openai.api.OpenAiApi
import org.springframework.ai.chat.client.ChatClient
import org.springframework.ai.tool.annotation.Tool
import org.springframework.ai.tool.annotation.ToolParam
val apiKey = System.getenv("OPENAI_API_KEY")
?: error("請先設定 OPENAI_API_KEY(參考 Lesson 0)")
val openAiApi = OpenAiApi.builder().apiKey(apiKey).build()
val chatModel = OpenAiChatModel.builder().openAiApi(openAiApi).build()
val chatClient = ChatClient.builder(chatModel).build()
println("✓ 環境準備完成")
Step 2:第一個工具——日期時間查詢
定義一個最簡單的工具:取得目前的日期和時間。只需在方法上加 @Tool 註解:
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class DateTimeTools {
@Tool(description = "取得目前的日期和時間,包含星期幾")
fun getCurrentDateTime(): String {
val now = LocalDateTime.now()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss (EEEE)")
return now.format(formatter)
}
}
使用 .tools() 將工具註冊到 ChatClient:
val answer = chatClient.prompt()
.system("你是一位助手,請用繁體中文回答。")
.user("現在幾點了?今天星期幾?")
.tools(DateTimeTools())
.call().content()
println(answer)
預期輸出:
現在是 22:48,今天是星期三。
關鍵觀察——如果問一個不需要工具的問題,AI 不會呼叫任何工具:
val noToolAnswer = chatClient.prompt()
.system("你是一位助手。")
.user("什麼是 Spring AI?")
.tools(DateTimeTools()) // 工具已註冊,但 AI 判斷不需要
.call().content()
println(noToolAnswer)
// AI 直接用自身知識回答,不會觸發 getCurrentDateTime()
Step 3:理解 @Tool 與 @ToolParam 註解
@Tool 的 description 是 AI 用來判斷「何時該使用這個工具」的關鍵資訊[1]。寫給 AI 看,不是寫給人看:
@Tool(description = "工具說明——AI 根據這段文字決定何時使用此工具")
fun myTool(
@ToolParam(description = "參數說明——AI 根據這段文字決定傳什麼值")
param1: String,
@ToolParam(description = "參數說明")
param2: Int
): String {
// 你的邏輯
return "結果"
}
好的 description vs 壞的 description:
| 品質 | description | 問題 |
|---|---|---|
| ❌ 差 | "查詢資料" | 太模糊,AI 不知道什麼時候該用 |
| ❌ 差 | "getOrderById" | 方法名不是說明 |
| ✅ 好 | "根據訂單編號查詢訂單詳情,包含商品、金額和出貨狀態。訂單編號格式為 ORD-XXX" | 清楚說明功能、回傳內容、參數格式 |
Step 4:多工具協作——計算器
定義一組數學計算工具,觀察 AI 如何自動選擇並串接多個工具完成一個任務:
class CalculatorTools {
@Tool(description = "計算兩個數字的加法")
fun add(
@ToolParam(description = "第一個數字") a: Double,
@ToolParam(description = "第二個數字") b: Double
): Double = a + b
@Tool(description = "計算兩個數字的乘法")
fun multiply(
@ToolParam(description = "第一個數字") a: Double,
@ToolParam(description = "第二個數字") b: Double
): Double = a * b
@Tool(description = "計算一個數字的百分比,例如 200 的 15% 就是 30")
fun percentage(
@ToolParam(description = "原始數字") value: Double,
@ToolParam(description = "百分比數值") percent: Double
): Double = value * percent / 100.0
}
val calcAnswer = chatClient.prompt()
.system("你是一位購物助手,幫客戶計算價格。")
.user("我買了 3 件商品,每件 1280 元,打 85 折,總共多少錢?")
.tools(CalculatorTools())
.call().content()
println(calcAnswer)
AI 的思考過程:
1. 先算總價: multiply(3, 1280) → 3840
2. 再算折扣: percentage(3840, 85) → 3264
3. 組合回答: "3 件商品總價 3,840 元,打 85 折後為 3,264 元。"
AI 自動將一個複雜計算拆解成兩步工具呼叫——這就是工具鏈的雛形。
Step 5:企業實戰——訂單查詢系統
模擬一個企業級場景:AI 客服需要查詢訂單資料庫。先建立模擬資料:
data class Order(
val orderId: String,
val customer: String,
val items: List<String>,
val amount: Double,
val status: String
)
val orderDatabase = mapOf(
"ORD-001" to Order("ORD-001", "王小明",
listOf("MacBook Pro", "滑鼠"), 52800.0, "已出貨"),
"ORD-002" to Order("ORD-002", "李小華",
listOf("iPhone 16 Pro"), 36900.0, "處理中"),
"ORD-003" to Order("ORD-003", "張大偉",
listOf("AirPods Pro", "保護殼"), 8790.0, "已完成")
)
定義三個查詢工具,分別對應不同的查詢方式:
class OrderTools {
@Tool(description = "根據訂單編號查詢訂單詳情,包含商品清單、金額和出貨狀態。訂單編號格式為 ORD-XXX")
fun getOrderById(
@ToolParam(description = "訂單編號,格式為 ORD-XXX") orderId: String
): String {
val order = orderDatabase[orderId]
?: return "找不到訂單 $orderId"
return """
訂單編號: ${order.orderId}
客戶: ${order.customer}
商品: ${order.items.joinToString("、")}
金額: NT$ ${order.amount}
狀態: ${order.status}
""".trimIndent()
}
@Tool(description = "根據客戶姓名查詢該客戶的所有訂單")
fun getOrdersByCustomer(
@ToolParam(description = "客戶姓名") customerName: String
): String {
val orders = orderDatabase.values.filter { it.customer == customerName }
if (orders.isEmpty()) return "找不到 $customerName 的訂單"
return orders.joinToString("\n\n") { order ->
"訂單 ${order.orderId}: ${order.items.joinToString("、")} — NT$ ${order.amount}(${order.status})"
}
}
@Tool(description = "取得所有訂單的統計摘要,包含訂單總數、總金額、各狀態的數量")
fun getOrderSummary(): String {
val total = orderDatabase.values.sumOf { it.amount }
val statusCounts = orderDatabase.values.groupBy { it.status }
.map { "${it.key}: ${it.value.size} 筆" }
return """
訂單總數: ${orderDatabase.size} 筆
總金額: NT$ $total
狀態分佈: ${statusCounts.joinToString("、")}
""".trimIndent()
}
}
測試 AI 的自動路由能力——三個不同類型的問題:
// 問題 1:用訂單編號查詢 → 自動選擇 getOrderById
val q1 = chatClient.prompt()
.user("幫我查一下 ORD-002 的狀態")
.tools(OrderTools())
.call().content()
println("Q1: $q1")
// 問題 2:用客戶姓名查詢 → 自動選擇 getOrdersByCustomer
val q2 = chatClient.prompt()
.user("王小明有哪些訂單?")
.tools(OrderTools())
.call().content()
println("\nQ2: $q2")
// 問題 3:要求統計 → 自動選擇 getOrderSummary
val q3 = chatClient.prompt()
.user("目前有多少訂單?總金額多少?")
.tools(OrderTools())
.call().content()
println("\nQ3: $q3")
AI 能根據問題的語意自動選擇正確的工具,不需要你寫任何路由邏輯。
Step 6:工具鏈——多步驟任務
真正強大的是同時註冊多個工具類別,讓 AI 串接多個工具完成複雜任務:
val chainAnswer = chatClient.prompt()
.system("""你是一位客服助手,提供訂單查詢與物流預估服務。
已出貨的訂單通常 2-3 個工作天到貨,處理中的訂單需要 5-7 個工作天。""")
.user("請查一下 ORD-001 的狀態,順便告訴我今天日期,預估什麼時候到貨?")
.tools(OrderTools(), DateTimeTools()) // 同時註冊兩組工具
.call().content()
println(chainAnswer)
AI 的工具鏈執行過程:
Step 1: 呼叫 getOrderById("ORD-001")
→ 取得: 狀態「已出貨」,商品 MacBook Pro + 滑鼠
Step 2: 呼叫 getCurrentDateTime()
→ 取得: 2026-03-20 (星期五)
Step 3: AI 綜合分析
→ 已出貨 + 2-3 工作天 + 今天週五
→ 預估下週一至週二到貨
預期輸出:
ORD-001 目前狀態為「已出貨」,內含 MacBook Pro 與滑鼠,金額 NT$ 52,800。
今天是 2026 年 3 月 20 日(星期五),已出貨的訂單通常 2-3 個工作天到貨,
預估下週一(3/23)至週二(3/24)送達。
Step 7:實戰——智慧客服助手
將所有工具整合成一個可重用的客服函式:
fun askCustomerService(question: String): String {
return chatClient.prompt()
.system("""你是「AI 購物助手」,一位親切的客服人員。
你可以查詢訂單、計算價格、查看時間。
回答時使用友善的語氣,適時加入實用建議。""")
.user(question)
.tools(OrderTools(), DateTimeTools(), CalculatorTools())
.call().content() ?: "(無回應)"
}
// 模擬客戶對話
val questions = listOf(
"ORD-002 的訂單到哪了?",
"張大偉之前買了什麼?花了多少錢?",
"如果我要買 5 台 iPhone 16 Pro,總共多少錢?"
)
questions.forEach { q ->
println("🧑 客戶: $q")
println("🤖 助手: ${askCustomerService(q)}")
println()
}
三個問題分別觸發不同的工具路徑:
| 客戶問題 | AI 選擇的工具 | 原因 |
|---|---|---|
| ORD-002 的訂單到哪了? | getOrderById("ORD-002") | 提到訂單編號 |
| 張大偉之前買了什麼? | getOrdersByCustomer("張大偉") | 提到客戶姓名 |
| 買 5 台 iPhone 16 Pro 多少錢? | multiply(5, 36900) | 需要數學計算 |
工具設計最佳實踐
好的工具設計決定了 AI 能否正確使用它們[3]:
| 原則 | 說明 | 範例 |
|---|---|---|
| 單一職責 | 每個工具只做一件事 | 查訂單和改訂單分成兩個工具 |
| 清晰描述 | description 要讓 AI 能判斷何時使用 | 包含功能、回傳內容、參數格式 |
| 明確參數 | 每個參數都用 @ToolParam 描述 | @ToolParam("訂單編號,格式 ORD-XXX") |
| 回傳字串 | AI 理解字串最好 | 回傳格式化的文字,不是原始物件 |
| 友善錯誤 | 回傳錯誤訊息,不要拋例外 | return "找不到訂單 $id" |
企業應用場景: 資料庫查詢、外部 API(天氣、匯率、物流追蹤)、內部系統(ERP、CRM、HR 系統)、檔案操作、通知發送(Email、Slack、LINE)。Function Calling 是建構 AI Agent 的基礎能力。
本課重點回顧
| 概念 | 重點 |
|---|---|
| Tool Calling | 讓 AI 呼叫你定義的函式,突破知識與能力限制 |
| @Tool | 標記方法為工具,description 是 AI 判斷依據 |
| @ToolParam | 描述參數,幫助 AI 正確傳值 |
| .tools() | 在 ChatClient 中註冊工具 |
| 自動選擇 | AI 根據問題自動判斷是否需要工具、該用哪個 |
| 工具鏈 | AI 可以依序呼叫多個工具完成複雜任務 |
| 設計原則 | 單一職責、清晰描述、明確參數、回傳字串 |
下一步:Lesson 5 — Chat Memory
目前每次對話都是「無記憶」的——AI 不記得你上一句說了什麼。下一堂課將介紹 ChatMemory,讓 AI 擁有對話記憶:
- MessageChatMemoryAdvisor — 自動管理對話歷史
- 多輪對話 — AI 記得上下文,不用每次重複背景資訊
- 記憶策略 — 視窗記憶、摘要記憶、Token 限制管理


