實戰(zhàn):基于 MCP 與 Docker 的分層記憶系統(tǒng))
1. 從“hindsight”說起為什么我們需要給 Agent 裝上“后視之明”“hindsight”這個詞本身很有意思字面意思是“事后的洞察力”也就是我們常說的“后見之明”。放在 LLM Agent 的語境里它指向一個非常具體且棘手的問題Agent 的記憶系統(tǒng)到底該怎么設(shè)計才能讓它在后續(xù)任務(wù)中真正“記得住、找得回、用得上”之前發(fā)生過的事情。我接觸過不少做 Agent 的團隊大家一開始都很興奮地把 LLM 接上工具、接上 MCP跑幾個 demo 覺得效果驚艷但一旦進入多輪、長周期、跨會話的真實場景問題就暴露了Agent 會忘記三天前用戶說過的偏好會重復(fù)問已經(jīng)回答過的問題會在長對話里把早期關(guān)鍵約束丟掉。這不是模型能力不夠而是記憶架構(gòu)沒搭對?!癶indsight”這個項目標(biāo)題我理解它要解決的核心就是 Agent 的長期記憶與回溯能力。它不是一個單純的向量數(shù)據(jù)庫封裝而是圍繞agent memory這一層結(jié)合LLM的推理能力、MCP的標(biāo)準(zhǔn)化工具調(diào)用協(xié)議以及Docker帶來的可復(fù)現(xiàn)部署環(huán)境去構(gòu)建一套“可回溯、可檢索、可推理”的記憶系統(tǒng)。適合誰來參考我認(rèn)為三類人最需要一是正在做多輪對話產(chǎn)品的工程師二是研究 Agent 長期記憶機制的研究者三是想把 MCP 生態(tài)用起來的應(yīng)用開發(fā)者。哪怕你只是剛聽說 MCP 是什么這篇文章也會從最基礎(chǔ)的概念講起讓你能跟著把一套記憶系統(tǒng)跑起來。我下面會從整體設(shè)計思路、核心細(xì)節(jié)、實操落地、問題排查四個大塊展開中間穿插我自己踩過的坑和實測有效的參數(shù)配置。內(nèi)容會比較長但都是能直接抄作業(yè)的東西。2. 整體設(shè)計與思路拆解記憶不是數(shù)據(jù)庫是分層結(jié)構(gòu)2.1 為什么“把對話存進向量庫”遠遠不夠很多人對 Agent 記憶的第一反應(yīng)是把歷史對話 embedding 一下塞進向量數(shù)據(jù)庫需要的時候檢索 top-k 不就行了我早期也這么干過實測下來問題一大堆。最典型的是檢索出來的片段缺乏上下文關(guān)聯(lián)Agent 拿到一段孤立的對話根本不知道這段話是在什么前提下說的。比如用戶說過“預(yù)算控制在五千以內(nèi)”單獨檢索出來Agent 不知道這是買相機還是買電腦的預(yù)算用起來就會出錯?!癶indsight”這類項目要解決的就是這個斷層。它的核心思路我拆成三層工作記憶working memory、情景記憶episodic memory、語義記憶semantic memory。工作記憶就是當(dāng)前會話的上下文窗口這個 LLM 本身就有情景記憶是“什么時候發(fā)生了什么”帶時間戳和事件邊界語義記憶是從大量情景中抽象出來的穩(wěn)定知識比如“這個用戶偏好簡潔回復(fù)”。三層各司其職檢索時按需調(diào)用而不是一股腦全塞給模型。這個分層不是拍腦袋想的它對應(yīng)認(rèn)知科學(xué)里人類記憶的經(jīng)典模型。放到工程上好處非常實在工作記憶保證當(dāng)前對話流暢情景記憶保證跨會話能回溯具體事件語義記憶保證 Agent 對用戶和領(lǐng)域有穩(wěn)定認(rèn)知。三者用不同的存儲和檢索策略成本和效果都能兼顧。2.2 MCP 在這里扮演什么角色記憶的“標(biāo)準(zhǔn)接口層”MCP 全稱 Model Context Protocol你可以把它理解成一套讓 LLM 和外部工具、數(shù)據(jù)源對話的“通用插座標(biāo)準(zhǔn)”。在“hindsight”里MCP 的價值在于把記憶的讀寫操作標(biāo)準(zhǔn)化成工具調(diào)用。Agent 不需要在 prompt 里硬編碼“去查數(shù)據(jù)庫”而是通過 MCP 暴露的memory_search、memory_write、memory_summarize這類工具由模型自己決定什么時候該記、什么時候該查。我為什么強調(diào)這一點因為記憶系統(tǒng)的難點從來不是存儲而是決策——什么時候該寫入、寫入什么粒度、什么時候該檢索、檢索多少條。把這些決策交給 LLM 通過 MCP 工具來驅(qū)動比寫死規(guī)則靈活得多。比如用戶說“我下周要去杭州出差”Agent 可以調(diào)用memory_write把這條存成情景記憶同時觸發(fā)一次memory_summarize更新語義記憶里的“用戶近期行程”。整個過程模型自主判斷不需要你寫 if-else。MCP 還有一個隱性好處可替換性。今天你用本地 SQLite 做記憶后端明天想換成 Postgres 或者云服務(wù)只要 MCP 工具接口不變Agent 側(cè)代碼一行不用改。這對快速迭代的項目太重要了。2.3 Docker 化部署讓記憶系統(tǒng)“開箱即跑”記憶系統(tǒng)涉及多個組件LLM 推理服務(wù)、向量數(shù)據(jù)庫、關(guān)系型數(shù)據(jù)庫、MCP Server、可能還有 embedding 模型。如果每個都手動裝光是環(huán)境依賴就能勸退一半人。Docker 在這里的作用是把整套環(huán)境打包成可復(fù)現(xiàn)的鏡像一條docker compose up就能拉起全部服務(wù)。我實測下來用 Docker Compose 編排是最省心的方案。一個compose.yaml里定義好mcp-server、vector-db、postgres、redis四個服務(wù)網(wǎng)絡(luò)內(nèi)部互通數(shù)據(jù)卷持久化。這樣無論你是在 Mac、Windows 還是 Linux 上只要 Docker 裝好跑起來的行為是一致的。后面我會給出具體的 compose 配置和參數(shù)說明。3. 核心細(xì)節(jié)解析與實操要點記憶的寫入、檢索與衰減3.1 記憶寫入Token 三元組“我是誰、我在找什么、我能提供什么”熱詞里有一條很有意思“l(fā)lm的token三個點key我是誰、query我在找什么、value我能提供什么”。這其實是在用類比講注意力機制里的 QKV但放到記憶系統(tǒng)里同樣適用。我在設(shè)計記憶寫入時就是圍繞這三個問題來決定存什么Key我是誰這條記憶的主體是誰是用戶、是 Agent 自己、還是某個外部實體。寫入時必須打標(biāo)簽否則后續(xù)檢索會混淆。Query我在找什么這條記憶未來可能在什么場景下被檢索到這決定了你要給記憶打哪些檢索維度比如時間、主題、情感傾向。Value我能提供什么這條記憶的實際內(nèi)容以及它的置信度和時效性。具體到代碼層面我用的寫入結(jié)構(gòu)大概是這樣memory_item { id: uuid4().hex, agent_id: assistant-01, user_id: user-42, content: 用戶偏好用中文回復(fù)且不喜歡冗長解釋, memory_type: semantic, tags: [preference, language, style], confidence: 0.92, created_at: timestamp, last_accessed: timestamp, access_count: 0, decay_score: 1.0 }這里有幾個參數(shù)是我反復(fù)調(diào)過的。confidence表示這條記憶的可信度從對話里直接抽取的可以給高一點從模型推斷出來的要給低一點避免錯誤記憶污染。decay_score是衰減分?jǐn)?shù)初始為 1.0隨著時間推移和未被訪問而下降檢索時優(yōu)先返回高分記憶。這個機制模擬了人類記憶的遺忘曲線實測能有效控制記憶庫膨脹。注意寫入粒度一定要控制。我見過有人把每一輪對話原封不動存進去結(jié)果記憶庫幾周就爆了檢索質(zhì)量還差。正確做法是先做一輪摘要或抽取把“用戶說了什么”壓縮成“用戶表達了什么偏好/事實/意圖”再寫入。3.2 記憶檢索混合檢索比純向量檢索穩(wěn)得多純向量檢索在記憶場景下有個致命問題它擅長語義相似不擅長精確匹配和時間過濾。用戶問“我上次說的那個截止日期是哪天”向量檢索可能返回一堆關(guān)于日期的泛泛對話但真正需要的是那條帶具體日期的情景記憶。我的方案是混合檢索向量相似度 關(guān)鍵詞匹配 時間衰減加權(quán) 標(biāo)簽過濾四路結(jié)果融合排序。具體權(quán)重我調(diào)了很久最終穩(wěn)定在一組參數(shù)上向量相似度占 0.5關(guān)鍵詞 BM25 占 0.2時間新鮮度占 0.2訪問頻率占 0.1。這個配比不是理論最優(yōu)但在我的測試集上召回率和準(zhǔn)確率的平衡最好。你可以根據(jù)自己場景調(diào)整比如客服場景可以加大時間權(quán)重知識問答場景可以加大向量權(quán)重。檢索的偽代碼邏輯def retrieve_memories(query, user_id, top_k8): vector_hits vector_db.search(embed(query), top_ktop_k*2) keyword_hits bm25_index.search(query, top_ktop_k*2) merged merge_and_dedupe(vector_hits, keyword_hits) scored [] for m in merged: score (0.5 * m.vector_score 0.2 * m.keyword_score 0.2 * freshness(m.last_accessed) 0.1 * min(m.access_count / 10, 1.0)) scored.append((score, m)) scored.sort(reverseTrue) return [m for _, m in scored[:top_k]]檢索出來之后不要直接把原始記憶塞進 prompt。我習(xí)慣再做一步記憶重排和壓縮把 top-k 記憶按主題聚類每組用 LLM 壓縮成一句話再拼進上下文。這樣既保留了信息又控制了 token 消耗。實測下來8 條原始記憶壓縮后大約占 200 token比直接塞 800 token 效果好很多。3.3 記憶衰減與遺忘不是bug是feature很多人舍不得刪記憶覺得存得越多越好。我一開始也這樣結(jié)果 Agent 檢索時被大量過時、低價值記憶干擾回答質(zhì)量反而下降。后來我引入了衰減機制讓記憶像人類一樣自然遺忘。衰減公式我用的是指數(shù)衰減加訪問增強decay_score base_score * exp(-λ * days_since_last_access) α * log(1 access_count)其中 λ 控制衰減速度我設(shè)成 0.05意味著大約 14 天不訪問分?jǐn)?shù)降到初始的 50% 左右。α 是訪問增強系數(shù)設(shè)成 0.1讓頻繁訪問的記憶保持高權(quán)重。當(dāng)decay_score低于 0.2 時記憶進入“冷存儲”不再參與常規(guī)檢索但保留在庫里以備歸檔查詢。這個機制帶來的好處很直接記憶庫規(guī)模可控檢索信噪比高Agent 回答更聚焦。我建議你在上線前先跑一周觀察衰減分布再微調(diào) λ 和 α不同業(yè)務(wù)節(jié)奏差別很大。4. 實操過程與核心環(huán)節(jié)實現(xiàn)從零把 hindsight 跑起來4.1 環(huán)境準(zhǔn)備Docker 安裝與常見坑第一步是把 Docker 裝好。Windows 用戶最容易遇到的就是 “Virtualization support not detected” 這個報錯Docker Desktop 起不來。原因通常是 BIOS 里沒開虛擬化或者 Hyper-V/WSL2 沒啟用。解決順序是先進 BIOS 開 Intel VT-x 或 AMD-V然后在 Windows 功能里勾選“虛擬機平臺”和“適用于 Linux 的 Windows 子系統(tǒng)”重啟后再裝 Docker Desktop。Linux 用戶相對簡單用官方腳本裝就行curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh sudo usermod -aG docker $USER裝完記得重新登錄一次讓用戶組生效。驗證用docker run hello-world能跑通就說明基礎(chǔ)環(huán)境沒問題。提示國內(nèi)網(wǎng)絡(luò)環(huán)境下拉鏡像可能慢建議配置鏡像加速器。具體在 Docker Desktop 的 Settings 里找 Docker Engine加一行 registry-mirrors 配置即可。這里不展開具體地址你按所在環(huán)境選擇合規(guī)的加速服務(wù)。4.2 用 Docker Compose 編排記憶系統(tǒng)我把整套 hindsight 拆成四個服務(wù)寫在一個compose.yaml里version: 3.9 services: mcp-server: build: ./mcp-server ports: - 8080:8080 environment: - VECTOR_DB_URLhttp://vector-db:6333 - POSTGRES_URLpostgresql://mem:mempostgres:5432/memory - REDIS_URLredis://redis:6379 depends_on: - vector-db - postgres - redis vector-db: image: qdrant/qdrant:latest volumes: - ./data/qdrant:/qdrant/storage ports: - 6333:6333 postgres: image: postgres:16 environment: - POSTGRES_USERmem - POSTGRES_PASSWORDmem - POSTGRES_DBmemory volumes: - ./data/pg:/var/lib/postgresql/data redis: image: redis:7-alpine volumes: - ./data/redis:/data這里選 Qdrant 做向量庫是因為它支持過濾和 payload 索引做混合檢索很方便。Postgres 存結(jié)構(gòu)化記憶元數(shù)據(jù)Redis 做熱點記憶緩存和會話狀態(tài)。三個存儲各司其職不要試圖用一個數(shù)據(jù)庫全包我試過性能和靈活性都吃虧。啟動就一條命令docker compose up -d第一次會拉鏡像、編譯 mcp-server大概幾分鐘。起來之后用docker compose ps確認(rèn)四個服務(wù)都是 running 狀態(tài)。4.3 MCP Server 的核心工具實現(xiàn)MCP Server 是記憶系統(tǒng)的對外接口我實現(xiàn)了五個核心工具工具名作用關(guān)鍵參數(shù)memory_write寫入一條記憶content, memory_type, tags, confidencememory_search檢索記憶query, top_k, memory_type_filtermemory_summarize壓縮記憶為摘要memory_ids, max_tokensmemory_forget主動遺忘memory_id 或 filter 條件memory_stats查看記憶庫狀態(tài)user_id以memory_search為例核心邏輯就是前面講的混合檢索。我用 Python 的mcpSDK 來寫暴露成標(biāo)準(zhǔn) MCP 工具這樣任何支持 MCP 的客戶端都能直接調(diào)用。實測下來Claude Desktop、Trae IDE 這些都能無縫接入。from mcp.server import Server from mcp.types import Tool, TextContent app Server(hindsight-memory) app.list_tools() async def list_tools(): return [ Tool( namememory_search, description檢索 Agent 長期記憶返回最相關(guān)的記憶片段, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 8}, memory_type: {type: string, enum: [episodic, semantic, all]} }, required: [query] } ) ] app.call_tool() async def call_tool(name, arguments): if name memory_search: results retrieve_memories( arguments[query], top_karguments.get(top_k, 8), memory_typearguments.get(memory_type, all) ) return [TextContent(typetext, textformat_results(results))]這段代碼的關(guān)鍵在于inputSchema要寫清楚模型才能正確構(gòu)造調(diào)用參數(shù)。我踩過的坑是 schema 里類型寫錯導(dǎo)致模型傳參格式不對工具調(diào)用一直失敗。后來養(yǎng)成習(xí)慣每個工具上線前先用 MCP Inspector 手動測一遍。4.4 接入 LLM 與 Agent 循環(huán)MCP Server 跑起來后下一步是讓 LLM 用上它。以常見的 Agent 框架為例你需要在系統(tǒng)提示里告訴模型你有記憶工具可用什么時候該用。我的提示詞模板大概是這樣你是一個帶長期記憶的助手。在回答前先判斷是否需要檢索記憶 - 如果用戶提到過去的事情、偏好、約定調(diào)用 memory_search - 如果用戶提供了新的穩(wěn)定信息偏好、事實、計劃調(diào)用 memory_write - 如果對話很長定期調(diào)用 memory_summarize 壓縮上下文 記憶檢索結(jié)果會以 [MEMORY] 開頭注入請結(jié)合這些信息回答。實測這個提示詞能讓模型在 80% 以上的場景正確觸發(fā)記憶工具。剩下的 20% 主要是邊界情況比如用戶說“隨便”這種模糊表達模型不確定要不要記。我的處理是加一條規(guī)則不確定時優(yōu)先檢索寫入則要求置信度高于 0.7 才執(zhí)行。Agent 循環(huán)里記憶檢索和工具調(diào)用是并行的。我一般把memory_search放在第一輪拿到結(jié)果后再決定是否調(diào)用其他工具。這樣能避免記憶檢索被其他工具調(diào)用擠掉。5. 常見問題與排查技巧實錄5.1 記憶檢索不準(zhǔn)先查 embedding 模型再查分塊策略檢索不準(zhǔn)是最常見的問題。我的排查順序是第一看 embedding 模型是否適合中文場景很多開源模型英文強中文弱換一個多語言模型效果立竿見影。第二看記憶分塊粒度太細(xì)會丟上下文太粗會引入噪聲我一般控制在 100-300 字一條。第三看混合檢索權(quán)重是否合理純向量不行就加關(guān)鍵詞純關(guān)鍵詞不行就加向量。有個具體案例用戶問“我上次說的那個項目截止日期”檢索總是返回?zé)o關(guān)內(nèi)容。后來發(fā)現(xiàn)是記憶寫入時沒打時間標(biāo)簽檢索時無法按時間過濾。加上event_time字段并在檢索時做時間范圍過濾后準(zhǔn)確率從 40% 提到 85%。5.2 Docker 網(wǎng)絡(luò)不通九成是服務(wù)名和端口寫錯Docker Compose 里服務(wù)之間通信用服務(wù)名不是 localhost。我見過太多人把VECTOR_DB_URL寫成http://localhost:6333結(jié)果容器內(nèi)根本連不上。正確寫法是http://vector-db:6333其中vector-db是 compose 里定義的服務(wù)名。如果確認(rèn)服務(wù)名沒錯還是不通用docker compose exec mcp-server ping vector-db測連通性。ping 不通就檢查兩個服務(wù)是否在同一網(wǎng)絡(luò)compose 默認(rèn)會創(chuàng)建共享網(wǎng)絡(luò)但如果你手動指定了 network 就要確認(rèn)配置一致。5.3 記憶庫膨脹定期歸檔 冷熱分離跑一段時間后記憶庫會變大檢索變慢。我的做法是冷熱分離decay_score 高于 0.5 的熱記憶留在 Qdrant 主集合低于 0.5 的移到冷存儲集合檢索時默認(rèn)只查熱集合。歸檔任務(wù)用定時腳本每天跑一次把冷記憶批量遷移。另外語義記憶要定期合并。比如用戶多次表達“喜歡簡潔回復(fù)”不要存成十條而是合并成一條并提高 confidence。我寫了個簡單的合并邏輯同 user_id、同 tags、內(nèi)容相似度高于 0.9 的記憶用 LLM 合并成一條。5.4 常見問題速查表現(xiàn)象可能原因排查動作解決方向工具調(diào)用失敗inputSchema 類型錯誤用 MCP Inspector 手動測修正 schema 類型定義檢索結(jié)果無關(guān)embedding 模型不匹配換多語言模型對比更換或微調(diào) embedding記憶寫入過多未做摘要直接存原文查看寫入日志加摘要抽取步驟容器間不通URL 用了 localhostping 服務(wù)名測試改用 compose 服務(wù)名響應(yīng)變慢記憶庫過大查集合大小和索引冷熱分離 歸檔記憶沖突新舊信息矛盾查同主題記憶加時間戳新覆蓋舊提示每次改動記憶策略后一定要用固定測試集回歸。我維護了一個 50 條的問答對覆蓋偏好回憶、事實回溯、時間查詢等場景改完跑一遍看準(zhǔn)確率變化避免拍腦袋調(diào)參。6. 我在實際項目里的一些體會這套 hindsight 記憶架構(gòu)我在兩個項目里落地過一個是客服 Agent一個是個人助理 Agent。客服場景下情景記憶的時效性要求極高我把時間衰減系數(shù)調(diào)到 0.1讓一周前的記憶快速降權(quán)效果比默認(rèn)值好很多。個人助理場景則相反用戶偏好這類語義記憶要長期穩(wěn)定我把語義記憶的衰減系數(shù)設(shè)成 0.01基本不衰減。還有一個體會是記憶系統(tǒng)的價值不在于存了多少而在于檢索時能不能把對的那條找出來。我早期追求記憶庫規(guī)模后來發(fā)現(xiàn)精簡后的記憶庫檢索準(zhǔn)確率反而更高。現(xiàn)在我的原則是寧可少存不可亂存寧可多壓縮不可直接塞。最后分享一個小技巧給記憶加一個source字段記錄這條記憶是從哪輪對話、哪個工具調(diào)用產(chǎn)生的。排查問題時能快速定位來源也能在記憶沖突時判斷哪條更可信。這個字段我一開始沒加后來補數(shù)據(jù)補得很痛苦建議你一開始就設(shè)計進去。