🎯 课程主题
深入理解 InMemorySaver 的工作流程、状态读取机制,以及实际开发中常见的 4 个问题及其解决方案。
📝 核心知识点
1. InMemorySaver 的完整工作流程
每次 agent.invoke({"messages": [...new_msg...]}, config) 的执行过程:
读取历史 State
│
▼
┌─────────────────────────────────┐
│ thread_id = "1" │
│ history: [msg1, msg2, msg3] │ ← 从 InMemorySaver 读取
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 追加新消息到列表 │
│ new_list: [msg1, msg2, msg3, │
│ msg_new_user] │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 发送给大模型 │
│ → 模型基于完整历史给出回复 │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 将 AI 回复追加到列表中 │
│ [msg1, msg2, msg3, │
│ msg_new_user, ai_response] │
│ 写回 InMemorySaver │
└─────────────────────────────────┘
2. State 的消息增长规律
| 轮次 | invoke 前 messages 数量 | invoke 后 messages 数量 | 新增 |
|---|---|---|---|
| 第 1 轮 | 0 | 2(Human + AI) | +2 |
| 第 2 轮 | 2 | 4(+ Human + AI) | +2 |
| 第 3 轮 | 4 | 6(+ Human + AI) | +2 |
| 第 n 轮 | 2(n-1) | 2n | +2 |
每次 invoke 都追加 1 条 HumanMessage + 1 条 AIMessage,消息列表持续线性增长。
3. 生产环境的多场景应用
| 场景 | 实现方式 |
|---|---|
| 多用户聊天 | 不同用户 → 不同 thread_id(如 "user_alice", "user_bob"),记忆隔离 |
| 同一用户的多任务 | 不同任务 → 不同 thread_id(如 "task_coding", "task_writing"),互不干扰 |
| 清空历史重新对话 | 换一个新的 thread_id(如从 "1" → "2") |
thread_id是字符串类型,灵活设计就能满足各种业务场景。
4. 四个常见问题汇总
| # | 问题 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | Agent 不记得之前的信息 | ① 未创建/未传入 InMemorySaver;② invoke 时未传 config;③ thread_id 不一致 | 对照四步法逐一排查 |
| 2 | InMemorySaver 会丢失数据吗? | 会!存储在进程内存中,进程结束/重启即丢失。跨进程无法共享 | 生产环境改用 SqliteSaver 或 PostgresSaver |
| 3 | 内存会无限增长吗? | 会!消息列表持续追加,token 消耗越来越大,甚至超模型限制,响应变慢 | 使用上下文管理策略:消息裁剪、删除、摘要 |
| 4 | 如何清空某个会话的历史? | 内存数据无法直接清空 | 方案 A:换一个新的 thread_id;方案 B:重新创建 InMemorySaver 实例 |
🏗️ 架构与工作流
消息追加的可视化示意
第一次 invoke (thread_id="1"):
InMemorySaver["1"] → [Human:"你好"]
invoke 后 → [Human:"你好", AI:"你好!有什么可以帮你的?"]
第二次 invoke (thread_id="1"):
InMemorySaver["1"] → [Human:"你好", AI:"你好!...", Human:"我叫张三"]
invoke 后 → [Human:"你好", AI:"你好!...", Human:"我叫张三", AI:"好的张三"]
第三次 invoke (thread_id="2") ← 新线程
InMemorySaver["2"] → [] ← 全新,与 thread_id="1" 完全隔离
invoke 后 → [Human:"你好", AI:"你好!请问有什么需要?"]
上下文管理策略
消息数量增长
│
▼
问题: token 过多、响应变慢、超出模型限制
│
▼
策略:
├── 消息裁剪 (trimming): 只保留最近的 N 条
├── 消息删除 (deleting): 去除过期/无关消息
└── 摘要 (summarization): 用摘要替代冗长历史
💻 代码实战
查看 State 的完整历史
from rich import print as rprint
config = {"configurable": {"thread_id": "1"}}
# 获取当前线程的完整 State
state = agent.get_state(config)
rprint(state)
# 输出示例:
# {
# "messages": [
# HumanMessage(content="我叫张三"),
# AIMessage(content="你好张三!"),
# HumanMessage(content="我叫什么?"),
# AIMessage(content="你叫张三"),
# ],
# "config": {...}
# }
获取特定 State 的快照
# get_state 返回的是该 thread_id 下最新的 State 快照
# 其中 values["messages"] 列出了所有消息
messages = state.values.get("messages", [])
print(f"共有 {len(messages)} 条消息")
# 输出: 共有 4 条消息
⚠️ 常见问题与避坑指南
| 常见错误写法 | 正确写法 |
|---|---|
agent.invoke({"messages": [...]}) 不传 config | agent.invoke({"messages": [...]}, config=config) |
每次 invoke 换 config | 同一对话使用 同一个 config 对象 |
忘记先 invoke 就调用 get_state | get_state 需在 invoke 之后才有数据 |
| 生产环境使用 InMemorySaver | 改用 SqliteSaver 或 PostgresSaver 实现真正持久化 |
💡 个人总结与延伸
- InMemorySaver 是理解 LangGraph 短期记忆机制的最佳起点——它极简地展示了 State + Checkpointer + thread_id 的协作模式。
- 消息无限增长的隐患:在真实应用中,对话可能持续非常长,必须引入上下文工程进行管理(见后续章节的消息裁剪/摘要策略)。
- thread_id 的灵活性:它不仅用于用户隔离,还可用于会话分支(fork)、对话回放(replay)等高级场景。
- 下一节将过渡到生产方案——使用 SQLite / PostgreSQL 作为 Checkpointer 后端,实现持久化存储。