Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 29:結構化資料處理:讓 AI 回覆更精準的秘密武器

在前一篇文章中,我們學習了 AI 應用的測試策略,解決了回應隨機性帶來的挑戰。但你有沒有想過,如果能從源頭就讓 AI 的回應變得可預測,測試會不會變得更簡單?今天我們要探討 Koog 框架的結構化資料處理功能,這是確保 AI 回應一致性的關鍵技術

想像一下,當你的 AI 客服系統回覆客戶時,有時是 JSON 格式,有時是純文字,這不僅讓後續處理變得複雜,更讓測試變得困難重重。透過結構化資料處理,我們能夠確保 AI 總是按照預期的格式回應,大幅提升應用程式的穩定性和可測試性。今天我們將建立一個訂單處理系統和客服助手,實際展示如何運用這項功能

為什麼需要結構化資料處理

傳統 AI 回覆的痛點

在沒有結構化資料處理之前,我們常常會遇到這些問題

// AI 回覆可能是這樣...
"今天台北的氣溫是 25 度,天氣晴朗"

// 或者是這樣...
{
  "temperature": 25,
  "weather": "sunny",
  "city": "台北"
}

// 甚至是這樣...
溫度:25天氣:晴天
城市:台北

這種不一致的格式讓程式碼處理變得非常複雜,需要寫很多解析邏輯來處理各種可能的回應格式

結構化資料的優勢

使用 Koog 的結構化資料處理,我們能夠獲得

  • 可預測性:AI 總是按照定義的結構回應
  • 類型安全:編譯時就能檢查資料結構
  • 易於處理:直接得到 Kotlin 物件,無需額外解析
  • 錯誤修正:內建重試機制,自動修正格式錯誤

基礎資料模型定義

讓我們從一個簡單的天氣預報範例開始

@Serializable
@SerialName("WeatherForecast")
@LLMDescription("天氣預報資訊")
data class WeatherForecast(
    @property:LLMDescription("城市名稱")
    val city: String,
    @property:LLMDescription("攝氏溫度")
    val temperature: Int,
    @property:LLMDescription("天氣狀況(例如:晴天、多雲、雨天)")
    val conditions: String,
    @property:LLMDescription("降雨機率百分比")
    val precipitation: Int
)

關鍵註解說明

  • @Serializable:讓類別支援序列化
  • @SerialName:指定序列化時的名稱
  • @LLMDescription:為 AI 提供清楚的描述,幫助其理解欄位用途

集合與巢狀結構支援

Koog 支援各種複雜的資料結構

@Serializable
@SerialName("ComplexOrderData")
data class ComplexOrderData(
    // 清單支援
    @property:LLMDescription("商品清單")
    val items: List<OrderItem>,

    // 對應表支援
    @property:LLMDescription("配送資訊對應表")
    val shippingInfo: Map<String, String>,

    // 巢狀物件支援
    @property:LLMDescription("地址資訊")
    val address: Address
) {
    @Serializable
    @SerialName("Address")
    data class Address(
        @property:LLMDescription("郵遞區號")
        val zipCode: String,
        @property:LLMDescription("完整地址")
        val fullAddress: String
    )
}

多型資料結構

使用 sealed class 處理不同類型的通知系統

@Serializable
@SerialName("NotificationRequest")
@LLMDescription("通知請求的基礎類型")
sealed class NotificationRequest {

    @Serializable
    @SerialName("EmailNotification")
    @LLMDescription("電子郵件通知")
    data class EmailNotification(
        @property:LLMDescription("收件人電子郵件")
        val recipient: String,
        @property:LLMDescription("郵件主旨")
        val subject: String,
        @property:LLMDescription("郵件內容")
        val content: String,
        @property:LLMDescription("是否為重要郵件")
        val isUrgent: Boolean = false
    ) : NotificationRequest()

    @Serializable
    @SerialName("SMSNotification")
    @LLMDescription("簡訊通知")
    data class SMSNotification(
        @property:LLMDescription("手機號碼")
        val phoneNumber: String,
        @property:LLMDescription("簡訊內容")
        val message: String
    ) : NotificationRequest()

    @Serializable
    @SerialName("PushNotification")
    @LLMDescription("推播通知")
    data class PushNotification(
        @property:LLMDescription("使用者 ID")
        val userId: String,
        @property:LLMDescription("通知標題")
        val title: String,
        @property:LLMDescription("通知內容")
        val body: String,
        @property:LLMDescription("附加資料")
        val data: Map<String, String> = emptyMap()
    ) : NotificationRequest()
}

訂單處理系統 - 基本 Agent 請求範例

建立相關訂單資料結構

// 訂單資料模型
@Serializable
@SerialName("OrderInfo")
@LLMDescription("完整的訂單資訊")
data class OrderInfo(
    @property:LLMDescription("訂單編號")
    val orderId: String,
    @property:LLMDescription("客戶資訊")
    val customer: CustomerInfo,
    @property:LLMDescription("訂單商品清單")
    val items: List<OrderItem>,
    @property:LLMDescription("訂單狀態")
    val status: OrderStatus,
    @property:LLMDescription("總金額")
    val totalAmount: Double
)

// 客戶資訊
@Serializable
@SerialName("CustomerInfo")
@LLMDescription("客戶基本資料")
data class CustomerInfo(
    @property:LLMDescription("客戶姓名")
    val name: String,
    @property:LLMDescription("電子郵件地址")
    val email: String,
    @property:LLMDescription("聯絡電話")
    val phone: String
)

// 訂單商品
@Serializable
@SerialName("OrderItem")
@LLMDescription("單項商品資訊")
data class OrderItem(
    @property:LLMDescription("商品編號")
    val productId: String,
    @property:LLMDescription("商品名稱")
    val productName: String,
    @property:LLMDescription("購買數量")
    val quantity: Int,
    @property:LLMDescription("單價")
    val price: Double
)

// 訂單狀態枚舉
@Serializable
@SerialName("OrderStatus")
enum class OrderStatus {
    PENDING,    // 待處理
    CONFIRMED,  // 已確認
    SHIPPED,    // 已出貨
    DELIVERED,  // 已送達
    CANCELLED   // 已取消
}

JSON Schema 產生參數說明

在使用 JsonStructuredData.createJsonStructure 建立 JSON Schema 時,需要了解幾個重要參數

val orderStructure = JsonStructuredData.createJsonStructure<OrderInfo>(
    schemaFormat = JsonSchemaGenerator.SchemaFormat.JsonSchema,  // Schema 格式
    examples = exampleOrders,                                    // 範例資料
    schemaType = JsonStructuredData.JsonSchemaType.SIMPLE,      // Schema 類型
)

關鍵參數解釋

  • schemaFormat:指定 JSON Schema 格式

    • JsonSchemaGenerator.SchemaFormat.JsonSchema:標準 JSON Schema 格式,功能完整
    • JsonSchemaGenerator.SchemaFormat.Simple:簡化的 Schema 格式,有一些模型和使用上的限制 (例如沒有多型的支援)
  • schemaType:決定 Schema 的複雜度和相容性

    • JsonStructuredData.JsonSchemaType.SIMPLE
      • 支援標準 JSON 欄位
      • 不支援多型(polymorphism)
      • 與更多語言模型相容
      • 適合簡單的資料結構
    • JsonStructuredData.JsonSchemaType.FULL
      • 進階 JSON Schema 功能
      • 支援多型和繼承
      • 與較少語言模型相容
      • 適合複雜的資料結構
  • examples:提供範例資料清單,幫助 LLM 更好理解預期的資料結構

基本的結構化範例

suspend fun main() {

    val promptExecutor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!)

    // 建立範例資料幫助 AI 理解
    val exampleOrders = listOf(
        OrderInfo(
            orderId = "ORD-2025-001",
            customer = CustomerInfo(
                name = "王小明",
                email = "[email protected]",
                phone = "0912-345-678"
            ),
            items = listOf(
                OrderItem(
                    productId = "PROD-001",
                    productName = "智慧手錶",
                    quantity = 1,
                    price = 8999.0
                )
            ),
            status = OrderStatus.PENDING,
            totalAmount = 8999.0
        )
    )

    // 產生結構化資料定義
    val orderStructure = JsonStructuredData.createJsonStructure<OrderInfo>(
        schemaFormat = JsonSchemaGenerator.SchemaFormat.JsonSchema,
        examples = exampleOrders,
        schemaType = JsonStructuredData.JsonSchemaType.SIMPLE,
    )

    val orderContent = "幫我建立一筆購買 iPhone 17 Air 的訂單,客戶是 9527,電話 0912-345-678"

    // 執行結構化請求
    val structuredResponse = promptExecutor.executeStructured(
        prompt = prompt("order-creation") {
            system(
                """
                您是一個專業的訂單處理助手。
                根據使用者的要求建立訂單資訊,確保所有必要欄位都完整填寫。
                """.trimIndent()
            )
            user(orderContent)
        },
        mainModel = OpenAIModels.CostOptimized.GPT4_1Mini,
        structure = orderStructure,
        retries = 5,
    )

executeStructured 方法參數說明

executeStructured 方法是執行結構化請求的核心方法,包含以下重要參數

  • structure:定義預期資料結構的 JSON Schema,由 JsonStructuredData.createJsonStructure 產生
  • mainModel:執行主要任務的語言模型,負責理解提示詞並產生內容
  • retries:嘗試解析回應的次數(預設:1),當 AI 回應格式不正確時會重試
  • fixingModel(選用):用於修正格式錯誤的模型,預設為 GPT-4o

mainModel vs fixingModel

  • mainModel:負責理解任務並產生主要內容,可以選擇成本較低的模型
  • fixingModel:專門處理格式修正,通常使用更強大但成本較高的模型

這種分工可以在保證輸出品質的同時,優化整體的成本和效能

    println("訂單描述 - $orderContent")

    structuredResponse.getOrNull()?.let { order ->

        println("\nAI 回應內容")
        println("*".repeat(30))


        // 直接取得類型安全的 OrderInfo 物件
        println("\n完整內容:${order.structure}")

        println("\n訂單編號:${order.structure.orderId}")
        println("\n客戶姓名:${order.structure.customer.name}")
        println("\n總金額:$${order.structure.totalAmount}")
    }
}

執行 AI 回應內容

訂單描述 - 幫我建立一筆購買 iPhone 17 Air 的訂單,客戶是 9527,電話 0912-345-678

AI 回應內容
******************************

完整內容:OrderInfo(orderId=ORD-9527-001, customer=CustomerInfo(name=9527, email=9527@example.com, phone=0912-345-678), items=[OrderItem(productId=IP17AIR-001, productName=iPhone 17 Air, quantity=1, price=0.0)], status=PENDING, totalAmount=0.0)

訂單編號:ORD-9527-001

客戶姓名:9527

總金額:$0.0

AI 客服 - strategy (node) Agent 請求範例

讓我們建立一個客服 AI Agent

資料模型定義

// 客服回應的資料結構
@Serializable
@SerialName("CustomerServiceResponse")
@LLMDescription("客服助手的回應結構")
data class CustomerServiceResponse(
    @property:LLMDescription("回應類型")
    val responseType: ResponseType,
    @property:LLMDescription("回應內容")
    val content: String,
    @property:LLMDescription("建議的後續動作")
    val suggestedActions: List<String>,
    @property:LLMDescription("是否需要人工介入")
    val requiresHumanIntervention: Boolean = false,
    @property:LLMDescription("相關訂單編號(如果適用)")
    val relatedOrderId: String? = null
)

@Serializable
enum class ResponseType {
    ERROR,           // AI 錯誤
    INFORMATION,    // 資訊查詢
    COMPLAINT,      // 客訴處理
    ORDER_INQUIRY,  // 訂單查詢
    TECHNICAL_SUPPORT, // 技術支援
    GENERAL         // 一般詢問
}

系統配置物件

// 客服系統配置物件
object CustomerServiceSystem {
    // 建立範例資料幫助 AI 理解
    val exampleResponses = listOf(
        CustomerServiceResponse(
            responseType = ResponseType.ORDER_INQUIRY,
            content = "您的訂單 ORD-2025-001 目前正在處理中,預計 3-5 個工作天內出貨",
            suggestedActions = listOf("追蹤物流狀態", "聯繫客服確認詳細時程"),
            requiresHumanIntervention = false,
            relatedOrderId = "ORD-2025-001"
        ),
        CustomerServiceResponse(
            responseType = ResponseType.COMPLAINT,
            content = "很抱歉造成您的困擾,我們會立即處理您的退貨申請",
            suggestedActions = listOf("安排退貨流程", "聯繫品質管理部門", "提供補償方案"),
            requiresHumanIntervention = true,
            relatedOrderId = null
        )
    )

    // 產生結構化資料定義
    val customerServiceStructure = JsonStructuredData.createJsonStructure<CustomerServiceResponse>(
        schemaFormat = JsonSchemaGenerator.SchemaFormat.JsonSchema,
        examples = exampleResponses,
        schemaType = JsonStructuredData.JsonSchemaType.SIMPLE,
    )
}

客服 Agent 類別

// 客服助手 Agent 類別
class CustomerServiceAgent() {

    // 建立客服策略
    private fun createStrategy() = strategy("customer-service") {
        val setup by nodeLLMRequest()

        val analyzeInquiry by node<Message.Response, CustomerServiceResponse> { _ ->
            // 在 Strategy Node 中使用結構化請求
            // 這裡使用 requestLLMStructured 而非 executeStructured
            // 因為在 Agent 的 writeSession 中已經有了執行上下文
            val structureResponse = llm.writeSession {
                requestLLMStructured(
                    structure = CustomerServiceSystem().customerServiceStructure,
                    fixingModel = OpenAIModels.CostOptimized.GPT4oMini
                )
            }

            structureResponse.fold(
                onSuccess = {
                    it.structure
                },
                onFailure = {
                    // 提供預設的客服回應
                    CustomerServiceResponse(
                        responseType = ResponseType.ERROR,
                        content = "處理時發生錯誤,請稍後再試",
                        suggestedActions = listOf("重新提交請求", "聯繫人工客服"),
                        requiresHumanIntervention = true,
                        relatedOrderId = null
                    )
                }
            )
        }

        val processResponse by node<CustomerServiceResponse, String> { response ->
            val result = StringBuilder()
            result.appendLine("=== 客服助手回應 ===")
            result.appendLine("類型:${response.responseType}")
            result.appendLine("內容:${response.content}")

            if (response.suggestedActions.isNotEmpty()) {
                result.appendLine("\n建議後續動作:")
                response.suggestedActions.forEach { action ->
                    result.appendLine("• $action")
                }
            }

            if (response.requiresHumanIntervention) {
                result.appendLine("\n⚠️  此案件需要人工介入處理")
            }

            response.relatedOrderId?.let { orderId ->
                result.appendLine("\n相關訂單:$orderId")
            }

            result.toString()
        }

        edge(nodeStart forwardTo setup)
        edge(setup forwardTo analyzeInquiry)
        edge(analyzeInquiry forwardTo processResponse)
        edge(processResponse forwardTo nodeFinish)
    }

    // 執行客服查詢
    suspend fun handleInquiry(inquiry: String): String {

        // 初始化 PromptExecutor
        val promptExecutor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!)

        val agent = AIAgent(
            promptExecutor = promptExecutor,
            toolRegistry = ToolRegistry.EMPTY,
            strategy = createStrategy(),
            agentConfig = AIAgentConfig(
                prompt = prompt("customer-service-prompt") {
                    system(
                        """
                        您是一個專業的客服助手,負責處理各種客戶詢問。
                        請根據客戶的問題分析類型,提供適當的回應,並建議後續處理動作。

                        處理原則:
                        1. 保持友善和專業的態度
                        2. 準確判斷問題類型
                        3. 提供清楚的解決方案或資訊
                        4. 必要時建議轉接人工客服
                        """.trimIndent()
                    )
                },
                model = OpenAIModels.CostOptimized.GPT4oMini,
                maxAgentIterations = 5
            )
        )

        return agent.run(inquiry)
    }
}

AI 客服使用範例

suspend fun main() {

    // 建立客服助手 Agent
    val customerServiceAgent = CustomerServiceAgent(promptExecutor)

    // 模擬客戶詢問
    val inquiries = listOf(
        "我的訂單 ORD-2025-001 什麼時候會到貨?",
        "產品有品質問題,我要退貨!",
        "如何更改我的會員資料?",
        "APP 一直當機,無法正常使用"
    )

    // 處理每個客戶詢問
    inquiries.forEach { inquiry ->
        println("\n客戶詢問:$inquiry")
        println("=" * 50)

        val response = customerServiceAgent.handleInquiry(inquiry)
        println(response)
        println()
    }
}

執行 AI 回應內容

客戶詢問:我的訂單 ORD-2025-001 什麼時候會到貨?
==================================================
=== 客服助手回應 ===
類型:ORDER_INQUIRY
內容:您的訂單 ORD-2025-001 目前應該在運送途中,建議查看訂單跟蹤資訊以獲得即時狀態更新。

建議後續動作:
• 查看訂單跟蹤資訊
• 聯繫客服以獲取更多詳細資訊

⚠️  此案件需要人工介入處理

相關訂單:ORD-2025-001


客戶詢問:產品有品質問題,我要退貨!
==================================================
=== 客服助手回應 ===
類型:COMPLAINT
內容:很抱歉聽到您遇到產品品質問題,我們會協助您處理退貨事宜。請您提供訂單號碼和產品資訊,以便我幫助您進行退貨流程。

建議後續動作:
• 提供訂單號碼和產品名稱
• 閱讀退貨政策
• 聯繫人工客服以獲取更直接的協助


客戶詢問:如何更改我的會員資料?
==================================================
=== 客服助手回應 ===
類型:INFORMATION
內容:要更改您的會員資料,請登入您的帳戶,前往會員中心或帳戶設置,然後修改所需資料並保存更改。

建議後續動作:
• 登入帳戶
• 前往會員中心進行資料修改
• 聯絡人工客服以獲得幫助


客戶詢問:APP 一直當機,無法正常使用
==================================================
=== 客服助手回應 ===
類型:TECHNICAL_SUPPORT
內容:您好!感謝您聯繫我們,抱歉聽到您在使用我們的APP時遇到問題。建議您檢查是否有可用的APP更新、重新啟動設備、清除APP緩存以及檢查網絡連接。希望這些步驟能幫助解決問題!

建議後續動作:
• 檢查應用商店中APP的更新
• 重新啟動您的設備
• 清除APP緩存
• 檢查您的網絡連接
• 聯絡人工客服獲取進一步協助

⚠️  此案件需要人工介入處理

最佳實踐指南

使用清楚的描述

@Serializable
data class Product(
    // 好的描述
    @property:LLMDescription("產品的唯一識別碼,格式如 PROD-001")
    val productId: String,

    // 不好的描述
    @property:LLMDescription("名稱")
    val name: String
)

提供範例資料

val examples = listOf(
    Product("PROD-001", "智慧手錶"),
    Product("PROD-002", "藍牙耳機")
)

val productStructure = JsonStructuredData.createJsonStructure<Product>(
    schemaGenerator = BasicJsonSchemaGenerator.Default,
    examples = examples  // 範例幫助 AI 更好地理解結構
)

結語

透過本篇文章,我們深入學習了 Koog 框架的結構化資料處理功能,這是建立可靠 AI 應用的核心技術之一。從簡單的資料模型定義到複雜的多型結構,我們掌握了如何讓 AI 產生可預測、類型安全的回應

結構化資料處理不僅解決了 AI 回應不一致的問題,更與我們在 Day 28 學習的測試策略相輔相成。當 AI 的輸出格式固定時,測試變得更加簡單直接,品質保證也更容易實施。這種「從源頭確保品質」的方法,是建立生產級 AI 應用的最佳實踐

下一篇文章,我們將探索 Koog AI 框架的 Embeddings 向量嵌入強大技術。結合結構化資料處理、測試策略和這些進階功能,你將能夠建立真正可靠的企業級 AI 應用

參考文件


支持創作

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


圖片來源:AI 產生