端AI記憶共享系統(tǒng)的自研實(shí)踐:從mem0對(duì)比到混合檢索落地)
先交代背景。我一直在做 AI 輔助日常工作的落地桌面端、Web 端、手機(jī)端、編輯器插件輪著用。用了半年多最煩的一個(gè)問(wèn)題就是同一個(gè) AI 服務(wù)在這個(gè)客戶(hù)端里聊過(guò)的上下文換到另一個(gè)客戶(hù)端就全斷了。比如我在電腦上讓 AI 梳理了一份項(xiàng)目的技術(shù)方案轉(zhuǎn)頭在手機(jī)上問(wèn)“那個(gè)方案里的數(shù)據(jù)庫(kù)選型定了沒(méi)有”它一臉茫然。這種“記憶斷裂”在 AI Agent 場(chǎng)景下尤其致命因?yàn)?Agent 的連續(xù)推理、多步任務(wù)執(zhí)行都依賴(lài)上下文而上下文一旦散落在多個(gè)客戶(hù)端里等于沒(méi)有上下文。后來(lái)我去調(diào)研了 mem0業(yè)內(nèi)很火的開(kāi)源記憶層方案理念很吸引人把每次對(duì)話提取成結(jié)構(gòu)化記憶用向量和圖混合存儲(chǔ)查詢(xún)時(shí)做智能重排。但我把它接入到真實(shí)的多客戶(hù)端工作流里跑了兩周之后還是決定自己寫(xiě)一套跨客戶(hù)端 AI 記憶共享系統(tǒng)。這篇文章把我當(dāng)時(shí)的對(duì)比過(guò)程、踩過(guò)的坑、最終的自研設(shè)計(jì)和關(guān)鍵參數(shù)完整記錄下來(lái)給同樣在折騰 AI Agent 記憶層的朋友做個(gè)參考。1. 我為什么放棄了 mem0不是它不夠好而是場(chǎng)景不匹配先說(shuō)結(jié)論mem0 本身是個(gè)好項(xiàng)目但它的默認(rèn)設(shè)計(jì)是為“單客戶(hù)端、單 Agent、以查詢(xún)?yōu)楹诵摹钡膱?chǎng)景服務(wù)的。而我實(shí)際面對(duì)的是“多個(gè)客戶(hù)端、多個(gè) Agent、以同步為核心”的場(chǎng)景這兩者對(duì)架構(gòu)的要求完全不一樣。1.1 先說(shuō)我的實(shí)際場(chǎng)景多客戶(hù)端共享的真正困境我的日常使用方式是這樣的電腦上的瀏覽器插件負(fù)責(zé)長(zhǎng)文閱讀和資料整理手機(jī)上的 AI 助手負(fù)責(zé)碎片化記錄和語(yǔ)音問(wèn)答IDE 里的 AI 插件負(fù)責(zé)代碼生成和項(xiàng)目理解還有一個(gè)跑批任務(wù)的腳本會(huì)定期和 AI 交互生成日?qǐng)?bào)。這些客戶(hù)端理論上都在服務(wù)同一個(gè)“我”但它們各自的對(duì)話歷史、用戶(hù)畫(huà)像、項(xiàng)目知識(shí)是完全割裂的。這意味著什么我在 Web 端告訴 AI“我項(xiàng)目 A 的技術(shù)棧是 Python FastAPI PostgreSQL”換到手機(jī)端去問(wèn)“項(xiàng)目 A 的部署腳本在哪”它回答不了。它連項(xiàng)目 A 是什么都不知道。更麻煩的是如果兩個(gè)客戶(hù)端同時(shí)問(wèn)我同一個(gè)問(wèn)題它們的回答會(huì)基于完全不同的上下文給出兩個(gè)互相矛盾的結(jié)論。所以跨客戶(hù)端記憶共享的第一步不是把記憶“存起來(lái)”而是把記憶“從單機(jī)私有狀態(tài)變成多端一致的公共狀態(tài)”。這個(gè)從“私有”到“公共”的轉(zhuǎn)變是架構(gòu)層面的大改動(dòng)而不是在現(xiàn)有框架上打個(gè)補(bǔ)丁。mem0 在我的場(chǎng)景里吃虧就吃虧在這一點(diǎn)。1.2 mem0 的設(shè)計(jì)優(yōu)勢(shì)以及它在我這里的三個(gè)硬傷必須客觀說(shuō)mem0 的理念和模塊劃分是漂亮的。它把 memory 抽象成三類(lèi)用戶(hù)記憶、會(huì)話記憶、Agent 記憶底層用向量庫(kù)做語(yǔ)義召回用圖數(shù)據(jù)庫(kù)存實(shí)體關(guān)系查詢(xún)時(shí)會(huì)做一種“來(lái)自不同來(lái)源的智能重排”把最相關(guān)的記憶優(yōu)先拿出來(lái)。這種設(shè)計(jì)在“單 Agent 連續(xù)對(duì)話”的場(chǎng)景下表現(xiàn)很好我單獨(dú)測(cè)試時(shí)也確實(shí)覺(jué)得它有靈氣。但放到多客戶(hù)端共享場(chǎng)景里三個(gè)硬傷立刻暴露第一mem0 本質(zhì)上是“庫(kù)”而不是“服務(wù)”。默認(rèn)用法是每個(gè)客戶(hù)端進(jìn)程內(nèi)初始化一個(gè) Memory 實(shí)例各自獨(dú)立工作。雖然官方也提供了服務(wù)化和自托管方案但客戶(hù)端要共享記憶必須自己解決登錄態(tài)、用戶(hù)映射、會(huì)話歸屬等一系列問(wèn)題。這部分官方給的引導(dǎo)偏少集成時(shí)基本靠猜。第二記憶提取和查詢(xún)重排都重度依賴(lài) LLM。每輪對(duì)話要調(diào)用多次模型接口做提取、打分、重排。在我每天幾十條消息的體量下賬單不至于嚇人但一旦有多個(gè)客戶(hù)端同時(shí)在線又都往同一個(gè)記憶服務(wù)上寫(xiě)成本翻倍波動(dòng)很明顯。而且 LLM 調(diào)用是有延遲的提取一次記憶平均 300-600 毫秒這個(gè)延遲會(huì)直接疊加在用戶(hù)可感知的響應(yīng)路徑上。第三數(shù)據(jù)模型偏“單用戶(hù)單 AI”。它設(shè)計(jì)了一套以 person_id 和 memory 為核心的簡(jiǎn)單結(jié)構(gòu)但我的場(chǎng)景里需要給不同項(xiàng)目、不同 Agent、不同客戶(hù)端做隔離和權(quán)限控制。比如公司項(xiàng)目的記憶不應(yīng)該出現(xiàn)在個(gè)人閑聊的上下文里這個(gè)需求用 mem0 的默認(rèn)數(shù)據(jù)模型得自行擴(kuò)展很多字段和過(guò)濾邏輯等于在別人設(shè)計(jì)的骨架上做二次重構(gòu)。1.3 成本、延遲、集成度我跑的一組對(duì)比數(shù)據(jù)為了讓“放棄 mem0”這個(gè)決定不是憑感覺(jué)我專(zhuān)門(mén)做了一組對(duì)照測(cè)試。測(cè)試環(huán)境是同一臺(tái) 8 核 16G 的服務(wù)器記憶條目數(shù)控制在 3000 條客戶(hù)端接了 3 個(gè)模擬連續(xù)對(duì)話 100 輪。對(duì)比維度mem0自托管 云端 LLM我的自研方案本地模型 混合檢索單次查詢(xún)平均延遲620ms包含 LLM 重排96ms詞法 向量融合單次記憶提取成本約 0.01-0.02 元API 調(diào)用幾乎為 0本地 embedding 本地小模型多客戶(hù)端同步需要自行搭同步層服務(wù)端原生支持客戶(hù)端接 API 即同步客戶(hù)端接入耗時(shí)約 1-2 天登錄態(tài) 同步邏輯約 2 小時(shí)一個(gè) SDK 搞定數(shù)據(jù)隔離粒度粗需要自行擴(kuò)展細(xì)namespace client 雙維度這組數(shù)據(jù)說(shuō)明了一個(gè)樸素的問(wèn)題在單機(jī)、單客戶(hù)端、數(shù)據(jù)量幾千條的場(chǎng)景里mem0 完全夠用。但我的核心訴求是“多端一致”和“可控成本”這兩點(diǎn)它給不了。所以我決定自己寫(xiě)一個(gè)哪怕?tīng)奚粢恍┗ㄉ诘闹嘏拍芰σ惨劝芽缈蛻?hù)端記憶同步這個(gè)地基打牢。2. 動(dòng)手前先想清楚記憶系統(tǒng)的邊界條件和設(shè)計(jì)取舍說(shuō)實(shí)話一開(kāi)始我也想“上一個(gè)完整的記憶系統(tǒng)”做了幾天之后發(fā)現(xiàn)方向偏了。記憶系統(tǒng)的目標(biāo)不是“記住所有東西”而是“在需要的時(shí)候把恰好相關(guān)的信息準(zhǔn)確找出來(lái)”。想明白這一點(diǎn)很多功能都可以砍掉架構(gòu)也會(huì)簡(jiǎn)單很多。2.1 跨客戶(hù)端記憶到底要解決哪三個(gè)問(wèn)題我把需求壓縮成三個(gè)問(wèn)題后續(xù)所有設(shè)計(jì)都是圍繞它們展開(kāi)的。第一是寫(xiě)入一致性。客戶(hù)端 A 寫(xiě)入一條記憶客戶(hù)端 B 必須能立刻看到。這里的關(guān)鍵不是“最終一致”而是“低延遲一致”。因?yàn)?AI 對(duì)話是交互式的如果手機(jī)端問(wèn)了問(wèn)題卻拿到的是桌面端 1 小時(shí)前的記憶快照用戶(hù)立刻會(huì)感覺(jué)到不對(duì)。第二是檢索準(zhǔn)確性。記憶庫(kù)里可能存了用戶(hù)幾個(gè)月以來(lái)的對(duì)話摘要、項(xiàng)目信息、偏好設(shè)置。用戶(hù)問(wèn)“上次說(shuō)好的接口返回格式是什么”系統(tǒng)要能準(zhǔn)確找到那一條而不是把所有含“接口”兩個(gè)字的記憶都倒出來(lái)。這要求檢索不能只靠語(yǔ)義相似度還要有詞法匹配、時(shí)間衰減、重要性加權(quán)等多重信號(hào)。第三是隔離與安全。多個(gè)客戶(hù)端共用一個(gè)記憶庫(kù)不代表所有客戶(hù)端可以看所有記憶。我明確要求公司項(xiàng)目的記憶只對(duì)工作客戶(hù)端可見(jiàn)個(gè)人偏好只對(duì)個(gè)人助手可見(jiàn)。這個(gè)隔離必須在系統(tǒng)層面做好不能靠每個(gè)客戶(hù)端自覺(jué)。2.2 記憶分層短期、長(zhǎng)期、全局、局部的設(shè)計(jì)思路我參考了認(rèn)知科學(xué)里工作記憶和長(zhǎng)期記憶的區(qū)分把記憶分成四個(gè)池子。短期記憶池保存的是最近若干輪對(duì)話的摘要TTL 很短可能是 2 小時(shí)或者一個(gè)會(huì)話的生命周期。它解決的是“同一會(huì)話內(nèi)的連續(xù)性”比如你剛才讓 AI 寫(xiě)了一段代碼現(xiàn)在問(wèn)它“這個(gè)代碼里為什么用了異步”它得記得剛才的上下文。長(zhǎng)期記憶池保存的是跨會(huì)話的穩(wěn)定信息比如用戶(hù)的偏好、項(xiàng)目背景、技術(shù)選型、做事習(xí)慣。這類(lèi)記憶的 TTL 很長(zhǎng)重要性高是檢索時(shí)的重點(diǎn)對(duì)象。全局記憶池保存的是關(guān)于用戶(hù)身份的基礎(chǔ)信息比如“這個(gè)用戶(hù)是一名后端開(kāi)發(fā)者”“他傾向于先寫(xiě)測(cè)試再寫(xiě)實(shí)現(xiàn)”。全局記憶會(huì)被所有客戶(hù)端共享所有 Agent 在首次交互時(shí)都會(huì)先讀取它。局部記憶池則帶 namespace 隔離比如某個(gè)具體項(xiàng)目的記憶歸到 project:xxx 命名空間下只有處理這個(gè)項(xiàng)目的 Agent 才能訪問(wèn)。這四個(gè)池子不是物理上分開(kāi)存儲(chǔ)的而是同一份數(shù)據(jù)帶上不同標(biāo)簽在寫(xiě)入時(shí)通過(guò)標(biāo)簽分類(lèi)在檢索時(shí)通過(guò)標(biāo)簽過(guò)濾。這樣存儲(chǔ)層保持簡(jiǎn)潔邏輯層的靈活性也夠。2.3 存儲(chǔ)選型為什么我選了詞法 向量混合而不是純向量很多人一想到“AI 記憶”就默認(rèn)得用向量數(shù)據(jù)庫(kù)。我一開(kāi)始也這么想但做了實(shí)驗(yàn)之后改變了主意。純向量檢索有個(gè)隱蔽的缺陷語(yǔ)義相近不代表因果相關(guān)。用戶(hù)問(wèn)“明天早上提醒我開(kāi)會(huì)”向量檢索很可能召回“他每天早上有跑步習(xí)慣”這種語(yǔ)義上挨得著、實(shí)際上沒(méi)用的記憶因?yàn)閮烧叩南蛄烤嚯x確實(shí)不遠(yuǎn)。所以我的存儲(chǔ)層沒(méi)有走“單一向量庫(kù)”路線而是做了混合檢索。對(duì)每一次寫(xiě)入既生成 embedding 向量存入向量索引也把原文做分詞后存入全文索引。查詢(xún)的時(shí)候兩路檢索并行執(zhí)行再通過(guò)一個(gè)融合算法把結(jié)果合并排序。這樣既保留了語(yǔ)義召回對(duì)“同義不同詞”的泛化能力也保住了詞法匹配對(duì)準(zhǔn)確關(guān)鍵詞的精確命中。生產(chǎn)環(huán)境我用了 PostgreSQL 加 pgvector單機(jī)開(kāi)發(fā)環(huán)境直接用 SQLite 加 FTS5 和內(nèi)置向量擴(kuò)展。SQLite 版本在我測(cè)試 5000 條記憶時(shí)混合檢索的耗時(shí)大概在 50-80 毫秒完全夠用不用一上來(lái)就想著上分布式。3. 自研跨客戶(hù)端 AI 記憶共享系統(tǒng)的整體架構(gòu)這一章講清楚系統(tǒng)長(zhǎng)什么樣、數(shù)據(jù)怎么流動(dòng)、各模塊之間怎么配合。3.1 核心組件與數(shù)據(jù)流我的系統(tǒng)分成四個(gè)核心組件記憶服務(wù)端、客戶(hù)端 SDK、維護(hù)腳本、LLM 提取模塊。記憶服務(wù)端是中心所有讀寫(xiě)請(qǐng)求都經(jīng)過(guò)它??蛻?hù)端 SDK 是一個(gè)輕量 HTTP 封裝負(fù)責(zé)把各端的對(duì)話上下文快照發(fā)送到服務(wù)端并拉取相關(guān)記憶。維護(hù)腳本負(fù)責(zé)定時(shí)做記憶衰減、歸檔、一致性校驗(yàn)。LLM 提取模塊是從對(duì)話中抽取結(jié)構(gòu)化記憶的關(guān)鍵環(huán)節(jié)但它被設(shè)計(jì)成獨(dú)立服務(wù)可以隨時(shí)降級(jí)或替換。完整的數(shù)據(jù)流是這樣的用戶(hù)在某個(gè)客戶(hù)端里說(shuō)了一句話客戶(hù)端先把這句話作為查詢(xún)條件調(diào)用記憶服務(wù)的檢索接口拿到與當(dāng)前語(yǔ)境最相關(guān)的歷史記憶拼接到 Prompt 里再發(fā)給大模型。模型返回回答后客戶(hù)端把這一輪對(duì)話發(fā)送到記憶服務(wù)的寫(xiě)入接口。寫(xiě)入接口先做一輪隱私過(guò)濾把明顯的身份證號(hào)、手機(jī)號(hào)、密鑰打碼或剔除然后交給 LLM 提取模塊抽取出偏好、事實(shí)、決策等結(jié)構(gòu)化記憶再生成 embedding最后落庫(kù)。落庫(kù)成功后會(huì)通過(guò)消息隊(duì)列廣播一個(gè)“記憶更新”事件其他在線客戶(hù)端收到事件后自動(dòng)刷新本地記憶緩存。這個(gè)流程的核心原則是“讀優(yōu)先、寫(xiě)異步”。用戶(hù)發(fā)出的查詢(xún)必須盡快返回所以檢索路徑一定要短寫(xiě)入可以放到異步隊(duì)列里慢慢處理不阻塞用戶(hù)的對(duì)話響應(yīng)。3.2 記憶條目的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)是在傳統(tǒng)鍵值對(duì)基礎(chǔ)上擴(kuò)展出來(lái)的核心字段如下字段類(lèi)型說(shuō)明idstring全局唯一記憶 IDnamespacestring隔離域如 project:alpha / personal:generalclientstring寫(xiě)入客戶(hù)端標(biāo)識(shí)如 web / mobile / idetypestring記憶類(lèi)型preference / fact / decision / entitycontentstring記憶正文通常是一句完整的話embeddingvector向量化的內(nèi)容表示importancefloat重要性分?jǐn)?shù) 0-1影響檢索排序ttlint過(guò)期時(shí)間默認(rèn) -1 表示永久created_atdatetime創(chuàng)建時(shí)間updated_atdatetime更新時(shí)間versionint版本號(hào)用于沖突合并metajson擴(kuò)展元信息如來(lái)源對(duì)話 ID、關(guān)聯(lián)實(shí)體列表這個(gè)結(jié)構(gòu)里最有用的是 namespace 和 type 兩個(gè)字段。namespace 解決隔離問(wèn)題type 解決記憶多樣化問(wèn)題。比如 typedecision 的記憶在排序時(shí)權(quán)重會(huì)高一些因?yàn)椤坝脩?hù)拍板過(guò)的決定”比“隨便說(shuō)過(guò)的一句話”更值得被記住。3.3 API 設(shè)計(jì)與客戶(hù)端接入方式客戶(hù)端只需要對(duì)接兩個(gè)核心接口一個(gè)是檢索一個(gè)是寫(xiě)入。檢索接口接收 query、namespace、client、top_k 等參數(shù)。服務(wù)端把 query 做詞法檢索和向量檢索混合排序后返回命中的記憶列表。寫(xiě)入接口接收 session_id、client、messages 數(shù)組服務(wù)端自行完成提取和落庫(kù)。還有一個(gè)可選的訂閱接口客戶(hù)端通過(guò) WebSocket 訂閱某個(gè) namespace 的記憶變更事件用于實(shí)時(shí)刷新本地緩存。這樣的接口設(shè)計(jì)讓客戶(hù)端接入成本降到很低。我現(xiàn)在的做法是每個(gè)客戶(hù)端集成一個(gè) 200 行左右的 SDK封裝好這三個(gè)接口其他什么都不用管。實(shí)測(cè)下來(lái)接入一個(gè)新客戶(hù)端從開(kāi)發(fā)到聯(lián)調(diào)半天能完事。對(duì)比之前用 mem0 時(shí)自己搭同步層的 1-2 天效率提升非常明顯。4. 核心模塊的實(shí)操實(shí)現(xiàn)與關(guān)鍵參數(shù)接下來(lái)是重點(diǎn)我會(huì)把每個(gè)模塊的具體實(shí)現(xiàn)方式、關(guān)鍵參數(shù)、以及我當(dāng)時(shí)怎么調(diào)優(yōu)的細(xì)節(jié)都寫(xiě)出來(lái)。4.1 記憶寫(xiě)入管線從對(duì)話到結(jié)構(gòu)化記憶寫(xiě)入管線是整個(gè)系統(tǒng)里最復(fù)雜的一環(huán)也是直接決定記憶質(zhì)量的一環(huán)。它的核心工作是把一段自由對(duì)話壓縮成幾條結(jié)構(gòu)化的記憶條目同時(shí)過(guò)濾掉噪音和隱私信息。我用的 LLM 提取 Prompt 模板大概是這樣的你是記憶提取助手。從下面的對(duì)話中提取值得長(zhǎng)期記住的信息。 只提取以下四類(lèi) 1. preference用戶(hù)的偏好、習(xí)慣、禁忌 2. fact客觀事實(shí)、項(xiàng)目背景、技術(shù)選型 3. decision用戶(hù)做出的決策、拍板過(guò)的結(jié)論 4. entity重要的人、項(xiàng)目、工具、時(shí)間節(jié)點(diǎn) 輸出 JSON 數(shù)組每個(gè)元素包含 type, content, importance0到1, expires_in小時(shí)-1表示永久。 如果沒(méi)有可提取的內(nèi)容輸出空數(shù)組。這個(gè)模板看似簡(jiǎn)單但它起到的作用非常關(guān)鍵。它強(qiáng)制 LLM 用固定格式輸出方便程序解析分類(lèi)別提取又方便后續(xù)按類(lèi)型做權(quán)重排序。我在實(shí)際使用中給 importance 做了一個(gè)啟發(fā)式修正如果對(duì)話里出現(xiàn)了“我總是”“我從不”“一定不要”這類(lèi)強(qiáng)偏好詞importance 就自動(dòng)加 0.2如果記憶內(nèi)容涉及用戶(hù)明確給出的項(xiàng)目代號(hào)或時(shí)間節(jié)點(diǎn)importance 也會(huì)上調(diào)。embedding 生成我一開(kāi)始用的是云端接口后來(lái)為了降延遲和成本換成了本地部署的 embedding 模型單條文本的向量化時(shí)間約 10-20 毫秒。提取用的 LLM 則用了一個(gè)量化到 4bit 的 7B 開(kāi)源模型跑在 GPU 上單次提取延遲約 400 毫秒。因?yàn)槭钱惒教幚磉@個(gè)延遲不會(huì)暴露給用戶(hù)客戶(hù)端。寫(xiě)入有一個(gè)重要細(xì)節(jié)不是每一輪對(duì)話都需要提取記憶。我把消息按“是否觸發(fā)新信息”做了過(guò)濾高頻的寒暄、重復(fù)提問(wèn)、簡(jiǎn)單確認(rèn)語(yǔ)都不會(huì)進(jìn)入提取流程。這個(gè)過(guò)濾規(guī)則讓 LLM 的調(diào)用量減少了約 70%成本下降非常明顯。4.2 記憶檢索管線混合檢索和重排的權(quán)衡檢索管線是用戶(hù)感知最強(qiáng)的部分我把目標(biāo)定在 150 毫秒內(nèi)返回結(jié)果。第一步是詞法檢索。用全文索引的 BM25 算法把 query 分詞后匹配命中的就帶上一路候選集。第二步是向量檢索。用 embedding 模型把 query 向量化在向量索引里按余弦相似度取 top 50。第三步是融合排序。我用的是經(jīng)典 RRFReciprocal Rank Fusion公式score sum(1 / (k rank_i))其中 k 設(shè)成 60rank_i 是該條記憶在某一檢索路中的排名。融合后取 top 20再做過(guò)濾和重排。過(guò)濾規(guī)則按順序執(zhí)行先過(guò)濾掉 namespace 不匹配的記憶再過(guò)濾超過(guò) TTL 的過(guò)期記憶最后過(guò)濾掉帶隱私標(biāo)簽的記憶。重排規(guī)則用線性加權(quán)最終分 0.5 x 融合分 0.3 x importance 0.2 x 時(shí)間衰減權(quán)重。時(shí)間衰減權(quán)重的公式是 exp(-age_days / 180)即 180 天半衰期。這樣設(shè)計(jì)的結(jié)果是近期的重要決定排在前面陳舊且不重要的記憶自然沉底語(yǔ)義相關(guān)但實(shí)際無(wú)用的噪音也有機(jī)會(huì)被壓下去。4.3 跨客戶(hù)端同步與沖突合并我在這里踩過(guò)一個(gè)深坑跨客戶(hù)端同步是整個(gè)系統(tǒng)的招牌功能也是踩坑最多的部分。我最初的方案很簡(jiǎn)單每次寫(xiě)入直接改數(shù)據(jù)庫(kù)客戶(hù)端查詢(xún)時(shí)實(shí)時(shí)讀庫(kù)。結(jié)果發(fā)現(xiàn)一個(gè)問(wèn)題——客戶(hù)端為了降低延遲會(huì)在本地做緩存而緩存更新的觸發(fā)條件如果設(shè)計(jì)得不好就會(huì)出現(xiàn)“桌面端已經(jīng)更新了記憶手機(jī)端還在用舊數(shù)據(jù)”的同步延遲甚至因?yàn)閮蛇呁瑫r(shí)寫(xiě)同一條記憶出現(xiàn)版本互相覆蓋的沖突。后來(lái)我把同步機(jī)制改成“服務(wù)端推送 本地緩存失效”。服務(wù)端每次寫(xiě)入成功后通過(guò)消息隊(duì)列向訂閱了該 namespace 的在線客戶(hù)端推送一條變更通知??蛻?hù)端收到通知后把本地緩存里的對(duì)應(yīng)記憶標(biāo)記為過(guò)期下次查詢(xún)時(shí)強(qiáng)制回源。本地緩存用 LRU 策略熱點(diǎn)記憶 TTL 設(shè)為 15 分鐘普通記憶 2 小時(shí)。沖突合并策略則用“版本號(hào) 時(shí)間戳”雙管齊下。每條記憶帶 version 字段客戶(hù)端讀取一下版本再寫(xiě)入。服務(wù)端比較版本號(hào)只接受高于當(dāng)前版本的寫(xiě)入。如果兩個(gè)客戶(hù)端同時(shí)基于同一版本修改了同一條記憶則取 updated_at 更新的一條為準(zhǔn)。為了唯一性每次寫(xiě)入都配一個(gè)全局唯一的 request_id服務(wù)端用這個(gè) ID 做冪等避免網(wǎng)絡(luò)重試導(dǎo)致重復(fù)寫(xiě)入。這個(gè)方案在內(nèi)存條款里犧牲了一些精細(xì)合并能力但它簡(jiǎn)單可靠尤其適合記憶這種“取最新有效版本即可”的數(shù)據(jù)類(lèi)型。4.4 記憶衰減、過(guò)期和冷熱分層記憶不是越多越好存得太多反而會(huì)拉低檢索準(zhǔn)確性。我專(zhuān)門(mén)加了衰減和歸檔機(jī)制。維護(hù)腳本每隔 6 小時(shí)跑一次掃描對(duì) TTL 到期且 importance 低于 0.3 的記憶直接標(biāo)記為“已歸檔”從主索引里移除但保留在冷存儲(chǔ)里可追溯。對(duì) TTL 到期但 importance 較高或 typedecision 的記憶則延長(zhǎng) TTL例如再續(xù) 180 天。對(duì)超過(guò) 90 天沒(méi)有命中的記憶即便沒(méi)有過(guò)期也會(huì)降權(quán)處理避免陳舊記憶持續(xù)影響檢索排序。冷熱分層不是一開(kāi)始就做的。我最初把所有記憶都放在同一個(gè)索引里結(jié)果數(shù)據(jù)量到了 8 萬(wàn)條時(shí)檢索耗時(shí)明顯上升約 400 毫秒。后來(lái)把“最近 30 天活躍記憶”放入熱索引其余放到冷索引查詢(xún)時(shí)先查熱索引未命中再降級(jí)查冷索引。這樣一個(gè)簡(jiǎn)單的改動(dòng)讓 95% 的查詢(xún)都停在熱索引階段耗時(shí)回落到了 80 毫秒以?xún)?nèi)。5. 性能、成本與穩(wěn)定性的一線實(shí)測(cè)技術(shù)方案不能只停留在理念上我把上線以來(lái)的實(shí)測(cè)數(shù)據(jù)整理出來(lái)這些數(shù)字基本可以復(fù)現(xiàn)。5.1 性能數(shù)據(jù)常規(guī)量級(jí)和多租戶(hù)情況場(chǎng)景記憶總量單次查詢(xún)耗時(shí)單次寫(xiě)入耗時(shí)異步攤分個(gè)人日常5000 條60-90ms約 300ms項(xiàng)目知識(shí)庫(kù)2 萬(wàn)條100-120ms約 350ms多 Agent 共享8 萬(wàn)條180-250ms約 400ms這里關(guān)鍵的一條優(yōu)化是批量寫(xiě)入。原來(lái)我一條一條提取、一條一條落庫(kù)效率低。后來(lái)把同一會(huì)話中連續(xù)的 5-10 輪對(duì)話合并成一個(gè)大請(qǐng)求批量提取、批量寫(xiě)入寫(xiě)入吞吐提升了大概 3 倍LLM 調(diào)用次數(shù)也顯著下降。5.2 成本對(duì)比自研和 mem0 的賬單差異成本是我決定自研的最現(xiàn)實(shí)原因之一。我按每月 30 萬(wàn)條消息的規(guī)模粗略算過(guò)一筆賬。用 mem0 加云端 LLM 方案假設(shè) 30% 的消息觸發(fā)記憶提取每次提取消耗約 500 token大約要花掉 15 萬(wàn)次 LLM 調(diào)用按當(dāng)前市場(chǎng)價(jià)算一個(gè)月光提取費(fèi)用就在 100-200 元。如果查詢(xún)時(shí)開(kāi)啟 LLM 重排這個(gè)數(shù)字還要再漲 30%。自研方案里L(fēng)LM 提取用的是本地開(kāi)源模型embedding 也走本地電費(fèi)和 GPU 折舊攤下來(lái)每個(gè)月大概 30 元。兩個(gè)方案差了一個(gè)數(shù)量級(jí)。這還是不談數(shù)據(jù)隱私的代價(jià)。云端 LLM 要把對(duì)話原文傳出去做提取這一條在我們處理項(xiàng)目文檔時(shí)是不能接受的。5.3 穩(wěn)定性設(shè)計(jì)和容災(zāi)方案我最擔(dān)心的是本地 LLM 提取服務(wù)掛了之后整個(gè)系統(tǒng)會(huì)不會(huì)跟著掛。后來(lái)做了降級(jí)設(shè)計(jì)提取服務(wù)不可用時(shí)寫(xiě)入接口自動(dòng)降級(jí)為“不提取結(jié)構(gòu)化記憶只保留原始對(duì)話摘要”檢索時(shí)靠詞法匹配和向量檢索兜底系統(tǒng)仍然可用。也就是說(shuō)AI 提取是增強(qiáng)項(xiàng)不是必需項(xiàng)。消息隊(duì)列也做了持久化。即使服務(wù)端在寫(xiě)入后、廣播同步通知前崩潰客戶(hù)端下次主動(dòng)查詢(xún)時(shí)也能從數(shù)據(jù)庫(kù)拿到最新數(shù)據(jù)只是同步延遲從毫秒級(jí)變成秒級(jí)。我的容災(zāi)目標(biāo)不是“零丟失”而是“關(guān)鍵記憶不丟、服務(wù)不整體不可用”。6. 我從這套系統(tǒng)上線前后踩過(guò)的坑這篇內(nèi)容如果只說(shuō)設(shè)計(jì)不說(shuō)坑價(jià)值少一半。下面這幾個(gè)問(wèn)題都是我真實(shí)遇到過(guò)、花時(shí)間排查過(guò)的按典型程度排列。6.1 語(yǔ)義搜索并不萬(wàn)能召回偏差的典型案例有一次用戶(hù)我自己在手機(jī)端問(wèn)“明天開(kāi)會(huì)材料準(zhǔn)備了嗎”系統(tǒng)召回的三條記憶里有一條是“用戶(hù)每天早上有晨跑的習(xí)慣”理由是“明天早上”和“晨跑”語(yǔ)義相近。這屬于召回偏差。單純靠向量距離無(wú)法區(qū)分“明天早上開(kāi)會(huì)”和“平時(shí)早上跑步”的關(guān)系。后來(lái)我加了兩個(gè)修正一是對(duì)包含明確時(shí)間詞的查詢(xún)加時(shí)間過(guò)濾二是把詞法檢索結(jié)果在融合中的權(quán)重調(diào)高確保精確匹配不會(huì)輸給語(yǔ)義泛化。6.2 同步風(fēng)暴多個(gè)客戶(hù)端同時(shí)寫(xiě)同一條記憶多客戶(hù)端同時(shí)在線的場(chǎng)景里最恐怖的問(wèn)題就是同步風(fēng)暴。桌面端和手機(jī)端同時(shí)編輯同一條項(xiàng)目記憶兩個(gè)客戶(hù)端各自基于舊版本生成新版本造成持續(xù)互相覆蓋日志里反復(fù)出現(xiàn) version conflict。我最后靠“讀取時(shí)帶上版本號(hào)、寫(xiě)入時(shí)校驗(yàn)版本號(hào)”解決另外在客戶(hù)端 SDK 里加了 200 毫秒的寫(xiě)入去抖同一客戶(hù)端在 200 毫秒內(nèi)對(duì)同一條記憶的多次修改只提交最后一次。這個(gè)去抖大大減少了沖突發(fā)生頻率。6.3 隱私與安全在跨客戶(hù)端場(chǎng)景下的具體要求跨客戶(hù)端意味著數(shù)據(jù)會(huì)從多個(gè)入口進(jìn)來(lái)權(quán)限邊界必須清晰。我的處理是每個(gè)客戶(hù)端啟動(dòng)時(shí)向服務(wù)端申請(qǐng)一個(gè) client_tokentoken 綁定 namespace 列表。Web 端可以讀寫(xiě) project:xxx 和 personal:general手機(jī)端默認(rèn)只能讀寫(xiě) personal:generalIDE 插件額外可讀寫(xiě) project:codebase。服務(wù)端在每條讀寫(xiě)請(qǐng)求里校驗(yàn) token 與 namespace 的對(duì)應(yīng)關(guān)系不匹配直接拒絕。這種做法的好處是即使某個(gè)客戶(hù)端的數(shù)據(jù)泄露了被波及的記憶也限定在它被授權(quán)的范圍內(nèi)不會(huì)把整個(gè)記憶庫(kù)拖下水。6.4 一點(diǎn)關(guān)于 token 開(kāi)銷(xiāo)的教訓(xùn)剛上線時(shí)我把每一輪對(duì)話都交給 LLM 提取記憶成本飆升到讓人心疼。后來(lái)加了一個(gè)“信息增量”判斷如果當(dāng)前消息和上一輪提取過(guò)的記憶語(yǔ)義重復(fù)度過(guò)高就跳過(guò)提取只更新原記憶的時(shí)間戳和權(quán)重。這個(gè)判斷用向量相似度實(shí)現(xiàn)超過(guò) 0.9 就跳過(guò)。效果是提取次數(shù)下降了約 70%幾乎感覺(jué)不到對(duì)比度差異。所以對(duì)于記憶系統(tǒng)真正省錢(qián)的不是選更便宜的模型而是減少無(wú)效提取。7. 這套系統(tǒng)的工程化擴(kuò)展方向?qū)懲曜匝邢到y(tǒng)之后我并沒(méi)有停下來(lái)。有幾個(gè)方向是我已經(jīng)在做或準(zhǔn)備做的對(duì)同場(chǎng)景的人可能有參考價(jià)值。第一個(gè)是支持多 Agent 協(xié)作?,F(xiàn)在多個(gè)客戶(hù)端共享記憶本質(zhì)上還是一個(gè)用戶(hù)和一個(gè) AI 服務(wù)之間的記憶。下一步我想把這個(gè)系統(tǒng)擴(kuò)展成多個(gè) Agent 之間的共享黑板讓不同的 Agent 能夠讀取彼此的中間狀態(tài)、任務(wù)進(jìn)度、決策記錄真正實(shí)現(xiàn)多體協(xié)作。第二個(gè)是更精細(xì)的記憶權(quán)限?,F(xiàn)在的 namespace 隔離是粗粒度的。未來(lái)想做成類(lèi)似“記憶級(jí) ACL”每條記憶單獨(dú)標(biāo)注可見(jiàn)的 Agent 列表或用戶(hù)組列表。第三個(gè)是記憶閉環(huán)反饋。系統(tǒng)目前只做存取沒(méi)有做“記憶是否真的幫助了后續(xù)回答”的效果回傳。我準(zhǔn)備在檢索接口里加入一個(gè) feedback 字段客戶(hù)端在回答結(jié)束之后回傳哪些記憶被用到系統(tǒng)據(jù)此調(diào)整記憶的重要性權(quán)重讓高價(jià)值記憶越用越靠前。還有一個(gè)現(xiàn)實(shí)問(wèn)題需要提一下如果你也想自研記憶系統(tǒng)不必從零開(kāi)始造所有輪子。我在實(shí)現(xiàn)中發(fā)現(xiàn)大部分存儲(chǔ)和檢索能力用現(xiàn)成的 SQLite、PostgreSQL 加開(kāi)源 embedding 模型就能搞定真正需要自己寫(xiě)的只有三個(gè)點(diǎn)讀取和寫(xiě)入的結(jié)構(gòu)化提取、跨客戶(hù)端的同步?jīng)_突邏輯、以及貼合自己業(yè)務(wù)場(chǎng)景的重排規(guī)則。把這三塊想清楚系統(tǒng)就成功了一大半。我在這套系統(tǒng)的開(kāi)發(fā)過(guò)程中最大的體會(huì)是不要被“AI 記憶”這個(gè)概念嚇住本質(zhì)上它就是一個(gè)帶有語(yǔ)義檢索能力的數(shù)據(jù)庫(kù)難點(diǎn)不在存儲(chǔ)而在“知道什么該被記住、什么該被忘掉”。mem0 在很多場(chǎng)景下確實(shí)值得一試尤其是單客戶(hù)端、數(shù)據(jù)量不大、對(duì)延遲不敏感的項(xiàng)目但如果你像我一樣需要多客戶(hù)端共享、數(shù)據(jù)可控、成本敏感自己寫(xiě)一套輕量級(jí)的記憶服務(wù)反而是一條更踏實(shí)、更可控的路。