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