让 Agent 不再失忆:LangChain 短期记忆实战
在多轮对话里,Agent 默认每轮都是「新开始」------你刚说过名字,下一轮它就忘了。LangChain 的短期记忆解决这个问题:在同一会话内自动带上历史上下文,包括工具调用记录,无需手动拼消息。
本文基于前文 工具调用 Agent(硅基 DeepSeek + @tool),只加三处改动即可开启记忆;并覆盖窗口截断 与 Redis 持久化。配套可运行脚本。
一、一句话理解:短期记忆是什么?
短期记忆 = 同一会话内的多轮对话上下文缓存。
| 特点 | 说明 |
|---|---|
| 会话隔离 | 只在同一个 thread_id 内生效,不同 ID 互不干扰 |
| 存什么 | 完整对话状态(用户消息、AI 回复、工具调用与结果) |
| 默认行为 | 调用前自动读历史,调用后自动存档 |
| 持久化 | 可选:内存重启即丢;Redis / Postgres 重启不丢 |
注意区分两个层次:概念上 叫「短期记忆」(相对长期记忆 / 用户画像);实现上可以持久化到 Redis,重启后仍能续聊。
二、三个核心概念
1. thread_id(会话 ID)
对话的唯一标识,类似微信里每个聊天窗口。
- 相同
thread_id→ 同一场对话,Agent 有记忆 - 不同
thread_id→ 完全隔离(相当于新开聊天)
易混淆点 :此处的
thread_id不是操作系统里的「线程 ID」。LangGraph 把每段独立对话的执行链路抽象成一条 thread,因此沿用这个命名。
2. checkpointer(检查点存储器)
Agent 每执行一步,就把当前完整状态 拍快照存下来;下次同 thread_id 调用时从最新快照恢复。
| 场景 | 实现 | 特点 |
|---|---|---|
| 开发调试 | InMemorySaver |
零依赖,进程退出即丢失 |
| 生产上线 | RedisSaver / Postgres |
持久化,重启可续聊 |
类比:游戏自动存档 + 读档。没有 checkpointer,Agent 就没有记忆。
3. 上下文自动管理
开启记忆后无需手动维护 messages 列表:
用户提问 → checkpointer 读出历史 → 拼上本轮消息 → 调模型/工具 → 写回 checkpoint
三、最简示例:内存记忆(5 行改动)
前置 :沿用项目里的 llm.py、tools.py(见 工具调用),无需修改。
相对无记忆 Agent,只需三步:
- 创建
checkpointer = InMemorySaver() create_agent(..., checkpointer=checkpointer)------ 必须关键字传参- 每次
invoke传入固定config
python
from llm import llm
from tools import tools
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
agent = create_agent(
llm,
tools,
checkpointer=checkpointer,
system_prompt="精准使用工具回答用户问题",
)
config = {"configurable": {"thread_id": "user_001"}}
res1 = agent.invoke({"messages": "我叫小明,帮我算 5+3"}, config)
print("第一轮:", res1["messages"][-1].content)
res2 = agent.invoke({"messages": "我叫什么?刚才的计算结果再乘以2"}, config)
print("第二轮:", res2["messages"][-1].content)
预期效果:
- 第一轮:算出
5+3=8 - 第二轮:回答「小明」,并调用计算器把
8*2=16------ 无需重复自我介绍
多用户隔离 :把 thread_id 换成 "user_002" 即开启全新对话,互不影响。
四、上下文截断:对话太长怎么办?
对话累积后会超出模型上下文窗口。入门阶段用窗口截断即可:发给模型前只保留最近 N 轮用户提问(及对应的 AI / 工具消息)。
关键机制(易踩坑)
| 组件 | 作用 |
|---|---|
checkpointer |
存完整历史,不受截断影响 |
trim_window 中间件 |
只限制发给模型的消息长度 |
因此:截断后 Agent 可能「想不起来」很早以前的信息------不是没存,是模型本轮看不到。
python
KEEP_ROUNDS = 3 # 保留最近 3 轮 user 提问(含当前这轮)
@wrap_model_call
def trim_window(request, handler):
messages = request.messages
human_idxs = [i for i, m in enumerate(messages) if m.type == "human"]
if len(human_idxs) > KEEP_ROUNDS:
start = human_idxs[-KEEP_ROUNDS]
messages = messages[start:]
return handler(request.override(messages=messages))
agent = create_agent(
llm, tools,
checkpointer=checkpointer,
middleware=[trim_window],
system_prompt="精准使用工具回答用户问题;记住用户提到的姓名、喜好等信息",
)
验证截断效果 (KEEP_ROUNDS = 3):
| 轮次 | 用户说 | 模型能否记住 |
|---|---|---|
| 1 | 我叫小明,职业是数学老师 | ✓(在窗口内) |
| 2 | 我喜欢打乒乓球 | ✓ |
| 3 | 我喜欢钓鱼 | ✓ |
| 4 | 我喜欢什么?职业是什么? | 爱好 ✓;职业 ✗(第 1 轮已被截出窗口) |
进阶方案(消息摘要、智能清理)见 官方文档。
五、持久化:Redis Stack
本地调试用 InMemorySaver 足够;上线必须换持久化存储,否则重启后记忆全部丢失。
RedisSaver 依赖 RediSearch 模块,macOS 上 brew install redis 的普通版不支持,需用 Redis Stack。
1. 安装依赖
shell
pip install langgraph-checkpoint-redis
2. 启动 Redis Stack(Docker)
shell
docker pull redis/redis-stack-server:latest
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack-server:latest
# 验证:必须能看到 search 模块
redis-cli MODULE LIST
redis-cli ping # PONG
.env 可选(默认 redis://localhost:6379):
shell
REDIS_URI=redis://localhost:6379
常用管理:
shell
docker ps
docker stop redis-stack && docker start redis-stack
docker rm -f redis-stack # 删除容器(数据一并清空)
3. Python 代码
只需把 InMemorySaver 换成 RedisSaver,thread_id 与测试逻辑不变:
python
import os
from llm import llm
from tools import tools
from langchain.agents import create_agent
from langgraph.checkpoint.redis import RedisSaver
REDIS_URI = os.getenv("REDIS_URI", "redis://localhost:6379")
with RedisSaver.from_conn_string(REDIS_URI) as checkpointer:
checkpointer.setup() # 首次初始化 RediSearch 索引,重复调用安全
agent = create_agent(llm, tools, checkpointer=checkpointer, ...)
config = {"configurable": {"thread_id": "user_001"}}
res1 = agent.invoke({"messages": "我叫小明,帮我算 5+3"}, config)
res2 = agent.invoke({"messages": "我叫什么?刚才的计算结果再乘以2"}, config)
4. 验证持久化
- 第一次运行:完成上述两轮对话
- 关闭 Python 进程,再次运行
- 注释掉第一轮,只执行:
agent.invoke({"messages": "我叫什么?"}, config) - 仍应回答「小明」
5. 查看 Redis 中的数据
shell
redis-cli KEYS 'checkpoint*user_001*'
redis-cli GET checkpoint_latest:user_001:__empty__
redis-cli JSON.GET "checkpoint:user_001:__empty__:xxxx" # 替换为实际 key
若使用带 UI 的镜像 redis/redis-stack(端口 8001),可在 http://localhost:8001 的 Browse 中搜索 user_001。
6. 常见报错
| 报错 | 原因 | 处理 |
|---|---|---|
unknown command 'FT._LIST' |
连的是普通 Redis,非 Redis Stack | 停掉 brew redis,改用 Docker 镜像 |
Connection refused |
Redis 未启动 | docker start redis-stack,检查 6379 端口 |
| Docker 拉镜像超时 | 网络问题 | 配置镜像加速 |
六、避坑清单
- 没配
checkpointer= 没记忆 ------ 这是唯一开关,漏写即每轮失忆 - 记忆跟
thread_id走 ------ 续聊必须用同一个 ID;生产环境用用户 ID / 会话 ID 生成 InMemorySaver不能上线 ------ 仅调试用,生产用 Redis Stack 或 Postgres- Redis 必须是 Stack 版 ------ 普通 Redis 缺 RediSearch,
setup()会报FT._LIST - 截断 ≠ 删除 ------ checkpoint 仍存全量历史,只是模型本轮看不到被截掉的部分