技能点
- 消息存储在内存
- 消息持久化到redis
- 修改聊天历史
- 裁剪消息
消息存储在内存
下面我们展示一个简单的示例,其中聊天历史保存在内存中,默认情况下,期望配置参数是一个字符串thread_id。
使用单参数默认值:
python
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
model = ChatDeepSeek(model="deepseek-chat")
# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
ability: str
def call_model(state: ChatState):
# 从状态里读取ability字段,拼出系统提示词
system_message = SystemMessage(
content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
)
# 把系统提示+历史/本轮消息一起发给模型
response = model.invoke([system_message, *state["messages"]])
# 返回 {"messages": [response]},把模型回复追加回状态里的messages
# 也就是说:每轮都会把AI回复写会会话消息历史
return {"messages": [response]}
# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END
# 这是记住历史的核心
# MemorySaver(): 把每次运行后的状态做 checkpoint (内存里)。
graph = builder.compile(checkpointer=MemorySaver())
# 同一个会话id,下一次你在用同一个thread_id调graph.invoke()时
# LangGraph会先把这条会话之前保存的状态取出来,在合并本次输入继续跑
config = {"configurable": {"thread_id": "123"}}
response_1 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
config=config,
)
print("第一轮:", response_1["messages"][-1].content)
response_2 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第二轮:", response_2["messages"][-1].content)
config = {"configurable": {"thread_id": "456"}}
response_3 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第三轮:", response_3["messages"][-1].content)
本次调用完成后,状态里会有这轮用户消息+模型回复,并按thread_id保存起来
记住历史的本质
- 同一个thread_id
- 每轮把messages写回状态
- checkpointer保存并在下一轮恢复状态
三者组合形成会话记忆
一个重要的实现点:
现在是用的是MemorySaver,它是进程内存,脚本不退出时能记住,脚本重启后,记忆丢失
如果想要重启后也记住,需要持久化checkpointer(比如:sqlite、postgres对应实现)
配置会话唯一键
下面我们使用了两个参数:user_id和thread_id
配置user_id和thread_id作为会话唯一键
python
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
model = ChatDeepSeek(model="deepseek-chat")
# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
ability: str
def call_model(state: ChatState):
# 从状态里读取ability字段,拼出系统提示词
system_message = SystemMessage(
content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
)
# 把系统提示+历史/本轮消息一起发给模型
response = model.invoke([system_message, *state["messages"]])
# 返回 {"messages": [response]},把模型回复追加回状态里的messages
# 也就是说:每轮都会把AI回复写会会话消息历史
return {"messages": [response]}
# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END
# 这是记住历史的核心
# MemorySaver(): 把每次运行后的状态做 checkpoint (内存里)。
graph = builder.compile(checkpointer=MemorySaver())
def build_config(user_id: str, thread_id: str) -> dict:
# 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
session_key = f"{user_id}:{thread_id}"
return {"configurable": {"thread_id": session_key}}
# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
config=config,
)
print("第一轮:", response_1["messages"][-1].content)
response_2 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第二轮:", response_2["messages"][-1].content)
# 同一用户不同线程:不复用上面的历史
config = build_config(user_id="u_001", thread_id="t_456")
response_3 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第三轮:", response_3["messages"][-1].content)
# 不同用户同一线程号:也不会串历史
config = build_config(user_id="u_002", thread_id="t_123")
response_4 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第四轮:", response_4["messages"][-1].content)
消息持久化
请查看 memory integrations 页面,了解使用 Redis 和其他提供程序实现聊天消息历史的方法。
配置redis环境
启动redis服务,调用聊天接口,看Redis是否存储历史记录
安装redis依赖:pip install langgraph-checkpoint-redis redis
python
import os
import atexit
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
model = ChatDeepSeek(model="deepseek-chat")
# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
ability: str
def call_model(state: ChatState):
# 从状态里读取ability字段,拼出系统提示词
system_message = SystemMessage(
content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
)
# 把系统提示+历史/本轮消息一起发给模型
response = model.invoke([system_message, *state["messages"]])
# 返回 {"messages": [response]},把模型回复追加回状态里的messages
# 也就是说:每轮都会把AI回复写会会话消息历史
return {"messages": [response]}
# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END
_redis_checkpointer_cm = None
def _close_redis_checkpointer():
global _redis_checkpointer_cm
if _redis_checkpointer_cm is not None:
_redis_checkpointer_cm.__exit__(None, None, None)
_redis_checkpointer_cm = None
def build_checkpointer():
# 优先使用 Redis 做持久化;如果本地未安装 Redis checkpointer,则回退到内存。
global _redis_checkpointer_cm
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
try:
from langgraph.checkpoint.redis import RedisSaver # type: ignore[reportMissingImports]
checkpointer_or_cm = RedisSaver.from_conn_string(redis_url)
# 某些版本返回上下文管理器,需要先 enter 拿到真正的 saver。
if hasattr(checkpointer_or_cm, "__enter__") and hasattr(checkpointer_or_cm, "__exit__"):
_redis_checkpointer_cm = checkpointer_or_cm
checkpointer = _redis_checkpointer_cm.__enter__()
atexit.register(_close_redis_checkpointer)
else:
checkpointer = checkpointer_or_cm
# 某些实现需要先初始化索引/结构,存在 setup 方法时执行一次。
if hasattr(checkpointer, "setup"):
checkpointer.setup()
print(f"已启用 Redis 持久化: {redis_url}")
return checkpointer
except Exception as exc:
print(f"Redis checkpointer 不可用,回退到内存模式: {exc}")
return MemorySaver()
# 这是记住历史的核心:checkpointer 负责按 thread_id 保存/恢复状态。
graph = builder.compile(checkpointer=build_checkpointer())
def build_config(user_id: str, thread_id: str) -> dict:
# 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
session_key = f"{user_id}:{thread_id}"
return {"configurable": {"thread_id": session_key}}
# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
config=config,
)
print("第一轮:", response_1["messages"][-1].content)
response_2 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第二轮:", response_2["messages"][-1].content)
# 同一用户不同线程:不复用上面的历史
config = build_config(user_id="u_001", thread_id="t_456")
response_3 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第三轮:", response_3["messages"][-1].content)
# 不同用户同一线程号:也不会串历史
config = build_config(user_id="u_002", thread_id="t_123")
response_4 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第四轮:", response_4["messages"][-1].content)
执行过程中,没有走持久化redis,走的内存,查了下原因是因为需要这个报错的根因是:你现在连的是普通 Redis,但 langgraph 的 Redis checkpointer 需要 RediSearch 命令(FT.*)。
FT._LIST 不存在就说明当前实例没装 RediSearch 模块。
所有打算换 Redis Stack服务,
-
第一步安装windows下安装了docker desktop
-
docker启动redis stack
shelldocker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest这样启动完redis之后,不仅有服务,还有一个web界面8001端口,可以访问redis,还是蛮方便的。

-
在次执行脚本,正常可以持久化记住历史记忆了。
修改聊天历史
修改存储的聊天消息可以帮助您的聊天机器人处理各种情况。以下是一些示例:
裁剪消息
LLM 和聊天模型有限的上下文窗口,即使您没有直接达到限制,您可能也希望限制模型处理的干扰量。一种解决方案是只加载和存储最近的 n 条消息。让我们使用一个带有一些预加载消息的示例历史记录:
python
import os
import atexit
from langchain_core.messages import HumanMessage, RemoveMessage, SystemMessage
from langchain_deepseek import ChatDeepSeek
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, START, MessagesState, StateGraph
model = ChatDeepSeek(model="deepseek-chat")
# MessagesState是LangGraph的内置状态,里面最关键的是messages(消息列表)
# 然后又扩展了一个字段alibity,用于系统提示词(比如:math)
# 之后在运行时,会围绕这个状态读写
class ChatState(MessagesState):
ability: str
MAX_ROUNDS = 3
MAX_MESSAGES = MAX_ROUNDS * 2
def _trim_to_recent_rounds(messages: list, max_messages: int = MAX_MESSAGES) -> list:
"""删除超出窗口的旧消息,仅保留最近 max_messages 条。"""
if len(messages) <= max_messages:
return []
old_messages = messages[: -max_messages]
# LangGraph 通过 RemoveMessage 实现"从状态中删除指定消息"
return [RemoveMessage(id=msg.id) for msg in old_messages]
def call_model(state: ChatState):
# 从状态里读取ability字段,拼出系统提示词
system_message = SystemMessage(
content=f"你是一个{state.get('ability', 'general')}的助手,响应20字以内"
)
# 把系统提示+最近窗口内历史/本轮消息一起发给模型
recent_messages = state["messages"][-MAX_MESSAGES:]
response = model.invoke([system_message, *recent_messages])
# 追加本轮回复后,再把旧消息删掉,保证持久化中最多只保留最近 3 轮。
remove_updates = _trim_to_recent_rounds([*state["messages"], response])
# 返回 {"messages": [response]},把模型回复追加回状态里的messages
# 也就是说:每轮都会把AI回复写回会话消息历史
# 此处的messages这个字段在LangGraph离是"增量更新指令",不是整份最终消息列表
# return {"messages": [*remove_updates, response]}的含义是:
# remove_updates里每个元素是RemoveMessage(id=...), 表示把这些旧消息从状态里删除掉
# response是新增一条AI回复,表示把这条消息追加到状态里
# 是把删除动作提交给系统,如果只返回response,旧消息不回被删除,历史会一直累积
# 可以理解成一次事务:先删除旧的,在增加新的
return {"messages": [*remove_updates, response]}
# 这是单节点流程,START-chatbot-END
# 每次调用就走一遍这个流程
# 搭建执行流程图
# 创建一个状态图构建器,声明整张图的状态结构是 ChatState
builder = StateGraph(ChatState)
# 注册一个节点,名字叫chatbot,执行逻辑是call_model函数
builder.add_node("chatbot", call_model)
# 定义起点:图开始时先进入chatbot节点
builder.add_edge(START, "chatbot")
# 定义终点:chatbot执行完结束本次流程
builder.add_edge("chatbot", END)
# 合起来就是一个最简单的单节点流程:START-chatbot-END
_redis_checkpointer_cm = None
def _close_redis_checkpointer():
global _redis_checkpointer_cm
if _redis_checkpointer_cm is not None:
_redis_checkpointer_cm.__exit__(None, None, None)
_redis_checkpointer_cm = None
def build_checkpointer():
# 优先使用 Redis 做持久化;如果本地未安装 Redis checkpointer,则回退到内存。
global _redis_checkpointer_cm
redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
try:
from langgraph.checkpoint.redis import RedisSaver # type: ignore[reportMissingImports]
checkpointer_or_cm = RedisSaver.from_conn_string(redis_url)
# 某些版本返回上下文管理器,需要先 enter 拿到真正的 saver。
if hasattr(checkpointer_or_cm, "__enter__") and hasattr(checkpointer_or_cm, "__exit__"):
_redis_checkpointer_cm = checkpointer_or_cm
checkpointer = _redis_checkpointer_cm.__enter__()
atexit.register(_close_redis_checkpointer)
else:
checkpointer = checkpointer_or_cm
# 某些实现需要先初始化索引/结构,存在 setup 方法时执行一次。
if hasattr(checkpointer, "setup"):
checkpointer.setup()
print(f"已启用 Redis 持久化: {redis_url}")
return checkpointer
except Exception as exc:
print(f"Redis checkpointer 不可用,回退到内存模式: {exc}")
return MemorySaver()
# 这是记住历史的核心:checkpointer 负责按 thread_id 保存/恢复状态。
graph = builder.compile(checkpointer=build_checkpointer())
def build_config(user_id: str, thread_id: str) -> dict:
# 用 user_id + thread_id 组合成唯一会话键,避免不同用户的同名线程串历史
session_key = f"{user_id}:{thread_id}"
return {"configurable": {"thread_id": session_key}}
# 同一用户同一线程:会复用历史
config = build_config(user_id="u_001", thread_id="t_123")
response_1 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫小明。")]},
config=config,
)
print("第一轮:", response_1["messages"][-1].content)
response_2 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第二轮:", response_2["messages"][-1].content)
response_3 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="今天天气是晴天的,38度。")]},
config=config,
)
print("第三轮:", response_3["messages"][-1].content)
response_4 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我喜欢打篮球。")]},
config=config,
)
print("第四轮:", response_4["messages"][-1].content)
response_5 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我喜欢啥?")]},
config=config,
)
print("第五轮:", response_5["messages"][-1].content)
response_6 = graph.invoke(
{"ability": "得力", "messages": [HumanMessage(content="我叫啥?")]},
config=config,
)
print("第六轮:", response_6["messages"][-1].content)
# 测试持久化消息是否真正保存了最近3轮
state_snapshot = graph.get_state(config)
persisted_messages = state_snapshot.values.get("messages", [])
print(f"持久化消息总数: {len(persisted_messages)}(预期 <= {MAX_MESSAGES})")
for idx, msg in enumerate(persisted_messages, start=1):
msg_type = getattr(msg, "type", msg.__class__.__name__)
print(f"{idx:02d}. {msg_type}: {msg.content}")
保留了最近3轮的历史对话,第六轮询问叫啥时,大模型已经不知道了。
如何验证redis里的最新状态确实只剩3轮
新建一个脚本,只读取同一个 thread_id的状态,不在invoke, redis_view.py:
python
from chat_history_message import graph, build_config, MAX_MESSAGES
config = build_config(user_id="u_001", thread_id="t_123")
state = graph.get_state(config)
msgs = state.values.get("messages", [])
print("从Redis恢复的消息数:", len(msgs), "预期<=", MAX_MESSAGES)
for i, m in enumerate(msgs, 1):
print(i, getattr(m, "type", m.__class__.__name__), m.content)
结果是拿到了最新的3轮对话,六条消息
text
已启用 Redis 持久化: redis://localhost:6379/0
从Redis恢复的消息数: 6 预期<= 6
1 human 我喜欢打篮球。
2 ai 好运动,注意补水。
3 human 我喜欢啥?
4 ai 你喜欢打篮球。
5 human 我叫啥?
6 ai 抱歉,您尚未告知您的姓名。
说明 Redis 中"最新可恢复状态"确实已经被裁剪成最近3轮。
redis为啥不显示执行DEL
因为checkpoint的模型是:
- 每轮写入一个新的checkpoint(包含应用了删除操作后的新状态)
- 读取时取 "最新checkpoint"
所以旧的checkpoint可能还在(用于追溯历史版本),但当前会话恢复看到的状态已经是裁剪后的。