
最近我做了一輪記憶系統基準測試把自己的記憶圖實現和 Memora 放在同一組數據集上對比自建方案得分 0.831Memora 得分 0.801。差距不到 0.03這個結果不能說自研方案全面勝出更不能說明 Memora 不行。它真正說明的是兩套方案在記憶抽取、存儲結構和召回鏈路上的側重點不一樣。這篇內容就是把這輪測試從數據集設計、評測方法到實際復現步驟完整拆開適合正在做長期記憶、用戶畫像、多輪對話記憶或 RAG 增強的開發者看。最值得關注的不是那個 0.83 對 0.80 的結果而是“為什么會出現這種差異”以及“換一個數據集之后結果會不會翻過來”。1. 先搞清楚這次評測在測什么能力1.1 記憶圖不是把文檔塞進向量庫很多人提到 AI 記憶第一反應就是 embedding 加向量檢索。向量庫確實能解決“哪段文本和當前問題語義最接近”但它解決不了“A 和 B 是什么關系”“C 項目用了哪些組件”“過去五次對話里用戶的偏好是否發生過變化”這類需要跨片段關聯的問題。記憶圖的思路是先把文本里的實體、關系、事件、屬性抽取出來形成類似“張三 - 就職于 - 某公司”“項目X - 依賴 - 框架Y”的三元組結構再用圖去維護這些關系。相比純向量檢索記憶圖有幾項天然優勢關系型問題可以直接沿著邊回答不需要把所有相關文本都塞進上下文。同一實體的多條事實可以合并形成長期畫像。多跳查詢比向量檢索更可控比如先找到“項目X的負責人”再找“這個負責人還負責過哪些項目”。但這不意味著圖可以替代向量。實際工程里更常見的是圖加向量的混合方案向量負責候選召回圖負責關系擴展和結構化篩選。1.2 Memora 這類方案解決什么問題Memora 在帖子標題里是作為對照出現的。我沒有辦法確認它當前版本的所有功能只能把它理解為一個側重長期記憶管理的方案通常這類系統會把歷史內容整理成人設、偏好、事件時間線或者按會話摘要來組織記憶。這類方案的優勢在“開箱即用”接入成本低不需要自己設計抽取 Schema也不需要維護一套實體對齊邏輯。你只要把內容丟進去它自己會生成可檢索的記憶單元。適合快速驗證一個想法也適合那些不想在圖結構上投入太多精力的團隊。但“開箱即用”的另一面是“內部邏輯不可控”。你很難知道它到底抽取了哪些關系也很難在丟分時定位是抽取問題、召回問題還是生成問題。這也是我堅持自建記憶圖做對比的原因我想知道自己可控的部分到底能比通用方案高多少。2. 評測方法和數據設計比分數更重要2.1 評測集是怎么構造的這一輪測試用的不是公開 benchmark因為我想要的是“貼近長期記憶場景”的數據而不是通用問答數據。我構造的數據集包含三塊內容20 份虛擬用戶檔案覆蓋工作經歷、技術偏好、社交關系。一批項目會議記錄里面有明確的負責人、時間節點、依賴關系和決策理由。多輪技術選型討論涉及候選方案、最終結論和替換原因。這 20 份檔案不是單獨存在的它們之間互相有引用。比如用戶 A 是用戶 B 的項目負責人用戶 B 在會議里提到過某個老系統而這個老系統又出現在用戶 C 的履歷里。之所以這樣設計是為了制造跨文檔、跨會話的關系型問題不然記憶圖根本沒有發揮空間。最終我從這些材料里整理出 80 道記憶問題分為四類問題類型示例數量事實型哪個項目在 3 月進入測試階段20偏好型用戶 A 更傾向使用哪類數據庫20關系型用戶 B 和用戶 C 在哪家公司共事過20多跳型用戶 A 負責的項目依賴了誰主導開發的工具20事實型和偏好型相對容易靠向量檢索摘要通常就能答對。關系型和多跳型才是區分兩套系統的關鍵。2.2 打分標準怎么定我采用的做法是對每個問題由同一個答案生成模型分別讀取兩個系統召回的記憶塊生成回答然后由一個裁判模型按 0 到 1 分打分。打分規則不是只看“語義是否差不多”而是拆成三個維度是否包含正確答案中的核心實體。實體之間的關系是否表達正確。是否引入了與問題無關的錯誤信息。每個維度可以得 0 分或 0.5 分三個維度相加后換算到 0 到 1。比如“哪個項目在 3 月進入測試階段”正確答案是“訂單系統 3 月進入測試階段”。如果系統回答了“訂單系統在 3 月進入開發階段”實體對了但關系錯誤只能拿約 0.33 分。最終得分是 80 道題的平均值。自建記憶圖是 0.831Memora 是 0.801。只看總分會覺得差距很小但分項差異其實更明顯關系型問題上自建方案領先約 0.05事實型問題上 Memora 反而略微領先。2.3 不能只憑一個總分下結論0.831 和 0.801 的差距大概在 0.03。如果樣本量不是 80 道題而是 20 道題這個差距很容易被一兩個隨機結果翻轉。所以這次對比更像一次工程測試而不是嚴格的學術 benchmark。它不能證明誰的架構更好只能說明在這個數據集分布下自建記憶圖的整體召回略好。關系型問題確實是圖的優勢區。兩者的絕對差距不大說明通用記憶方案并沒有被“吊打”。看這類評測時先看數據集分布再看分項分數最后才看總分。總分只適合判斷“有沒有明顯短板”。3. 0.831 與 0.801 的差異到底從哪來3.1 抽取粒度和結構化程度不同自建記憶圖在抽取階段就強制輸出三元組比如{subject: 訂單系統, relation: 進入階段, object: 測試} {subject: 用戶B, relation: 曾經主導, object: 日志組件}這種結構化輸出的好處是關系型問題可以走圖查詢而不是靠語義相似度猜。Memora 這類方案通常會把記憶整理為自然語言摘要比如“用戶 B 之前主導過日志組件該組件在訂單系統里被使用”。當問題明確要求“誰主導了日志組件”時摘要型記憶也能答對但當問題變成“用戶 A 負責的項目依賴了誰主導開發的工具”時摘要里可能沒有現成路徑只能靠語言模型自己把多個摘要串起來容易丟中間環節。分項數據也印證了這一點多跳型問題上自建記憶圖得分 0.79Memora 是 0.74。差距不算大但穩定出現在不同隨機種子下。3.2 召回鏈路不同我的召回鏈路是先向量、后圖擴展。具體來說用向量檢索撈出和問題相關的 5 條三元組。把三元組里的實體作為種子節點做一跳鄰居擴展。擴展后的節點再次映射回文本記憶塊。最后把原始文本和三元組結構一起送給生成模型。Memora 的召回方式更接近“直接檢索記憶塊”沒有額外的關系擴展。這也是為什么它在事實型問題上表現不差因為事實型問題通常只需要命中一段話但遇到需要拼接兩段記憶的問題時它會稍微吃虧。圖擴展不是越多越好。我在測試里試過二跳擴展結果多跳型分數沒漲事實型分數反而掉了因為二跳鄰居帶進來很多與問題無關的歷史信息生成模型被噪聲干擾了。3.3 有些差距來自數據偏差和裁判偏差我必須承認這組測試數據本身更偏向圖結構。20 份檔案和會議記錄里人物關系和項目依賴關系都寫得很明確實體名稱也相對規范。如果換成大量口語化聊天記錄實體抽取會難很多圖方案的優勢會被削弱。裁判模型也可能存在偏好。如果裁判模型本身就擅長處理結構化輸出它可能會給三元組結果更高的分。為了降低這種偏差我在兩個系統里都只讓生成模型輸出自然語言答案裁判看不到候選是來自三元組還是摘要。但模型內部的偏好仍然沒法完全消除。所以更穩妥的說法是這 0.03 的差距一部分來自關系召回的真實優勢另一部分來自數據分布和評測方法帶來的偏差。4. 自己復現一版最小記憶圖4.1 環境準備這個方案不需要很高的硬件條件。我用的環境是Python 3.10 或更高版本。一個可以用 API 調用的 LLM負責抽取和答案生成。圖存儲用 NetworkX數據量不大時內存足夠。向量索引用輕量方案即可比如 Chroma 或基于 TF-IDF 的檢索器。如果只是做實驗不需要上 Neo4j。NetworkX 配合 JSON 持久化足夠跑完 80 道評測題。等到圖節點到幾十萬級別時再換正式圖數據庫也不遲。4.2 抽取記憶三元組第一步是把原始文檔切成長度合適的文本塊。我按段落切而不是按固定 token 切這樣可以盡量保證一個完整事實不被切斷。然后對每個文本塊調用 LLM抽取三元組。示例 Prompt 大致是這樣請從下面的文本中抽取與長期記憶相關的事實只保留客觀事實不要推測。 輸出 JSON 數組每項包含 subject、relation、object。 如果同一主體有多個賓語分別輸出多條三元組。 文本{text}對應的 Python 骨架import json def extract_memory(llm, text): prompt ( 請從下面的文本中抽取與長期記憶相關的事實只保留客觀事實不要推測。\n 輸出 JSON 數組每項包含 subject、relation、object。\n 如果同一主體有多個賓語分別輸出多條三元組。\n 文本\n text ) resp llm.generate(prompt, temperature0) triples json.loads(resp) return [(t[subject], t[relation], t[object]) for t in triples]這里有兩個容易注意不到的地方。第一溫度必須設成 0 或接近 0。記憶抽取是確定性問題不希望模型每次對同一段文本輸出不同實體。第二Prompt 里要限制“不要推測”。沒有這個約束模型會腦補出材料里不存在的關系比如把“項目 A 可能使用項目 B”當成確定事實。這類幻覺會直接影響后續召回質量。4.3 構建圖索引和向量索引拿到三元組后先做一輪實體名歸一化否則“NVIDIA”“nvidia”“英偉達”會被當成三個節點。import networkx as nx graph nx.MultiDiGraph() def normalize_entity(name): name name.strip().lower() # 這里可以加規則去掉公司后綴、全角轉半角、同義詞映射等 return name for subj, rel, obj in triples: s normalize_entity(subj) o normalize_entity(obj) graph.add_node(s, typeentity) graph.add_node(o, typeentity) graph.add_edge(s, o, relationrel)同時把每個三元組成一種可檢索的文本memory_units [] for subj, rel, obj in triples: memory_units.append(f{subj} {rel} {obj})這些文本寫入向量索引。我用的是 Chroma因為本地運行省事。生產環境可以換成更重的向量庫但實驗階段不必過度設計。4.4 用圖擴展召回相關記憶召回階段的核心邏輯是先用向量找種子再用圖做擴展。def recall(question, vec_index, graph, top_k5, max_hops1): seed vec_index.query(question, ktop_k) candidates set(seed) if max_hops 1: for node in seed: neighbors graph.neighbors(node) candidates.update(neighbors) # 也可以把入邊節點加入進來這里簡化處理 result [] for item in candidates: if item in graph.nodes: for edge in graph.out_edges(item, dataTrue): result.append(edge) return result這一步是記憶圖相對純向量檢索的核心差異。向量只負責“找到相似”圖負責“從相似實體繼續往外走”。需要注意 hop 數量的選擇。我實測下來max_hops1 最穩。擴展成二跳會有提升但伴隨著噪聲變多生成答案時經常把不是重點的歷史信息也帶進去。如果你的問題集里多跳問題很少建議直接從一跳開始。最后把命中的三元組和對應原始文本塊一起拼進 Prompt交給生成模型基于以下記憶內容回答問題。請直接給出答案不要解釋推理過程。 記憶內容 {retrieved_memories} 問題{question}4.5 評測打分評測打分階段我用另一個 LLM 實例做裁判。裁判輸入包括問題、標準答案、系統生成答案。裁判不會看到存儲結構避免格式偏好。def judge(question, ground_truth, answer): prompt f對比標準答案和模型回答從三個維度打分。 1. 是否包含核心實體如果是打0.5否則0。 2. 實體關系是否正確如果是打0.5否則0。 3. 是否包含錯誤信息如果沒有打0.5包含則0。 輸出JSON包含維度分和總分。\n問題{question}\n標準答案{ground_truth}\n模型回答{answer} result llm.generate(prompt, temperature0) return json.loads(result)這里有一個公平性問題如果裁判和生成模型是同一個模型裁判可能對同模型的表達方式更友好。我在這次測試里沒有完全隔離只用了不同 Prompt 和不同會話。嚴謹的評測應該使用不同廠商的模型或至少使用不同系列的模型。5. 哪些參數真的會影響最終分數5.1 關鍵參數清單以下參數是我在調這輪評測時實際關注過的參數推薦初始值影響抽取溫度0溫度高會引入實體幻覺文本切片方式按段落切按 token 切容易切斷實體關系向量召回 top_k5太小會漏掉關系種子太大會引入噪聲圖擴展 hop10 就是純向量檢索2 開始噪聲明顯生成答案溫度0評測階段必須可復現抽取模型和生成模型同梯隊抽取能力差會直接拖低后續所有環節評測樣本量至少 30 題少于 30 題的差距基本沒有統計意義5.2 怎樣判斷結果穩定如果只看一次評測0.831 對 0.801 可能只是偶然。我建議至少跑三到五次每次更換隨機種子或者打亂問題順序。然后看兩點總分均值差是否保持同向。關系型和多跳型的分項是否穩定領先。如果總分有時贏、有時輸說明兩套系統在目標數據集上能力接近沒必要折騰自建方案。如果關系型穩定領先但事實型落后就要評估你的實際場景更需要哪類記憶能力。還要記得做一次“零樣本基線”不接任何記憶系統直接把問題丟給生成模型。這能告訴你多少問題是靠模型常識答對的。如果零樣本基線已經到 0.7說明你的記憶系統只貢獻了 0.1 的提升這時得換個更難的數據集。6. 常見踩坑和排查順序6.1 分數偏低先查哪個環節我踩過最深的坑是自建記憶圖得分反而低于 Memora。最后排查發現問題不在圖模塊而在抽取環節。Prompt 里沒加“不要推測”模型抽出了大量“可能、也許、傾向于”這類事實導致問答時答案雖然流暢但實體和關系跟標準答案對不上。如果分數異常按下面的順序排查先看抽取結果。直接打印 10 條三元組看是否覆蓋了原始文本里的關鍵實體。再看召回結果。對某一道關系題把命中的三元組單獨輸出看是否包含標準答案里的實體。然后看生成結果。如果召回里有正確答案但生成答錯可能是 Prompt 拼接方式有問題或上下文太長導致模型忽略關鍵信息。最后判斷裁判是否誤判。抽取 10 道低分題人工復核確認打分是否合理。大多數記憶系統問題都出在抽取和召回而不是生成。6.2 公平對比需要控制哪些變量做 A/B 對比時最容易犯的錯誤是只換了記憶庫其他環節全都放任不管。這會導致分數差異根本說不清來源。建議至少固定以下變量生成答案的模型必須相同。裁判模型必須相同。所有模型溫度設成 0。兩套系統面對的是同一版原始文本。問題順序最好打亂避免“先問了 A再問 B”時模型上下文互相影響。另外要防止數據泄漏。評測問題不能提前進入記憶庫否則就變成了“開卷考試”。我在構造評測集時會把 80 道題的問題文本單獨隔離確保記憶圖只看到原始檔案和會議記錄不看到問題和答案。7. 真正落地時比分數更重要的幾件事7.1 增量更新比一次性索引更關鍵評測環境里記憶是靜態的文檔一次性寫入然后開始提問。生產環境不一樣用戶每說一句話每來一份文檔都要更新記憶。這時圖方案的優勢會變成負擔同一個實體的新事實可能和舊事實沖突比如用戶之前說“部署在 AWS”后來改成“遷移到自建機房”。如果不做沖突處理圖里會同時存在兩條矛盾邊召回時模型既看到舊信息又看到新信息答案就會搖擺。我建議給每條邊加上時間戳和來源 ID查詢時優先取最新時間戳。這個處理在實驗里不會影響 0.831 和 0.801 的差距但直接影響上線后的體驗。7.2 隱私和刪除能力決定了能不能上線記憶系統存的是用戶歷史這意味著必須有刪除能力。向量庫里刪除一條記錄相對簡單圖結構里刪除一個實體要同時刪除它關聯的所有邊。更麻煩的是一條記憶可能來自多條來源比如“用戶 B 主導日志組件”既出現在會議紀里也出現在用戶 B 的簡歷里。刪除時如果只刪一個來源另一個來源又會讓這條記憶復活。所以在建圖時就要保留來源列表而不是只存三元組。每條邊維護一個 source_ids 列表刪除請求到來時把所有關聯來源都清掉再判斷是否刪除節點。這個能力如果后期補會很痛苦。7.3 不是所有場景都適合記憶圖這次測試里自建記憶圖略微領先不代表它適合所有項目。如果你的場景只是“讓 AI 記住用戶喜歡喝什么咖啡”一個 JSON 文件就夠了不需要圖。如果對話歷史短每次問答只需要最近幾輪也不需要用記憶圖直接把上下文拼進 Prompt 反而更簡單。記憶圖最適用的場景是大量歷史信息、跨文檔關系、關系型提問頻繁、需要定期更新和沖突合并。不符合這些條件時Memora 這類通用方案更省事。最后留一句我這輪測試的總結0.831 對 0.801 的結果說明自建方案在關系記憶上有一定優勢但這優勢建立在可控的抽取、規范的實體歸一化和合理的圖擴展之上。真要在項目里落地最該盯住的不是誰比誰高 0.03而是歷史更新、沖突合并和刪除鏈路有沒有做好。