慣性聚合 高效追蹤和閱讀你感興趣的部落格、新聞、科技資訊
閱讀原文 在慣性聚合中打開

推薦訂閱源

WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
Jina AI
Jina AI
Last Week in AI
Last Week in AI
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
月光博客
月光博客
爱范儿
爱范儿
D
Docker
罗磊的独立博客
博客园 - 叶小钗
博客园 - 司徒正美

掘金

Win 安装Claude Code FastAPI 的 CORSMiddleware 跨域中间件 Java 自研 ReAct Agent 半年后,我用 LangGraph 验证了这些设计取舍 🚀AI编程工作流终极形态:GitNexus!零Token消耗实现代码知识图谱化!让Claude Code和Codex拥有上帝视角彻底告别盲目改代码,复杂项目重 LeetCode 72. 编辑距离:动态规划经典题解 被The Graph的GraphQL查询坑了三天,我用一个真实DeFi项目把链上数据索引彻底搞懂了 (AI) 编写简单 AI 助手 (ds-agent) 别再让 pnpm 跟着 nvm 跑了!独立安装终极指南 Claude Code 为什么这么顺?Anthropic 最新复盘:真正撑住它的不是模型,而是缓存 从 /simplify 指令深挖 Claude Code 多 Agent 协同机制 Function-Calling与工具使用 新手上路(六):Claude code装上ECC全家桶:38 个子代理、156 个技能、生产级 Hooks 与 Rules 体系 我在 Claude、Kimi、opencode 三个 AI 之间搭了一条自动协作管道 【技能篇】OpenClaw Skill 详解:给 AI 装上"专业外挂" wagmi v2 多链钱包切换:一个 Uniswap 仿盘项目让我踩了三天坑 两周浅学 RAG 我把 Python re 模块比喻成摸金手套 新手上路(三):Claude Code Skills 装了一堆没用?20+ 个 Skill 横向对比 + 三套组合方案,按需抄 K2.6、DeepSeek V4、GPT-5.5 都来了,组合拳打起来 Claude Code 进阶之路:从记忆系统到子代理编排 [java] 编译之后的记录类(Record Classes)长什么样子(上) 国产大模型能力大比拼,社区有话说 我研读了 500 个 Spring Boot 生产级代码库,90% 都犯了这 7 个致命错误 JAVA重点难点 转发-中央网信办部署开展“清朗·整治AI应用乱象”专项行动 合同同步逻辑 【合并已排序数组的三种实现策略,哪一种更可取?】 30天减20斤挑战:少一斤发100红包(2) 我竟然被JavaScript的隐式类型转换坑了三天! 二十五.Electron 初体验与进阶 本地到生产,解决 AI 全栈最后一公里——构建&部署&运维 程序员创业半年:顺的事、不顺的事,和我一直没想清楚的事 UI组件库elementplus 像使用 Redis 一样操作 LocalStorage 向量检索的流程是怎样的?Embedding 和 Rerank 各自的作用? LangChain DeepAgents 速通指南(七)—— DeepAgents使用Agent Skill 为什么越来越多的大厂抛弃MCP,转向CLI? 【节点】[SquareRoot节点]原理解析与实际应用 juejin.cn juejin.cn 从 “存得下” 到 “算得快”:工业物联网需要新一代时序数据平台越来越多工业用户开始意识到一个问题:**数据是存下来了, - 掘金 放弃 Claude 订阅?我用 8 年前的服务器,强跑 Google 最强开源模型 Gemma 4 真实测评! Python开发者狂喜!200+课时FastAPI全栈实战合集,10大模块持续更新中🔥 从 Claw-Code 看 AI 驱动的大型项目开发:2 人 + 10 个自治 Agent 如何产出 48K 行 Rust 代码 秒级创建实例,火山引擎 Milvus Serverless 让 AI Agent 开发更快更省火山引擎MilvusSer MediaPlayer 播放器架构:NuPlayer 的 Source/Decoder/Renderer 三驾马车 juejin.cn juejin.cn juejin.cn juejin.cn
Agent系列(六):記憶管理——讓 Agent 記住重要的事
冬奇Lab · 2026-05-27 · via 掘金

記憶,讓 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 替換為 PostgresStoreRedisStore 即可獲得真正的持久化。


自動摘要壓縮: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 結尾,提取用戶新信息
  • 避免每輪都寫(設置提取置信度或內容長度過濾)

本篇小結

幾個核心觀點:

  1. 四種記憶類型各司其職:感覺記憶是 LLM 調用本身;工作記憶是對話歷史;情景記憶是壓縮摘要;語義記憶是跨會話的 KV Store
  2. checkpointer 管會話內,store 管跨會話:thread_id 是會話維度,user_id 是用戶維度,兩個維度分開管理
  3. 摘要壓縮是長對話的核心方案:RemoveMessage + 摘要注入,讓 Agent 在無限長對話中保持 Token 可控
  4. 會話隔離是基礎:不同 thread_id 的歷史互不干擾,不同 user_id 的長期記憶互不干擾
  5. 從 InMemoryStore 到 PostgresStore 只需換一行:架構不變,後端可插拔

下一篇:知識庫集成——RAG 作為 Agent 工具和 RAG Pipeline 的本質區別,多知識庫路由,Agent 如何決定什麼時候檢索、檢索什麼、檢索幾次。


參考資料


歡迎來我的個人主頁找到更多有用的知識和有趣的產品