落地實(shí)戰(zhàn):三層架構(gòu)、MCP協(xié)議與Docker部署)
1. 為什么“記憶”才是Agent落地的真正瓶頸做過LLM應(yīng)用的人都有一個(gè)共同體會(huì)模型本身的能力在快速拉平真正拉開產(chǎn)品差距的是模型之外的那一圈工程設(shè)施。而在這圈設(shè)施里**記憶Memory**是最容易被低估、也最容易踩坑的一環(huán)。你讓一個(gè)Agent連續(xù)處理三天任務(wù)它第二天就忘了第一天定下的約束你讓它記住用戶的偏好它轉(zhuǎn)頭把偏好和事實(shí)混在一起開始一本正經(jīng)地胡說。這不是模型不行是記憶架構(gòu)沒設(shè)計(jì)好?!癶indsight”這個(gè)詞本身很有意思字面意思是“事后之明”——回頭看時(shí)才明白當(dāng)時(shí)該怎么做。把它作為項(xiàng)目標(biāo)題指向的正是Agent記憶系統(tǒng)里最核心的一個(gè)命題如何讓Agent在事后能夠正確回溯、檢索、利用過去發(fā)生過的交互而不是把上下文當(dāng)成一鍋粥全塞進(jìn)prompt里。結(jié)合熱搜詞里的agent memory、working memory、MCP、Docker、tencentdb agent memory這些線索可以判斷這是一個(gè)圍繞Agent記憶層展開的工程實(shí)踐項(xiàng)目涉及記憶的存儲(chǔ)、檢索、生命周期管理以及如何通過MCP協(xié)議把記憶能力標(biāo)準(zhǔn)化地暴露給上層Agent框架。這篇文章適合誰看如果你正在做LLM應(yīng)用尤其是多輪對(duì)話、任務(wù)型Agent、個(gè)人助理類產(chǎn)品并且已經(jīng)被“上下文窗口不夠用”“記憶檢索不準(zhǔn)”“多Agent之間狀態(tài)不同步”這些問題折磨過那這篇內(nèi)容就是寫給你的。我會(huì)從架構(gòu)設(shè)計(jì)講到Docker部署從MCP協(xié)議接入講到記憶檢索的調(diào)優(yōu)盡量把每個(gè)決策背后的“為什么”講清楚讓你能直接抄作業(yè)也能理解為什么這么抄。先說清楚一個(gè)基本認(rèn)知Agent的記憶不是簡(jiǎn)單的向量數(shù)據(jù)庫。很多人一上來就搭個(gè)向量庫把對(duì)話歷史embedding進(jìn)去檢索top-k塞回prompt然后就宣稱“我的Agent有記憶了”。這套方案在demo階段能跑通一上生產(chǎn)就崩。原因在于人類的記憶是有層次的——工作記憶working memory負(fù)責(zé)當(dāng)前任務(wù)的短期狀態(tài)長(zhǎng)期記憶負(fù)責(zé)跨會(huì)話的知識(shí)沉淀而兩者之間的轉(zhuǎn)換、遺忘、強(qiáng)化機(jī)制才是記憶系統(tǒng)的靈魂。hindsight這個(gè)項(xiàng)目要解決的正是這套層次化記憶的工程落地問題。2. 記憶系統(tǒng)的整體架構(gòu)與選型邏輯2.1 三層記憶模型working memory、episodic memory、semantic memory在動(dòng)手寫代碼之前先把記憶的層次分清楚這決定了你后面所有的存儲(chǔ)和檢索設(shè)計(jì)。我采用的是三層模型這套模型在認(rèn)知科學(xué)里有對(duì)應(yīng)理論落到工程上也非常好操作。第一層是working memory工作記憶。它對(duì)應(yīng)的是當(dāng)前任務(wù)正在進(jìn)行的短期狀態(tài)比如用戶剛才說的那句話、當(dāng)前對(duì)話輪次的目標(biāo)、臨時(shí)計(jì)算出來的中間結(jié)果。這一層的生命周期很短通常就是當(dāng)前會(huì)話或當(dāng)前任務(wù)周期任務(wù)結(jié)束就可以丟棄或歸檔。存儲(chǔ)上我直接用Redis或者進(jìn)程內(nèi)內(nèi)存讀寫要快不要求持久化。第二層是episodic memory情景記憶。它記錄的是“什么時(shí)候發(fā)生了什么”比如“用戶在周三下午讓我?guī)退喠艘粡埲ド虾5臋C(jī)票”。這一層是帶時(shí)間戳的事件流檢索時(shí)往往按時(shí)間范圍或事件類型來查。存儲(chǔ)上用關(guān)系型數(shù)據(jù)庫或者帶時(shí)間索引的文檔庫都行我實(shí)測(cè)下來PostgreSQL配合時(shí)間分區(qū)表很穩(wěn)。第三層是semantic memory語義記憶。它沉淀的是從多次交互中抽象出來的穩(wěn)定知識(shí)比如“用戶偏好靠窗座位”“用戶所在團(tuán)隊(duì)使用Python技術(shù)?!?。這一層是去時(shí)間化的檢索靠語義相似度向量數(shù)據(jù)庫是主力。注意很多人把episodic和semantic混在一起存結(jié)果檢索時(shí)要么召回一堆過時(shí)的事件要么把臨時(shí)狀態(tài)當(dāng)成長(zhǎng)期知識(shí)。分層存儲(chǔ)、分層檢索是hindsight這類項(xiàng)目能不能用的分水嶺。2.2 為什么選MCP作為記憶能力的暴露協(xié)議熱搜詞里反復(fù)出現(xiàn)MCP這里必須展開講。MCPModel Context Protocol本質(zhì)上是一套標(biāo)準(zhǔn)化的上下文交互協(xié)議它讓Agent框架和外部能力工具、數(shù)據(jù)源、記憶服務(wù)之間有了統(tǒng)一的接口。你可以把它類比成USB-C——以前每個(gè)設(shè)備一個(gè)接口現(xiàn)在統(tǒng)一了插上就能用。把記憶系統(tǒng)做成一個(gè)MCP Server好處非常直接。第一解耦。記憶的存儲(chǔ)和檢索邏輯完全獨(dú)立于Agent框架你換框架、換模型記憶服務(wù)不用動(dòng)。第二復(fù)用。同一個(gè)記憶服務(wù)可以同時(shí)被多個(gè)Agent、多個(gè)會(huì)話調(diào)用天然支持多Agent共享記憶。第三可測(cè)試。MCP有明確的請(qǐng)求響應(yīng)格式你可以單獨(dú)對(duì)記憶服務(wù)做單元測(cè)試和壓測(cè)不用把整個(gè)Agent跑起來。我試過不用MCP、直接在Agent代碼里硬編碼記憶邏輯的方案前期開發(fā)快但一旦要接第二個(gè)Agent或者換存儲(chǔ)后端重構(gòu)成本高得嚇人。用MCP雖然前期多寫一層協(xié)議適配但后期擴(kuò)展性完全不是一個(gè)量級(jí)。2.3 Docker化部署為什么不用裸機(jī)熱搜詞里docker、docker compose、docker desktop出現(xiàn)頻率極高說明部署方式是大家關(guān)心的重點(diǎn)。hindsight這類記憶服務(wù)我強(qiáng)烈建議Docker化理由有三。一是依賴隔離。記憶服務(wù)通常要同時(shí)跑向量庫、關(guān)系庫、緩存裸機(jī)裝這些依賴版本沖突能讓你懷疑人生。Docker把每個(gè)組件關(guān)進(jìn)自己的容器互不干擾。二是環(huán)境一致性。開發(fā)機(jī)是Windows服務(wù)器是Linux裸機(jī)部署時(shí)“在我電腦上能跑”是常態(tài)。Docker鏡像一旦構(gòu)建好到哪都是同一套環(huán)境。三是編排方便。用docker compose一個(gè)文件描述所有服務(wù)啟動(dòng)、停止、擴(kuò)容都是一條命令。下面這張表是我實(shí)際用的服務(wù)編排規(guī)劃服務(wù)名鏡像端口作用數(shù)據(jù)卷memory-api自構(gòu)建8080MCP Server主服務(wù)無狀態(tài)postgrespostgres:165432情景記憶存儲(chǔ)pgdataredisredis:7-alpine6379工作記憶緩存無qdrantqdrant/qdrant6333語義記憶向量庫qdrant_data這套組合我跑了大半年穩(wěn)定性沒問題。qdrant選它是因?yàn)閱螜C(jī)部署簡(jiǎn)單、API友好如果你團(tuán)隊(duì)已經(jīng)在用Milvus或Weaviate替換掉就行MCP層不用改。3. 核心細(xì)節(jié)解析記憶的寫入、檢索與遺忘3.1 記憶寫入不是所有對(duì)話都值得記新手最容易犯的錯(cuò)是把每一輪對(duì)話都往記憶庫里塞。結(jié)果就是記憶庫迅速膨脹檢索時(shí)噪聲比信號(hào)還多。hindsight的核心設(shè)計(jì)之一是寫入前的價(jià)值判斷。我的做法是在寫入鏈路上加一個(gè)輕量的“記憶提取器”它可以是規(guī)則也可以是一個(gè)小模型調(diào)用。提取器要回答三個(gè)問題這段內(nèi)容里有沒有值得長(zhǎng)期保留的事實(shí)有沒有用戶明確表達(dá)的偏好有沒有需要跨會(huì)話追蹤的任務(wù)狀態(tài)三個(gè)都沒有就直接丟棄只留在working memory里。具體實(shí)現(xiàn)上我用一個(gè)prompt模板讓LLM做結(jié)構(gòu)化抽取輸出JSON格式的記憶條目EXTRACT_PROMPT 從以下對(duì)話中抽取值得長(zhǎng)期記憶的條目。每條記憶包含 - type: fact / preference / task_state - content: 記憶內(nèi)容一句話 - confidence: 0-1之間的置信度 - ttl: 建議的存活時(shí)間秒事實(shí)類可長(zhǎng)任務(wù)狀態(tài)類要短 對(duì)話內(nèi)容 {dialogue} 只輸出JSON數(shù)組沒有值得記憶的內(nèi)容就輸出空數(shù)組。 這個(gè)抽取步驟會(huì)增加一次LLM調(diào)用成本上要權(quán)衡。我的經(jīng)驗(yàn)是對(duì)于高頻對(duì)話場(chǎng)景可以用規(guī)則先過濾掉明顯無價(jià)值的輪次比如純寒暄、純確認(rèn)只對(duì)包含實(shí)體、數(shù)字、偏好詞的輪次做LLM抽取能省下六七成的調(diào)用。實(shí)操心得抽取出來的記憶條目一定要帶confidence和ttl。confidence低的記憶在檢索時(shí)降權(quán)ttl到期的記憶自動(dòng)歸檔。這兩個(gè)字段是記憶庫保持“干凈”的關(guān)鍵別省。3.2 記憶檢索token的三個(gè)點(diǎn)——key、query、value熱搜詞里有一句很精辟的話“l(fā)lm的token三個(gè)點(diǎn)key我是誰、query我在找什么、value我能提供什么”。這其實(shí)是在講檢索的本質(zhì)。記憶檢索不是簡(jiǎn)單的向量相似度top-k而是要同時(shí)考慮查詢意圖query、**記憶主體key和記憶內(nèi)容value**三者的匹配。我的檢索鏈路是這樣的先根據(jù)當(dāng)前對(duì)話生成檢索query然后做混合檢索——向量相似度負(fù)責(zé)語義匹配關(guān)鍵詞匹配負(fù)責(zé)精確命中時(shí)間衰減因子負(fù)責(zé)給近期記憶加權(quán)。最后用一個(gè)重排序模型或者簡(jiǎn)單的加權(quán)公式把結(jié)果排序取top-n注入prompt。加權(quán)公式我調(diào)了很久最終穩(wěn)定在這套參數(shù)上final_score 0.5 * vector_similarity 0.3 * keyword_match_score 0.2 * time_decay_factortime_decay_factor用指數(shù)衰減半衰期設(shè)成7天。也就是說一條記憶如果7天內(nèi)沒被再次命中它的時(shí)間權(quán)重就減半。這個(gè)參數(shù)不是拍腦袋定的是根據(jù)用戶行為數(shù)據(jù)調(diào)的——大部分偏好類記憶的有效期就在一周左右超過一周要么已經(jīng)內(nèi)化成習(xí)慣要么已經(jīng)過時(shí)。3.3 記憶遺忘主動(dòng)刪除比被動(dòng)堆積更重要記憶系統(tǒng)最難的不是“記住”是“忘記”。一個(gè)不會(huì)遺忘的系統(tǒng)最終會(huì)被自己的歷史壓垮。hindsight里我設(shè)計(jì)了三層遺忘機(jī)制。第一層是TTL過期。寫入時(shí)帶的ttl到期記憶自動(dòng)從活躍區(qū)移到歸檔區(qū)檢索時(shí)默認(rèn)不召回。第二層是沖突消解。當(dāng)新記憶和舊記憶矛盾時(shí)比如用戶先說喜歡咖啡后說戒了咖啡不是簡(jiǎn)單覆蓋而是把舊記憶標(biāo)記為superseded保留歷史但降低權(quán)重。這樣Agent在被問到時(shí)能說“你之前喜歡咖啡后來戒了”而不是生硬地只認(rèn)最新一條。第三層是低頻淘汰。定期掃描記憶庫把長(zhǎng)期未被檢索命中、confidence又低的記憶清理掉。這個(gè)定期任務(wù)我用一個(gè)cron容器跑每周一次。注意遺忘機(jī)制一定要可配置、可回滾。我踩過的坑是一次性刪了太多記憶結(jié)果Agent突然“失憶”用戶體感極差。后來改成先歸檔、觀察一段時(shí)間再物理刪除穩(wěn)多了。4. 實(shí)操過程從零把hindsight跑起來4.1 環(huán)境準(zhǔn)備與Docker安裝要點(diǎn)先說環(huán)境。Windows用戶裝Docker Desktop最容易卡在“virtualization support not detected”這個(gè)報(bào)錯(cuò)上。這不是Docker的問題是BIOS里虛擬化沒開。進(jìn)BIOS把Intel VT-x或AMD-V打開重啟就好。如果開了還報(bào)錯(cuò)檢查是不是Hyper-V和WSL2沖突Windows11下建議直接用WSL2后端性能比Hyper-V好。Linux用戶裝Docker Engine別裝Desktop。裝完記得把當(dāng)前用戶加進(jìn)docker組否則每條命令都要sudosudo usermod -aG docker $USER newgrp docker驗(yàn)證安裝docker --version docker compose version兩個(gè)命令都有輸出環(huán)境就算齊了。這里提醒一句docker compose和docker-compose是兩個(gè)東西前者是v2插件后者是v1獨(dú)立二進(jìn)制?,F(xiàn)在統(tǒng)一用docker compose中間空格別再用帶橫杠的老命令。4.2 用docker compose一鍵拉起記憶服務(wù)把下面這個(gè)compose文件存成docker-compose.yml放在項(xiàng)目根目錄version: 3.9 services: memory-api: build: ./memory-api ports: - 8080:8080 environment: - POSTGRES_DSNpostgresql://mem:mem123postgres:5432/memory - REDIS_URLredis://redis:6379/0 - QDRANT_URLhttp://qdrant:6333 depends_on: - postgres - redis - qdrant restart: unless-stopped postgres: image: postgres:16 environment: - POSTGRES_USERmem - POSTGRES_PASSWORDmem123 - POSTGRES_DBmemory volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped redis: image: redis:7-alpine restart: unless-stopped qdrant: image: qdrant/qdrant volumes: - qdrant_data:/qdrant/storage restart: unless-stopped volumes: pgdata: qdrant_data:啟動(dòng)命令就一句docker compose up -d第一次跑會(huì)拉鏡像、構(gòu)建memory-api大概三五分鐘。起來之后用docker compose ps看狀態(tài)四個(gè)服務(wù)都是running就對(duì)了。如果memory-api起不來八成是依賴的服務(wù)還沒ready看日志docker compose logs -f memory-api4.3 MCP Server的接口設(shè)計(jì)與實(shí)現(xiàn)memory-api這個(gè)服務(wù)對(duì)外暴露的就是MCP協(xié)議定義的幾個(gè)方法。核心是三個(gè)memory.write、memory.search、memory.forget。我用Python的FastAPI做HTTP層MCP的JSON-RPC格式在中間做一層轉(zhuǎn)換。寫入接口的關(guān)鍵是冪等。同一條記憶重復(fù)寫入不能產(chǎn)生重復(fù)條目我用content的hash做去重鍵。檢索接口的關(guān)鍵是超時(shí)控制向量檢索偶爾會(huì)慢我設(shè)了500ms的超時(shí)超時(shí)就降級(jí)到只走關(guān)鍵詞檢索保證Agent不會(huì)因?yàn)橛洃浄?wù)卡住而整體卡住。app.post(/mcp) async def mcp_handler(request: dict): method request.get(method) params request.get(params, {}) if method memory.write: return await write_memory(params) elif method memory.search: return await search_memory(params) elif method memory.forget: return await forget_memory(params) else: return {error: unknown method}這套接口設(shè)計(jì)我用了很久最大的好處是Agent框架完全不用關(guān)心記憶存在哪、怎么檢索它只管調(diào)MCP方法。哪天你把qdrant換成別的向量庫Agent側(cè)一行代碼都不用改。4.4 接入Agent框架與聯(lián)調(diào)MCP Server跑起來后在Agent框架里配置MCP連接。以常見的配置方式為例在Agent的配置文件里加上{ mcpServers: { hindsight: { url: http://localhost:8080/mcp, timeout: 3000 } } }聯(lián)調(diào)時(shí)先做單點(diǎn)測(cè)試手動(dòng)調(diào)一次write再調(diào)一次search看能不能召回。這一步通了再接到Agent的對(duì)話循環(huán)里。我建議在Agent的每輪對(duì)話前后各加一個(gè)hook對(duì)話前search注入記憶對(duì)話后write沉淀記憶。聯(lián)調(diào)階段最容易出的問題是記憶注入過多導(dǎo)致prompt超長(zhǎng)。我的做法是給注入的記憶設(shè)一個(gè)token預(yù)算比如最多500 token超了就按score截?cái)唷_@個(gè)預(yù)算要根據(jù)你模型的實(shí)際上下文窗口來定別貪多。5. 常見問題與排查技巧實(shí)錄5.1 記憶檢索召回不準(zhǔn)的排查思路召回不準(zhǔn)是最常見的問題排查要按鏈路一步步來。先確認(rèn)寫入是否成功——直接查qdrant和postgres看數(shù)據(jù)在不在。數(shù)據(jù)在再確認(rèn)檢索query生成得對(duì)不對(duì)把query打印出來看。query沒問題再看相似度分?jǐn)?shù)分布如果所有分?jǐn)?shù)都差不多低說明embedding模型不適合你的語料考慮換模型。我整理了一張速查表現(xiàn)象可能原因排查方法解決完全召不回寫入失敗查向量庫條目數(shù)檢查write鏈路日志召回但不相干embedding不匹配打印相似度分布換embedding模型召回舊記憶時(shí)間衰減失效檢查time_decay計(jì)算修正衰減參數(shù)召回重復(fù)條目去重鍵失效查content hash修復(fù)冪等邏輯檢索超時(shí)向量庫負(fù)載高看qdrant監(jiān)控加索引或擴(kuò)容5.2 Docker網(wǎng)絡(luò)不通的經(jīng)典坑docker compose里服務(wù)之間用服務(wù)名互相訪問這是最容易踩的坑。memory-api里連postgreshost要寫postgres而不是localhost。寫localhost的話容器會(huì)去連自己當(dāng)然連不上。另一個(gè)坑是端口映射。compose里ports: 8080:8080前面是宿主機(jī)端口后面是容器端口。如果你宿主機(jī)8080被占了改成18080:8080然后從宿主機(jī)訪問用18080容器之間互訪還是用8080。還有Windows下Docker Desktop的WSL2網(wǎng)絡(luò)偶爾會(huì)出現(xiàn)宿主機(jī)訪問不了容器端口的情況。重啟Docker Desktop一般能解決實(shí)在不行在WSL里用curl localhost:8080先確認(rèn)容器本身是通的。5.3 記憶膨脹導(dǎo)致性能下降的處理跑了一段時(shí)間后如果發(fā)現(xiàn)檢索越來越慢八成是記憶庫膨脹了。先看條目數(shù)超過十萬條就要考慮分片或歸檔。我的做法是按時(shí)間分區(qū)超過90天的記憶移到冷存儲(chǔ)檢索時(shí)默認(rèn)只查熱區(qū)。另外向量庫的索引參數(shù)也要調(diào)。qdrant默認(rèn)的HNSW參數(shù)對(duì)大規(guī)模數(shù)據(jù)不是最優(yōu)m和ef_construct要根據(jù)數(shù)據(jù)量調(diào)大。我十萬條數(shù)據(jù)下用m32, ef_construct256檢索延遲能控制在50ms以內(nèi)。實(shí)操心得定期跑一次記憶庫的“體檢”統(tǒng)計(jì)條目數(shù)、平均檢索延遲、命中率。這三個(gè)指標(biāo)一旦異常提前處理別等用戶投訴了才動(dòng)手。5.4 MCP接入時(shí)的授權(quán)與schema報(bào)錯(cuò)熱搜詞里提到“l(fā)lm request failed: provider rejected the request schema or tool payload”和“codex無法找到mcp”這兩個(gè)問題我都遇到過。前者通常是MCP方法的參數(shù)schema和Agent框架期望的不一致檢查你的JSON-RPC參數(shù)名和類型別用框架不認(rèn)識(shí)的字段。后者多半是MCP Server沒啟動(dòng)或者URL配錯(cuò)先用curl手動(dòng)調(diào)一次確認(rèn)服務(wù)活著。授權(quán)方面如果MCP Server暴露在非本地環(huán)境一定要加token鑒權(quán)。我在memory-api里加了一個(gè)簡(jiǎn)單的Bearer token校驗(yàn)配置在環(huán)境變量里Agent側(cè)請(qǐng)求時(shí)帶上。別裸奔記憶數(shù)據(jù)里可能有用戶隱私。6. 記憶系統(tǒng)的擴(kuò)展方向與個(gè)人體會(huì)hindsight這套架構(gòu)跑通之后能擴(kuò)展的方向其實(shí)很多。比如多Agent共享記憶——幾個(gè)Agent連同一個(gè)MCP Server天然就能看到彼此沉淀的記憶協(xié)作時(shí)不用反復(fù)同步狀態(tài)。再比如記憶的可視化把記憶庫里的條目按類型、時(shí)間、命中率畫出來能直觀看到Agent“學(xué)到了什么”調(diào)試和演示都好用。還有一個(gè)我覺得很有價(jià)值的方向是記憶的主動(dòng)整理。現(xiàn)在的遺忘是被動(dòng)的TTL、低頻淘汰未來可以讓LLM定期回顧記憶庫把零散的情景記憶抽象成更高層的語義記憶就像人睡覺時(shí)大腦整理白天的經(jīng)歷一樣。這個(gè)思路在學(xué)術(shù)界已經(jīng)有相關(guān)研究工程上落地也不難無非是加一個(gè)定期任務(wù)。我個(gè)人在實(shí)際操作中的體會(huì)是記憶系統(tǒng)的價(jià)值不在于“記得多”而在于“記得準(zhǔn)、忘得對(duì)”。一開始我總想把所有東西都記下來結(jié)果Agent反而變笨了因?yàn)樵肼曁唷:髞砗菹滦淖鰷p法只記真正有價(jià)值的檢索準(zhǔn)確率反而上去了。這個(gè)道理說起來簡(jiǎn)單但真到寫代碼時(shí)克制住“多存點(diǎn)總沒壞處”的沖動(dòng)是需要點(diǎn)定力的。最后分享一個(gè)小技巧給記憶條目加上來源標(biāo)記source標(biāo)明這條記憶是從哪次對(duì)話、哪個(gè)Agent來的。排查問題時(shí)順著來源能快速定位到原始上下文比在記憶庫里瞎猜高效得多。這個(gè)字段我一開始沒加后來補(bǔ)上時(shí)已經(jīng)積累了幾萬條無來源記憶只能靠時(shí)間戳反推費(fèi)了老大勁。