Logo
Published on

Koog Kotlin AI 框架實戰 Day 09:自定義工具開發進階:為 Agent 添加異步操作能力

在 Day 5 的學習中,我們掌握了 Koog 工具系統的基礎知識,學會了建立像 CalculatorTool 這樣的簡單同步工具。隨著我們在 Day 8 建立了 SmartCustomerService 智能客服系統,現在是時候為它添加更強大的功能了。今天我們將學習如何開發異步工具,整合外部 API,讓我們的 AI Agent 能夠處理更複雜的業務需求。

從同步到異步:為什麼需要進階工具?

讓我們回顧一下 Day 5 的簡單計算器工具和現在的需求差異:

Day 5 的同步工具特點:

  • 立即執行,立即返回結果
  • 不需要等待外部服務
  • 處理邏輯簡單直接

進階異步工具的需求:

  • 需要調用外部 API(天氣服務、訂單系統)
  • 處理網路延遲和超時
  • 優雅處理各種錯誤情況
  • 支援更複雜的參數結構

異步工具開發基礎

1. 理解異步工具的結構

讓我們先建立一個簡單的異步工具,了解基本結構:

// 檔案:src/main/kotlin/tools/async/WeatherTool.kt
import ai.koog.agents.tools.SimpleTool
import ai.koog.agents.tools.ToolDescriptor
import ai.koog.agents.tools.ToolParameterDescriptor
import ai.koog.agents.tools.ToolParameterType
import ai.koog.agents.tools.ToolArgs
import kotlinx.serialization.Serializable
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout

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

    @Serializable
    data class Args(
        val city: String,
        val country: String = "TW"
    ) : ToolArgs

    override val argsSerializer = Args.serializer()

    override val descriptor = ToolDescriptor(
        name = "get_weather",
        description = "獲取指定城市的天氣資訊,支援全球主要城市查詢",
        requiredParameters = listOf(
            ToolParameterDescriptor(
                name = "city",
                description = "城市名稱(支援中文或英文)",
                type = ToolParameterType.String
            )
        ),
        optionalParameters = listOf(
            ToolParameterDescriptor(
                name = "country",
                description = "國家代碼,預設為 TW(台灣)",
                type = ToolParameterType.String
            )
        )
    )

    override suspend fun doExecute(args: Args): String {
        return try {
            // 設定超時時間,避免無限等待
            withTimeout(10000) { // 10秒超時
                fetchWeatherData(args.city, args.country)
            }
        } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
            "⏱️ 天氣查詢超時,請稍後再試"
        } catch (e: Exception) {
            "❌ 無法獲取 ${args.city} 的天氣資訊:${e.message}"
        }
    }

    private suspend fun fetchWeatherData(city: String, country: String): String {
        // 模擬網路請求延遲
        delay(1000)

        // 在實際應用中,這裡會調用真實的天氣 API
        // 例如:OpenWeatherMap、AccuWeather 等

        return when (city.lowercase()) {
            "taipei", "台北" -> {
                """
                🌤️ 台北市天氣資訊

                🌡️ 溫度:25°C
                ☁️ 天氣:多雲時晴
                💧 濕度:68%
                💨 風速:3.2 m/s

                適宜外出,建議攜帶薄外套
                """.trimIndent()
            }
            "kaohsiung", "高雄" -> {
                """
                🌤️ 高雄市天氣資訊

                🌡️ 溫度:28°C
                ☁️ 天氣:晴朗
                💧 濕度:72%
                💨 風速:2.8 m/s

                天氣晴朗,適合戶外活動
                """.trimIndent()
            }
            "tokyo", "東京" -> {
                """
                🌤️ 東京天氣資訊

                🌡️ 溫度:18°C
                ☁️ 天氣:陰天
                💧 濕度:65%
                💨 風速:4.1 m/s

                較為涼爽,建議穿著長袖衣物
                """.trimIndent()
            }
            else -> {
                """
                🌤️ ${city} 天氣資訊

                🌡️ 溫度:22°C
                ☁️ 天氣:多雲
                💧 濕度:60%
                💨 風速:3.0 m/s

                一般天氣狀況
                """.trimIndent()
            }
        }
    }
}

2. 建立訂單查詢工具

接下來,讓我們建立一個更複雜的訂單查詢工具,展示如何處理複雜的業務邏輯:

// 檔案:src/main/kotlin/tools/async/OrderStatusTool.kt
import ai.koog.agents.tools.SimpleTool
import ai.koog.agents.tools.ToolDescriptor
import ai.koog.agents.tools.ToolParameterDescriptor
import ai.koog.agents.tools.ToolParameterType
import ai.koog.agents.tools.ToolArgs
import kotlinx.serialization.Serializable
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

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

    @Serializable
    data class Args(
        val orderId: String,
        val customerEmail: String? = null
    ) : ToolArgs

    @Serializable
    data class OrderInfo(
        val orderId: String,
        val status: OrderStatus,
        val customerName: String,
        val items: List<OrderItem>,
        val totalAmount: Double,
        val orderDate: String,
        val estimatedDelivery: String?
    )

    @Serializable
    data class OrderItem(
        val name: String,
        val quantity: Int,
        val price: Double
    )

    @Serializable
    enum class OrderStatus {
        PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED
    }

    override val argsSerializer = Args.serializer()

    override val descriptor = ToolDescriptor(
        name = "check_order_status",
        description = "查詢訂單狀態和詳細資訊,協助客戶了解訂單進度",
        requiredParameters = listOf(
            ToolParameterDescriptor(
                name = "orderId",
                description = "訂單編號(格式:ORD-XXXXXX)",
                type = ToolParameterType.String
            )
        ),
        optionalParameters = listOf(
            ToolParameterDescriptor(
                name = "customerEmail",
                description = "客戶電子郵件地址,用於身份驗證",
                type = ToolParameterType.String
            )
        )
    )

    override suspend fun doExecute(args: Args): String {
        return try {
            // 驗證訂單編號格式
            if (!isValidOrderId(args.orderId)) {
                return "❌ 訂單編號格式不正確,正確格式為:ORD-XXXXXX"
            }

            withTimeout(8000) { // 8秒超時
                val orderInfo = fetchOrderInfo(args.orderId, args.customerEmail)
                formatOrderInfo(orderInfo)
            }
        } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
            "⏱️ 訂單查詢超時,請稍後再試"
        } catch (e: OrderNotFoundException) {
            "❌ 找不到訂單編號 ${args.orderId},請確認編號是否正確"
        } catch (e: UnauthorizedException) {
            "🔒 無法查詢此訂單,請提供正確的客戶電子郵件地址"
        } catch (e: Exception) {
            "❌ 訂單查詢失敗:${e.message}"
        }
    }

    private fun isValidOrderId(orderId: String): Boolean {
        val pattern = "^ORD-[A-Z0-9]{6}$".toRegex()
        return orderId.matches(pattern)
    }

    private suspend fun fetchOrderInfo(orderId: String, customerEmail: String?): OrderInfo {
        // 模擬資料庫查詢延遲
        delay(1500)

        // 模擬訂單資料
        val mockOrders = mapOf(
            "ORD-ABC123" to OrderInfo(
                orderId = "ORD-ABC123",
                status = OrderStatus.SHIPPED,
                customerName = "王小明",
                items = listOf(
                    OrderItem("iPhone 15 Pro", 1, 35900.0),
                    OrderItem("AirPods Pro", 1, 7490.0)
                ),
                totalAmount = 43390.0,
                orderDate = "2025-01-15 14:30:00",
                estimatedDelivery = "2025-01-18"
            ),
            "ORD-DEF456" to OrderInfo(
                orderId = "ORD-DEF456",
                status = OrderStatus.PROCESSING,
                customerName = "李小華",
                items = listOf(
                    OrderItem("MacBook Air M3", 1, 36900.0),
                    OrderItem("Magic Mouse", 1, 2590.0)
                ),
                totalAmount = 39490.0,
                orderDate = "2025-01-16 10:15:00",
                estimatedDelivery = "2025-01-20"
            ),
            "ORD-GHI789" to OrderInfo(
                orderId = "ORD-GHI789",
                status = OrderStatus.DELIVERED,
                customerName = "陳小美",
                items = listOf(
                    OrderItem("iPad Pro 12.9\"", 1, 35900.0)
                ),
                totalAmount = 35900.0,
                orderDate = "2025-01-10 16:45:00",
                estimatedDelivery = null
            )
        )

        val orderInfo = mockOrders[orderId] ?: throw OrderNotFoundException("訂單不存在")

        // 模擬電子郵件驗證
        if (customerEmail != null && !isAuthorizedCustomer(customerEmail, orderInfo)) {
            throw UnauthorizedException("電子郵件地址不匹配")
        }

        return orderInfo
    }

    private fun isAuthorizedCustomer(email: String, orderInfo: OrderInfo): Boolean {
        // 模擬電子郵件驗證邏輯
        val authorizedEmails = mapOf(
            "ORD-ABC123" to "[email protected]",
            "ORD-DEF456" to "[email protected]",
            "ORD-GHI789" to "[email protected]"
        )

        return authorizedEmails[orderInfo.orderId] == email
    }

    private fun formatOrderInfo(orderInfo: OrderInfo): String {
        val statusEmoji = when (orderInfo.status) {
            OrderStatus.PENDING -> "⏳"
            OrderStatus.CONFIRMED -> "✅"
            OrderStatus.PROCESSING -> "🔄"
            OrderStatus.SHIPPED -> "🚚"
            OrderStatus.DELIVERED -> "📦"
            OrderStatus.CANCELLED -> "❌"
        }

        val statusText = when (orderInfo.status) {
            OrderStatus.PENDING -> "待確認"
            OrderStatus.CONFIRMED -> "已確認"
            OrderStatus.PROCESSING -> "處理中"
            OrderStatus.SHIPPED -> "已出貨"
            OrderStatus.DELIVERED -> "已送達"
            OrderStatus.CANCELLED -> "已取消"
        }

        val itemsList = orderInfo.items.joinToString("\n") { item ->
            "  • ${item.name} x${item.quantity} - NT$${String.format("%,.0f", item.price)}"
        }

        val deliveryInfo = if (orderInfo.estimatedDelivery != null) {
            "\n📅 預計送達:${orderInfo.estimatedDelivery}"
        } else {
            ""
        }

        return """
            $statusEmoji 訂單狀態:$statusText

            📋 訂單資訊:
            • 訂單編號:${orderInfo.orderId}
            • 客戶姓名:${orderInfo.customerName}
            • 訂單日期:${orderInfo.orderDate}

            🛍️ 商品清單:
            $itemsList

            💰 訂單總額:NT$${String.format("%,.0f", orderInfo.totalAmount)}$deliveryInfo

            ${if (orderInfo.status == OrderStatus.SHIPPED) "🚚 您的訂單已出貨,請留意送貨通知" else ""}
        """.trimIndent()
    }

    // 自定義例外類別
    class OrderNotFoundException(message: String) : Exception(message)
    class UnauthorizedException(message: String) : Exception(message)
}

整合到智能客服系統

現在讓我們將這些新工具整合到 Day 8 建立的 SmartCustomerService 系統中:

// 檔案:src/main/kotlin/services/SmartCustomerService.kt
import ai.koog.agents.AIAgent
import ai.koog.agents.ToolRegistry
import ai.koog.agents.tools.SayToUser
import ai.koog.agents.tools.AskUser
import ai.koog.agents.tools.ExitTool
import ai.koog.agents.executors.simpleOpenAIExecutor
import ai.koog.agents.models.OpenAIModels

// 引用 Day 2 建立的 ApiKeyManager
object ApiKeyManager {
    val openAIKey: String by lazy {
        System.getenv("OPENAI_API_KEY")
            ?: error("請設定 OPENAI_API_KEY 環境變數")
    }
}

class SmartCustomerService {

    // 擴展工具註冊表,加入新的異步工具
    private val toolRegistry = ToolRegistry {
        // Day 5 的基礎工具
        tool(SayToUser)
        tool(AskUser)
        tool(ExitTool)

        // Day 9 新增的異步工具
        tool(WeatherTool)           // 天氣查詢功能
        tool(OrderStatusTool)       // 訂單狀態查詢
    }

    private val agent = AIAgent(
        executor = simpleOpenAIExecutor(ApiKeyManager.openAIKey), // 引用 Day 2 配置
        systemPrompt = createEnhancedCustomerServicePrompt(),     // 增強的系統提示
        llmModel = OpenAIModels.Chat.GPT4o,                      // Day 4 技術
        toolRegistry = toolRegistry,                              // Day 5+9 技術
        temperature = 0.7,                                        // Day 6 技術
        maxIterations = 15                                        // Day 6 技術,增加迭代次數
    )

    private fun createEnhancedCustomerServicePrompt(): String {
        return """
            你是一個專業且友善的客服代表,現在具備了以下增強功能:

            🔧 基本客服能力:
            - 回答客戶的各種問題
            - 提供專業的產品和服務諮詢
            - 協助解決客戶遇到的問題

            🌤️ 天氣查詢服務(NEW!):
            - 使用 get_weather 工具查詢全球城市天氣
            - 協助客戶安排活動和出行計劃
            - 提供天氣相關的貼心建議

            📦 訂單查詢服務(NEW!):
            - 使用 check_order_status 工具查詢訂單狀態
            - 協助客戶追蹤包裹運送進度
            - 處理訂單相關的各種問題

            💡 服務原則:
            1. 始終保持友善和專業的態度
            2. 主動詢問客戶需要什麼幫助
            3. 當客戶詢問天氣時,主動使用天氣查詢工具
            4. 當客戶詢問訂單時,請求訂單編號並查詢狀態
            5. 如果查詢失敗,提供替代解決方案
            6. 適時提供相關的建議和協助

            請用繁體中文與客戶互動,提供優質的客服體驗。
        """.trimIndent()
    }

    // Day 7 錯誤處理技巧的應用
    suspend fun handleCustomerQuery(query: String): String {
        return try {
            agent.run(query)
        } catch (e: Exception) {
            "非常抱歉,系統暫時無法處理您的請求。請稍後再試,或聯繫人工客服:0800-123-456"
        }
    }

    // 啟動客服對話
    suspend fun startService() {
        agent.run("""
            👋 您好!歡迎使用智能客服系統!

            我現在可以為您提供以下服務:
            • 📞 一般客服諮詢
            • 🌤️ 天氣資訊查詢
            • 📦 訂單狀態查詢
            • ❓ 產品和服務相關問題

            請告訴我您需要什麼協助?
        """.trimIndent())
    }

    // 便捷方法:直接查詢天氣
    suspend fun getWeatherInfo(city: String, country: String = "TW"): String {
        return agent.run("請幫我查詢 $city 的天氣資訊")
    }

    // 便捷方法:直接查詢訂單
    suspend fun checkOrderStatus(orderId: String, customerEmail: String? = null): String {
        val emailInfo = if (customerEmail != null) ",客戶信箱是 $customerEmail" else ""
        return agent.run("請幫我查詢訂單編號 $orderId 的狀態$emailInfo")
    }
}

實際使用範例

讓我們建立一個完整的使用範例,展示新功能的實際應用:

// 檔案:src/main/kotlin/examples/Day09Example.kt
import kotlinx.coroutines.runBlocking

suspend fun main() {
    println("🚀 Koog AI 框架實戰 Day 09:進階客服系統展示")
    println("=" * 50)

    val customerService = SmartCustomerService()

    // 啟動客服系統
    customerService.startService()

    println("\n" + "=" * 50)
    println("📋 測試案例 1:天氣查詢功能")
    println("=" * 50)

    // 測試天氣查詢
    val weatherQuery = "我明天要去台北出差,可以幫我查一下台北的天氣嗎?"
    val weatherResponse = customerService.handleCustomerQuery(weatherQuery)
    println("客戶:$weatherQuery")
    println("客服:$weatherResponse")

    println("\n" + "=" * 50)
    println("📋 測試案例 2:訂單狀態查詢")
    println("=" * 50)

    // 測試訂單查詢
    val orderQuery = "我想查詢我的訂單狀態,訂單編號是 ORD-ABC123"
    val orderResponse = customerService.handleCustomerQuery(orderQuery)
    println("客戶:$orderQuery")
    println("客服:$orderResponse")

    println("\n" + "=" * 50)
    println("📋 測試案例 3:複合查詢")
    println("=" * 50)

    // 測試複合查詢
    val complexQuery = "我的訂單 ORD-DEF456 什麼時候會到?另外明天東京的天氣如何?"
    val complexResponse = customerService.handleCustomerQuery(complexQuery)
    println("客戶:$complexQuery")
    println("客服:$complexResponse")

    println("\n" + "=" * 50)
    println("📋 測試案例 4:錯誤處理")
    println("=" * 50)

    // 測試錯誤處理
    val errorQuery = "請查詢訂單 INVALID-ORDER"
    val errorResponse = customerService.handleCustomerQuery(errorQuery)
    println("客戶:$errorQuery")
    println("客服:$errorResponse")

    println("\n" + "=" * 50)
    println("📋 直接功能測試")
    println("=" * 50)

    // 直接使用便捷方法
    println("🌤️ 直接天氣查詢:")
    val directWeather = customerService.getWeatherInfo("高雄")
    println(directWeather)

    println("\n📦 直接訂單查詢:")
    val directOrder = customerService.checkOrderStatus("ORD-GHI789")
    println(directOrder)
}

fun main() = runBlocking {
    main()
}

測試和驗證

建立簡單的測試來驗證工具功能:

// 檔案:src/test/kotlin/tools/AsyncToolsTest.kt
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
import kotlinx.coroutines.test.runTest

class AsyncToolsTest {

    @Test
    fun `WeatherTool should return weather info for valid city`() = runTest {
        // Given
        val args = WeatherTool.Args(city = "台北", country = "TW")

        // When
        val result = WeatherTool.doExecute(args)

        // Then
        assertTrue(result.contains("台北"))
        assertTrue(result.contains("°C"))
        assertTrue(result.contains("濕度"))
        assertFalse(result.contains("❌"))
    }

    @Test
    fun `OrderStatusTool should return order info for valid order ID`() = runTest {
        // Given
        val args = OrderStatusTool.Args(orderId = "ORD-ABC123")

        // When
        val result = OrderStatusTool.doExecute(args)

        // Then
        assertTrue(result.contains("ORD-ABC123"))
        assertTrue(result.contains("訂單狀態"))
        assertTrue(result.contains("iPhone 15 Pro"))
        assertFalse(result.contains("❌"))
    }

    @Test
    fun `OrderStatusTool should handle invalid order ID format`() = runTest {
        // Given
        val args = OrderStatusTool.Args(orderId = "INVALID-ORDER")

        // When
        val result = OrderStatusTool.doExecute(args)

        // Then
        assertTrue(result.contains("❌"))
        assertTrue(result.contains("格式不正確"))
    }

    @Test
    fun `OrderStatusTool should handle non-existent order`() = runTest {
        // Given
        val args = OrderStatusTool.Args(orderId = "ORD-NOTFOUND")

        // When
        val result = OrderStatusTool.doExecute(args)

        // Then
        assertTrue(result.contains("❌"))
        assertTrue(result.contains("找不到訂單"))
    }
}

與 Day 5 的對比總結

讓我們回顧一下從 Day 5 到 Day 9 的工具開發進化:

特性Day 5 同步工具Day 9 異步工具
執行方式同步,立即返回異步,支援網路調用
複雜度簡單計算邏輯複雜業務邏輯
錯誤處理基本例外處理多層次錯誤處理
超時管理不需要支援超時控制
參數驗證簡單驗證複雜格式驗證
實際應用計算輔助業務系統整合

下一步預告

今天我們成功為智能客服系統添加了天氣查詢和訂單查詢功能,掌握了異步工具開發的核心技術。下一篇文章(Day 10)中,我們將探討 Koog 框架的多模態內容處理能力,學習如何讓 AI Agent 處理圖像、音訊、文件等各種媒體類型,進一步擴展客服系統的服務範圍。

本日重點回顧

異步工具開發:掌握了 suspend 函數和協程的使用 ✅ 外部 API 整合:學會了模擬和整合外部服務 ✅ 錯誤處理進階:實作了多層次的錯誤處理機制 ✅ 超時管理:使用 withTimeout 避免長時間等待 ✅ 主線專案演進:成功擴展了智能客服系統的功能 ✅ 實用工具建立:開發了天氣查詢和訂單查詢工具

支持創作

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


圖片來源:AI 產生