- 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 產生