實(shí)戰(zhàn):從分層架構(gòu)到LangGraph+MCP落地)
我之前寫過兩篇Agent相關(guān)的內(nèi)容一篇聊Agent的基本盤怎么搭一篇聊Multi-Agent。這次我們聊一個(gè)更貼近日常使用體驗(yàn)的點(diǎn)——記憶。不管你是做客服機(jī)器人、個(gè)人知識(shí)助手還是企業(yè)內(nèi)部智能體只要Agent脫離“一句話問答”往“長期陪伴”方向走記憶就是繞不開的那道坎。AI Agent說了這么多真正拉開體驗(yàn)差距的其實(shí)就是記憶。同樣一個(gè)助手有記憶的版本記得你討厭香菜、知道你上次聊到哪、記得你項(xiàng)目的技術(shù)棧沒記憶的版本每次從零開始像極了每天上班都要重新做自我介紹的新同事。這篇文章我不講那些虛的框架概念直接拆一個(gè)可落地的記憶系統(tǒng)怎么分層、怎么存、怎么取、怎么避免“記錯(cuò)”和“亂記”最后用LangGraph和一個(gè)開放協(xié)議把代碼跑通。這篇文章適合正在做Agent應(yīng)用開發(fā)的工程師或者準(zhǔn)備系統(tǒng)學(xué)習(xí)AI Agent的讀者。如果你剛接觸Agent前面幾段能幫你建立整體認(rèn)知如果你已經(jīng)在寫業(yè)務(wù)代碼后面實(shí)現(xiàn)部分可以直接抄作業(yè)。1. 為什么Agent必須“記住你”——先拆解核心需求1.1 Agent的記憶到底在記什么我見過不少團(tuán)隊(duì)做Agent第一版就是“大模型提示詞知識(shí)庫”用戶問什么答什么。跑起來看著沒問題但用兩個(gè)月就發(fā)現(xiàn)用戶流失嚴(yán)重。原因很簡單用戶覺得這個(gè)機(jī)器人“沒感情”昨天剛說過的事情今天又問你一遍誰受得了。如果把Agent比作一個(gè)店員短期記憶就是他在當(dāng)次對(duì)話里記住你點(diǎn)了什么菜工作記憶是他手頭正在處理的訂單長期記憶則是他記得你是老顧客、口味偏辣、上次來投訴過上菜慢。對(duì)照到系統(tǒng)設(shè)計(jì)上就是三件事短期記憶當(dāng)前會(huì)話的上下文一般就是messages數(shù)組直接塞進(jìn)模型上下文窗口。工作記憶任務(wù)執(zhí)行過程中的中間狀態(tài)比如你讓它做一個(gè)調(diào)研它查到第幾步、收集了哪些素材。長期記憶用戶畫像、歷史偏好、跨會(huì)話的事實(shí)這是持久化存儲(chǔ)也是最值得做文章的部分。大部分團(tuán)隊(duì)短期記憶都處理得好長期記憶才是真正的分水嶺。原因在于短期記憶架構(gòu)簡單上下文窗口裝得下就行而長期記憶牽扯到你怎么抽取、怎么存、怎么在合適的時(shí)機(jī)取回來整個(gè)一套鏈路下來工程復(fù)雜度完全不在一個(gè)量級(jí)。1.2 為什么“把聊天記錄全塞進(jìn)上下文”不靠譜那你可能會(huì)說既然大模型上下文窗口越來越大我把所有歷史聊天記錄都丟給它不就行了我剛開始也這么干過后來被現(xiàn)實(shí)教育了。第一是成本。所有主流大模型都按Token計(jì)費(fèi)歷史記錄越堆越長每一輪對(duì)話都在重復(fù)燒錢。一個(gè)用戶聊了100輪你每次調(diào)模型都要把100輪的歷史重新發(fā)給大模型這成本不是線性增長是復(fù)利增長。第二是效果。上下文太長之后模型對(duì)中間部分的注意力會(huì)明顯下降這就是業(yè)內(nèi)常說的Lost in the middle——你塞進(jìn)去的舊信息不僅幫不上忙反而把當(dāng)前真正的意圖給淹沒掉。第三是噪音。用戶早期說的“我想買個(gè)手機(jī)”和他現(xiàn)在問的“手機(jī)到了怎么退貨”完全不是一回事你把“想買手機(jī)”的語境一直掛在系統(tǒng)提示詞里不干擾才怪。所以長期記憶的正確打開方式是“按需取用”不把所有歷史背在身上而是先把值得記的東西抽出來存好等用戶再次問到相關(guān)內(nèi)容時(shí)再把對(duì)應(yīng)記憶片段取出來注入到當(dāng)前對(duì)話里。這就是檢索增強(qiáng)生成的基本思路也是幾乎所有生產(chǎn)級(jí)Agent記憶系統(tǒng)的共同底座。2. 記憶系統(tǒng)怎么設(shè)計(jì)——三層記憶架構(gòu)與選型思路2.1 記憶分層短期、中期、長期怎么切分我在實(shí)際項(xiàng)目里習(xí)慣把記憶分成三層來設(shè)計(jì)不搞花里胡哨就按生命周期和用途來切。短期記憶最簡單每個(gè)會(huì)話獨(dú)立維護(hù)一個(gè)上下文消息列表會(huì)話結(jié)束就清理。中期記憶稍微復(fù)雜一點(diǎn)它要跨越同一用戶的多次會(huì)話但不需要永久保存比如用戶正在進(jìn)行的購物流程、還沒提交的表單數(shù)據(jù)、聊天中臨時(shí)說到的“下周去出差”。這些信息在一兩周內(nèi)有價(jià)值過期就沒意義了。長期記憶則是用戶的穩(wěn)定畫像和長期事實(shí)比如職業(yè)、常駐城市、技術(shù)棧偏好、對(duì)某些話題的態(tài)度這些幾乎不會(huì)變變了一次之后也要長期生效。這三層對(duì)應(yīng)到存儲(chǔ)方案上也不一樣。短期記憶直接放內(nèi)存或者Redis中期記憶可以放KV存儲(chǔ)或者輕量數(shù)據(jù)庫帶個(gè)過期時(shí)間長期記憶才需要進(jìn)向量數(shù)據(jù)庫走語義檢索。很多新手一上來就把所有記憶全部向量化存進(jìn)向量庫其實(shí)沒必要——很多臨時(shí)狀態(tài)你壓根不需要花成本做向量化直接一個(gè)鍵值對(duì)存JSON就搞定了。記憶層級(jí)生命周期存儲(chǔ)方案讀取方式典型內(nèi)容短期記憶單次會(huì)話內(nèi)存/Redis直接拼接對(duì)話歷史、臨時(shí)狀態(tài)中期記憶數(shù)天到數(shù)周Redis/KV存儲(chǔ)鍵值查詢表單草稿、臨時(shí)任務(wù)長期記憶持久保存向量數(shù)據(jù)庫語義檢索用戶畫像、歷史偏好2.2 向量數(shù)據(jù)庫 RAG的組合邏輯長期記憶為什么必須用向量檢索而不是用傳統(tǒng)的關(guān)鍵詞匹配我舉一個(gè)特別常見的例子。用戶第一次說“我平時(shí)寫Python比較多不太會(huì)Java?!蹦惆堰@句存進(jìn)記憶庫。過了兩周用戶問“幫我推薦一個(gè)適合后端開發(fā)的學(xué)習(xí)路線?!彼耆珱]有提Python或者Java但一個(gè)合格的Agent應(yīng)該記得他的技術(shù)棧。關(guān)鍵詞匹配在這里直接失效因?yàn)椴樵冋Z句和記憶片段之間沒有任何共同的分詞詞項(xiàng)。而向量化之后存儲(chǔ)的句子和當(dāng)前的查詢都會(huì)被映射到同一個(gè)語義空間里它們的向量距離會(huì)很近檢索系統(tǒng)就能把那條記憶撈出來。整個(gè)RAG記憶鏈路就是四步用戶輸入進(jìn)來之后先向量化成嵌入向量然后到向量庫里去檢索最相似的Top-K個(gè)記憶片段接著把檢索結(jié)果按照時(shí)間、相關(guān)性、重要程度排序最后拼裝成一個(gè)記憶塊注入到系統(tǒng)提示詞里。這里有一個(gè)細(xì)節(jié)很多人會(huì)忽略注入的記憶塊必須明確標(biāo)注是哪一輪對(duì)話提取的、關(guān)于哪個(gè)實(shí)體的這樣模型才能區(qū)分“用戶正在說的”和“用戶曾經(jīng)說過的”。2.3 嵌入模型與向量庫選型嵌入模型的選擇取決于你的數(shù)據(jù)語言和業(yè)務(wù)場(chǎng)景。中文為主的場(chǎng)景我建議優(yōu)先試bge-m3或者m3e這類對(duì)中文支持更好的模型英文場(chǎng)景用OpenAI的text-embedding-3-small就足夠。不是越大的模型越好嵌入模型要跟你的向量庫維度、存儲(chǔ)成本、推理延遲一起考慮。比如text-embedding-3-small輸出1536維bge-m3輸出1024維維度越高通常代表信息量越大但存儲(chǔ)和計(jì)算開銷也大。小項(xiàng)目用量低沒什么感覺量大了之后維度直接影響檢索延遲。向量庫這一層的選擇我的經(jīng)驗(yàn)是可以按規(guī)模分檔。個(gè)人項(xiàng)目或者短期內(nèi)數(shù)據(jù)量不超過10萬條直接用Chroma就行輕量、本地跑、Python接口友好。生產(chǎn)環(huán)境要扛并發(fā)和規(guī)模再考慮Qdrant、Milvus或者云上的Pinecone。還有一條路是直接用你已有的PostgreSQL裝上pgvector插件也一樣能用對(duì)團(tuán)隊(duì)來說少引入一個(gè)組件省了不少運(yùn)維成本。我見過不少團(tuán)隊(duì)為了一個(gè)還沒跑起來的臨時(shí)項(xiàng)目專門部署一套Milvus集群結(jié)果運(yùn)維成本比Agent本身還高沒必要。3. 實(shí)操給Agent裝上可落地的記憶系統(tǒng)3.1 用LangGraph搭建記憶工作流理論講再多不如直接看代碼。下面我用LangGraph演示一個(gè)帶記憶的Agent工作流。為什么選LangGraph因?yàn)樗袮gent推理過程拆成了節(jié)點(diǎn)和邊每個(gè)節(jié)點(diǎn)只干一件事狀態(tài)在節(jié)點(diǎn)之間流動(dòng)這種結(jié)構(gòu)特別適合記憶這種橫跨多個(gè)環(huán)節(jié)的組件。先定義一個(gè)狀態(tài)結(jié)構(gòu)。這里面既要有當(dāng)前對(duì)話的messages也要有從記憶庫檢索出來的memory_results以及待寫入的新記憶隊(duì)列。from typing import TypedDict, List, Dict, Any from langchain_core.messages import BaseMessage class AgentState(TypedDict): messages: List[BaseMessage] memory_results: List[Dict[str, Any]] memory_to_store: List[Dict[str, Any]]然后構(gòu)建Agent流程我按三個(gè)節(jié)點(diǎn)來分retrieve_memory負(fù)責(zé)在用戶提問進(jìn)來時(shí)先去記憶庫檢索assistant_node負(fù)責(zé)讓模型根據(jù)當(dāng)前消息和歷史記憶生成回復(fù)store_memory負(fù)責(zé)在每輪對(duì)話結(jié)束之后判斷有沒有值得寫入長期記憶的新信息。from langgraph.graph import StateGraph, START, END def retrieve_memory(state: AgentState): # 把當(dāng)前用戶最新一條消息向量化去記憶庫檢索 query state[messages][-1].content results memory_store.search(query, top_k5) return {memory_results: results} def assistant_node(state: AgentState): # 將檢索到的記憶注入系統(tǒng)提示詞 memory_block format_memory_block(state[memory_results]) messages build_prompt(state[messages], memory_block) response chat_model.invoke(messages) return {messages: [response]} def store_memory(state: AgentState): # 抽取本輪值得記憶的新信息 new_facts extract_memories(state[messages]) return {memory_to_store: new_facts} graph_builder StateGraph(AgentState) graph_builder.add_node(retrieve, retrieve_memory) graph_builder.add_node(assistant, assistant_node) graph_builder.add_node(store, store_memory) graph_builder.add_edge(START, retrieve) graph_builder.add_edge(retrieve, assistant) graph_builder.add_edge(assistant, store) graph_builder.add_edge(store, END) agent_graph graph_builder.compile()這個(gè)流程看起來簡單但涵蓋了記憶系統(tǒng)最核心的“先讀后寫”邏輯。調(diào)用Agent之前先讀記憶回復(fù)之后再把新信息寫回去。這兩步順序不能反否則Agent第一輪對(duì)話就是“失憶”狀態(tài)。3.2 記憶寫入與提取的完整實(shí)現(xiàn)記憶寫入是整個(gè)系統(tǒng)里最容易糊弄也最容易出錯(cuò)的地方。很多人會(huì)把整段對(duì)話歷史一股腦全存進(jìn)向量庫這樣做有兩個(gè)問題一是存了大量無意義的內(nèi)容檢索時(shí)噪音太大二是沒有做結(jié)構(gòu)化取出來之后模型不好用。我的做法是先用一個(gè)小模型對(duì)每一輪對(duì)話做信息抽取抽出來的是結(jié)構(gòu)化的事實(shí)列表再寫入記憶庫。比如用戶說“我在上海工作平時(shí)喜歡喝美式咖啡”抽取出來就是兩條事實(shí)location上海、coffee_preference美式。這樣存下來后續(xù)檢索到的就是干凈、明確的事實(shí)而不是一堆口水話。from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser extract_prompt ChatPromptTemplate.from_messages([ (system, 你是記憶抽取器。從對(duì)話中抽取關(guān)于用戶的可記憶事實(shí)只輸出JSON數(shù)組。 事實(shí)類型包括偏好(preference)、身份信息(identity)、長期目標(biāo)(goal)、禁忌(avoidance)。 如果一句話沒有任何可記憶信息輸出空數(shù)組。 規(guī)則只抽取確定的、長期有效的信息不抽取臨時(shí)情緒或隨口閑聊。), (human, {input}) ]) def extract_memories(messages): # 只取最近一輪用戶消息做抽取 latest_user_msg [m for m in messages if m.type human][-1].content chain extract_prompt | chat_model | StrOutputParser() raw chain.invoke({input: latest_user_msg}) # 解析JSON返回事實(shí)列表 return parse_fact_list(raw)記憶寫入之后檢索時(shí)也有一些技巧。除了基礎(chǔ)的相似度搜索我還會(huì)在存儲(chǔ)時(shí)給每條記憶加上時(shí)間戳和實(shí)體標(biāo)簽。檢索時(shí)優(yōu)先返回近期記憶和與當(dāng)前對(duì)話實(shí)體相關(guān)的記憶再按相似度排序。這樣一個(gè)簡單的加權(quán)策略能明顯提高記憶使用的準(zhǔn)確率。def recall_memory(query: str, top_k: int 5): query_embedding embed_model.embed_query(query) results vector_store.similarity_search_by_vector( query_embedding, ktop_k ) # 按時(shí)間衰減和相關(guān)性過濾 filtered [ r for r in results if r.metadata.get(score, 1.0) 0.75 ] return filtered這個(gè)相似度閾值0.75是我在多輪測(cè)試?yán)镎{(diào)出來的平衡點(diǎn)。設(shè)置太低會(huì)把大量不相關(guān)信息當(dāng)成記憶注入進(jìn)去設(shè)置太高又可能漏掉真正有用的記憶。不同嵌入模型的輸出范圍不一樣這個(gè)值需要實(shí)際測(cè)幾輪再定我建議你上線前用一批真實(shí)對(duì)話樣本跑一遍看看檢出來的記憶到底準(zhǔn)不準(zhǔn)。3.3 用MCP把記憶做成可復(fù)用服務(wù)如果你只是做一個(gè)單機(jī)小Agent上面那套已經(jīng)夠用。但一旦你要做Multi-Agent——比如客服Agent、推薦Agent、運(yùn)營Agent同時(shí)服務(wù)同一個(gè)用戶——那記憶就必須獨(dú)立出來做成一個(gè)所有Agent共享的服務(wù)。這時(shí)候MCP協(xié)議就能派上用場(chǎng)。MCP的全稱是Model Context Protocol它本質(zhì)上規(guī)定了Agent怎么調(diào)用外部工具、怎么讀取外部數(shù)據(jù)。把記憶封裝成一個(gè)MCP Server之后不管前端接的是Claude還是其他任何支持MCP的客戶端都能通過一套標(biāo)準(zhǔn)協(xié)議來讀寫記憶Agent和各人不再各自維護(hù)一套記憶庫數(shù)據(jù)也不打架。我以一個(gè)輕量級(jí)記憶服務(wù)為例把核心代碼貼出來。它的職責(zé)只有兩件事存一段記憶、查相關(guān)記憶。from mcp.server.fastmcp import FastMCP mcp FastMCP(MemoryServer) mcp.tool() def store_memory(user_id: str, content: str, metadata: dict None): 存儲(chǔ)一條關(guān)于用戶的事實(shí)記憶 doc_id f{user_id}_{timestamp()} vector_store.add_texts( texts[content], metadatas[{user_id: user_id, time: timestamp(), **metadata}], ids[doc_id] ) return {status: ok, memory_id: doc_id} mcp.tool() def query_memory(user_id: str, query: str, top_k: int 5): 查詢某個(gè)用戶的相關(guān)歷史記憶 results vector_store.similarity_search( query, ktop_k, filter{user_id: user_id} ) return [{content: r.page_content, metadata: r.metadata} for r in results] mcp.run()封裝完之后Agent只需要通過MCP客戶端去調(diào)用這兩個(gè)工具就能實(shí)現(xiàn)記憶的讀寫。好處是你后續(xù)換記憶實(shí)現(xiàn)——從Chroma換成Qdrant或者從本地存儲(chǔ)換成云服務(wù)——Agent那端的代碼一行都不用動(dòng)。這就是協(xié)議標(biāo)準(zhǔn)化的價(jià)值。4. 踩坑記錄記憶系統(tǒng)常見問題與排查4.1 記憶污染舊記憶干擾新對(duì)話記憶系統(tǒng)上線之后我最先遇到的坑是“記憶污染”。典型場(chǎng)景是用戶曾經(jīng)說過“我最近在學(xué)Python準(zhǔn)備轉(zhuǎn)行做開發(fā)”但過了半年他早就不學(xué)Python了結(jié)果每次對(duì)話Agent都在提“你上次學(xué)Python學(xué)得怎么樣”用戶體驗(yàn)極其糟糕。這個(gè)問題的根源在于寫入時(shí)沒有區(qū)分“臨時(shí)狀態(tài)”和“長期事實(shí)”檢索時(shí)也沒有做時(shí)間衰減。我的解決方案是雙管齊下。寫入側(cè)抽取事實(shí)時(shí)要求模型只抽穩(wěn)定的、不會(huì)有短期變化的信息凡是帶時(shí)間限定詞的內(nèi)容——比如“最近”“現(xiàn)在”“這周”——一律不寫入長期記憶。讀取側(cè)給每條記憶打上時(shí)間戳檢索結(jié)果在排序時(shí)對(duì)超過30天的記憶做時(shí)間衰減加權(quán)讓近期記憶靠前。4.2 存儲(chǔ)膨脹與檢索失效記憶系統(tǒng)跑久了第二個(gè)問題是存儲(chǔ)膨脹。用戶聊得越多記憶片段越多向量庫里攢了幾萬條最后檢索結(jié)果全部亂套——不同時(shí)期的記憶內(nèi)容互相矛盾相似度分?jǐn)?shù)全部擠在高位區(qū)間區(qū)分度下降召回的東西越來越不準(zhǔn)。這時(shí)候需要做記憶整理。我目前維護(hù)的Agent每天晚上會(huì)跑一個(gè)離線任務(wù)把同一個(gè)用戶的記憶按實(shí)體聚類相近的合并、矛盾的去重、超過180天沒被命中的冷記憶歸檔。這相當(dāng)于定期給你的記憶庫“斷舍離”雖然會(huì)消耗一些算力但換來的檢索質(zhì)量提升非常明顯。4.3 多Agent場(chǎng)景下的記憶一致性最后說一個(gè)Multi-Agent場(chǎng)景下特有的問題。多個(gè)Agent共同讀寫同一個(gè)記憶庫很容易出現(xiàn)數(shù)據(jù)不一致。比如推薦Agent記下了用戶“喜歡喝美式咖啡”但客服Agent在處理用戶投訴時(shí)又記下“用戶說這家店咖啡太苦了”。兩條記憶同時(shí)存在后續(xù)Agent讀取的時(shí)候不知道該信哪條。我的方案是給每條記憶增加一個(gè)版本號(hào)寫入時(shí)帶上來源Agent標(biāo)識(shí)和時(shí)間戳讀取時(shí)如果有沖突先比較時(shí)間后比較來源——客服對(duì)話中產(chǎn)生的用戶情緒反饋優(yōu)先于日常偏好。同時(shí)重要用戶的記憶變更會(huì)進(jìn)入一個(gè)人工審核的中間態(tài)確保高價(jià)值的記憶不被低質(zhì)量寫入污染。這套機(jī)制不復(fù)雜但能省掉后續(xù)大量排查的時(shí)間。寫在后面的一點(diǎn)體會(huì)把Agent記憶系統(tǒng)從頭到尾搭完我最大的體會(huì)是記憶系統(tǒng)的難點(diǎn)不在算法而在“什么時(shí)候該記、什么時(shí)候該取”的判斷上。你不需要掌握多高深的機(jī)器學(xué)習(xí)知識(shí)但需要對(duì)業(yè)務(wù)場(chǎng)景有足夠深的理解。我見過不少團(tuán)隊(duì)一上來就上大模型向量庫的豪華配置結(jié)果記了一堆沒用的東西關(guān)鍵信息反而檢索不出來。另一個(gè)值得說的經(jīng)驗(yàn)是不要一上來就做全套先用最簡單的方式把“對(duì)話摘要存庫關(guān)鍵詞檢索”跑通再逐步升級(jí)到結(jié)構(gòu)化抽取和向量檢索。你會(huì)在迭代過程中清楚看到每一步優(yōu)化帶來的實(shí)際收益而不是對(duì)著一個(gè)黑盒系統(tǒng)瞎調(diào)參數(shù)。做記憶系統(tǒng)最怕的就是“做完了但不知道它有沒有用”給自己設(shè)定一些可觀察的指標(biāo)比如記憶命中率、用戶二次提問率比什么都重要。