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