- Published on
第一次學 Kotlin Koog AI 就上手 Day 09:讓 AI 更強大:開發能與外部世界互動的工具
在 Day 5 的學習中,我們掌握了 Koog 工具系統的基礎知識,學會了建立像 AddTool
這樣的本地工具。這些工具使用 suspend
關鍵字,能夠處理輸入並返回結果,非常適合數學計算、字串處理等本地運算
但在現實世界中,AI Agent 經常需要與外部服務互動,例如
- 查詢天氣 API 獲取即時天氣資訊
- 呼叫物流 API 查詢包裹狀態
- 連接金融 API 取得匯率資訊
- 使用地圖 API 規劃路線
這些操作需要透過網路呼叫外部服務,涉及網路延遲和可能的連線問題。今天我們將學習如何建立能夠與真實外部 API 互動的工具
準備 OpenWeather API
在開始建立天氣查詢工具之前,我們需要設定真實的天氣 API。我們將使用 OpenWeather 的 One Call API 3.0 來獲取實際的天氣資料
申請 OpenWeather API 金鑰
- 前往 OpenWeather 註冊帳號
- 申請 One Call API 3.0(免費版提供每日 1,000 次呼叫)- 需要先升級帳號才可以使用相關的 API,我是測試完就把帳號降級
- 取得你的 API 金鑰
設定 API 金鑰管理
就如同 Day 02 的 OpenAI API 金鑰管理方式,我們為 OpenWeather API 也建立統一的管理機制
/**
* API 金鑰管理器 - 統一管理各種 API 金鑰
*/
object ApiKeyManager {
val openAIApiKey: String? = System.getenv("OPENAI_API_KEY")
val openWeatherApiKey: String? = System.getenv("OPENWEATHER_API_KEY")
init {
if (openAIApiKey.isNullOrBlank()) {
println("⚠️ 警告:未找到 OPENAI_API_KEY 環境變數")
}
if (openWeatherApiKey.isNullOrBlank()) {
println("⚠️ 警告:未找到 OPENWEATHER_API_KEY 環境變數")
}
}
}
在這裡多加上了,啟動時檢查金鑰的相關 log
在 IntelliJ IDEA 的環境變數設定中新增
OPENWEATHER_API_KEY=你的OpenWeather API金鑰
新增 HTTP 客戶端依賴
為了進行 HTTP 請求,我們使用 OkHttp
函式庫
在 build.gradle.kts
中新增依賴
dependencies {
// Koog 相關依賴
implementation("ai.koog:koog-agents:0.3.0")
// HTTP 客戶端 - 用於 API 呼叫
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// JSON 序列化 - 用於 API 回應解析
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
}
動手建立天氣查詢工具
現在讓我們建立一個真正能與 OpenWeather API 互動的 WeatherTool
,學習外部 API 整合工具的開發步驟
定義 Args 資料類別
首先,我們定義工具需要的參數結構
// API 回應資料模型
@Serializable
data class GeocodingResponse(
val name: String,
val lat: Double,
val lon: Double,
val country: String
)
@Serializable
data class WeatherResponse(
val lat: Double,
val lon: Double,
val current: CurrentWeather
)
@Serializable
data class CurrentWeather(
val temp: Double,
val humidity: Int,
val weather: List<WeatherDescription>
)
@Serializable
data class WeatherDescription(
val id: Int,
val main: String,
val description: String
)
object WeatherTool : SimpleTool<WeatherTool.Args>() {
@Serializable
data class Args(
val city: String, // 必填:城市名稱
val country: String = "TW" // 選填:國家代碼,預設台灣
) : ToolArgs
override val argsSerializer = Args.serializer()
// 其它程式碼 ...
}
設定工具描述資訊
告訴 AI 這個工具的用途和參數說明
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 {
// 設定 5 秒超時,避免無限等待
withTimeout(5000) {
fetchWeatherData(args.city, args.country)
}
} catch (e: kotlinx.coroutines.TimeoutCancellationException) {
"⏱️ 天氣查詢超時,請稍後再試"
} catch (e: Exception) {
"❌ 無法獲取 ${args.city} 的天氣資訊:${e.message}"
}
}
實作真實 API 呼叫
現在實作真正的 OpenWeather API 呼叫,讓工具能夠獲取實際的天氣資料
private suspend fun fetchWeatherData(city: String, country: String): String {
try {
// 使用 Geocoding API 獲取城市座標
val coordinates = getCoordinates(city, country)
// 使用座標呼叫 One Call API 獲取天氣
return callWeatherApi(coordinates.lat, coordinates.lon, city)
} catch (e: Exception) {
throw Exception("無法獲取 $city 的天氣資訊:${e.message}")
}
}
/**
* 使用 Geocoding API 將城市名稱轉換為座標
*/
private suspend fun getCoordinates(city: String, country: String): GeocodingResponse {
val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
val encodedCity = URLEncoder.encode(city, "UTF-8")
val url = "https://api.openweathermap.org/geo/1.0/direct?q=$encodedCity,$country&limit=1&appid=${ApiKeyManager.openWeatherApiKey}"
val request = Request.Builder()
.url(url)
.build()
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string() ?: throw Exception("Empty response")
parseCoordinates(body, city)
} else {
throw Exception("Geocoding API 呼叫失敗,狀態碼:${response.code}")
}
}
}
}
/**
* 解析 Geocoding API 回應獲取座標
*/
private fun parseCoordinates(response: String, city: String): GeocodingResponse {
val json = Json { ignoreUnknownKeys = true }
val locations = json.decodeFromString<List<GeocodingResponse>>(response)
if (locations.isNotEmpty()) {
return locations[0]
} else {
throw Exception("找不到城市 $city 的地理位置")
}
}
/**
* 呼叫 OpenWeather One Call API 獲取天氣資料
*/
private suspend fun callWeatherApi(lat: Double, lon: Double, cityName: String): String {
val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
val url = "https://api.openweathermap.org/data/3.0/onecall?lat=$lat&lon=$lon&appid=${ApiKeyManager.openWeatherApiKey}&units=metric&lang=zh_tw"
val request = Request.Builder()
.url(url)
.build()
return withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string() ?: throw Exception("Empty response")
parseWeatherData(body, cityName)
} else {
throw Exception("天氣 API 呼叫失敗,狀態碼:${response.code}")
}
}
}
}
/**
* 解析天氣 API 回應
*/
private fun parseWeatherData(response: String, cityName: String): String {
try {
val json = Json { ignoreUnknownKeys = true }
val weather = json.decodeFromString<WeatherResponse>(response)
val temperature = weather.current.temp.toInt()
val humidity = weather.current.humidity
val description = weather.current.weather.firstOrNull()?.description ?: "無資料"
return """
🌤️ $cityName 天氣
🌡️ 溫度:${temperature}°C
☁️ $description
💧 濕度:${humidity}%
即時天氣資料來自 OpenWeather
""".trimIndent()
} catch (e: Exception) {
throw Exception("解析天氣資料失敗:${e.message}")
}
}
關鍵技術要點解析
超時處理
withTimeout(5000) {
fetchWeatherData(args.city, args.country)
}
withTimeout
設定操作的最大執行時間- 如果超時會拋出
TimeoutCancellationException
- 防止因網路問題導致的無限等待
使用 OkHttp 進行 API 整合
val client = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url(url)
.build()
withContext(Dispatchers.IO) {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
val body = response.body?.string() ?: throw Exception("Empty response")
// 處理回應
}
}
}
OkHttp 的主要優勢
- 高效能連線池:自動重複使用連線,減少網路延遲
- 自動重試機制:內建智能重試和重定向處理
- 安全性:內建 HTTPS 支援和憑證管理
- 簡潔 API:比 HttpURLConnection 更易使用和理解
- resource 管理:使用
.use {}
自動關閉資源,避免記憶體洩漏
兩階段 API 呼叫
我們的天氣工具使用了兩個 OpenWeather API
- Geocoding API:將城市名稱轉換為經緯度座標
- One Call API:使用座標獲取詳細天氣資料
這種設計讓工具能支援任何城市名稱,包括中文城市名稱
結構化 JSON 解析
@Serializable
data class WeatherResponse(
val lat: Double,
val lon: Double,
val current: CurrentWeather
)
val json = Json { ignoreUnknownKeys = true }
val weather = json.decodeFromString<WeatherResponse>(response)
val temperature = weather.current.temp.toInt()
kotlinx-serialization 的主要優勢
- 類型安全:編譯時期檢查 JSON 結構,避免執行時錯誤
- 結構化資料:使用資料類別清楚定義 API 回應結構
- 自動轉換:JSON 字串自動轉換為 Kotlin 物件
- 容錯處理:
ignoreUnknownKeys = true
忽略不需要的欄位 - 效能優越:相比正則表達式更高效且可靠
建立天氣查詢 AI Agent
現在讓我們建立一個簡單的 AI Agent 來測試我們的 WeatherTool
class WeatherAgent {
private val toolRegistry = ToolRegistry {
tool(WeatherTool) // 我們的天氣查詢工具
}
private val agent = AIAgent(
executor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!),
systemPrompt = """
你是一個友善的天氣助手:
當使用者詢問天氣時:
- 使用 get_weather 工具查詢指定城市的天氣
- 提供清楚、有用的天氣資訊
- 可以給出貼心的建議(如是否需要帶傘、外套等)
請用正體中文回應,保持友善的語調。
""".trimIndent(),
llmModel = OpenAIModels.CostOptimized.GPT4_1Mini,
toolRegistry = toolRegistry,
temperature = 0.7
)
suspend fun queryWeather(query: String): String {
return try {
agent.run(query)
} catch (e: Exception) {
"抱歉,無法查詢天氣資訊:${e.message}"
}
}
}
實際使用範例
讓我們建立一個簡單的範例來測試天氣查詢功能
suspend fun main() {
println("外部 API 天氣查詢工具展示")
println("=".repeat(40))
val weatherAgent = WeatherAgent()
println("\n測試天氣查詢功能")
println("-".repeat(40))
val query = "台中市今天天氣如何?"
val response = weatherAgent.queryWeather(query)
println("使用者:$query")
println("天氣助手:$response")
println("\n測試完成!")
}
執行 AI 回應內容
外部 API 天氣查詢工具展示
========================================
測試天氣查詢功能(使用 OpenWeather API)
----------------------------------------
使用者:台中市今天天氣如何?
天氣助手:根據最新的天氣資訊,台中目前的天氣狀況如下
🌤️ 台中 天氣
🌡️ 溫度:23°C
☁️ 多雲
💧 濕度:78%
即時天氣資料來自 OpenWeather
天氣算是舒適的,有點多雲,溫度適中。如果要外出建議可以帶件薄外套,以防早晚溫差。有什麼其他需要幫忙的嗎?
測試完成!
注意:執行結果會根據查詢時間和實際天氣狀況而有所不同,以上展示的是真實 API 回應的格式
總結
今天我們成功從 Day 5 的本地工具進化到真正的外部 API 整合
對比項目 | Day 5 AddTool | Day 9 WeatherTool (更新版) |
---|---|---|
執行方式 | 本地計算,立即返回 | 外部 API 呼叫,網路請求 |
關鍵技術 | 基本的 doExecute | suspend + OkHttp + kotlinx-serialization |
錯誤處理 | 簡單 try-catch | 多層次錯誤處理 + 網路超時處理 |
適用場景 | 數學計算、字串處理 | 外部服務整合、實時資料獲取 |
實用性 | 基礎學習用途 | 生產環境可用的真實工具 |
資料來源 | 本地計算 | OpenWeather API 真實天氣資料 |
掌握了外部 API 整合工具開發後,下一篇文章我們將探討 Koog 框架的多模態內容處理能力,學習如何讓 AI Agent 處理圖像、音訊等多媒體內容,進一步擴展 AI Agent 的能力邊界
支持創作
如果這篇文章對您有幫助,歡迎透過 贊助連結 支持我持續創作優質內容。您的支持是我前進的動力!
圖片來源:AI 產生