實戰(zhàn):從原理到代碼的完整指南)
很多剛開始玩大模型 Agent 的朋友第一次做出來的機器人像個金魚聊完上一句就忘。這不是模型笨而是我一開始也沒搞懂 Agent 記憶技術(shù)到底在解決什么問題。這篇指南按照我自己從零到能跑通的路線整理面向小白程序員邏輯講透代碼可以直接抄。我會從原理、分層、代碼、參數(shù)、踩坑五個角度把“大模型 Agent 怎么記住東西”這件事拆開講清楚。你不需要一開始就懂向量數(shù)據(jù)庫也不需要背一堆新名詞跟著順序看下來基本就能在自己的項目里給 Agent 裝上“記憶”。適合誰讀剛接觸大模型應(yīng)用開發(fā)、想讓自己的聊天機器人具備跨會話記憶、做私人知識助手、或者想在團隊里搭一個帶記憶的客服問答系統(tǒng)的人。就算你只寫過一點 Python也一樣能跟上。1. 先搞清楚 Agent 為什么需要記憶1.1 大模型生來就是“無狀態(tài)”的大模型本質(zhì)上是一個函數(shù)給它一段文字它返回一段文字。它不會在上一次調(diào)用結(jié)束后保存任何東西。你發(fā)兩條消息它不會自動記得第一條聊了什么除非你把第一條的內(nèi)容重新塞進(jìn)第二條的輸入里。這一點和人的對話體驗差別巨大。人聊天時會自然記住“剛才聊到了什么”“對方叫什么名字”“他上次說過喜歡簡潔”。但模型沒有這個概念。所謂“模型有記憶”實際上是在模型外面加了存儲和檢索再把檢索到的內(nèi)容放回輸入里。我剛開始做 Agent 時犯過一個經(jīng)典錯誤以為把歷史消息存進(jìn)數(shù)據(jù)庫Agent 就有記憶了。結(jié)果發(fā)現(xiàn)存了等于沒存因為模型根本不會自己去查數(shù)據(jù)庫。它只認(rèn)當(dāng)前請求里的內(nèi)容。所以你真正要做的事情只有一件在合適的時機把合適的記憶內(nèi)容變成當(dāng)前請求的一部分。1.2 記憶的本質(zhì)是上下文管理不是單純存儲很多人把“記憶系統(tǒng)”理解成“聊天記錄存儲系統(tǒng)”這是誤區(qū)。存儲只是最底層記憶系統(tǒng)的核心是上下文管理。上下文窗口就像桌面記憶庫像檔案柜。檔案柜再滿也沒關(guān)系但桌面只有那么大面積。你不能把所有檔案都攤在桌面上不然真正有用的東西反而找不到。Agent 記憶要做的工作是判斷哪些檔案現(xiàn)在有用、把哪幾份抽出來放到桌面上、用完后再收回去。換句話說記憶系統(tǒng)不是“存得越多越好”而是“放得越準(zhǔn)越好”。精確地決定“放什么進(jìn) Prompt”比“存了多少條歷史”重要得多。1.3 這篇指南解決三類問題一類是“記不住”用戶昨天告訴你他喜歡簡短回復(fù)今天你又說了一大篇。這類問題源于跨會話信息丟失。另一類是“記太亂”歷史記錄、用戶偏好、臨時狀態(tài)全混在一起塞進(jìn) Prompt 后模型被舊信息帶偏回答前后矛盾。還有一類是“記不起”記憶明明存了但檢索的時候找不準(zhǔn)。比如用戶問“上次那個報錯解決了嗎”系統(tǒng)搜不到因為當(dāng)時的記錄里根本沒有“報錯”這個詞。后面所有內(nèi)容基本都在圍繞“怎么記住、怎么存、怎么取、怎么用、怎么忘”這五件事展開。2. 記憶體系的四層拆分2.1 第一層會話工作記憶第一層是“正在聊的這段對話”也就是當(dāng)前 prompt 里的消息列表。它由模型自帶的上下文窗口承載需要我們自己管理長度和裁剪策略。典型問題用戶連續(xù)聊二十分鐘后歷史太長超過窗口上限。簡單粗暴的做法是“只保留最近 N 條”但這樣會導(dǎo)致更早的關(guān)鍵信息丟失。稍好一點的做法是把早期內(nèi)容壓縮成摘要再把摘要放在消息列表最前面。這一層的管理頻率非常高幾乎是每次請求都要做。所以它必須足夠輕不能依賴外部數(shù)據(jù)庫查詢盡量在內(nèi)存里完成。2.2 第二層情景記憶情景記憶存的是“發(fā)生過的事件”。比如“用戶上午反饋了支付失敗”“上次對話里用戶要求換個方案”“上周已經(jīng)發(fā)送過一封確認(rèn)郵件”。這類記憶的特點是有時間、有事件、有結(jié)果通常按發(fā)生順序檢索。情景記憶解決的是連續(xù)性。沒有它用戶下次打開對話時Agent 完全不知道你們之前發(fā)生過什么。有了它Agent 可以說出“上次你提到支付失敗我查了一下可能是銀行接口超時”。實現(xiàn)時一般會給每條記憶打上時間戳、事件類型、關(guān)聯(lián)對象這樣既能按時間排序也能按事件類型過濾。2.3 第三層語義記憶語義記憶存的是“事實和偏好”不依賴某個具體事件。比如“用戶喜歡簡潔回復(fù)”“項目部署在測試環(huán)境”“用戶所在行業(yè)是教育”。這些內(nèi)容即使不提時間也長期有效。語義記憶通常適合用向量檢索。因為用戶問法千變?nèi)f化“我上次說過的偏好你還記得嗎”“回復(fù)別那么啰嗦”和“用戶喜歡簡短回答”這句話在字面上差別很大但語義距離很近。向量檢索能抓這種近似關(guān)系。語義記憶要特別注意覆蓋和去重。同一條偏好可能被寫進(jìn)去三次如果不去重檢索時會同時冒出三條重復(fù)記憶既占空間又干擾模型。2.4 第四層程序性記憶程序性記憶是“怎么做某件事”的經(jīng)驗比如“遇到數(shù)學(xué)計算先調(diào)用計算工具別自己心算”“生成周報時按數(shù)據(jù)、結(jié)論、行動計劃三段來寫”。在小項目里這部分通常被寫死在代碼邏輯或系統(tǒng)提示詞里。但做得復(fù)雜的 Agent 會把它沉淀為可檢索的技能模板。比如一個 Agent 遇到 Excel 處理任務(wù)時會自動調(diào)出“讀取表格-清洗-分析-出報告”的流程模板。程序性記憶的本質(zhì)是知識復(fù)用。它能讓 Agent 不去重復(fù)摸索流程而是直接使用已經(jīng)驗證過的方法。四層記憶可以共存。我的經(jīng)驗是先把會話工作記憶和語義記憶做扎實再考慮情景和程序性記憶。因為前兩個直接決定用戶體驗后兩個更多是錦上添花。記憶類型典型內(nèi)容存儲方式主要用途會話工作記憶當(dāng)前對話上下文內(nèi)存消息列表保證當(dāng)前對話連貫情景記憶某時某刻發(fā)生的事件數(shù)據(jù)庫記錄時間索引跨會話回溯語義記憶用戶偏好、事實知識向量庫規(guī)則過濾回答個性化問題程序性記憶任務(wù)流程、技能模板配置/模板庫自動決定執(zhí)行路徑3. 手把手實現(xiàn)一套可用的記憶組件3.1 最小版會話窗口管理先寫一個最簡單的會話窗口。作用是控制消息列表長度超出的部分?jǐn)D壓成摘要而不是直接扔掉。import json from collections import deque class SessionWindow: def __init__(self, max_messages20): self.max_messages max_messages self.messages deque(maxlenmax_messages) def add(self, role, content): self.messages.append({role: role, content: content}) def to_prompt(self): return list(self.messages) def compress_old(self): # 保留最近一半消息把更早的文本拼成一條摘要占位 keep_count self.max_messages // 2 old list(self.messages)[:-keep_count] recent list(self.messages)[-keep_count:] summary_text 之前對話摘要 .join(m[content] for m in old if m[content]) self.messages.clear() self.messages.append({role: system, content: summary_text}) for m in recent: self.messages.append(m)這里用deque(maxlen...)可以自動丟棄最舊的。但只丟棄不總結(jié)會丟關(guān)鍵信息。compress_old就是把早期內(nèi)容壓成一條摘要再把摘要放到 system 里。注意摘要不能太頻繁做。每輪都壓縮等于把每次回答都再調(diào)用一次模型做總結(jié)成本和延遲都會上去。建議當(dāng)消息條數(shù)達(dá)到上限時才觸發(fā)。3.2 升級版向量檢索長期記憶當(dāng)需要跨會話記住用戶偏好和事件時純會話窗口不夠。我們需要一個“寫進(jìn)去-查出來”的機制。最通用方案是向量檢索把文本變成向量用余弦相似度找最接近的內(nèi)容。下面用一個純 Python 標(biāo)準(zhǔn)庫演示核心邏輯。生產(chǎn)環(huán)境一般會用現(xiàn)成的向量庫或模型服務(wù)提供的 embedding原理一樣。import hashlib import json def tokenize(text): # 簡單按空格和標(biāo)點切分真實場景可以用分詞庫 return [t.lower() for t in text.replace(,, ).replace(。, ).split()] def hashed_embedding(text, dim128): vec [0.0] * dim for token in tokenize(text): digest hashlib.md5(token.encode()).hexdigest()[:8] index int(digest, 16) % dim vec[index] 1.0 return vec def cosine_similarity(a, b): dot sum(x * y for x, y in zip(a, b)) norm_a sum(x * x for x in a) ** 0.5 norm_b sum(x * x for x in b) ** 0.5 if norm_a 0 or norm_b 0: return 0.0 return dot / (norm_a * norm_b) class VectorMemory: def __init__(self, pathmemory.json): self.path path self.items self._load() def _load(self): try: with open(self.path, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return [] def _save(self): with open(self.path, w, encodingutf-8) as f: json.dump(self.items, f, ensure_asciiFalse, indent2) def add(self, text, categorygeneral): embedding hashed_embedding(text) self.items.append({ text: text, category: category, embedding: embedding, access_count: 0 }) self._save() def search(self, query, top_k3): query_vec hashed_embedding(query) scored [] for item in self.items: score cosine_similarity(query_vec, item[embedding]) scored.append((score, item)) scored.sort(keylambda x: x[0], reverseTrue) return scored[:top_k]這個寫法可以跑通但有個明顯缺陷哈希詞袋向量對“語義相似”不敏感比如“喜歡簡短”和“別啰嗦”幾乎匹配不到一起。真實項目里應(yīng)該把hashed_embedding替換成 embedding 模型生成的向量。替換后其他邏輯不用變。搜索后把返回結(jié)果按指定格式拼進(jìn) Prompt 就行memory VectorMemory() results memory.search(用戶喜歡什么風(fēng)格) memory_section \n.join(f- [{r[1][category]}] {r[1][text]} for r in results) prompt f以下是長期記憶\n{memory_section}\n\n請基于記憶回答當(dāng)前問題。每次命中時最好更新access_count后面做遺忘策略會用到。3.3 進(jìn)階版寫入與遺忘策略長期記憶最怕兩件事什么垃圾都存、存了永遠(yuǎn)不刪。所以要加兩道流程寫入前過濾定期遺忘。寫入前過濾舉例def should_store(text, min_length10): # 太短的內(nèi)容沒有存儲價值 if len(text) min_length: return False # 一些臨時寒暄不值得記 useless [你好, 哈哈, 再見, 不知道] if any(u in text for u in useless): return False return True遺忘策略可以用“低頻且長期未訪問優(yōu)先清理”的思路import time def clean_memory(memory, max_items200, max_age_days30): now time.time() kept [] for item in memory.items: last_access item.get(last_access_ts, now) age_days (now - last_access) / 86400 # 訪問次數(shù)低且超過30天未用準(zhǔn)備淘汰 if age_days max_age_days and item.get(access_count, 0) 3: continue kept.append(item) memory.items kept memory._save()這只是示例生產(chǎn)里更常用加權(quán)分?jǐn)?shù)綜合“最近訪問時間”“訪問次數(shù)”“內(nèi)容重要性”算一個衰減分低于閾值就刪除。操作心得遺忘一定要做成定時任務(wù)而不是每輪對話都掃一遍。原因很簡單記憶表小的時候怎么掃都無所謂但數(shù)據(jù)到幾萬條后每次掃描都是性能開銷。4. 記憶鏈路的四個關(guān)鍵步驟與參數(shù)選擇4.1 寫入判斷什么值得記記憶寫入最容易犯的錯是“全量寫入”。把每輪對話都原樣存進(jìn)去短期內(nèi)沒感覺時間一長向量庫塞滿了“今天天氣不錯”“好的”“嗯嗯”這類無意義內(nèi)容檢索質(zhì)量直線下降。我自己的篩選標(biāo)準(zhǔn)有三條包含事實比如用戶所在行業(yè)、項目名稱、聯(lián)系方式、產(chǎn)品版本。包含偏好比如“回復(fù)要簡短”“不喜歡 emoji”“報告要帶數(shù)據(jù)表”。包含狀態(tài)變化比如“支付失敗已解決”“計劃從下周二開始實施”。凡是用戶明確表達(dá)、重復(fù)提到、并且后續(xù)可能再次使用的信息才寫入。相反寒暄、臨時情緒、一次性的流程確認(rèn)通常不寫。寫入時還要做歸一化。比如用戶說“以后叫我老王就行”和“請稱呼我為老王”本質(zhì)是同一件事至少要把稱呼統(tǒng)一成“用戶希望被稱呼為老王”。不做歸一化檢索時會同時命中兩條類似內(nèi)容白白浪費上下文空間。4.2 檢索如何讓“相關(guān)”變成“精準(zhǔn)”檢索是記憶系統(tǒng)里最影響體驗的環(huán)節(jié)。相關(guān)不代表有用。用戶問“昨天那個問題解決了嗎”檢索出十條“昨天聊過的東西”模型反而不知道具體是哪件事。通用做法是“先過濾再排序”。先用元數(shù)據(jù)縮小范圍比如時間范圍、對話場景、記憶分類然后做向量檢索按相似度排序最后按分?jǐn)?shù)閾值截斷。相關(guān)參數(shù)我建議這樣設(shè)置top_k從 3 開始調(diào)。太少容易漏太多容易雜。min_score低于 0.2 的記憶不要放進(jìn)上下文。這個值需要配合你用的 embedding 模型來定不同模型分?jǐn)?shù)分布差很多。recency_bonus給近期內(nèi)容加一點權(quán)重。例如最終得分 相似度 * 0.8 時間新鮮度 * 0.2。調(diào)參時建議每次只改一個變量別同時動 top_k 和閾值不然你不知道是哪個改動帶來的效果。4.3 融合檢索結(jié)果如何安全放進(jìn) Prompt檢索結(jié)果進(jìn)了 Prompt不代表模型會正確使用。你需要告訴模型“哪些是記憶哪些是當(dāng)前對話”。我常用的格式【長期記憶】 - 用戶偏好喜歡簡短回復(fù) - 事實當(dāng)前項目使用測試環(huán)境 【當(dāng)前對話】 用戶昨天那個報錯后來怎么解決的這樣模型可以清楚區(qū)分信息來源不容易把舊記憶當(dāng)成新輸入。如果記憶內(nèi)容和當(dāng)前對話沖突比如記憶說“用戶喜歡詳細(xì)報告”但當(dāng)前用戶說“別廢話直接給結(jié)論”我會在 Prompt 里加一句“當(dāng)長期記憶與當(dāng)前對話矛盾時以當(dāng)前對話為準(zhǔn)”。融合階段還要控制體量。我給自己定過一個預(yù)算記憶內(nèi)容不超過整個 Prompt 的 30%。超過這個比例模型會迷失重點回答又臭又長。4.4 遺忘記憶系統(tǒng)必須會做減法遺忘和寫入同樣重要。沒有遺忘記憶庫就是垃圾堆。舊信息會干擾新判斷甚至讓 Agent 用過時信息回答當(dāng)前問題。我項目里至少維護(hù)三種遺忘機制過期刪除比如“臨時任務(wù)”類記憶 24 小時后失效。低頻刪除超過 30 天沒被檢索到的記憶權(quán)重下調(diào)最終清理。沖突覆蓋當(dāng)新記憶與舊記憶語義高度相似且信息更新時刪除舊記錄。遺忘是記憶系統(tǒng)和日志系統(tǒng)的分界線。日志是”全都要存永不刪除“記憶是”只保留對后續(xù)決策有用的信息“。很多人把這兩者混在一起最后系統(tǒng)既不像日志也不像記憶。5. 落地實踐五個經(jīng)典問題5.1 檢索不到用戶覺得你“失憶”了現(xiàn)象是用戶明確告訴你“我上次說過”Agent 卻說“抱歉我沒有找到相關(guān)信息”。先檢查三步寫入有沒有成功、檢索條件是不是太嚴(yán)、文本有沒有分詞或編碼問題。我踩過一個坑是 embedding 用的是舊模型新對話里的詞分布變化太大導(dǎo)致相似度普遍偏低。換新 embedding 模型后問題消失。如果檢索確實沒問題但用戶換了說法可以加一層“關(guān)鍵詞向量混合檢索”。關(guān)鍵詞負(fù)責(zé)精確匹配向量負(fù)責(zé)語義近似兩邊各自返回一批候選最后合并排序。這樣能覆蓋“字面不同但意思相近”的情況。5.2 檢索出一堆但全用不上現(xiàn)象是明明返回了記憶模型還是答非所問。原因通常是 top_k 太大或者檢索時沒有限定記憶類型。比如用戶問的是項目狀態(tài)結(jié)果系統(tǒng)把用戶閑聊偏好、歷史情緒、任務(wù)記錄全檢索出來了。正確做法是按“場景 分類”過濾。查詢前先識別意圖這是任務(wù)咨詢那就只檢索“任務(wù)類”記憶這是閑聊那就只檢索“偏好類”記憶。過濾后再做向量排序準(zhǔn)確率和效率都會好很多。5.3 上下文越來越長費用吃不住隨著記憶累積每次請求都會變得越來越長。如果不對上下文做限制Token 費用會線性上漲。我的做法是三層存儲熱記憶最近 1 小時內(nèi)的會話直接放內(nèi)存。溫記憶跨天但頻繁使用的內(nèi)容放向量庫按需檢索。冷記憶一個月前的內(nèi)容壓縮成摘要后歸檔。每次請求前先算一次總 Token。如果超過窗口預(yù)算就降低“溫記憶”的召回條數(shù)。比如正常取 5 條超預(yù)算時就只取 3 條。優(yōu)先保當(dāng)前對話長期記憶永遠(yuǎn)讓路。5.4 記憶被污染越修越錯這是最隱蔽的問題。“污染”指錯誤的記憶被寫入后模型每次都會基于錯誤信息回答用戶糾正完后舊的錯誤還在又會被檢索出來。避免污染的關(guān)鍵是更新機制。當(dāng)出現(xiàn)一條新記憶與舊記憶明顯矛盾時不應(yīng)“并列存儲”而應(yīng)該做覆蓋。比如舊記憶是“項目部署在測試環(huán)境”用戶今天說“項目已經(jīng)上線生產(chǎn)了”這時直接刪除舊條目或標(biāo)記為過期只保留新條目。另外必須給用戶提供“刪除記憶”的能力。哪怕只是提供一個簡單的“清除我的所有歷史記憶”按鈕也能兜底解決大部分污染問題。5.5 安全與隱私怎么處理記憶里往往包含很多個人信息和業(yè)務(wù)敏感信息。至少要遵守這幾條不記錄密碼、密鑰、Token 等敏感字段。寫入前先做脫敏手機號、身份證號、銀行卡號用掩碼替換。記憶數(shù)據(jù)按用戶隔離不能在多用戶間串?dāng)?shù)據(jù)。明確告知用戶我們存儲了哪些信息并支持一鍵清除。合規(guī)這塊不能等項目上線才補。從第一版設(shè)計記憶組件開始就要把數(shù)據(jù)隔離和刪除機制考慮進(jìn)去。否則后期改造成本會很高。癥狀可能原因排查順序快速對策完全檢索不到寫入失敗或閾值過高先查寫入日志再查檢索分?jǐn)?shù)放寬 min_score啟用混合檢索檢索結(jié)果不相關(guān)top_k 過大、未按場景過濾看返回條目的類型分布先按記憶分類過濾再向量排序上下文超預(yù)算記憶未分級、沒有限制條數(shù)統(tǒng)計每次請求 Token 消耗三層存儲限制溫記憶召回記憶越用越錯沖突信息未覆蓋、污染寫入檢查是否有重復(fù)矛盾記錄增加覆蓋機制提供用戶刪除入口數(shù)據(jù)安全風(fēng)險敏感字段明文入庫檢查入庫前是否有脫敏邏輯脫敏隔離可刪除6. 進(jìn)階方向與選型建議6.1 從向量檢索邁向知識圖譜向量檢索擅長回答“語義相近”但不擅長回答“多跳關(guān)系”。比如“A 用戶負(fù)責(zé)的 B 項目里有哪些模塊是用 C 技術(shù)實現(xiàn)的”這種問題如果只靠向量匹配很難一次命中。知識圖譜則可以把實體和關(guān)系串起來支持精確的關(guān)系查詢。但知識圖譜的構(gòu)建成本高需要維護(hù)實體、關(guān)系、屬性和更新邏輯。我的建議是先跑通向量記憶等確實遇到“關(guān)系推理”類需求再引入圖結(jié)構(gòu)。小項目上來就做圖譜大概率會被建圖工作量拖垮。6.2 記憶壓縮的兩種路線一是摘要壓縮。把長段對話交給模型生成一段摘要放入長期記憶。優(yōu)點是保存的信息密度高缺點是有損細(xì)節(jié)會丟失。二是結(jié)構(gòu)化壓縮。從對話中抽出結(jié)構(gòu)化信息如字段、時間、意圖、結(jié)論保存成 JSON。這種格式適合程序處理也適合做條件查詢。一條可行路線是“先結(jié)構(gòu)化再按需摘要”。重要內(nèi)容抽成結(jié)構(gòu)化字段長期不用的內(nèi)容再合并成摘要。這樣兼顧了即時檢索和長期歸檔。6.3 什么時候該自研什么時候用現(xiàn)成組件如果是快速做 Demo、驗證想法直接用現(xiàn)成框架自帶的記憶模塊最省事。但默認(rèn)實現(xiàn)通常比較粗糙比如全量存歷史、簡單截斷、不做遺忘。一旦你的用戶量上來或者對記憶準(zhǔn)確率有要求就值得自己寫一套輕量記憶模塊。我個人的判斷標(biāo)準(zhǔn)是記憶邏輯不超過幾百行代碼時自研更劃算。你能精確控制什么時候?qū)懭?、檢索多少條、如何融合、如何遺忘。而使用重型框架時光是理解它的記憶生命周期就要花很久。如果你打算在自己的項目里長期迭代 Agent我建議至少把“寫入過濾檢索召回遺忘清理”這三個核心組件自己寫一遍。這樣獨立于任何框架后續(xù)遷移也方便。最后再分享一點個人體會我最初做的記憶系統(tǒng)很蠢一個 JSON 文件存所有歷史每次對話都把所有歷史塞進(jìn) Prompt。結(jié)果模型越聊越亂成本越燒越高。后來慢慢改成“寫入前過濾、檢索時排序、融合前控制體量、定期做遺忘”整個 Agent 才穩(wěn)定下來。如果你也在做類似項目我的建議很直白不要一開始就上重型設(shè)施。先在本地跑一個 JSON 存儲 向量檢索的最小閉環(huán)把“寫入-檢索-融合-遺忘”體會一遍。跑通之后自然知道哪些地方必須換數(shù)據(jù)庫、哪些地方必須加規(guī)則。記憶系統(tǒng)的價值從來不是存得多而是用得準(zhǔn)。希望這份指南能讓你少走一點彎路。