Logo
Published on

第一次學 Kotlin Koog AI 就上手 Day 14:賦予 AI 記憶:實現個人化服務的關鍵

在前一篇文章中,我們學習了提示快取機制,大幅提升了系統回應速度並節省了 API 成本。快取幫助我們解決了重複查詢的效能問題,而回應時間可能從秒級降至毫秒級

今天我們要探索一個完全不同的概念:Agent 記憶體系統。如果說快取是為了短期效能優化,那記憶體系統就是為了長期學習和個人化。透過記憶體功能,AI 代理能夠記住重要資訊、學習使用者偏好,並在未來的互動中提供更智慧、更個人化的服務

記憶體 vs 快取:關鍵差異一覽表

在深入實作之前,讓我們用一個簡單的對比表格來理解這兩個系統的差異

比較項目快取系統(Day 13)記憶體系統(Day 14)
主要目的短期效能優化長期學習和個人化
儲存內容完整的提示和回應提取的重要資訊和偏好
生命週期短期(分鐘到小時)長期(天到年)
使用時機重複查詢的快速回應個人化服務和智慧推薦
範例應用"API 金鑰設定步驟" 的完整回答"使用者偏好詳細說明"
價值體現節省成本,提升速度增強體驗,建立關係

簡單來說

  • 快取:讓 AI 回答得更快 ⚡
  • 記憶體:讓 AI 回答得更貼心 ❤️

PersonalizedGreeter:個人化問候範例

讓我們透過一個簡單而實用的範例來學習記憶體系統。我們將建立一個 PersonalizedGreeter,它能

  • 檢查記憶體中是否有使用者姓名
  • 如果有,提供個人化問候
  • 如果沒有,詢問姓名並儲存到記憶體中

在這裡是把對話中的姓名截取出來,如果要比較簡單的話,可以把整個句子塞到記憶體裡面,拿出來丟給 AI,它應該有辦法處理 XD

基礎記憶體配置

class PersonalizedGreeter {

    // 配置 memory provider
    private val memoryProvider = LocalFileMemoryProvider(
        config = LocalMemoryConfig("personalized-greeter"),
        storage = SimpleStorage(JVMFileSystemProvider.ReadWrite),
        fs = JVMFileSystemProvider.ReadWrite,
        root = Path("memory/user-data"),
    )

    // 定義使用者資訊概念
    private val userInfoConcept = Concept(
        "user-info",
        "使用者的基本資訊,包含姓名和偏好",
        FactType.SINGLE
    )

    // 使用者記憶體主題
    private val userSubject = object : MemorySubject() {
        override val name: String = "user"
        override val promptDescription: String = "使用者的個人資訊和偏好設定"
        override val priorityLevel: Int = 1
    }

    // 創建具備記憶體功能的 Agent
    private val agent = AIAgent(
        executor = simpleOpenAIExecutor(ApiKeyManager.openAIApiKey!!),
        systemPrompt = createSystemPrompt(),
        llmModel = OpenAIModels.CostOptimized.GPT4_1Mini
    ) {
        // 安裝記憶體功能
        install(AgentMemory) {
            memoryProvider = this@PersonalizedGreeter.memoryProvider
            agentName = "personalized-greeter"        // Agent 識別名稱
            featureName = "personalized-greeter"      // 功能名稱
            organizationName = "demo-app"             // 組織名稱
            productName = "greeting-service"          // 產品名稱
        }
    }

    // 💡 參數與記憶體作用域的關係
    //
    // AgentMemory 安裝時的參數會影響記憶體的組織層級
    // • agentName      → 對應 MemoryScope.Agent
    // • featureName    → 對應 MemoryScope.Feature
    // • productName    → 對應 MemoryScope.Product
    // • organizationName → 提供額外的組織上下文
    //
    // 這些參數幫助建立記憶體的層級結構,讓您可以根據不同的業務需求來組織和檢索記憶體

    private fun createSystemPrompt() = """
        你是一個友善的個人化助手。

        核心能力:
        - 記住使用者的姓名和偏好
        - 提供個人化的問候和服務
        - 在初次見面時主動詢問並記住使用者資訊

        行為準則:
        - 如果知道使用者姓名,要親切地稱呼他們
        - 如果是新使用者,要禮貌地詢問姓名並記住
        - 始終保持友善和專業的態度
        - 使用正體中文回應
    """.trimIndent()

    /**
     * 處理使用者互動的主要方法
     */
    suspend fun greetUser(userInput: String): PersonalizedResponse {

        try {
            // 嘗試從記憶體載入使用者資訊
            val userName = loadUserName()

            // 根據是否有記憶決定回應方式
            val enhancedInput = if (userName != null) {
                // 有記憶:提供個人化上下文
                "使用者姓名:$userName\n使用者說:$userInput"
            } else {
                // 無記憶:正常處理
                userInput
            }

            // 處理請求
            val response = agent.run(enhancedInput)

            // 嘗試從回應中學習新資訊
            learnFromInteraction(userInput, response)

            return PersonalizedResponse(
                response = response,
                hasMemory = userName != null,
                userName = userName
            )

        } catch (e: Exception) {
            return PersonalizedResponse(
                response = "很抱歉,系統暫時無法處理您的請求。",
                hasMemory = false,
                error = e.message
            )
        }
    }

    /**
     * 從記憶體載入使用者姓名
     */
    private suspend fun loadUserName(): String? {
        return try {

            val userMemories = memoryProvider.load(
                concept = userInfoConcept,
                subject = userSubject,
                scope = MemoryScope.Product("personalized-service")
            )

            userMemories.firstOrNull()?.let { memory ->
                when (memory) {
                    is SingleFact -> memory.value
                    else -> null
                }
            }
        } catch (e: Exception) {
            println("⚠️ 載入使用者記憶時發生錯誤: ${e.message}")
            null
        }
    }

    /**
     * 從互動中學習新資訊
     */
    private suspend fun learnFromInteraction(
        userInput: String,
        response: String
    ) {
        try {
            // 簡單的姓名識別邏輯
            when {
                userInput.contains("我是") || userInput.contains("我叫") -> {
                    val possibleName = extractNameFromInput(userInput)
                    if (possibleName != null) {
                        saveUserName(possibleName)
                    }
                }

                response.contains("請問您的姓名") || response.contains("可以告訴我您的名字") -> {
                    // AI 正在詢問姓名,暫不學習
                }
            }
        } catch (e: Exception) {
            println("⚠️ 學習過程中發生錯誤: ${e.message}")
        }
    }

    /**
     * 從使用者輸入中提取姓名
     */
    private fun extractNameFromInput(input: String): String? {
        val patterns = listOf(
            Regex("我是\\s*([^\\s,,]{2,4})"),
            Regex("我叫\\s*([^\\s,,]{2,4})"),
            Regex("叫我\\s*([^\\s,,]{2,4})")
        )

        for (pattern in patterns) {
            pattern.find(input)?.let { matchResult ->
                return matchResult.groupValues[1]
            }
        }
        return null
    }

    /**
     * 將使用者姓名儲存到記憶體
     */
    private suspend fun saveUserName(userName: String) {
        try {
            memoryProvider.save(
                fact = SingleFact(
                    concept = userInfoConcept,
                    value = userName,
                    timestamp = System.currentTimeMillis()
                ),
                subject = userSubject,
                scope = MemoryScope.Product("personalized-service")
            )

            println("🧠 已記住使用者姓名:$userName")

        } catch (e: Exception) {
            println("⚠️ 儲存使用者資訊時發生錯誤: ${e.message}")
        }
    }
}

/**
 * 個人化回應資料類別
 */
data class PersonalizedResponse(
    val response: String,
    val hasMemory: Boolean,
    val userName: String? = null,
    val error: String? = null
)

實際使用範例

讓我們透過一個完整的示範來看看記憶體系統如何運作

suspend fun main() {
    val greeter = PersonalizedGreeter()

    println("🤖 個人化問候助手啟動")
    println("=".repeat(50))

    // === 第一次互動:新使用者 ===
    println("\n👋 第一次見面")
    println("=".repeat(20))

    val firstResponse = greeter.greetUser(
        userInput = "你好"
    )

    println("使用者:你好")
    println("助手:${firstResponse.response}")
    println("📊 記憶體狀態:${if (firstResponse.hasMemory) "有記憶" else "無記憶"}")

    delay(1000)

    // === 自我介紹:儲存姓名 ===
    println("\n📝 自我介紹")
    println("=".repeat(20))

    val introResponse = greeter.greetUser(
        userInput = "我是 Cash"
    )

    println("使用者:我是 Cash")
    println("助手:${introResponse.response}")
    println("📊 記憶體狀態:${if (introResponse.hasMemory) "有記憶" else "無記憶"}")
    println("👤 記住的姓名:${introResponse.userName ?: "未記住"}")

    delay(1000)

    // === 第二次互動:展現記憶 ===
    println("\n🎯 個人化服務")
    println("=".repeat(20))

    val personalizedResponse = greeter.greetUser(
        userInput = "今天天氣如何?"
    )

    println("使用者:今天天氣如何?")
    println("助手:${personalizedResponse.response}")
    println("📊 記憶體狀態:${if (personalizedResponse.hasMemory) "有記憶" else "無記憶"}")
    println("👤 識別身份:${personalizedResponse.userName ?: "未識別"}")

    delay(1000)

    println("\n✨ 記憶體系統展示完成!")
}

執行 AI 回應內容

重點觀察

  • 第一次互動:系統沒有記憶,一般性回應
  • 自我介紹後:系統學習並記住姓名
  • 後續互動:系統能識別身份,提供個人化服務

如果覺得下面的 log 看起來怪怪的,是因為執行的先後順序的關係,可以看一下程式碼的順序就會比較了解

🤖 個人化問候助手啟動
==================================================

👋 第一次見面
====================
使用者:你好
助手:你好,Cash!很高興再次見到你,有什麼我可以幫忙的嗎?
📊 記憶體狀態:無記憶

📝 自我介紹
====================
🧠 已記住使用者姓名:Cash
使用者:我是 Cash
助手:你好,Cash!很高興認識你。有什麼我可以幫助你的嗎?
📊 記憶體狀態:無記憶
👤 記住的姓名:未記住

🎯 個人化服務
====================
使用者:今天天氣如何?
助手:您好,Cash!請問您想查詢哪個地區的天氣呢?我可以幫您查詢當地的天氣狀況。
📊 記憶體狀態:有記憶
👤 識別身份:Cash

✨ 記憶體系統展示完成!

儲存到檔案的記憶內容

{
  "user-info": [
    {
      "type": "ai.koog.agents.memory.model.SingleFact",
      "concept": {
        "keyword": "user-info",
        "description": "使用者的基本資訊,包含姓名和偏好",
        "factType": "SINGLE"
      },
      "timestamp": 1755224556246,
      "value": "Cash"
    }
  ]
}

記憶體系統的核心概念

雖然我們的範例相對簡單,但背後涉及幾個重要概念

記憶體生命週期

// 記憶體的三個階段
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    儲存      │ => │    載入      │ => │    學習      │
save()      │    │ load()      │    │ 持續優化      │
└─────────────┘    └─────────────┘    └─────────────┘

資料結構

  • Concept:定義要記住的資訊類型(如:使用者資訊)
  • Subject:定義資訊的主體(歸屬)者(如:使用者)
  • Fact:具體的資訊內容(如:姓名是李小明)

記憶體作用域

// 不同作用域的記憶體用途
MemoryScope.Product("personalized-service")  // 產品層級:個人化服務
MemoryScope.Feature("greeting")               // 功能層級:問候功能
MemoryScope.Agent("agent-id")                 // Agent 層級:特定 agent 專用記憶體
MemoryScope.CrossProduct                      // 跨產品層級:多產品共享記憶體

實際應用範例:根據需求選擇記憶體作用域

// 產品層級:個人化服務中的使用者偏好(所有相關 agent 都能存取)
memoryProvider.save(
    fact = SingleFact(concept, "Cash", timestamp),
    subject = subject,
    scope = MemoryScope.Product("personalized-greeting")  // 整個問候服務產品
)

// 功能層級:特定功能內的設定(僅該功能相關的 agent 能存取)
memoryProvider.save(
    fact = SingleFact(concept, "Cash", timestamp),
    subject = subject,
    scope = MemoryScope.Feature("greeting")  // 僅個人化問候功能
)

// Agent 層級:特定 agent 的內部狀態(僅該 agent 能存取)
memoryProvider.save(
    fact = SingleFact(concept, "Cash", timestamp),
    subject = subject,
    scope = MemoryScope.Agent("agent-id")  // 僅此 agent
)

// 跨產品層級:多個產品間共享的使用者資訊
memoryProvider.save(
    fact = SingleFact(concept, "Cash", timestamp),
    subject = subject,
    scope = MemoryScope.CrossProduct  // 所有產品都能存取
)

選擇建議

  • Product:使用者在該產品中的偏好和資訊
  • Feature:功能特定的設定和狀態
  • Agent:個別 agent 的內部狀態和臨時資料
  • CrossProduct:跨產品的全域使用者資訊

事實類型:SingleFact vs MultipleFacts

除了 SingleFact,Koog 還支援 MultipleFacts 來儲存多個值

// 儲存使用者的多個興趣愛好
val interestsConcept = Concept(
    "user-interests",
    "使用者的興趣愛好清單",
    FactType.MULTIPLE
)

// 儲存多個興趣
memoryProvider.save(
    fact = MultipleFacts(
        concept = interestsConcept,
        values = listOf("程式設計", "機器學習", "遊戲開發"),
        timestamp = System.currentTimeMillis()
    ),
    subject = userSubject,
    scope = MemoryScope.Product("user-profile")
)

// 載入時會取得所有值
val userInterests = memoryProvider.load(interestsConcept, userSubject, scope)
userInterests.firstOrNull()?.let { memory ->
    when (memory) {
        is MultipleFacts -> {
            println("使用者興趣:${memory.values.joinToString(", ")}")
        }
        else -> { /* 處理其他類型 */ }
    }
}

使用場景建議

  • SingleFact:姓名、年齡、偏好語言等單一值
  • MultipleFacts:興趣清單、技能標籤、歷史記錄等多值資料

記憶體 vs 快取的共同協作

在實際應用中,記憶體和快取系統可以完美協作

class SmartCustomerServiceWithBoth {
    // 快取:效能優化
    private val cache = InMemoryPromptCache(maxEntries = 1000)
    // 記憶體:個人化
    private val memory = LocalFileMemoryProvider(...)

    suspend fun handleQuery(query: String, customerId: String): String {
        // 1. 先檢查快取(如果是常見問題,立即回應)
        val cachedResponse = cache.get(createPromptRequest(query))
        if (cachedResponse != null) {
            // 加入個人化元素
            return addPersonalTouch(cachedResponse, customerId)
        }

        // 2. 從記憶體載入個人化資訊
        val personalInfo = loadUserInfoFromMemory(customerId)

        // 3. 處理查詢(結合個人化資訊)
        val response = agent.run("$personalInfo\n使用者問題:$query")

        // 4. 存入快取(為下次同樣問題準備)
        cache.put(createPromptRequest(query), createPromptResponse(response))

        return response
    }
}

上面的程式碼為示意,並非真正可以執行的程式碼,僅供參考

這樣的設計帶來雙重好處

  • 快取:常見問題秒回,節省成本
  • 記憶體:個人化體驗,建立關係

記憶體供應商類型

除了我們在範例中使用的 LocalFileMemoryProvider,Koog 還提供其他類型的記憶體供應商

NoMemory(預設選項)

// 不儲存任何記憶體資訊的預設供應商
val agent = AIAgent(...) {
    // 未安裝 AgentMemory 時,預設使用 NoMemory
    // 適用於不需要記憶功能的簡單場景
}

自定義記憶體供應商

// 實作自定義記憶體供應商
class CustomMemoryProvider : AgentMemoryProvider {
    override suspend fun save(
        fact: Fact,
        subject: MemorySubject,
        scope: MemoryScope
    ) {
        // 自定義儲存邏輯(如:資料庫、雲端服務等)
    }

    override suspend fun load(
        concept: Concept,
        subject: MemorySubject,
        scope: MemoryScope
    ): List<Fact> {
        // 自定義載入邏輯
        return emptyList()
    }
}

選擇建議

  • LocalFileMemoryProvider:適合單機應用或開發階段
  • NoMemory:適合無狀態的簡單對話場景
  • 自定義供應商:適合需要整合既有系統(如:資料庫、Redis)的場景

總結

在本篇文章中,我們透過範例學習了 Koog 記憶體系統的核心概念。我們明確區分了記憶體與快取的不同用途,並實作了一個能記住使用者姓名,提供個人化問候的 AI 助手

關鍵收穫

  • 概念理解:記憶體用於長期學習,快取用於短期優化
  • 實作經驗:LocalFileMemoryProvider 的基本配置和使用
  • 設計模式:資訊的提取、儲存、載入三階段流程
  • 協同效應:記憶體與快取可以相互配合,創造更好的使用體驗

下一篇文章,我們將探索 Koog 的事件處理與生命週期管理,學習如何監控和調試 Agent 的執行過程,為我們不斷進化的 AI 系統加入更強大的觀測能力

參考資料


支持創作

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


圖片來源:AI 產生