記憶,讓 Agent 從"工具"變成"助手"
一個沒有記憶的 Agent,每次對話都是從零開始。你告訴它你叫李雷、你是 Python 工程師、你喜歡動手實踐——下次對話,它完全不記得。
這不是 Bug,是架構缺失。
LLM 本身沒有持久記憶,每次調用都是無狀態的。要讓 Agent 記住事情,需要在架構層顯式地存儲、管理和讀取記憶。這就是記憶管理模塊要解決的問題。
本篇從四個維度拆解 Agent 記憶:記憶類型的分類模型、上下文管理的三種策略、LangGraph 提供的兩種記憶機制(checkpointer 和 store)、以及超長對話的自動壓縮方案。
四種記憶類型:從認知科學到工程實現
借鑑認知科學對人類記憶的分類,Agent 的記憶也可以分為四層,每一層在 LangGraph 中都有對應的實現方式:
┌──────────────────────────────────────────────────────────┐
│ 記憶層級 │
├──────────────────────┬───────────────────────────────────┤
│ 感覺記憶 (Sensory) │ 當前 Turn 的 in-flight 消息 │
│ │ 生命週期:單次 LLM 調用 │
├──────────────────────┼───────────────────────────────────┤
│ 工作記憶 (Working) │ 對話歷史 MessageHistory(有限 K 輪)│
│ │ 實現:messages 列表注入 Prompt │
├──────────────────────┼───────────────────────────────────┤
│ 情景記憶 (Episodic) │ 向量化/摘要化的歷史片段 │
│ │ 實現:摘要壓縮 + VectorStore 檢索 │
├──────────────────────┼───────────────────────────────────┤
│ 語義記憶 (Semantic) │ 長期存儲的用戶偏好、事實 │
│ │ 實現:LangGraph store (KV Store) │
└──────────────────────┴───────────────────────────────────┘
感覺記憶:當前 Turn 的消息
最短暫的記憶。一次 LLM 調用的輸入和輸出,用完即棄:
q = "Python 中 len([1, 2, 3]) 等於多少"
answer = llm.invoke([HumanMessage(q)])
# answer.content → "len([1, 2, 3]) 的結果等於 3。"
# 這次 invoke 結束後,這個 answer 就消失了
不需要任何機制來"管理"感覺記憶——它是 LLM 調用本身。
工作記憶:有限的對話歷史
把之前幾輪對話消息拼接進 Prompt,是最直接的記憶實現。效果立竿見影:
history = [
HumanMessage("我叫李雷,是一名 Python 工程師"),
AIMessage("你好,李雷!很高興認識你。"),
HumanMessage("我最近在學習 LangGraph"),
AIMessage("LangGraph 很強大,特別適合構建有狀態的 Agent。"),
]
test_q = "我之前告訴你我叫什麼名字?"
實測對比:
有歷史 → "是的,你之前告訴我你的名字是李雷,並且你是一名 Python 工程師..."
無歷史 → "抱歉,我無法回憶起您之前告訴我的名字,因為作為一個 AI,我沒有
持久的記憶功能來存儲個人數據..."
差距非常直觀。工作記憶的限制是 Token 成本隨對話長度線性增長,需要結合截斷或摘要來管理。
情景記憶:摘要化的歷史片段
當對話歷史很長時,把全部歷史直接塞進 Prompt 代價太高。情景記憶的做法是先壓縮,再存儲:
long_history = history * 4 # 16 條消息
summary = llm.invoke([
SystemMessage("將以下對話壓縮為 60 字以內的摘要,保留關鍵信息"),
HumanMessage(str([m.content for m in long_history])),
])
# → "李雷,Python工程師,積極學習LangGraph,贊其強大,適合構建有狀態Agent。"
16 條消息壓縮成 28 個字,下一輪用摘要代替原始歷史,Token 消耗大幅下降。
語義記憶:跨會話的用戶事實
最持久的記憶層。不隨對話結束而消失,專門存儲關於用戶的長期事實(姓名、職業、偏好等):
# 把用戶信息存入 KV Store,下一次會話直接讀取
user_profile = {
"name": "李雷",
"role": "Python 工程師",
"interests": ["LangGraph", "Agent 開發"],
"level": "中級",
}
# 基於這些信息,Agent 能給出個性化的回答
# "下一步學習方向" → 推薦 LangGraph 進階用法而不是 Python 入門
上下文管理三策略:截斷 / 摘要 / 檢索
當對話歷史越來越長,上下文窗口撐不住怎麼辦?三種策略各有取捨:
策略一:截斷(Truncation)
最簡單粗暴——保留最近 N 條消息,其餘全部丟棄:
# 保留最近 4 條消息
truncated = history[-4:]
resp = llm.invoke(truncated + [HumanMessage(test_q)])
實測用 8 個主題(16 條消息)的歷史,截斷到最近 4 條後問"Python 列表是什麼":
截斷後最早可見:'解釋一下 Python 裝飾器'(第 5 個主題,"列表"在第 1 個)
回答:Python 中的列表是一種內置的數據結構... (靠 LLM 自身知識回答)
⚠ 丟失了我們"學過"列表這個事實,LLM 只是在用通用知識回答
適用場景:對歷史連貫性要求不高的場景,或歷史本身就不重要的純問答類 Agent。
策略二:摘要(Summarization)
用 LLM 把長曆史壓縮成一段摘要,後續對話以摘要代替原始歷史:
summary_resp = llm.invoke([
SystemMessage("將對話歷史壓縮為一段摘要(不超過 80 字),保留所有已介紹的主題名稱"),
HumanMessage("\n".join([f"{m.type}: {m.content}" for m in history])),
])
# → "Python列表可變有序,元組不可變省內存,字典鍵值對映射,集合唯一元素,
# 函數封裝邏輯,類面向對象,裝飾器函數包裝,生成器惰性計算。"
# 16 條消息 → 66 字摘要
同樣問"Python 列表是什麼",摘要方案的回答能體現"這是我們討論過的話題",而不只是通用知識。
三種策略對比:
| 策略 | Token 消耗 | 信息保留 | 實現複雜度 | 適用場景 |
|---|---|---|---|---|
| 截斷 | 最低 | 只有近期 | 極低 | 純問答、歷史不重要 |
| 摘要 | 低 | 全局脈絡 | 中 | 教學、諮詢、長期對話 |
| 檢索 | 最低 | 精準相關 | 高(需向量庫) | 知識庫、多領域 Agent |
策略三:檢索(Retrieval)
只拉取與當前問題語義相關的歷史片段——是效果最好也最複雜的方案:
# 簡化演示:按關鍵詞過濾(生產中用向量相似度)
relevant = [m for m in history if "列表" in m.content or "list" in m.content.lower()]
# 16 條 → 3 條相關歷史
resp = llm.invoke(relevant + [HumanMessage(test_q)])
適用場景:知識庫型 Agent、用戶有大量歷史記錄的個人助手。
LangGraph checkpointer:會話內狀態持久化
LangGraph 的 MemorySaver(checkpointer)用 thread_id 區分不同會話,在同一會話內自動積累對話歷史:
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
checkpointer = MemorySaver()
agent = create_react_agent(model=llm, tools=[get_weather], checkpointer=checkpointer)
# 同一 thread_id = 同一個會話,狀態持久化
config_a = {"configurable": {"thread_id": "weather_001"}}
跨輪引用實測
會話內連續三輪問天氣:
[Turn 1] 用戶: 北京今天天氣怎麼樣?
Agent: 北京今天的天氣是晴,溫度為 25°C,東北風 3 級,空氣良好。
(當前狀態中消息數: 4)
[Turn 2] 用戶: 那上海呢? ← "那" 字沒有明確指代,需要上文理解
Agent: 上海今天的天氣是多雲,22°C,東南風 2 級,輕度霧霾。
(當前狀態中消息數: 8)
[Turn 3] 用戶: 這兩個城市哪個更適合今天出行? ← 需要前兩輪的查詢結果
Agent: 考慮到上海的霧霾情況,建議您在北京出行。
(當前狀態中消息數: 10)
Turn 2 和 Turn 3 的回答都依賴了前面的歷史,checkpointer 自動完成了跨輪上下文管理。
會話隔離
不同 thread_id 之間完全獨立:
[新會話 - thread_id: weather_002]
用戶: 我剛才問的是哪個城市?
Agent: 您剛才問的是"哪個城市",但是沒有提供具體的城市名稱。
如果您需要查詢某個城市的天氣,請告訴我具體的城市名稱。
→ 新 thread_id 沒有任何歷史,完全不知道剛才問過什麼
會話繼續
同一 thread_id 隔一段時間再回來,歷史依然存在:
[會話 A 繼續 - 同一 thread_id]
用戶: 剛才查的兩個城市,再查一下深圳對比一下
Agent: 深圳今天的天氣是陣雨,27°C,西南風 2 級,同時還有雷暴預警。
(進行了北京/上海/深圳三城比較)
→ 記住了前面查過北京和上海
MemorySaver 是內存實現,進程重啟後數據丟失。生產中用 SqliteSaver(本地文件)或 PostgresSaver(數據庫)代替。
LangGraph InMemoryStore:跨會話長期記憶
checkpointer 解決的是單次會話內的記憶。跨會話的長期記憶需要 store:
checkpointer → 綁定 thread_id,會話生命週期內有效
store → 綁定 user_id,跨會話永久存在
核心 API
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
# 寫入:(namespace, key, value)
store.put(("user_facts", user_id), key, {"fact": "李雷,後端工程師"})
# 讀取:搜索某個 namespace 下的所有條目
facts = store.search(("user_facts", user_id))
for item in facts:
print(item.value["fact"])
# 精確讀取
item = store.get(("user_facts", user_id), specific_key)
跨會話記憶實測
在會話 A 中,Agent 自動從對話中提取用戶信息並存入 store:
[會話 A] 用戶說了三句話 → 自動提取並存儲:
• 李雷,後端工程師
• Python, Go, LangGraph, Agent 開發
• 動手實踐,不喜歡純看文檔
在完全不同的會話 B(新 thread_id,但同一 user_id)中問"你認識我嗎?":
[會話 B - 全新 thread_id]
用戶: 你好,你認識我嗎?
Agent: 你好!根據你提供的信息,我認識你。你是李雷,一位後端工程師,
擅長使用 Python、Go、LangGraph 和 Agent 進行開發。
你更喜歡動手實踐,而不是僅僅閱讀文檔。有什麼可以幫助你的嗎?
→ 雖然是全新 thread_id,但 store 中的用戶信息跨會話持久
不同 user_id 的數據完全隔離,互不干擾。
checkpointer vs store 對比
# 短期記憶:checkpointer — 綁定 thread_id,會話內有效
app = graph.compile(checkpointer=MemorySaver())
result = app.invoke(input, config={"configurable": {"thread_id": "abc"}})
# 長期記憶:store — 綁定 user_id,跨會話有效
store = InMemoryStore()
app = graph.compile(store=store, checkpointer=MemorySaver())
# 在 node 中操作 store
store.put(("user_facts", user_id), key, {"fact": "..."})
stored = store.search(("user_facts", user_id))
生產中 InMemoryStore 替換為 PostgresStore 或 RedisStore 即可獲得真正的持久化。
自動摘要壓縮:RemoveMessage + 摘要輪替
當消息數超過閾值時,觸發自動壓縮——這是讓 Agent 在無限長對話中保持清醒的核心機制。
圖結構
[chat 節點]
│
├─ 消息數 ≤ 閾值 → END
└─ 消息數 > 閾值 → [compress 節點] → END
compress 節點:RemoveMessage 刪除舊消息
def compress_node(state: SummaryState) -> dict:
messages = state["messages"]
to_compress = messages[:-2] # 保留最新 2 條,其餘全部壓縮
keep = messages[-2:]
# 舊消息 → 新摘要
new_summary = llm.invoke([
SystemMessage("將以下內容壓縮為 120 字以內的摘要"),
HumanMessage(existing_summary + old_messages_text),
]).content
# RemoveMessage:告訴 add_messages reducer 刪除這些消息
remove_ops = [RemoveMessage(id=m.id) for m in to_compress]
return {"messages": remove_ops, "summary": new_summary}
RemoveMessage 是 LangGraph 專用的消息刪除操作符,add_messages reducer 看到它會從狀態中刪除對應 id 的消息。
實測效果
11 輪對話,閾值設為 8 條消息:
[Turn 1-4] 消息數: 2/4/6/8 | 摘要: ○ 無
[壓縮觸發] 10 條 → 壓縮 8 條,保留 2 條
[新摘要] Python列表常用方法包括查找、排序、添加刪除等。
`dict.get()` 避免 `KeyError`,返回默認值。
`*args` 接收任意位置參數,`**kwargs` 接收任意關鍵字參數...
[Turn 5] 消息數: 2 | 摘要: ✓ 已壓縮 ← 壓縮後從 2 條重新開始
[壓縮再次觸發] 10 條 → 壓縮 8 條,保留 2 條
[Turn 11] 最終彙總:
"根據我們之前的討論,以下是您掌握的 Python 知識點彙總:
1. Python 列表推導式...
2. 集合推導式... ← 通過摘要鏈條傳承,第 1 輪的內容還在
3. Lambda 函數..."
關鍵結果:11 輪對話始終只有 2-8 條 active 消息,但通過摘要鏈,所有歷史知識都傳承了下來。
摘要狀態設計
class SummaryState(TypedDict):
messages: Annotated[list, add_messages] # add_messages 處理 RemoveMessage
summary: Optional[str] # 累積的歷史摘要,注入 system prompt
def chat_node(state: SummaryState) -> dict:
summary = state.get("summary") or ""
system_parts = ["你是助手。"]
if summary:
system_parts.append(f"\n\n【歷史摘要】{summary}") # 摘要注入 system prompt
resp = llm.invoke([SystemMessage("".join(system_parts))] + state["messages"])
return {"messages": [resp]}
記憶管理設計清單
實現一套完整的 Agent 記憶系統需要考慮的點:
短期記憶(checkpointer)
- 選擇合適的 checkpointer 後端(開發用 MemorySaver,生產用 SqliteSaver/PostgresSaver)
- 為每個用戶/會話分配唯一的
thread_id - 設置歷史截斷閾值,防止 Token 無限增長
長期記憶(store)
- 按 namespace 組織用戶數據:
(類型, user_id)如("user_facts", uid) - 提取記憶時要有置信度過濾,避免存入無意義的噪聲
- 生產環境替換為 PostgresStore / RedisStore
上下文壓縮
- 確定壓縮閾值(建議 8-20 條消息,視場景而定)
- 摘要 Prompt 中明確要保留哪些信息(主題名稱、關鍵決策、用戶偏好)
- 測試摘要鏈:第 N 輪壓縮的摘要是否包含了第 1 輪的關鍵信息
- 用
RemoveMessage而不是替換整個 messages 列表(後者在有 checkpointer 時會有問題)
記憶的讀寫時機
- 讀記憶:在 chat_node 開頭,注入 system prompt
- 寫記憶:在 chat_node 結尾,提取用戶新信息
- 避免每輪都寫(設置提取置信度或內容長度過濾)
本篇小結
幾個核心觀點:
- 四種記憶類型各司其職:感覺記憶是 LLM 調用本身;工作記憶是對話歷史;情景記憶是壓縮摘要;語義記憶是跨會話的 KV Store
- checkpointer 管會話內,store 管跨會話:thread_id 是會話維度,user_id 是用戶維度,兩個維度分開管理
- 摘要壓縮是長對話的核心方案:RemoveMessage + 摘要注入,讓 Agent 在無限長對話中保持 Token 可控
- 會話隔離是基礎:不同 thread_id 的歷史互不干擾,不同 user_id 的長期記憶互不干擾
- 從 InMemoryStore 到 PostgresStore 只需換一行:架構不變,後端可插拔
下一篇:知識庫集成——RAG 作為 Agent 工具和 RAG Pipeline 的本質區別,多知識庫路由,Agent 如何決定什麼時候檢索、檢索什麼、檢索幾次。
參考資料
歡迎來我的個人主頁找到更多有用的知識和有趣的產品










