實(shí)戰(zhàn):從存儲、抽取到注入的完整實(shí)現(xiàn))
1. 從聊完就忘說起claude-mem 到底想解決什么如果你用 Claude 這類對話式 AI 干過稍微長一點(diǎn)的活兒大概率遇到過這種尷尬前面聊了半小時(shí)把項(xiàng)目背景、代碼規(guī)范、命名習(xí)慣、甚至幾個(gè)關(guān)鍵決策都交代清楚了結(jié)果新開一個(gè)會(huì)話它又變回一張白紙你得從頭再講一遍。更氣人的是同一個(gè)會(huì)話里聊到后面早期說過的細(xì)節(jié)它也開始記不清回答逐漸跑偏。claude-mem這個(gè)名字直譯過來就是Claude 記憶。它要解決的核心問題非常樸素讓 AI 在跨會(huì)話、跨時(shí)間的使用中記住那些本該記住的東西。注意這里說的不是模型本身的能力升級而是在模型之外給它搭一套外掛記憶系統(tǒng)——把重要的上下文、偏好、決策、事實(shí)存下來在需要的時(shí)候再喂回去。我先把話說在前頭claude-mem不是一個(gè)官方標(biāo)準(zhǔn)產(chǎn)品名它更像是一類做法的統(tǒng)稱——圍繞 Claude 構(gòu)建持久化記憶層。市面上有各種實(shí)現(xiàn)思路有基于本地文件的有基于向量庫的也有基于結(jié)構(gòu)化數(shù)據(jù)庫的。所以這篇不會(huì)假裝有一個(gè)官方 claude-mem 安裝包讓你npm install就完事而是把這類記憶系統(tǒng)為什么這么設(shè)計(jì)、怎么落地、坑在哪講透。你讀完能自己搭一套也能看懂別人開源的實(shí)現(xiàn)。適合誰看三類人一是天天用 Claude 寫代碼、寫文檔被重復(fù)交代背景折磨的開發(fā)者二是想把 AI 接入自己工作流需要它記住業(yè)務(wù)規(guī)則的技術(shù)負(fù)責(zé)人三是對AI 記憶這個(gè)概念好奇想搞清楚它和模型微調(diào)長上下文到底啥關(guān)系的同學(xué)。不需要你有多深的 AI 背景但得會(huì)點(diǎn)基本的命令行和配置文件操作。2. 記憶系統(tǒng)的三種流派為什么大多數(shù)人的第一版都做錯(cuò)了2.1 最直覺的做法把所有對話都存下來新手搭記憶系統(tǒng)第一反應(yīng)往往是那我全存不就行了。每次對話結(jié)束把完整記錄追加到一個(gè)文件里下次會(huì)話開頭整個(gè)塞回去。聽起來很美好實(shí)測下來問題一堆。第一個(gè)問題是上下文窗口不是無限的。Claude 的上下文再大也有上限你把幾十次對話全塞進(jìn)去要么超限被截?cái)嘁窗颜嬲匾男畔⒀蜎]在噪音里。第二個(gè)問題是成本和延遲。每次請求都帶上幾萬 token 的歷史錢花得飛快響應(yīng)也變慢。第三個(gè)問題最隱蔽無關(guān)信息會(huì)干擾模型判斷。你三個(gè)月前隨口說的一句這個(gè)方案先試試可能和今天的決策完全沖突模型看到兩段矛盾的信息輸出就開始搖擺。所以全量存儲這條路只適合極短期的場景比如同一個(gè)任務(wù)連續(xù)幾輪對話。一旦跨天、跨項(xiàng)目必須做篩選和壓縮。2.2 進(jìn)階做法只存事實(shí)和偏好真正能用的記憶系統(tǒng)核心動(dòng)作是抽取。不是存原始對話而是從對話里提煉出結(jié)構(gòu)化的、可復(fù)用的信息。通常分兩類事實(shí)型記憶Fact項(xiàng)目用的是什么技術(shù)棧、數(shù)據(jù)庫表結(jié)構(gòu)、API 的鑒權(quán)方式、某個(gè)函數(shù)的用途。這類信息相對穩(wěn)定變更頻率低。偏好型記憶Preference用戶喜歡用 TypeScript 而不是 JavaScript、注釋要寫中文、提交信息遵循 Conventional Commits、回答要先給結(jié)論再給理由。這類信息跨項(xiàng)目通用價(jià)值極高。抽取的好處是信息密度高。一段 2000 字的對話可能提煉出 3 條事實(shí)、2 條偏好壓縮到 200 字以內(nèi)。下次會(huì)話只需要帶上這些精華既省 token 又精準(zhǔn)。但抽取本身有難度怎么判斷哪句是值得記的我的經(jīng)驗(yàn)是優(yōu)先記那些如果忘了會(huì)導(dǎo)致返工的信息。比如這個(gè)字段不能為空忘了代碼就出 bug用戶要求所有時(shí)間戳用 UTC忘了就得改一遍。至于閑聊、試探性的討論、被否決的方案一律不記。2.3 高級做法帶檢索的記憶庫當(dāng)記憶條目多到幾百上千條就不能每次全帶上了得按需檢索。這就引入了向量檢索或關(guān)鍵詞檢索把當(dāng)前問題轉(zhuǎn)成查詢從記憶庫里撈出最相關(guān)的幾條拼進(jìn)上下文。這里有個(gè)關(guān)鍵設(shè)計(jì)點(diǎn)檢索的召回率和精確率要平衡。召回率太低該記的沒撈出來等于沒記精確率太低撈出一堆無關(guān)的反而干擾。實(shí)踐中我一般把 top-k 設(shè)在 3 到 5 條并且加一個(gè)相似度閾值低于閾值的直接丟棄寧可少帶也別帶錯(cuò)。三種流派的對比我整理成一張表方便你按自己的場景選流派存儲內(nèi)容優(yōu)點(diǎn)缺點(diǎn)適用場景全量存儲原始對話實(shí)現(xiàn)簡單零信息損失貴、慢、易干擾單任務(wù)連續(xù)對話事實(shí)抽取結(jié)構(gòu)化條目密度高、精準(zhǔn)抽取邏輯要調(diào)跨會(huì)話的長期助手檢索記憶庫條目向量可擴(kuò)展到海量工程復(fù)雜度高多項(xiàng)目、多用戶大多數(shù)個(gè)人開發(fā)者從第二種起步就夠了等條目真的多起來再上檢索。3. 拆解 claude-mem 的核心機(jī)制存儲、抽取、注入三段式不管具體實(shí)現(xiàn)用什么語言、什么數(shù)據(jù)庫一套能用的記憶系統(tǒng)骨架都是三段寫入存儲、加工抽取、讀取注入。我把每一段的關(guān)鍵設(shè)計(jì)講清楚。3.1 存儲層為什么我推薦從純文本文件起步很多人一上來就想上數(shù)據(jù)庫Postgres 加 pgvector聽著就專業(yè)。但我踩過的坑告訴我個(gè)人項(xiàng)目階段純文本文件Markdown 或 JSONL是性價(jià)比最高的選擇。理由有三。第一可讀可改。記憶抽錯(cuò)了你直接打開文件改一行就行不用寫 SQL。第二零依賴。不用起服務(wù)、不用管連接池一個(gè)腳本讀寫文件五分鐘跑起來。第三天然可版本控制。把記憶文件放進(jìn) Git每次變更都有記錄出問題能回滾還能看到AI 對我的了解是怎么一步步積累的。具體格式我習(xí)慣用 JSONL每行一個(gè) JSON 對象因?yàn)樽芳臃奖憬馕鲆埠唵?。一條記憶大概長這樣{id: mem_001, type: preference, content: 用戶偏好 TypeScript禁用 any, scope: global, created: 2025-01-15, confidence: 0.9}字段說明一下type區(qū)分事實(shí)和偏好scope區(qū)分是全局偏好還是某個(gè)項(xiàng)目專屬confidence是置信度抽取時(shí)不確定的條目給低分檢索時(shí)可以過濾掉。這個(gè)confidence字段是我后來加的非常有用——AI 抽取難免有誤判有了它就能做二次篩選。等條目超過一兩千條或者需要多用戶隔離再遷移到 SQLite輕量或 Postgres多用戶。遷移時(shí) JSONL 直接導(dǎo)入就行不虧。3.2 抽取層讓 AI 自己總結(jié)但別全信它抽取這一步最省事的做法是再調(diào)一次 Claude給它一段對話讓它輸出結(jié)構(gòu)化記憶。提示詞大概是這樣從以下對話中提取值得長期記住的信息分兩類 1. 事實(shí)項(xiàng)目相關(guān)的穩(wěn)定信息 2. 偏好用戶的工作習(xí)慣和喜好 每條信息用一句話表述并給出 0-1 的置信度。 只提取忘了會(huì)導(dǎo)致返工的內(nèi)容忽略閑聊和臨時(shí)討論。 輸出 JSON 數(shù)組。這里有幾個(gè)實(shí)操要點(diǎn)。第一明確告訴它忽略什么比只告訴它提取什么更有效。模型天生傾向于多提取你不劃邊界它能把客套話都記下來。第二要求輸出置信度方便后續(xù)過濾。第三抽取要冪等。同一段對話重復(fù)抽取應(yīng)該得到相似結(jié)果不能每次都不一樣。如果發(fā)現(xiàn)結(jié)果飄忽把 temperature 調(diào)低。抽取完還有一步去重和合并。比如這次抽出用戶喜歡簡潔回答上次已經(jīng)有一條用戶要求回答不要啰嗦這倆其實(shí)是一回事得合并。簡單做法是用字符串相似度比如編輯距離或余弦相似度做粗篩超過閾值的人工確認(rèn)或自動(dòng)合并。別小看這步不去重的話記憶庫很快就充斥重復(fù)條目檢索時(shí)全是冗余。提示抽取層是整個(gè)系統(tǒng)里最需要調(diào)優(yōu)的部分。建議先手動(dòng)跑幾十段真實(shí)對話看看抽出來的東西質(zhì)量如何再?zèng)Q定要不要自動(dòng)化。我見過太多人直接上自動(dòng)化結(jié)果記憶庫里一半是垃圾。3.3 注入層什么時(shí)候把記憶喂回去喂多少注入的時(shí)機(jī)很關(guān)鍵。我的做法是在每次新會(huì)話的第一輪請求時(shí)注入而不是每輪都注入。原因很簡單記憶是背景信息第一輪交代清楚就夠了后續(xù)對話模型自己會(huì)保持。每輪都注入既浪費(fèi) token又可能讓模型過度關(guān)注記憶而忽略當(dāng)前問題。注入的內(nèi)容也要控制量。我一般限制在500 到 1000 token以內(nèi)按相關(guān)性和置信度排序取前若干條。格式上用清晰的分隔把它和用戶當(dāng)前問題區(qū)分開[背景記憶] - 用戶偏好 TypeScript禁用 any - 當(dāng)前項(xiàng)目使用 PostgreSQL 15 - API 鑒權(quán)用 JWT有效期 2 小時(shí) [當(dāng)前問題] 幫我寫一個(gè)查詢用戶的函數(shù)這樣模型能明確知道哪些是背景、哪些是當(dāng)前任務(wù)。實(shí)測下來這種顯式分隔比把記憶混在系統(tǒng)提示里效果更好模型更少張冠李戴。還有一個(gè)細(xì)節(jié)記憶要能失效。項(xiàng)目從 PostgreSQL 遷到 MySQL 了那條使用 PostgreSQL的記憶就得更新或刪除。我一般給每條記憶加一個(gè)last_verified時(shí)間戳超過一定時(shí)間比如 90 天沒被引用或確認(rèn)的標(biāo)記為待驗(yàn)證檢索時(shí)降權(quán)。這個(gè)機(jī)制能防止記憶庫變成歷史垃圾堆。4. 手把手搭一套最小可用版本理論講夠了直接上代碼。我用 Python 寫一個(gè)最小可用版本總共不到 150 行能跑通存儲-抽取-注入全流程。你照著抄半小時(shí)能跑起來。4.1 環(huán)境準(zhǔn)備與目錄結(jié)構(gòu)先建目錄mkdir claude-mem cd claude-mem mkdir data touch data/memories.jsonl依賴就一個(gè)調(diào)用 Claude API 的官方 SDK。裝一下pip install anthropicAPI key 通過環(huán)境變量傳別硬編碼進(jìn)代碼export ANTHROPIC_API_KEY你的key目錄結(jié)構(gòu)保持極簡data/memories.jsonl存記憶mem.py是主邏輯。夠用了。4.2 寫入與抽取把對話變成記憶條目核心函數(shù)是extract_memories輸入一段對話文本輸出記憶條目列表import json import os from anthropic import Anthropic client Anthropic() MEM_FILE data/memories.jsonl EXTRACT_PROMPT 從以下對話中提取值得長期記住的信息分兩類 1. fact項(xiàng)目相關(guān)的穩(wěn)定事實(shí) 2. preference用戶的工作習(xí)慣和喜好 每條用一句話表述給出 0-1 置信度。 只提取忘了會(huì)導(dǎo)致返工的內(nèi)容忽略閑聊和臨時(shí)討論。 嚴(yán)格輸出 JSON 數(shù)組不要任何額外文字。 格式[{type:fact,content:...,confidence:0.9}] 對話 {dialog} def extract_memories(dialog): resp client.messages.create( modelclaude-sonnet-4-20250514, max_tokens1000, temperature0, messages[{role: user, content: EXTRACT_PROMPT.format(dialogdialog)}] ) text resp.content[0].text.strip() # 容錯(cuò)去掉可能的 markdown 代碼塊包裹 if text.startswith(): text text.split(\n, 1)[1].rsplit(, 1)[0] try: return json.loads(text) except json.JSONDecodeError: return []這里temperature0是為了讓抽取結(jié)果穩(wěn)定同一段對話多跑幾次結(jié)果基本一致。容錯(cuò)那段別省模型有時(shí)候會(huì)手賤加個(gè)代碼塊標(biāo)記不加處理直接json.loads會(huì)崩。寫入函數(shù)負(fù)責(zé)追加到文件并做簡單的去重def save_memories(items): existing load_memories() existing_contents {m[content] for m in existing} new_items [] for it in items: if it[content] in existing_contents: continue it[id] fmem_{len(existing) len(new_items):04d} it[scope] global new_items.append(it) with open(MEM_FILE, a, encodingutf-8) as f: for it in new_items: f.write(json.dumps(it, ensure_asciiFalse) \n) return len(new_items)去重用的是精確匹配簡單但夠用。等條目多了再上相似度匹配。4.3 讀取與注入按相關(guān)性挑記憶讀取時(shí)按置信度排序取前 N 條def load_memories(min_confidence0.6, top_k8): if not os.path.exists(MEM_FILE): return [] items [] with open(MEM_FILE, encodingutf-8) as f: for line in f: line line.strip() if not line: continue m json.loads(line) if m.get(confidence, 0) min_confidence: items.append(m) items.sort(keylambda x: x[confidence], reverseTrue) return items[:top_k] def build_context(memories): if not memories: return lines [f- {m[content]} for m in memories] return [背景記憶]\n \n.join(lines) \nmin_confidence0.6這個(gè)閾值是我調(diào)出來的。太低會(huì)把不確定的垃圾帶進(jìn)去太高會(huì)漏掉有用信息。你可以根據(jù)自己的數(shù)據(jù)微調(diào)但別設(shè)成 0.9那樣基本啥都留不下。4.4 串起來跑一遍主流程def chat(user_input): memories load_memories() context build_context(memories) full_prompt context [當(dāng)前問題]\n user_input resp client.messages.create( modelclaude-sonnet-4-20250514, max_tokens2000, messages[{role: user, content: full_prompt}] ) return resp.content[0].text if __name__ __main__: # 模擬一段對話抽取記憶 dialog 用戶我們這個(gè)項(xiàng)目用 TypeScript別用 any。 助手好的明白了。 用戶數(shù)據(jù)庫是 PostgreSQL 15連接串在 .env 里。 助手收到。 items extract_memories(dialog) n save_memories(items) print(f新增 {n} 條記憶) print(chat(幫我寫個(gè)查詢用戶的函數(shù)))跑一次你會(huì)看到它抽出了用戶偏好 TypeScript 禁用 any項(xiàng)目使用 PostgreSQL 15這類條目存進(jìn)文件。下次再問問題這些背景就自動(dòng)帶上了。5. 實(shí)測中那些文檔不會(huì)告訴你的坑代碼能跑通只是第一步真正用起來坑都在細(xì)節(jié)里。這部分是我踩過之后總結(jié)的含金量最高。5.1 記憶污染錯(cuò)誤信息一旦進(jìn)去就很難清最頭疼的問題是記憶污染。比如某次對話里你隨口說這個(gè)接口暫時(shí)用 HTTP抽取時(shí)被記成項(xiàng)目使用 HTTP結(jié)果后面所有生成的代碼都不帶 HTTPS。這種錯(cuò)誤記憶會(huì)持續(xù)影響輸出而且因?yàn)橹眯哦雀邫z索時(shí)總被優(yōu)先帶上。我的應(yīng)對策略是雙保險(xiǎn)。第一抽取時(shí)對臨時(shí)性詞匯敏感比如暫時(shí)先試試以后再說遇到這類詞要么不記要么把置信度壓到 0.5 以下。第二定期人工審查。我每周花十分鐘掃一遍新增記憶看到不對的直接刪。別嫌麻煩記憶庫的質(zhì)量直接決定 AI 輸出的質(zhì)量。5.2 沖突記憶新舊信息打架怎么辦項(xiàng)目演進(jìn)過程中記憶必然沖突。上個(gè)月記的用 REST API這個(gè)月改成 GraphQL 了兩條記憶并存模型看到就懵。解決辦法是給記憶加時(shí)效和狀態(tài)。我現(xiàn)在的做法是每條記憶有status字段active / deprecated抽取新記憶時(shí)如果發(fā)現(xiàn)和舊記憶語義沖突就把舊的標(biāo)記為 deprecated而不是直接刪。這樣既保留了歷史又不會(huì)干擾當(dāng)前判斷。判斷沖突可以用一次額外的模型調(diào)用讓它對比新舊兩條是否矛盾成本很低。5.3 檢索失靈明明記了卻想不起來有時(shí)候你確定記過某條信息但 AI 就是沒用上。原因通常是檢索沒匹配上。如果你用的是純置信度排序就像我上面的最小版本它根本不看當(dāng)前問題永遠(yuǎn)返回那幾條高分記憶自然可能漏掉相關(guān)的。升級方案是引入相關(guān)性排序。把當(dāng)前問題和每條記憶都轉(zhuǎn)成向量算余弦相似度再和置信度加權(quán)。權(quán)重我一般設(shè)相關(guān)性 0.7、置信度 0.3。這樣既保證相關(guān)又不會(huì)把低質(zhì)量的撈上來。向量化可以用現(xiàn)成的 embedding 接口也可以先用簡單的關(guān)鍵詞匹配頂著效果差些但零成本。5.4 成本失控記憶系統(tǒng)本身也在燒錢別忘了抽取要調(diào)模型檢索如果用向量也要調(diào)接口這些都是成本。我見過有人把每輪對話都抽一遍一個(gè)月賬單翻倍??刂瞥杀镜年P(guān)鍵是批量抽取。不要每輪對話都抽而是攢夠一段比如一次任務(wù)結(jié)束再抽一次。另外抽取用的模型可以選便宜的小模型因?yàn)槌槿∪蝿?wù)相對簡單不需要最強(qiáng)的模型。注入時(shí)用的才是主力模型。這個(gè)抽取用便宜模型、生成用強(qiáng)模型的分工能省不少錢。6. 從個(gè)人玩具到團(tuán)隊(duì)工具還能怎么擴(kuò)展最小版本跑順之后如果你想讓它在團(tuán)隊(duì)里發(fā)揮作用有幾個(gè)方向可以擴(kuò)展。多用戶隔離。給每條記憶加user_id檢索時(shí)只撈當(dāng)前用戶的。團(tuán)隊(duì)共享的事實(shí)型記憶比如項(xiàng)目規(guī)范可以設(shè)成scope: team所有人可見。這樣既有個(gè)性化又有共享知識。記憶的可視化。把記憶庫渲染成一個(gè)網(wǎng)頁按類型、時(shí)間、置信度分類展示支持搜索和手動(dòng)編輯。團(tuán)隊(duì)新人入職時(shí)直接看這個(gè)頁面就能了解項(xiàng)目的歷史決策比翻聊天記錄高效得多。和現(xiàn)有工具集成。比如接入你的代碼編輯器寫代碼時(shí)自動(dòng)把相關(guān)記憶作為注釋提示或者接入項(xiàng)目管理工具每次開新任務(wù)自動(dòng)帶上項(xiàng)目背景。這些集成的價(jià)值在于讓記憶無感地發(fā)揮作用而不是每次都要你手動(dòng)觸發(fā)。記憶的導(dǎo)出與遷移。用 JSONL 存的好處這時(shí)候就體現(xiàn)出來了導(dǎo)出就是復(fù)制文件遷移就是換個(gè)路徑。別用私有格式把自己鎖死。最后分享一個(gè)我自己的體會(huì)記憶系統(tǒng)的價(jià)值不在于記得多而在于記得準(zhǔn)。一個(gè)只有 50 條高質(zhì)量記憶的系統(tǒng)比一個(gè)有 5000 條垃圾的系統(tǒng)好用得多。所以別急著堆量先把抽取質(zhì)量和去重做好讓每一條記憶都經(jīng)得起推敲。我現(xiàn)在的記憶庫穩(wěn)定在兩百條左右覆蓋了常用的技術(shù)棧偏好和幾個(gè)主力項(xiàng)目的關(guān)鍵決策日常用起來已經(jīng)非常順手了。