Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 23:進階節點操作:LLM 互動與工具執行

在上一篇文章中,我們學習了策略圖的基礎概念和自訂節點的使用方法。今天我們要進入更進階的領域,學習如何使用 Koog 提供的預定義節點來與 LLM 互動、執行工具,以及實現更複雜的流程控制

LLM 互動節點

nodeLLMRequest:發送請求給 LLM

這是最基本也是最重要的 LLM 互動節點,它將使用者訊息發送給 LLM 並取得回應

val strategy = strategy<String, String>("llm_interaction") {
    // 基本 LLM 請求節點 - 允許工具呼叫(預設)
    val askLLMNode by nodeLLMRequest("ask_llm")

    // 設定不允許工具呼叫的 LLM 節點 - 純文字回應
    val simpleResponseNode by nodeLLMRequest("simple_response", allowToolCalls = false)

    edge(nodeStart forwardTo askLLMNode)
    edge(askLLMNode forwardTo nodeFinish onAssistantMessage { true })
}

nodeUpdatePrompt:更新提示詞

在執行過程中動態修改 Agent 的提示詞

val updateContextNode by nodeUpdatePrompt<String>("update_context") {
    user("你是一位有 AI 客服助理")
    system("請根據輸入調整回應風格")
}

nodeLLMSendToolResult:回傳工具結果

當工具執行完成後,將結果發送回 LLM。這個節點會將工具執行結果加入對話歷史,並請求 LLM 根據工具結果產生回應

// 將工具執行結果發送給 LLM,並取得基於結果的回應
val sendResultNode by nodeLLMSendToolResult("send_result")

工具執行節點

nodeExecuteTool:執行單一工具

處理 LLM 發出的工具呼叫請求。這個節點會接收工具呼叫訊息,執行對應的工具,並回傳執行結果

// 執行 LLM 要求的單一工具呼叫
val executeToolNode by nodeExecuteTool("execute_tool")

nodeExecuteMultipleTools:平行執行多個工具

當需要同時執行多個工具時使用。這個節點可以平行處理多個工具呼叫,提升效率

// 平行執行多個工具呼叫
val executeMultipleToolsNode by nodeExecuteMultipleTools("execute_multiple")
// 將多個工具結果發送給 LLM 進行處理
val processMultipleResultsNode by nodeLLMSendMultipleToolResults("process_multiple")

edge(executeMultipleToolsNode forwardTo processMultipleResultsNode)

nodeExecuteSingleTool:執行指定工具

直接呼叫特定工具,不需要 LLM 決定。這個節點用於在已知需要執行特定工具時使用

// 注意:此節點用於直接執行特定工具,而非依賴 LLM 的工具呼叫決定
// 需要根據官方 API 文件確認正確的實作方式
val executeSpecificToolNode by nodeExecuteSingleTool<String, String>(
    tool = MyCustomTool()
    // 具體參數設定請參考官方 API 文件
)

進階流程控制

條件式邊(Conditional Edges)

Koog 提供多種條件類型來控制流程

// 基於輸出內容的條件
edge(sourceNode forwardTo targetNode onCondition { output ->
    output.contains("特定關鍵字")
})

// 基於 LLM 回應類型的條件
edge(llmNode forwardTo toolNode onToolCall { true })
edge(llmNode forwardTo finishNode onAssistantMessage { true })

// 基於多個工具呼叫的條件
edge(llmNode forwardTo multiToolNode onMultipleToolCalls { true })

// 當沒有工具被呼叫時
edge(llmNode forwardTo errorNode onToolNotCalled { true })

輸出轉換(Transformed)

在節點間傳遞資料時轉換格式

edge(sourceNode forwardTo targetNode
    transformed { output ->
        "處理後的資料:${output.uppercase()}"
    }
)

// 結合條件和轉換
edge(sourceNode forwardTo targetNode
    onCondition { it.isNotEmpty() }
    transformed { it.trim() }
)

平行節點執行(Parallel)

同時執行多個節點並選擇結果,這在需要比較不同處理方式或使用不同模型時很有用:

// 定義三個不同的處理節點
val nodeA by node<String, Int>("node_a") { 10 }
val nodeB by node<String, Int>("node_b") { 20 }
val nodeC by node<String, Int>("node_c") { 15 }

// 平行執行並選擇最佳結果
val parallelResult by parallel<String, Int>(
    nodeA, nodeB, nodeC
) {
    selectByMax { it } // 選擇最大值作為最終結果
}

智慧客服系統範例

讓我們建立一個完整的智慧客服系統,展示各種進階節點的使用

智慧客服系統流程圖

flowchart TD
    A[開始] --> B[分析客戶意圖節點]
    B --> C{意圖判斷}
    C -->|一般詢問| D[設定一般提示詞節點]
    C -->|訂單查詢| E[設定訂單提示詞節點]
    C -->|投訴處理| F[設定投訴提示詞節點]

    D --> G[處理一般詢問節點]
    E --> H[處理訂單查詢節點]
    F --> I[處理投訴處理節點]

    G --> J[結束]
    H --> K{LLM 回應類型}
    I --> L{LLM 回應類型}

    K -->|助手訊息| J
    K -->|工具呼叫| M[執行工具節點]
    L -->|助手訊息| J
    L -->|工具呼叫| M

    M --> N[發送工具結果節點]
    N --> O{回應類型}
    O -->|工具呼叫| M
    O -->|助手訊息| P[生成最終回應節點]
    P --> J

    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#fff3e0
    style D fill:#e8f5e8
    style E fill:#e8f5e8
    style F fill:#e8f5e8
    style G fill:#f3e5f5
    style H fill:#f3e5f5
    style I fill:#f3e5f5
    style J fill:#e8f5e8
    style K fill:#fff3e0
    style L fill:#fff3e0
    style M fill:#f3e5f5
    style N fill:#f3e5f5
    style O fill:#fff3e0
    style P fill:#f3e5f5

這個流程圖展示了智慧客服系統的完整處理流程

  • 意圖分析階段:根據客戶訊息判斷查詢類型(一般詢問、訂單查詢、投訴處理)
  • 提示詞設定階段:為不同類型的查詢設定對應的提示詞和處理策略
  • LLM 處理階段:使用不同的節點處理不同類型的查詢
    • 一般詢問:直接回應,不需要工具
    • 訂單查詢:可能需要查詢工具
    • 投訴處理:可能需要轉接或通知工具
  • 工具執行階段:當需要執行工具時的循環處理流程
  • 結果整合階段:整合工具結果並生成最終回應

新增相關工具

// 自訂工具:查詢訂單狀態
class QueryOrderTool : SimpleTool<QueryOrderTool.Args>() {
    @Serializable
    class Args(val orderId: String) : ToolArgs

    override val argsSerializer = Args.serializer()
    override val descriptor = ToolDescriptor("query_order", "查詢訂單狀態")

    override suspend fun doExecute(args: Args): String {
        // 模擬查詢訂單
        val orders = mapOf(
            "ORDER001" to "已出貨",
            "ORDER002" to "處理中",
            "ORDER003" to "已取消"
        )
        return orders[args.orderId] ?: "訂單不存在"
    }
}

// 自訂工具:發送通知
class SendNotificationTool : SimpleTool<SendNotificationTool.Args>() {
    @Serializable
    class Args(val message: String, val channel: String) : ToolArgs

    override val argsSerializer = Args.serializer()
    override val descriptor = ToolDescriptor("send_notification", "發送通知給客戶")

    override suspend fun doExecute(args: Args): String {
        println("📱 通過 ${args.channel} 發送通知:${args.message}")
        return "通知已發送"
    }
}

// 自訂工具:升級到人工客服
class EscalateToHumanTool : SimpleTool<EscalateToHumanTool.Args>() {
    @Serializable
    class Args(val reason: String) : ToolArgs

    override val argsSerializer = Args.serializer()
    override val descriptor = ToolDescriptor("escalate_to_human", "轉接人工客服")

    override suspend fun doExecute(args: Args): String {
        println("🎧 轉接人工客服,原因:${args.reason}")
        return "已轉接至人工客服"
    }
}

新增客服使用相關工具的流程

// 封裝客戶查詢和意圖的資料結構
data class CustomerQuery(
    val originalMessage: String,
    val intent: String
)

class SmartCustomerServiceAgent {

    private val toolRegistry = ToolRegistry {
        tool(QueryOrderTool())
        tool(SendNotificationTool())
        tool(EscalateToHumanTool())
    }

    private val agent = AIAgent(
        executor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!),
        toolRegistry = toolRegistry,
        systemPrompt = """
            你是一個專業的客服助手,能夠:
            1. 回答一般問題
            2. 查詢訂單狀態
            3. 處理客戶投訴
            4. 必要時轉接人工客服

            請使用正體中文回應客戶。
        """.trimIndent(),
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini,
        strategy = createCustomerServiceStrategy()
    )

    private fun createCustomerServiceStrategy() = strategy<String, String>("customer_service") {

        // 節點:分析客戶意圖並保留原始訊息
        val analyzeIntentNode by node<String, CustomerQuery>("analyze_intent") { userMessage ->
            println("🔍 分析客戶意圖:$userMessage")

            // 簡單的意圖分析
            val intent = when {
                userMessage.contains("訂單") || userMessage.contains("ORDER") -> "order_query"
                userMessage.contains("投訴") || userMessage.contains("問題") || userMessage.contains("不滿") -> "complaint"
                userMessage.contains("退貨") || userMessage.contains("退款") -> "return_request"
                else -> "general_inquiry"
            }

            CustomerQuery(userMessage, intent)
        }

        // 節點:設定一般詢問提示詞
        val setupGeneralPromptNode by node<CustomerQuery, CustomerQuery>("setup_general_prompt") { query ->
            llm.writeSession {
                rewritePrompt {
                    prompt("setup_general_prompt") {
                        system("你是一個友善的客服助手,請專業地回答客戶的一般詢問。")
                        user(query.originalMessage)
                    }
                }
            }
            query // 傳遞 query 到下一個節點
        }

        // 節點:設定訂單查詢提示詞
        val setupOrderPromptNode by node<CustomerQuery, CustomerQuery>("setup_order_prompt") { query ->
            llm.writeSession {
                rewritePrompt {
                    prompt("setup_order_prompt") {
                        system("""
                            你是一個客服助手,專門處理訂單查詢。
                            重要:如果客戶提到具體的訂單號碼(如 ORDER001、ORDER002 等),你必須使用 query_order 工具查詢訂單狀態,不要直接回答。
                            請先使用工具查詢,然後基於查詢結果回應客戶。
                        """.trimIndent())
                        user(query.originalMessage)
                    }
                }
            }
            query // 傳遞 query 到下一個節點
        }

        // 節點:設定投訴處理提示詞
        val setupComplaintPromptNode by node<CustomerQuery, CustomerQuery>("setup_complaint_prompt") { query ->
            llm.writeSession {
                rewritePrompt {
                    prompt("setup_complaint_prompt") {
                        system("""
                            客戶有投訴需要處理。請:
                            1. 表達同理心和歉意
                            2. 詢問具體問題詳情
                            3. 提供解決方案
                            4. 重要:如果問題嚴重或複雜,你必須使用 escalate_to_human 工具轉接人工客服
                            5. 如果需要發送通知給客戶,使用 send_notification 工具
                            優先使用適當的工具來處理投訴。
                        """.trimIndent())
                        user(query.originalMessage)
                    }
                }
            }
            query // 傳遞 query 到下一個節點
        }

        // 節點:處理一般詢問(使用 nodeLLMRequest)
        val handleGeneralInquiryNode by nodeLLMRequest("handle_general", allowToolCalls = false)

        // 節點:處理訂單查詢(使用 nodeLLMRequest 支援工具)
        val handleOrderQueryNode by nodeLLMRequest("handle_order", allowToolCalls = true)

        // 節點:處理投訴(使用 nodeLLMRequest 支援工具)
        val handleComplaintNode by nodeLLMRequest("handle_complaint", allowToolCalls = true)

        // 節點:執行工具
        val executeToolNode by nodeExecuteTool("execute_tool")

        // 節點:發送工具結果給 LLM
        val sendToolResultNode by nodeLLMSendToolResult("send_tool_result")

        // 節點:生成最終回應(基於工具結果)
        val generateFinalResponseNode by node<String, String>("generate_final_response") { _ ->
            llm.writeSession {
                updatePrompt {
                    system("""
                        現在請根據工具執行的結果,為客戶提供完整、專業且有幫助的回應。
                        請:
                        1. 直接回答客戶的問題
                        2. 基於工具查詢結果提供具體信息
                        3. 保持友善和專業的語調
                        4. 如果需要,詢問客戶是否還有其他需要協助的地方

                        不要只是確認工具已執行,而要基於結果提供實質性的回應。
                    """.trimIndent())
                }
                requestLLMWithoutTools().content
            }
        }

        // 定義執行流程
        edge(nodeStart forwardTo analyzeIntentNode)

        // 根據意圖分流到提示詞設定節點
        edge(analyzeIntentNode forwardTo setupGeneralPromptNode onCondition { query ->
            query.intent == "general_inquiry"
        })

        edge(analyzeIntentNode forwardTo setupOrderPromptNode onCondition { query ->
            query.intent == "order_query"
        })

        edge(analyzeIntentNode forwardTo setupComplaintPromptNode onCondition { query ->
            query.intent == "complaint" || query.intent == "return_request"
        })

        // 從提示詞設定節點到 LLM 處理節點
        edge(setupGeneralPromptNode forwardTo handleGeneralInquiryNode transformed { _ -> "" })
        edge(setupOrderPromptNode forwardTo handleOrderQueryNode transformed { _ -> "" })
        edge(setupComplaintPromptNode forwardTo handleComplaintNode transformed { _ -> "" })

        // 一般詢問直接結束(不需要工具)
        edge(handleGeneralInquiryNode forwardTo nodeFinish onAssistantMessage { true })

        // 訂單查詢流程:優先工具執行,但有fallback
        edge(handleOrderQueryNode forwardTo executeToolNode onToolCall { true })
        edge(handleOrderQueryNode forwardTo nodeFinish onAssistantMessage { true })

        // 投訴處理流程:優先工具執行,但有fallback
        edge(handleComplaintNode forwardTo executeToolNode onToolCall { true })
        edge(handleComplaintNode forwardTo nodeFinish onAssistantMessage { true })

        // 工具執行流程
        edge(executeToolNode forwardTo sendToolResultNode)
        edge(sendToolResultNode forwardTo executeToolNode onToolCall { true })
        edge(sendToolResultNode forwardTo generateFinalResponseNode onAssistantMessage { true })

        // 最終回應後結束
        edge(generateFinalResponseNode forwardTo nodeFinish)
    }

    suspend fun handleCustomerQuery(query: String): String {
        return agent.run(query)
    }
}

使用客服工具流程使用範例

suspend fun main() {
    val customerService = SmartCustomerServiceAgent()

    println("=== 智慧客服系統演示 ===\n")

    val testQueries = listOf(
        "你好,我想查詢 ORDER001 的訂單狀態",
        "我的商品有問題,沒有辦法開機,有沒有辦法換新的,我真的很生氣,可不可以請人跟我聯絡",
        "請問你們的營業時間是什麼時候?"
    )

    testQueries.forEachIndexed { index, query ->
        println("📞 客戶諮詢 ${index + 1}$query")
        println("=".repeat(50))

        try {
            val response = customerService.handleCustomerQuery(query)
            println("\n🤖 客服回應:")
            println(response)
        } catch (e: Exception) {
            println("❌ 處理失敗:${e.message}")
        }

        println("\n" + "=".repeat(60) + "\n")
    }
}

執行 AI 回應內容

=== 智慧客服系統演示 ===

📞 客戶諮詢 1:你好,我想查詢 ORDER001 的訂單狀態
==================================================
🔍 分析客戶意圖:你好,我想查詢 ORDER001 的訂單狀態
🔧 執行 query_order:查詢訂單 ORDER001

🤖 客服回應:
您好,您的訂單 ORDER001 已經出貨了,您可以留意配送進度。如果有需要協助追蹤運送或其他訂單相關問題,歡迎隨時告訴我!還有什麼我可以幫助您的嗎?

============================================================

📞 客戶諮詢 2:我的商品有問題,沒有辦法開機,有沒有辦法換新的,我真的很生氣,可不可以請人跟我聯絡
==================================================
🔍 分析客戶意圖:我的商品有問題,沒有辦法開機,有沒有辦法換新的,我真的很生氣,可不可以請人跟我聯絡
🎧 轉接人工客服,原因:客戶商品無法開機,要求換新並請人工聯絡

🤖 客服回應:
您好,非常抱歉聽到您的商品無法開機,讓您感到不便和不快。我已經幫您將問題轉接給我們的人工客服專員,專員將會盡快與您聯繫,協助您辦理換新或其他後續處理。若您還有其他問題或需要額外的協助,隨時告訴我,我會很樂意幫忙!

============================================================

📞 客戶諮詢 3:請問你們的營業時間是什麼時候?
==================================================
🔍 分析客戶意圖:請問你們的營業時間是什麼時候?

🤖 客服回應:
您好!我們的營業時間為週一至週五,上午9點至下午6點。如有其他問題,歡迎隨時向我們詢問!

============================================================

說實在的,這個呼叫工具的流程真的很難寫,除了 API 本身之外,文件其實也沒有寫的很詳細,有點麻煩。就算是寫了相關的流程,AI 也不一定會呼叫使用工具,呼叫了工具產生的結果也不一定是正確的,前後花了很多的時間,在測試寫法和流程

總結

今天我們了解了 Koog 框架的相關工具節點操作

  • LLM 互動節點:nodeLLMRequest、nodeUpdatePrompt、nodeLLMSendToolResult
  • 工具執行節點:nodeExecuteTool、nodeExecuteMultipleTools
  • 進階流程控制:條件邊、輸出轉換、平行處理
  • 實際應用:客服系統的相關工具使用實作

這些進階節點讓我們能夠建構出功能豐富、邏輯複雜的 AI 應用系統

下一篇文章我們將學習子圖與模組化,了解如何將複雜的策略圖分解成可重用的模組,進一步提升系統的可維護性和擴展性

參考資源


支持創作

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


圖片來源:AI 產生