Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 32:歷史記錄壓縮:優化對話上下文

在前一篇文章中,我們學習了 RAG(檢索增強生成)系統,讓 AI 能夠基於文件知識庫回答問題,大幅提升回應的準確性和可靠性。RAG 解決了知識來源的問題,但在實際應用中,當對話持續進行,我們會面臨另一個挑戰:如何有效管理不斷增長的對話歷史?

想像你的客服 AI 代理已經和客戶對話了 50 輪,每一輪都包含問題和回答,還有 RAG 檢索到的文件內容,這時候歷史記錄可能已經包含數千個 token。這不僅會觸及模型的 context window 限制,更會讓 API 成本快速累積

今天我們要探討 Koog AI 框架的 History Compression(歷史記錄壓縮)功能。這項技術會在 LLM session 內部自動將冗長的對話歷史壓縮成精簡的摘要,直接替換原本 prompt 中的訊息。與前面學到的 RAG 和 Embeddings 相輔相成——Embeddings 幫助我們理解內容的語意重要性,RAG 提供可靠的知識來源,而歷史壓縮則確保我們只保留最關鍵的資訊

為什麼需要歷史記錄壓縮?

遇到的問題

  • Token 限制:大多數 LLM 都有 context window 限制,超過就會出錯
  • 成本增加:每次請求都會計算所有歷史記錄的 token,費用會快速累積
  • 效能下降:處理過長的上下文會讓回應速度變慢
  • 注意力分散:太多無關的歷史記錄可能會影響 AI 的判斷能力

壓縮的好處

  • 節省成本:移除不重要的歷史記錄,減少 token 使用量
  • 提升效能:較短的上下文處理更快
  • 保持專注:留下重要資訊,讓 AI 更專注於當前任務
  • 避免限制:不會因為 token 超限而中斷對話

何時進行歷史壓縮?

在邏輯步驟(subgraphs)之間

當 Agent 完成一個任務階段,準備進入下一個階段時,這是進行壓縮的理想時機

val strategy = strategy<String, String>("multi-stage-agent") {

    // 第一階段:收集資訊
    val collectInformation by subgraph<String, String> {
        // 收集用戶需求和相關資訊
    }

    // 壓縮歷史,為下一階段準備乾淨的上下文
    val compressHistory by nodeLLMCompressHistory<String>(
        strategy = HistoryCompressionStrategy.WholeHistory
    )

    // 第二階段:分析和決策
    val analyzeAndDecide by subgraph<String, String> {
        // 基於收集的資訊進行分析和決策
    }

    // 連接各階段
    nodeStart then collectInformation then compressHistory then analyzeAndDecide
}

當上下文變得太長時

當對話歷史的訊息數量或 token 數超過設定的閾值時,系統會自動觸發壓縮

// 基於訊息數量判斷
private suspend fun AIAgentContextBase.shouldCompress(): Boolean {
    return llm.readSession { prompt.messages.size > 20 }
}

// 基於內容長度判斷
private suspend fun AIAgentContextBase.shouldCompressByLength(): Boolean {
    return llm.readSession {
        prompt.messages.sumOf { it.content.length } > 8000
    }
}

// 在策略中使用條件判斷
edge(executeTool forwardTo compressHistory onCondition { shouldCompress() })

壓縮時機的策略考量

選擇適當的壓縮時機非常重要

  • 太早壓縮:可能失去重要的上下文資訊
  • 太晚壓縮:會浪費 token 和降低效能
  • 階段性壓縮:在任務完成節點進行壓縮,確保不會遺失進行中的重要資訊

Koog AI 的四種壓縮策略

Koog AI 提供了四種不同的歷史記錄壓縮策略,我們來一一了解

WholeHistory(完整歷史壓縮)

這是預設策略,會將整個對話歷史壓縮成一個摘要

val compressHistory by nodeLLMCompressHistory<String>(
    strategy = HistoryCompressionStrategy.WholeHistory
)

適用情況

  • 對話內容都很重要
  • 需要保持完整的上下文理解
  • 不確定哪些內容可以捨棄

FromLastNMessages(保留最近 N 條訊息)

保留最近的 N 條訊息,壓縮其餘部分

val compressHistory by nodeLLMCompressHistory<String>(
    strategy = HistoryCompressionStrategy.FromLastNMessages(10)
)

適用情況

  • 最近的對話最重要
  • 早期的對話可能不太相關
  • 需要快速壓縮且效果可預期

Chunked(分塊壓縮)

將歷史記錄分成多個區塊,分別壓縮

val compressHistory by nodeLLMCompressHistory<String>(
    strategy = HistoryCompressionStrategy.Chunked(chunkSize = 10)
)

適用情況

  • 對話有明確的主題區段
  • 希望保留不同時期的重要資訊
  • 需要平衡壓縮效率和內容保留

RetrieveFactsFromHistory(提取特定事實)

這是最進階的策略,可以指定要從歷史記錄中提取哪些特定資訊

val compressHistory by nodeLLMCompressHistory<String>(
    strategy = HistoryCompressionStrategy.RetrieveFactsFromHistory(
        Concept(
            keyword = "user_preferences",
            description = "使用者的個人偏好設定,包括語言、主題風格等",
            factType = FactType.MULTIPLE
        ),
        Concept(
            keyword = "issue_resolved",
            description = "客戶的問題是否已經解決?",
            factType = FactType.SINGLE
        )
    )
)

適用情況

  • 明確知道需要保留哪些資訊
  • 不同類型的資訊有不同重要性
  • 需要精確控制壓縮結果

歷史壓縮的實作方式

有兩種主要的方式來實作歷史記錄壓縮

在策略圖中實作

這是最常用和推薦的方式,使用 nodeLLMCompressHistory 節點在策略圖中定義壓縮邏輯

val strategy = strategy<String, String>("agent-with-compression") {
    val processRequest by nodeLLMRequest()
    val executeTool by nodeExecuteTool()
    val sendToolResult by nodeLLMSendToolResult()

    // 在策略圖中加入壓縮節點
    val compressHistory by nodeLLMCompressHistory<ReceivedToolResult>(
        strategy = HistoryCompressionStrategy.FromLastNMessages(10)
    )

    // 定義何時觸發壓縮
    edge(executeTool forwardTo compressHistory onCondition {
        llm.readSession { prompt.messages.size > 15 }
    })
    edge(compressHistory forwardTo sendToolResult)
}

優點

  • 清晰的流程控制,易於理解和維護
  • 可以靈活設定觸發條件
  • 與其他節點整合方便

適用場景

  • 一般的 Agent 應用
  • 需要條件式觸發壓縮
  • 團隊協作開發

在自定義節點中實作

在自定義節點內部使用 replaceHistoryWithTLDR() 函數進行壓縮,提供更精細的控制

特點

  • 在節點內部直接調用 llm.writeSession { replaceHistoryWithTLDR() }
  • 可以在壓縮前後執行自定義邏輯
  • 支援所有四種壓縮策略
  • 能夠結合複雜的業務邏輯

優點

  • 更精細的控制權
  • 可以在壓縮前後執行額外處理
  • 適合複雜的壓縮邏輯

適用場景

  • 需要複雜的壓縮邏輯
  • 要在壓縮前後執行特殊處理
  • 高度客製化的應用

如何選擇實作方式?

實作方式適用情況優勢注意事項
策略圖實作一般應用、清晰流程易維護、清晰可見較少自定義空間
自定義節點實作複雜邏輯、特殊需求高度靈活、精細控制實作複雜度較高

對於大多數應用,建議優先選擇策略圖實作方式,因為它提供了良好的可讀性和維護性

AI 客服歷史記錄壓縮範例(策略圖實作)

這個範例展示了策略圖實作方式的具體應用,讓我們看看如何在客服系統中整合歷史記錄壓縮功能

範例會展示壓縮是如何在 LLM session 內部自動進行的,以及如何診斷壓縮前後的差異

注意:下面是一個簡化版本的流程範例,主要用於說明策略圖實作的概念。在實際應用中,可能需要配合更複雜的工具和業務邏輯(如 Day 23 或 Day 24 的範例)才能觀察到明顯的壓縮效果

class CustomerServiceAgentWithHistoryCompression {

    // 檢查歷史記錄是否過長(超過 10 條訊息就壓縮)
    private suspend fun AIAgentContextBase.shouldCompressHistory(): Boolean {
        return llm.readSession { prompt.messages.size > 10 }
    }

    private val agent = AIAgent(
        executor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!),
        systemPrompt = """
            你是一個專業的客服助手,負責回答客戶問題。
            請用正體中文回應客戶,保持友善和專業的態度。
        """.trimIndent(),
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini,
        strategy = createStrategy()
    )

    private fun createStrategy() = strategy<String, String>("customer-service-with-compression") {

        // 定義主要處理節點
        val processRequest by nodeLLMRequest()
        val executeTool by nodeExecuteTool()
        val sendToolResult by nodeLLMSendToolResult()

        // 歷史記錄壓縮節點 - 使用 FromLastNMessages 策略
        val compressHistory by nodeLLMCompressHistory<ReceivedToolResult>(
            strategy = HistoryCompressionStrategy.FromLastNMessages(5)
        )

        // 診斷節點 - 展示壓縮前後的狀態
        val diagnosticNode by node<ReceivedToolResult, ReceivedToolResult>("diagnostic") { toolResult ->
            println("📊 === 歷史記錄壓縮觸發 ===")

            // 顯示壓縮將要發生
            val beforeMessages = llm.readSession { prompt.messages.size }
            println("🔍 壓縮前訊息數量: $beforeMessages 條")
            println("⚡ 即將觸發壓縮:保留最近 5 條訊息,將早期對話摘要化")

            toolResult
        }

        // 壓縮後檢查節點
        val postCompressionCheck by node<ReceivedToolResult, ReceivedToolResult>("post_compression") { toolResult ->
            val afterMessages = llm.readSession { prompt.messages.size }
            println("✅ 壓縮完成!目前訊息數量: $afterMessages 條")

            toolResult
        }

        // 建立執行流程
        edge(nodeStart forwardTo processRequest)

        // 如果是助理回應,直接結束
        edge(processRequest forwardTo nodeFinish onAssistantMessage { true })

        // 如果需要使用工具,執行工具
        edge(processRequest forwardTo executeTool onToolCall { true })

        // 執行工具後檢查是否需要壓縮歷史
        edge(executeTool forwardTo diagnosticNode onCondition { shouldCompressHistory() })
        edge(diagnosticNode forwardTo compressHistory)
        edge(compressHistory forwardTo postCompressionCheck)
        edge(postCompressionCheck forwardTo sendToolResult)

        // 如果不需要壓縮,直接發送工具結果
        edge(executeTool forwardTo sendToolResult onCondition { !shouldCompressHistory() })

        // 處理工具結果後的後續動作
        edge(sendToolResult forwardTo executeTool onToolCall { true })
        edge(sendToolResult forwardTo nodeFinish onAssistantMessage { true })
    }

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

最佳實踐建議

選擇合適的壓縮策略

// 一般客服:保留最近對話即可
HistoryCompressionStrategy.FromLastNMessages(15)

// 技術支援:需要保留完整上下文
HistoryCompressionStrategy.WholeHistory

// 銷售對話:提取客戶偏好和需求
RetrieveFactsFromHistory(
    Concept("customer_needs", "客戶需求和偏好", FactType.MULTIPLE),
    Concept("budget_range", "客戶預算範圍", FactType.SINGLE)
)

// 長期專案:分段保留不同階段資訊
HistoryCompressionStrategy.Chunked(20)

使用 preserveMemory 參數

val compressHistory by nodeLLMCompressHistory<String>(
    strategy = HistoryCompressionStrategy.FromLastNMessages(10),
    preserveMemory = true  // 保留 AgentMemory feature 的記憶訊息
)

什麼時候使用 preserveMemory = true

  • 你的 Agent 使用了 AgentMemory feature
  • 需要保留記憶系統相關的 prompt 訊息
  • 避免壓縮時誤刪重要的記憶上下文
  • 確保個人化功能正常運作

總結

透過本篇文章,我們深入學習了 Koog AI 框架的歷史記錄壓縮功能,這是建立高效能 AI 應用的關鍵技術。從四種壓縮策略到實際的範例說明,我們掌握了如何在長時間對話中自動維持效能與品質的平衡

  • 自動化壓縮:無需外部儲存,LLM session 內部自動處理壓縮
  • 控制成本:透過減少 token 使用量來降低 API 費用(可節省 70-80% 成本)
  • 提升效能:較短的上下文處理更快,回應時間顯著改善
  • 靈活應對場景:四種策略(WholeHistory、FromLastNMessages、Chunked、RetrieveFactsFromHistory)適應不同使用情況

明天我們將學習如何自定義 AI 模型設定,探討當新模型(如 GPT-5)發布時,如何快速在 Koog 框架中配置和使用。結合今天學到的歷史壓縮技術,我們將能夠充分發揮新模型的能力,同時控制使用成本,建立更強大的 AI 應用系統

參考文件


支持創作

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


圖片來源:AI 產生