Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 05:賦予 AI 動手能力:工具系統入門

在前一篇文章中,我們學習了如何整合多個 LLM 供應商,能夠靈活選擇最適合的模型 (供應商)。但到目前為止,我們的 Agent 只能進行對話,還無法執行具體的操作。今天我們將學習 Koog 框架的工具系統基礎,讓 AI Agent 具備實際的操作能力

什麼是工具系統?

在 AI Agent 的世界中,工具(Tool) 是連接 AI 思維與現實世界的橋樑。就像人類使用計算機、電話、筆記本來完成各種任務一樣,AI Agent 也需要工具來執行具體操作

Koog 的工具系統設計理念很簡單

讓 AI Agent 能夠安全且可控地執行實際操作

為什麼需要工具系統?

  • 擴展能力:讓 AI 能執行計算、查詢資料等實用功能
  • 保持安全:所有操作都通過明確定義的介面執行
  • 易於控制:開發者可以精確控制 Agent 能做什麼
  • 提升價值:讓 AI 從「聊天機器人」變成「實用助手」

工具系統與 Function Calling 的關係

如果你有使用其他 AI 開發框架的經驗,你可能會發現 Koog 的「工具(Tool)」概念似曾相識。沒錯!Koog 的工具系統本質上就是 LLM Function Calling 的實作

在不同的語言和框架中,這個概念有著不同的名稱

框架/語言名稱描述
Koog (Kotlin)Tool使用 SimpleTool 或註解方式定義
OpenAI APIFunction Calling直接定義 JSON Schema
LangChain (Python)Tool使用裝飾器或繼承 BaseTool
AutoGenFunction Calling註冊 Python 函數
Semantic Kernel (C#)Plugin/Function使用屬性標註方法

核心概念都相同:讓 LLM 能夠呼叫預先定義的函數來執行具體操作,並將結果整合到對話流程中

Koog 的優勢在於

  • 類型安全:Kotlin 的靜態類型檢查
  • 結構化:清晰的工具定義架構
  • 靈活性:支援 Class-based 和 Annotation-based 兩種方式
  • 整合性:與 Kotlin 生態系統無縫整合

Koog 內建工具介紹

Koog 框架提供了三個基本的內建工具,讓我們先了解這些工具的基本用法

SayToUser:向使用者說話

SayToUser 工具讓 Agent 能夠主動向使用者輸出訊息

suspend fun main() {
    // 註冊工具
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
        systemPrompt = "你是友善的 AI 助手,請使用 SayToUser 工具與使用者對話",
        toolRegistry = toolRegistry,
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    )

    agent.run("你好!")
}

執行 AI 回應內容

Agent says: 你好!很高興見到你,有什麼我可以幫助你的嗎?

AskUser:向使用者提問

AskUser 工具讓 Agent 能夠主動詢問使用者問題

suspend fun main() {

    // 註冊工具
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
        tool(AskUser)
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
//        systemPrompt = "請使用 AskUser 詢問使用者的姓名,然後用 SayToUser 打招呼。",
        systemPrompt = "請先詢問使用者的姓名,然後在跟使用者打招呼。",
        toolRegistry = toolRegistry,
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    )

    agent.run("你好!")
}

實測,有沒有在 system prompt 裡寫明使用 工具,得到的結果都是一樣的

執行 AI 回應內容

您好!請問您的姓名是?
Cash
Agent says: 您好,Cash!很高興見到您,有什麼我可以幫助您的嗎?

ExitTool:結束對話

ExitTool 工具讓 Agent 能夠優雅地結束對話

suspend fun main() {

    // 註冊工具
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
        tool(AskUser)
        tool(ExitTool)
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
//        systemPrompt = "請使用 AskUser 詢問使用者的姓名,然後用 SayToUser 打招呼。",
        systemPrompt = "請先詢問使用者的姓名,然後在跟使用者打招呼。",
        toolRegistry = toolRegistry,
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    )

    agent.run("你好!")
}

執行 AI 回應內容

你好!請問您的姓名是?
> Cash
Agent says: Cash,您好!很高興認識您。請問有什麼我可以幫助您的嗎?
請問您今天想聊些什麼,或者有什麼需要幫忙的?
> 沒有

自定義工具開發 (Class-based)

內建工具雖然實用,但真正的威力在於開發自定義工具。Koog 提供了 SimpleTool 抽象類,讓我們能夠輕鬆建立自己的工具

SimpleTool 基礎結構

每個自定義工具都需要四個基本要素

  • 參數類別:定義工具接受的參數
  • 參數序列化器:處理參數的序列化
  • 工具描述:告訴 LLM 如何使用這個工具
  • 執行邏輯:工具的實際功能

序列化設定

先確認 build.gradle.kts 中是否已加入 serializtion plugin

plugins {
    kotlin("jvm") version "2.2.0"
    // 確認有加上相關的序列化插件
    kotlin("plugin.serialization") version "2.2.0"
}

建立 SimpleTool 加法工具

object AddTool : SimpleTool<AddTool.Args>() {

    // 1. 定義參數類別
    @Serializable
    data class Args(val number1: Int, val number2: Int) : ToolArgs

    // 2. 設定序列化器
    override val argsSerializer = Args.serializer()

    // 3. 定義工具描述
    override val descriptor = ToolDescriptor(
        name = "add_numbers",
        description = "將兩個數字相加",
        requiredParameters = listOf(
            ToolParameterDescriptor(
                name = "number1",
                description = "第一個數字",
                type = ToolParameterType.Integer
            ),
            ToolParameterDescriptor(
                name = "number2",
                description = "第二個數字",
                type = ToolParameterType.Integer
            )
        )
    )

    // 4. 實作執行邏輯
    override suspend fun doExecute(args: Args): String {
        return try {
            val result = args.number1 + args.number2
            "計算結果:${args.number1} + ${args.number2} = $result"
        } catch (e: Exception) {
            "計算錯誤:${e.message}"
        }
    }
}

使用 SimpleTool 工具

suspend fun main() {
    // 註冊工具
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
        tool(AddTool)
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
        systemPrompt = """
            你是一個數學助手。當使用者需要計算兩個數字相加時:
            1. 使用 add_numbers 工具進行計算
            2. 使用 SayToUser 工具告訴使用者結果

            請用友善的正體中文回應
        """.trimIndent(),
        toolRegistry = toolRegistry,
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    )

    // 測試加法功能
    agent.run("請幫我計算 15 + 27")
}

執行這個程式時,AI Agent 會

  • 識別使用者想要計算 15 + 27
  • 透過 doExecute 來計算結果

我覺得這裡和其它語言的 tool 方式不太一樣,不用明確的實作工具的方法,而是實作 doExecute,所以程式碼裡面可以沒有 addNumbers 函數

  • 使用 SayToUser 工具告訴使用者答案是 42

執行 AI 回應內容

Agent says: 1527 的結果是 42

自定義工具開發 (Annotation-based)

除了 Class-based 的方式,Koog 還提供了更簡潔的 Annotation-based 工具開發方式。這種方法使用註解來自動生成工具描述,讓開發者能夠以更宣告式的方式建立工具

什麼是 Annotation-based 工具?

Annotation-based 工具提供了一種宣告式的方式來將函數公開為 LLM 可用的工具。透過使用註解,我們可以將現有的函數轉換為 LLM 可使用的工具,而無需手動實作工具描述

核心註解介紹

  • @Tool:標記函數為可供 LLM 使用的工具
  • @LLMDescription:為工具、參數提供描述資訊

建立 ToolSet

首先,我們需要建立一個繼承 ToolSet 的類別

@LLMDescription("數學計算工具集")
class MathToolSet : ToolSet {

    @Tool
    @LLMDescription("將兩個數字相加")
    fun addNumbers(
        @LLMDescription("第一個數字")
        number1: Int,
        @LLMDescription("第二個數字")
        number2: Int
    ): String {
        return try {
            val result = number1 + number2
            "計算結果:$number1 + $number2 = $result"
        } catch (e: Exception) {
            "計算錯誤:${e.message}"
        }
    }
}

使用 Annotation-based 工具

suspend fun main() {
    // 註冊工具集
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
        // 使用 tools 方法註冊
        tools(MathToolSet())
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
        systemPrompt = """
            你是一個數學助手。當使用者需要計算兩個數字相加時:
            1. 使用 addNumbers 工具進行計算
            2. 使用 SayToUser 工具告訴使用者結果

            請用友善的正體中文回應
        """.trimIndent(),
        toolRegistry = toolRegistry,
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    )

    // 測試加法功能
    agent.run("請幫我計算 25 + 17")
}

執行 AI 回應內容

Agent says: 2517 的結果是 42

與 Class-based 方式的比較

特性Class-basedAnnotation-based
語法複雜度較複雜,需要實作多個部分較簡潔,使用註解即可
參數定義需要定義 Args 類別和序列化器直接在函數參數上使用註解
工具描述手動定義 ToolDescriptor自動從註解生成
適用場景複雜工具,需要精細控制簡單工具,快速開發

進階功能:多工具組合

一個 ToolSet 可以包含多個工具

@LLMDescription("數學計算工具集")
class MathToolSet : ToolSet {

    @Tool
    @LLMDescription("將兩個數字相加")
    fun addNumbers(
        @LLMDescription("第一個數字")
        number1: Int,
        @LLMDescription("第二個數字")
        number2: Int
    ): String {
        return try {
            val result = number1 + number2
            "計算結果:$number1 + $number2 = $result"
        } catch (e: Exception) {
            "計算錯誤:${e.message}"
        }
    }

    @Tool
    @LLMDescription("將兩個數字相乘")
    fun multiplyNumbers(
        @LLMDescription("第一個數字")
        number1: Int,
        @LLMDescription("第二個數字")
        number2: Int
    ): String {
        return try {
            val result = number1 * number2
            "乘法結果:$number1 × $number2 = $result"
        } catch (e: Exception) {
            "計算錯誤:${e.message}"
        }
    }

    @Tool
    @LLMDescription("檢查數字是否為質數")
    fun isPrime(
        @LLMDescription("要檢查的數字")
        number: Int
    ): String {
        if (number <= 1) return "$number 不是質數"

        for (i in 2..sqrt(number.toDouble()).toInt()) {
            if (number % i == 0) return "$number 不是質數"
        }
        return "$number 是質數"
    }
}
suspend fun main() {
    // 註冊工具集
    val toolRegistry = ToolRegistry {
        tool(SayToUser)
        // 使用 tools 方法註冊
        tools(MathToolSet())
    }

    val agent = AIAgent(
        executor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
        systemPrompt = """
            你是一個數學助手。你有一些數學工具可以使用
            請用友善的正體中文回應
        """.trimIndent(),
        // systemPrompt = """
        //     你是一個數學助手。你有一些數學工具可以使用:
        //     1. 將兩個數字相加 - addNumbers
        //     2. 將兩個數字相乘 - multiplyNumbers
        //     3. 檢查數字是否為質數 - isPrime
        //     當使用者有相關的數學問題時,請選擇相對應的工具來處理和回應
        //     請用友善的正體中文回應
        // """.trimIndent(),
        toolRegistry = toolRegistry,
        // llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
        llmModel = OpenAIModels.CostOptimized.GPT4_1
    )

    // 測試加法功能
    agent.run("請幫我計算 25 + 17")
    // 測試乘法功能
    agent.run("請幫我計算 4 * 5")
    // 測試質數功能
    agent.run("請問一下 5 是不是質數")
}

這裡我試了 4.1 mini 好幾次,都沒有辦法順利執行,就算是把工具描述的很清楚也是一樣,最後改用 4.1 就可以成功執行

如果有遇到工具沒有辦法順序執行的,可以嘗試換一個 (更聰明的) 模型試試看

執行 AI 回應內容

Agent says: 25 + 17 的答案是 42Agent says: 4 × 5 = 20
Agent says: 5 是質數,因為它只有 1 和自己可以整除。

選擇建議

使用 Class-based

  • 需要複雜的參數驗證
  • 工具邏輯較為複雜
  • 需要精細控制工具行為

使用 Annotation-based

  • 快速原型開發
  • 工具邏輯相對簡單
  • 偏好宣告式的程式設計風格
  • 需要將現有函數快速轉換為工具

工具開發基本原則

保持簡單

每個工具都應該專注於一個明確的任務

// ✅ 好的設計:功能明確
object AddTool : SimpleTool<AddTool.Args>() {
    // 只做加法運算
}

// ❌ 避免的設計:功能過於複雜
object MathTool : SimpleTool<MathTool.Args>() {
    // 同時處理加減乘除、三角函數、統計等
}

明確的參數描述

讓 LLM 能夠正確理解如何使用工具

ToolParameterDescriptor(
    name = "number1",
    description = "第一個數字", // 清楚說明參數用途
    type = ToolParameterType.Integer
)

基本錯誤處理

提供有意義的錯誤訊息

override suspend fun doExecute(args: Args): String {
    return try {
        // 執行工具邏輯
        performOperation(args)
    } catch (e: Exception) {
        "操作失敗:${e.message}"
    }
}

總結

今天我們學習了 Koog 工具系統的基礎概念

  • 讓 AI Agent 從「聊天機器人」變成「實用助手」
  • SayToUser、AskUser、ExitTool 三個基本工具
  • 使用 Class-based - SimpleTool 建立結構化的自定義工具
  • 使用 Annotation-based 註解快速建立簡潔的工具
  • 基本原則:保持簡單、明確描述、適當錯誤處理

下一篇文章中,我們將學習如何調校 AI Agent 的行為參數,讓它更符合我們的需求

參考資料


支持創作

如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!


圖片來源:AI 產生