Key Findings
  • 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 註解

@Tooldescription 是 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 限制管理

掌握工具呼叫,繼續前進!

AI 會用工具了,但還不會記住對話。下一步學習 ChatMemory,讓 AI 擁有多輪對話記憶。

前往 Lesson 5:ChatMemory →