前言
前面 逐个学习了 LangGraph 的一些基础能力:Reducers、Send、Interrupt、Command。今天继续看 LangGraph 的其他基础能力。
checkpoint
Checkpoints 是 LangGraph 在图执行过程中,在每个超级步骤(Super-step)结束时自动创建的状态快照。每个Checkpoint都是一个 StateSnapshot 对象。 values: 当前所有状态通道的具体数据(如图中的foo、bar字段值)
-
next: 下一个要执行的节点名称(为空表示执行结束) -
config: 关联的配置,包含唯一标识符thread_id和checkpoint_id -
metadata: 元数据,如执行来源、修改记录、步骤编号 -
created_at/ parent_config: 创建时间和父检查点配置(用于构建执行链路)
Checkpointer是负责保存和管理这些快照的工具。LangGraph提供了多种实现:
InMemorySaver: 内存存储,适用于开发和测试SqliteSaver: SQLite 文件存储,适用于小型生产或本地开发PostgresSaver: PostgreSQL 存储,适用于生产环境,支持并发
几个常用的方法:
get_state:获取当前快照get_state_history:读取所有快照:包括图每一步的执行情况
记忆绑定
要想让 graph 带快照功能,必须在 graph 编译的时候带上 checkpointer 参数。
Python
# 内存记忆器:保存每一步执行状态,支持回溯
checkpointer = InMemorySaver()
# 编译并绑定记忆
graph = workflow.compile(checkpointer=checkpointer)
会话标识
会话ID:区分不同对话/执行上下文
Python
config = {"configurable": {"thread_id": "thread-1"}}
快照获取
Python
# 获取快照
snapshot = graph.get_state(config)
# 获取全部历史快照
history = list(graph.get_state_history(config))
图调用
graph 本身并不存储快照,只负责处理流程。快照存储是和 CheckPointer 绑定的。所以在 graph 调用时要传入 config。内部原理是:
-
graph 根据 config 里的
thread_id和checkpoint绑定 -
获取
snapshot时,graph 再根据thread_id去 checkpoint 取数据
ini
# 执行图
result = graph.invoke(initial_state, config)
Context Memory
像之前 LangChain 的 ChatModel 都会接受 Message 对象列表作为输入。这些消息以各种形式出现(HumanMessage或 AIMessage)。在 LangGraph 中的 State 同样可以模拟实现类似 BaseMessage 的功能。
自动合并消息的 State
Python
class ChatState(TypedDict):
# messages 自动合并消息的历史记录
# Annotated:附加信息
# Sequence:有序列表
messages: Annotated[Sequence[BaseMessage], operator.add]
用户输入
Python
def handle_user_input(state: ChatState):
"""
处理用户输入节点(增强健壮性)
"""
try:
user_input = input("\n用户输入(输入'Q'结束): ").strip()
if user_input.lower() == "Q":
logger.info(f"正在结束对话...")
return END
# 保留历史记录并追加新消息
return {"messages": [HumanMessage(content=user_input)]}
except Exception :
return END
AI 回复
Python
def generate_ai_response(state: ChatState):
"""
生成AI响应节点(增加错误处理)
"""
try:
# 使用最近6条消息保持上下文连贯性
recent_history = state["messages"][-6:]
"""做个摘要"""
response = model.invoke(recent_history)
return {"messages": [response]}
except Exception as e:
error_msg = f"系统暂时无法响应,请稍后再试(错误代码:{str(e)[:30]})"
return {"messages": [AIMessage(content=error_msg)]}
节点注册
RunnableLambda: 可不写,add_node内部会自动包装成RunnableLambda
Python
# 节点注册
# RunnableLambda 可不写,add_node 内部会自动包装成 RunnableLambda
builder.add_node("user_input", handle_user_input)
builder.add_node("ai_response", RunnableLambda(generate_ai_response))
对话循环
Python
while True:
try:
# 执行对话流程
result = conversation.invoke(state)
if result is None or "messages" not in result:
break
# 提取最新交互记录
new_messages = result["messages"][len(state["messages"]):]
# 打印AI响应
for msg in new_messages:
if isinstance(msg, AIMessage):
logger.info(f"\n【AI响应】\n{msg.content}\n")
# 更新对话状态
state = result
# 检查退出条件
if any(isinstance(m, HumanMessage) and m.content.lower() == "退出" for m in state["messages"]):
break
except Exception as e:
logger.info(f"\n系统异常:{str(e)}")
break
Subgraphs
Subgraphs 就是子图。就好比我们开发语言的父类/子类关系,LangGraph 允许将复杂的工作流分解为更小、更易管理的子图。然后将这些子图作为 Node 添加到主图中。LangGraph 提供了两种不同模式的子图:
- 共享模式 :父图和子图有共享的状态键
- 共享的状态键(如
foo)会自动在父图和子图间同步,子图可以直接读写这些共享状态。就好比父类的public变量。
- 共享的状态键(如
- 不同状态模式 :父图和子图状态结构完全不同
- 这种模式下需要在父图节点中进行"输入映射→子图调用→输出映射"的手动转换
共享状态模式:
Python
class SharedParentState(TypedDict):
text: str # 父图与子图共享的键
result: str # 父图私有键(可选)
class SharedSubgraphState(TypedDict):
text: str # 与父图共享的键(必须同名)
processed: str # 子图私有键(仅子图可见)
def subgraph_node(state: SharedSubgraphState) -> SharedSubgraphState:
"""子图节点:将共享键"text"转大写,存到私有键"processed"""
return {
"text": state["text"].upper(), # 修改共享键(自动同步到父图)
"processed": f"Processed: {state['text'].upper()}" # 子图私有数据
}
def parent_node(state: SharedParentState) -> SharedParentState:
"""父图节点:直接调用子图(共享键自动传递)"""
return state # 子图会修改"text"键,父图后续可直接用
不同状态模式,和共享状态不同,这里需要在父图节点中进行"输入映射→子图调用→输出映射"的手动转换。
subgraph_input = {"task": state["user_query"]}: 子图输入映射compiled_different_subgraph.invoke(subgraph_input):子图调用{ "answer": subgraph_output["result"], "user_query": state["user_query"] # 保留父图原始输入 }:输出映射
Python
def parent_wrapper_node(state: DifferentParentState) -> DifferentParentState:
"""
父图包装节点:【核心:手动状态映射】
因为父子图状态完全独立,必须手动:
1. 父图状态 → 子图输入
2. 调用子图
3. 子图输出 → 父图状态
"""
# ---------------------- 输入映射 ----------------------
# 父图键:user_query → 子图键:task
subgraph_input = {"task": state["user_query"]}
# ---------------------- 调用独立子图 ----------------------
# 子图与父图状态隔离,不会互相污染
subgraph_output = compiled_different_subgraph.invoke(subgraph_input)
# ---------------------- 输出映射 ----------------------
# 子图键:result → 父图键:answer
return {
"answer": subgraph_output["result"],
"user_query": state["user_query"] # 保留父图原始输入
}
根据主图的输入映射 {"task": state["user_query"]},子图在 state 里操作主图传递的 task 字段。
Python
def different_subgraph_node(state: DifferentSubgraphState) -> DifferentSubgraphState:
# 处理逻辑:给 task 增加子图前缀
return {"result": f"[Subgraph] Processed: {state['task']}"}
visualization
我们可以将编译后的 LangGraph 整体流程绘制成一张流程图。
Python
graph.get_graph().draw_mermaid_png(output_file_path=save_file_path)