2026.08.22 13:12
学习路线
rust
Python -> Python数据分析 -> LangChain -> LangGraph -> 机器学习 -> 神经网络 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding -> Hermes
复习巩固总结
1. 内存持久化
1.1 InMemorySaver实现
python
from langgraph.checkpoint.memory import InMemorySaver
# 1. 声明状态...
# 2. 声明节点
def llm_node(state: OverallState) -> OverallState:
messages = state["messages"]
res = model.invoke(messages)
return {
"messages": [res]
}
def output_node(state: OverallState) -> OverallState:
return {
"output": state["messages"][-1].content
}
# 3. 构建图
builder = StateGraph(state_schema=OverallState)
builder.add_node("llm_node", llm_node)
builder.add_node("output_node", output_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", "output_node")
builder.add_edge("output_node", END)
# 4. 配置检查点存储器
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
# 5. 使用的时候必须填写线程ID
config = {
"configurable": {
"thread_id": "chapter03-01"
}
}
# 6. 执行图
graph.invoke({"messages": [HumanMessage(content="你好,我是老王")]}, config=config)
1.2 PostgresSaver实现
bash
docker pull postgres:16 # 拉取PostgreSQL镜像
docker images | findstr postgres # 查看本地已有的镜像
docker run -d --name langgraph-postgres -e POSTGRES_DB=langgraph_db -e POSTGRES_USER=langgraph_user -e POSTGRES_PASSWORD=123456 -p 5432:5432 postgres:16 # 运行PostgreSQL容器
docker ps # 看正在运行的容器
docker start langgraph-postgres # 如果容器没有运行,可以通过以下命令启动
python
# 1. 声明状态...
# 2. 声明节点...
# 3. 构建图...
# 4. 配置检查点存储器
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 5. 第一次使用PostgresSaver作为检查点 需要调用方法 setup()
checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
# 6. 指定线程ID
config = {
"configurable": {
"thread_id": "chapter03-02"
}
}
# 7. 调用图
res = graph.invoke({"messages": [HumanMessage("你好,我是谁")]}, config=config)
print(res)
2. 查看检查点记录
2.1 所有检查点
python
history_checkpoints = list(graph.get_state_history(config=config))
print(history_checkpoints)
2.2 查询单独一个检查点
python
# 示例一
target_config = {
"configurable": {
"thread_id": "123",
"checkpoint_id": "某个历史 checkpoint_id"
}
}
latest_history_checkpoint = graph.get_state(config=target_config)
print(latest_history_checkpoint)
# 示例二
history_checkpoints = list(graph.get_state_history(config=config))
checkpointe_id = history_checkpoints[-2].config["configurable"]["checkpoint_id"]
target_config = {
"configurable": {
"thread_id": config["configurable"]["thread_id"],
"checkpoint_id": checkpointe_id
}
}
specific_history_checkpoint = graph.get_state(config=target_config)
print(specific_history_checkpoint)
2.3 结果内容
python
[
StateSnapshot(
# 当前检查点的状态值
values={
"topic": "猫咪",
"poem": """... """,
"joke": """...""",
"final_output": """..."""
},
# 从该检查点继续执行时,下一步将要执行的节点,即归属于下一个超步的节点
next=("node_poem", "node_joke"),
# 当前检查点配置
config={
# 用于记录检查点的可配置信息,可能包含用户自定义字段,它一定包含以下三个字段
# 这三个字段可以唯一标识一条检查点记录,在大多数检查点存储器实现中,写入检查点后端时都会将它们作为检查点的唯一键
"configurable": {
"thread_id": "123", # 会话唯一标识
"checkpoint_ns": "", # 检查点命名空间。根图的命名空间通常为空字符串;子图会使用非空命名空间
"checkpoint_id": "1f163e6a-4cc7-61bd-8002-bcd7f63c3238" # 检查点唯一标识
}
},
# 检查点元数据,本节只需要关注 step 字段,后者是当前检查点对应的超步编号
metadata={
"source": "loop",
"step": 2,
"parents": {}
},
# 检查点创建时间
created_at="2026-06-09T09:36:08.368575+00:00",
# 父检查点、即上一个检查点的配置
parent_config={
"configurable": {
"thread_id": "123",
"checkpoint_ns": "",
"checkpoint_id": "1f163e6a-4cc3-6d96-8001-8314b5dbbfe3"
}
},
# 当前检查点关联的待执行任务信息,元素类型通常是 PregelTask
# tasks 通常和 next 对应,表示从当前检查点继续执行时,下一步将要运行的任务。
# 需要注意的是,tasks 中还可能包含这些任务已经成功(result)或失败(error)的任务记录
tasks=(
PregelTask(
id="80d8974c-3450-6faa-8b06-dcf8787d74fc",
name="node_poem",
path=("__pregel_pull", "node_poem"),
error=None,
interrupts=(),
state=None,
# 会记录下个节点的结果,在下个节点完成后写入,如果有值说明下个节点已完成,
result={
"poem": """..."""
}
),
),
# 当前图的中断信息
interrupts=()
)
]
3. 失败恢复运行
要在失败后基于历史检查点恢复运行,需要满足以下条件:
- 启用检查点存储器
- 如果是
InMemorySaver,不要重建检查点存储器对象,否则历史检查点丢失,无法恢复 - 如果希望跨进程、服务重启后仍可恢复,应使用基于
SQLite、Postgres等持久化检查点后端的存储器
- 如果是
- 再次运行时用
None作为计算图的状态输入 - 传递的配置信息应包含
thread_id而不能包含checkpoint_id
LangGraph学习
65. 错误恢复运行
- 启用持久化检查点(如
PostgresSaver) - 输入为
None(不是新的{"topic": "猫"}) config只含thread_id,不含checkpoint_id
python
# 1. 声明状态...
# 2. 定义节点
# ...
def node_joke(state: OverallState) -> OverallState:
logger.info("node_joke正在执行")
topic = state['topic']
# time.sleep(5)
# raise Exception("人为抛异常")
joke = model.invoke([HumanMessage(f"写一首关于{topic}主题的笑话")]).content
return {
"joke": joke,
}
# ...
# 3. 构建图...
# 4. 添加检查点后端
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 5. 第一次使用PostgresSaver作为检查点 需要调用方法 setup()
# checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
from IPython.display import display
display(graph)
config = {
"configurable": {
"thread_id": "chapter03-05",
}
}
# res = graph.invoke({"topic": "猫"}, config=config)
res = graph.invoke(None, config=config)
print(res)
演示:修复失败节点代码后,用 invoke(None, config) 从同一 thread_id 继续执行。
拓展:常见错误InvalidUpdateError(同一 key 被并行写入)
sql
At key 'poem': Can receive only one value per step.
原因 :同一超步内两个并行节点都写入了 poem 。本教程若 node_joke 误写 "poem": joke 而非 "joke": joke,或与 node_poem 冲突;若曾用错误代码恢复运行,pending_writes 会残留多条 poem,即使后续修正代码也会报错。
处理:
python
tup = checkpointer.get_tuple(config)
poem_writes = [w for w in (tup.pending_writes or []) if w[1] == "poem"]
if len(poem_writes) > 1:
checkpointer.delete_thread(config["configurable"]["thread_id"])
66. 检查点重新运行
Time Travel** 直译为 时间旅行 ,在当前场景下太过生硬,从技术实现和使用场景来看,译为 检查点回溯 更为合理。
检查点回溯有两种形式,根据是否更改历史状态区分:
Replay:检查点重放,回到某个历史检查点,沿着原先的执行路径重新执行后续节点。Fork:检查点分叉,回到某个历史检查点,修改状态,从该位置创建一条新的执行分支。
二者的共同点是:
检查点之前的节点不会重新执行,检查点之后的节点会重新执行。
二者的区别是:
Replay不修改历史状态;Fork会基于历史检查点应用新的状态更新,并创建新的检查点分支。
python
# 1. 声明状态...
# 2. 定义节点...
# 3. 构建图...
# 4. 添加检查点后端
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 5. 第一次使用PostgresSaver作为检查点 需要调用方法 setup()
# checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
from IPython.display import display
display(graph)
config = {
"configurable": {
# "thread_id": "chapter03-05",
"thread_id": "chapter03-08",
}
}
# res = graph.invoke(None, config=config)
res = graph.invoke({"topic": "猫"}, config=config)
print(res)
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 5. 第一次使用PostgresSaver作为检查点 需要调用方法 setup()
# checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
from IPython.display import display
display(graph)
config = {
"configurable": {
"thread_id": "chapter03-08",
}
}
res = graph.invoke({"topic": "猫"}, config=config)
print(res)
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 5. 第一次使用PostgresSaver作为检查点 需要调用方法 setup()
# checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
from IPython.display import display
display(graph)
config = {
"configurable": {
"thread_id": "chapter03-08",
}
}
# 获取到检查点历史
history_checkpoints = list(graph.get_state_history(config=config))
new_checkpoint = None
# next = ('node_poem', 'node_joke')
for checkpoint in history_checkpoints:
if checkpoint['next'] == ('node_poem', 'node_joke'):
new_checkpoint = checkpoint
break
# 如果想要实现replay的效果 状态填写为None config填写为之前某一个检查点的config
# res = graph.invoke({"topic": "猫"}, config=config)
res = graph.invoke(None, config=new_checkpoint.config)
print(res)
拓展:上述InvalidUpdateError是否可以使用检查点回放?
不能直接用来"修复"那个已损坏的检查点,但可以从更早的干净检查点重放,作为绕行方案。
67. fork的案例代码
router根据mode决定后续分支。- 如果
mode == "poem",进入node_poem。 - 如果
mode == "joke",进入node_joke。 - 如果无法识别,则进入
node_default。
python
from typing import Annotated, Literal, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage
from langchain_deepseek import ChatDeepSeek
from langchain_community.chat_models import ChatZhipuAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from loguru import logger
from rich import print as rprint
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
model = ChatZhipuAI(
model="glm-5.2"
).bind(
thinking={
"type": "disabled"
}
)
# 1. 声明状态
# 1.1 全局状态
class OverallState(TypedDict):
username: str
user_input: str
output: str
# 1.2 私有状态
class StructedOutputState(TypedDict):
topic: Annotated[str, "主题"]
mode: Annotated[Literal["poem", "joke"], "模式"]
model_with_structured = model.with_structured_output(schema=StructedOutputState)
# 2. 声明节点
def router_node(state: OverallState) -> StructedOutputState:
logger.info("路由节点已经执行")
user_input = state["user_input"]
res = model_with_structured.invoke([HumanMessage(user_input)])
logger.info("路由结果为: {}", res)
return res
def router(state: StructedOutputState) -> Literal["node_poem", "node_joke", "node_default"]:
logger.info("路由函数已经执行")
if state["mode"] == "poem":
logger.info("路由至 node_poem 节点")
return "node_poem"
elif state["mode"] == "joke":
logger.info("路由至 node_joke 节点")
return "node_joke"
logger.info("路由到兜底的节点")
return "node_default"
# 2.1 写诗节点
def node_poem(state: StructedOutputState) -> StructedOutputState:
logger.info(f"node_poem 已经执行")
topic = state["topic"]
poem = model.invoke([HumanMessage(f"写一首关于{topic}主题的七言绝句,不要赏析,只写诗句的部分")]).content
return {
"output": poem
}
# 2.2 写笑话节点
# def node_poem(state: StructedOutputState) -> StructedOutputState:
def node_joke(state: StructedOutputState) -> StructedOutputState:
# logger.info(f"node_poem 已经执行")
logger.info(f"node_joke 已经执行")
topic = state["topic"]
# poem = model.invoke([HumanMessage(f"写一首关于{topic}主题的七言绝句,不要赏析,只写诗句的部分")]).content
joke = model.invoke([HumanMessage(f"写一个关于{topic}主题的笑话,字数在100以内")]).content
return {
# "output": poem
"output": joke
}
# 2.3 兜底节点
def node_default(state: StructedOutputState) -> StructedOutputState:
logger.info(f"node_default 已经执行")
return {
"output": "无法处理的任务类型"
}
# 3. 构建图
builder = StateGraph(state_schema=OverallState)
builder.add_node("router_node", router_node)
builder.add_node("node_poem", node_poem)
builder.add_node("node_joke", node_joke)
builder.add_node("node_default", node_default)
builder.add_edge(START, "router_node")
builder.add_conditional_edges(
"router_node",
router,
path_map=['node_poem', 'node_joke', 'node_default']
)
builder.add_edge("node_poem", END)
builder.add_edge("node_joke", END)
builder.add_edge("node_default", END)
# 4. 添加内存的检查点
checkpointer = InMemorySaver()
config = {
"configurable": {
"thread_id": "123"
}
}
graph = builder.compile(checkpointer=checkpointer)
from IPython.display import display
display(graph)
res = graph.invoke({
"username": "小王",
"user_input": "写一首关于荷花的诗"
}, config=config)
# print(res)
rprint(res)
68. fork实现演示
python
history_checkpoints = list(graph.get_state_history(config=config))
# print(history_checkpoints)
rprint(history_checkpoints)
# 1. 获取某一个节点的位置
before_router_checkpoint = next(h for h in history_checkpoints if h.next == ("router_node",))
# print(before_router_checkpoint)
rprint(before_router_checkpoint)
从这个检查点分叉,本文采用两种方案(还可以有别的方案):
68.1 修改输入,让 router_node 重新执行
python
change_input = graph.update_state(
config=before_router_checkpoint.config,
values={"user_input": "写一首关于荷花的笑话"},
as_node=START
)
# print(change_input)
rprint(change_input)
graph.get_state(change_input)
68.2 直接伪造 router_node 的输出,从而跳过
python
skip_router_config = graph.update_state(
config=before_router_checkpoint.config,
# values={"topic": "狸花猫", "mode": "笑话"},
values={"topic": "狸花猫", "mode": "joke"},
as_node="router_node"
)
# print(skip_router_config)
rprint(skip_router_config)
graph.get_state(skip_router_config)
res = graph.invoke(None, config=skip_router_config)
# print(res)
rprint(res)
-
update_state()创建了一个新的检查点,返回的skip_router_config指向这个新检查点。 -
日志中出现了:
text路由函数已执行 路由至兜底节点这说明
router_node的节点函数没有被重新执行,但与router_node条件边相关的路由函数被执行了。
这种方式常用于测试场景。例如:
- 跳过不稳定的大模型路由节点。
- 手动指定路由结果,测试不同分支。
不过在正式业务流程中,应谨慎使用这种方式。
69. 长期记忆数据库存储
-
短期记忆 :通过运行时状态
State访问,并由检查点存储器Checkpointer保存,它按照thread_id组织,可以实现线程内的记忆共享。 -
长期记忆 :通过长期记忆存储器
Store访问和存储。 -
运行时上下文 :通过上下文对象
Context访问,只对本次调用生效,不会被持久化。它更适合传递本次运行所需的外部依赖或调用参数
Agent 底层就是一个简易的 ReAct 架构的 LangGraph 状态图,所以 Agent 的记忆机制本质上就是 LangGraph 状态图的记忆机制。
生产环境建议用基于持久化数据库的记忆存储器,如 PostgresSaver 和 PostgresStore。
python
from typing import Final, Tuple
from langgraph.store.postgres import PostgresStore
DB_URL = "postgresql://langgraph_user:123456@localhost:5432/langgraph_db?sslmode=disable"
with PostgresStore.from_conn_string(DB_URL) as store:
# 1. 创建长期记忆存储的表格
store.setup()
# 2. 构建永久记忆数据
# 2.1 构建命名空间
USERS_NS: Final[Tuple[str]] = ("users",)
PREFERENCES_KEY: Final[str] = "preferences"
namespace1 = (*USERS_NS, "Alice")
namespace2 = (*USERS_NS, "Bob")
namespace3 = (*USERS_NS, "Black")
value1 = {
"course": "计算机组成原理",
"sports": "跑步",
"food": "紫光园奶皮子酸奶"
}
value2 = {
"course": "数字电路与模拟电路",
"sports": "跑步",
"food": "奶皮子糖葫芦"
}
value3 = {
"course": "数字电路与模拟电路",
"sports": "羽毛球",
"food": "紫光园奶皮子酸奶"
}
# 3. 写入永久记忆数据
store.put(namespace1, PREFERENCES_KEY, value1)
store.put(namespace2, PREFERENCES_KEY, value2)
store.put(namespace3, PREFERENCES_KEY, value3)
for item in store.search(USERS_NS):
print(item)
70. 长期记忆数据的使用
store.search(USERS_NS) 会查询所有以 ("users",) 开头的记忆数据
namespace 使用元组是硬性约束,天然表达层级结构
如果希望支持语义检索,需要在 Store 中配置索引和 embedding 函数。未配置索引时,search() 只能按照命名空间和过滤条件检索
python
from typing import Annotated, Literal, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from langchain_community.chat_models import ChatZhipuAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from loguru import logger
from rich import print as rprint
from langgraph.runtime import Runtime
from langgraph.graph.message import MessagesState
from langgraph.checkpoint.postgres import PostgresSaver
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
model = ChatZhipuAI(
model="glm-5.2"
).bind(
thinking={
"type": "disabled"
}
)
# 1. 声明全局状态
class OverallState(MessagesState):
username: str
user_input: str
output: str
preferences: dict[str, str]
# 2. 声明节点
# 2.1 路由函数 如果状态中没有用户偏好 则从长期记忆中查询,否则直接执行的大模型节点
def router(state: OverallState) -> Literal["check_preference_node", "llm_node"]:
if not state.get("preferences"):
logger.info("需要从长期记忆中读取用户偏好")
return "check_preference_node"
logger.info("用户偏好已经存在,不需要查询")
return "llm_node"
# 2.2 检查长期记忆节点
def check_preference_node(state: OverallState, runtime: Runtime) -> OverallState:
# 1. 拼接命名空间
username = state["username"]
namespace = (*USERS_NS, username)
key = PREFERENCES_KEY
# 2. 获取长期记忆数据
run_store = runtime.store
run_item = run_store.get(namespace, key)
if not run_item:
logger.warning("长期记忆中没有{}的偏好数据", username)
return {}
logger.info("长期记忆中保存的{}的偏好数据是{}", username, run_item.value)
# 3. 更新状态
return {
"preferences": run_item.value
}
def llm_node(state: OverallState) -> OverallState:
# 检查是否存在长期记忆
preferences = state.get("preferences", {})
user_input = state["user_input"]
human_prompt = (f"这是用户的偏好: {preferences}\n,这是用户的需求: {user_input}\n")
system_prompt = "请根据用户的偏好解决用户的需求"
messages: list[SystemMessage | HumanMessage | AIMessage | ToolMessage] = [SystemMessage(content=system_prompt)] if not state.get("messages", []) else state["messages"]
model_response = model.invoke(messages + [HumanMessage(content=human_prompt)])
output = model_response.content
return {
"messages": messages + [HumanMessage(content=human_prompt), model_response],
"output": output
}
# 3. 构建图
builder = StateGraph(state_schema=OverallState)
builder.add_node("check_preference_node", check_preference_node)
builder.add_node("llm_node", llm_node)
builder.add_conditional_edges(
START,
router,
path_map=["check_preference_node", "llm_node"]
)
builder.add_edge("check_preference_node", "llm_node")
builder.add_edge("llm_node", END)
# 4. 构建长期记忆和短期记忆
with PostgresStore.from_conn_string(DB_URL) as store, PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 幂等操作 多次执行不会重新创建表格 不会删除数据库中的数据
checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer, store=store)
from IPython.display import display
display(graph)
config = {
"configurable": {
"thread_id": "777",
}
}
res = graph.invoke(
{
"username": "Alice",
"user_input": "我有点无聊,和我聊聊天吧"
},
config=config
)
print("=" * 50)
# print(res)
rprint(res)
res1 = graph.invoke(
{
"username": "Alice",
"user_input": "推荐一下酸奶"
},
config=config
)
print("=" * 50)
# print(res1)
rprint(res1)
71. 环境上下文记忆
仅对本次调用生效,不会被持久化,也不会在同一会话的下一次调用中自动恢复。
场景 :不同的用户调用同一个机器人,机器人根据运行时上下文中的用户名 和会员等级,生成不同风格的回复。
python
from dataclasses import dataclass
from typing import Annotated, Literal, TypedDict
from dotenv import load_dotenv
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage, ToolMessage
from langchain_deepseek import ChatDeepSeek
from langchain_community.chat_models import ChatZhipuAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START, END
from loguru import logger
from rich import print as rprint
from langgraph.runtime import Runtime
from langgraph.graph.message import MessagesState
from langgraph.checkpoint.postgres import PostgresSaver
load_dotenv(override=True)
model = ChatDeepSeek(
model="deepseek-v4-flash",
extra_body={
"thinking": {
"type": "disabled"
}
}
)
model = ChatZhipuAI(
model="glm-5.2"
).bind(
thinking={
"type": "disabled"
}
)
# 1. 定义运行时的环境上下文
@dataclass
class UserContext:
username: str
membership_level: str
# 2. 定义状态
class OverallState(MessagesState):
user_input: str
output: str
# 3. 定义节点
def llm_node(state: OverallState, runtime: Runtime[UserContext]) -> OverallState:
# 1. 获取环境上下文 判断当前的用户等级
runtime_context = runtime.context
# level = runtime_context.membership_level
if runtime_context:
level = runtime_context.membership_level
username = runtime_context.username
logger.info(f"当前用户: {username} 等级: {level}")
if level == "VIP":
system_prompt = f"你是高级客户助理,当前VIP用户是{username},请使用尊称"您",预期热情周到,回复末尾加上"VIP🥇服务""
else:
system_prompt = f"你是普通客户助理,当前用户是{username},请友好简介回复问题"
else:
system_prompt = f"你是普通客户助理,请友好简介回复问题"
user_input = state['user_input']
messages = state.get('messages', [])
response = model.invoke(
[SystemMessage(content=system_prompt)] + messages + [HumanMessage(content=user_input)]
).content
return {
"messages": messages,
"output": response
}
# 4. 构建图
builder = StateGraph(state_schema=OverallState, context_schema=UserContext)
builder.add_node("llm_node", llm_node)
builder.add_edge(START, "llm_node")
builder.add_edge("llm_node", END)
graph = builder.compile()
# ===============第一次调用:传入VIP上下文用户==================
res = graph.invoke(
{"user_input": "你好,帮我查一下最近有什么优惠活动"},
context=UserContext(
username="Alice",
membership_level="VIP"
)
)
# print(res)
rprint(res)
# ===============第二次调用:传入普通用户上下文==================
res1 = graph.invoke(
{"user_input": "你好,帮我查一下最近有什么优惠活动"},
context=UserContext(
username="Alice",
membership_level="普通用户"
)
)
# print(res1)
rprint(res1)
| 适合放入运行时上下文 | 不适合(应放入图状态) |
|---|---|
| 当前登录用户信息 | 需要在多轮对话间共享的数据 |
| 请求来源(Web / API / 小程序) | 需要在检查点中恢复的执行进度 |
| 调用方标识、Trace ID | 需要跨调用持久化的业务数据 |
| 本次调用的功能开关 | 节点间需要传递的计算结果 |
简单来说:
跨调用共享、需要持久化的数据 → 放入 State;仅当次调用有效的信息 → 放入 Runtime Context。
拓展:上下文记忆、输入状态与私有状态的区别
InputState:用户本轮提交的业务输入 (如user_input),通过invoke({"字段": 值})传入,会写入 State,可被 checkpointer 持久化。PrivateState:图内部节点之间传递的临时字段,不对外暴露,但仍属于 State,会进检查点。Runtime Context:本次调用的环境信息 (如用户名、会员等级、请求来源),通过invoke(..., context=...)传入,用runtime.context读取,不进检查点、下次调用不自动恢复。
VIP 示例中:user_input 走 State;username、membership_level 走 context------它们是调用身份,不是用户 payload,且每次请求可能不同,不应被持久化到同一线程的历史状态里。
context_schema 可用 @dataclass、TypedDict、Pydantic BaseModel 定义,不限于 dataclass。
怎么选: 要持久化的业务数据 → State / Store;用户每轮输入 → InputState;仅当次有效的环境参数 → Context;节点间内部临时值 → PrivateState。
72. 节点总结
LangGraph 的节点通常是一个可调用对象,最常见的是同步或异步 Python 函数:
python
def sync_node(state: State) -> State:
...
async def async_node(state: State) -> State:
...
节点也可以是 Runnable 实例。
从源码实现角度看,节点最终会被转换或包装为可运行对象,并作为 PregelNode.bound 保存。
普通图节点通常使用以下四个参数:
-
state:输入节点的图状态。state是位置传参,因此参数名称不重要,但通常约定命名为state -
config:状态图的运行时配置,它是一个RunnableConfig实例可以通过
config访问当前线程的thread_id、超步序号、递归限制等配置信息。如下pythonconfig["configurable"]["thread_id"] config["recursion_limit"] config["metadata"] -
runtime:状态图的运行时对象,可以通过它访问运行时上下文、长期记忆存储器等信息。 -
writer:流式写入器,通常用于自定义流式输出。详见流处理章节。
典型写法如下:
python
def node(state: State, config: RunnableConfig, runtime: Runtime[Context]) -> State:
...
- 编译图时传入
checkpointer,并在调用时传入config,可以在节点中通过config间接访问当前调用的配置信息,如thread_id。 - 编译图时传入
store后,可以在节点中通过runtime.store访问长期记忆存储器。 - 初始化状态图时传入
context_schema,并在调用时传入context后,可以在节点中通过runtime.context访问运行时上下文。
73. 节点的触发和执行总结
73.1 节点的触发
从用户层面看,节点由图中的控制流关系(普通边、条件边和动态控制指令)触发:
-
普通边决定固定的后继节点。
-
条件边根据路由函数的返回值决定后继节点。
- 可以返回普通节点名称
- 也可以返回
Send实例,从而动态派发多个并行任务,常用于Map-Reduce场景。
-
Command.goto可以在节点返回时动态指定后继节点。
从源码实现角度看,LangGraph 会将图状态和节点之间的触发关系组织为通道。
其中,状态字段通常对应状态通道,节点之间的控制流关系则通过触发通道或屏障通道表示。对于普通的单节点触发关系,目标节点通常会订阅如下形式的内部通道:
text
branch:to:<node_name>
例如,对于普通边:
python
builder.add_edge("node_a", "node_b")
运行时 node_a 的写入器会向以下通道写入数据:
text
branch:to:node_b
而 node_b 会将该通道注册为自己的触发通道。
对于多个前驱节点共同汇聚到同一个节点的情况,底层还可能使用类似下面的屏障通道:
text
join:<node_a>+<node_b>:<node_c>
用于等待指定的前驱节点全部完成。
73.2 节点的执行
从用户层面看,节点执行过程可以理解为:
-
LangGraph从当前图状态中读取节点需要的字段,并构造节点输入。 -
调用节点函数,执行其业务逻辑。
-
节点函数返回状态更新、
Command或其他受支持的结果。 -
LangGraph将节点返回值转换为状态通道和控制通道的写入条目。 -
如果当前节点挂载了条件边,则执行条件边路由逻辑,根据包含当前节点更新的状态决定后继节点。
-
当前超步的所有任务执行完毕后,
LangGraph汇总这些写入,并根据各状态字段的Reducer规则更新图状态。
节点通常只需要返回发生变化的字段,而不需要返回完整状态。例如:
python
def node_a(state: State) -> State:
return {"count": state["count"] + 1}
其中:
python
{"count": state["count"] + 1}
表示对状态的更新,而不是一份必须包含所有字段的完整状态。
从源码实现角度看,节点业务逻辑、状态写入逻辑和控制流写入逻辑会被组合成一个可顺序执行的可运行对象。
编译节点时,LangGraph 会创建一个 PregelNode。
python
self.nodes[key] = PregelNode(
# ...
triggers=[branch_channel], # 保存能够触发该节点的通道 - 能够触发该节点的通道
writers=[ChannelWrite(write_entries)], # 保存节点执行完成后需要调用的写入器 - 状态和控制流写入器
bound=node.runnable, # type: ignore[arg-type] 保存节点的业务逻辑 - 节点业务逻辑
)
因此,从整体上看,节点执行并不是简单地"调用一个函数并返回结果",而是:
执行业务逻辑,生成状态和控制流写入,并通过目标节点触发通道或动态任务写入,为下一超步生成待调度任务,持续推动计算图运行。
74. LangChain 与 LangGraph 短期记忆 / 长期记忆对比
74.1 短期记忆
LangChain
python
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
checkpointer = InMemorySaver()
agent = create_agent(model=model, tools=[], checkpointer=checkpointer)
config = {"configurable": {"thread_id": "1"}}
agent.invoke({"messages": [...]}, config=config)
# 查看状态
agent.get_state(config)
LangGraph
python
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "chapter03-01"}}
graph.invoke({"messages": [...]}, config=config)
# 查看状态 / 历史
graph.get_state(config)
graph.get_state_history(config)
74.2 长期记忆
LangChain
python
from langgraph.store.memory import InMemoryStore
from langchain.agents import create_agent
from langchain.tools import tool, ToolRuntime
store = InMemoryStore()
agent = create_agent(model=model, tools=[...], store=store)
# 基础 API
store.put(("users", "user_123", "preferences"), "profile", {"language": "zh-CN"})
store.get(("users", "user_123", "preferences"), "profile")
# 工具 / 中间件中访问
@tool
def save_user_info(name: str, runtime: ToolRuntime) -> str:
runtime.store.put(("users",), runtime.state["user_id"], {"name": name})
return "已保存"
LangGraph
python
from langgraph.store.postgres import PostgresStore
from langgraph.runtime import Runtime
with PostgresStore.from_conn_string(DB_URL) as store, \
PostgresSaver.from_conn_string(DB_URL) as checkpointer:
graph = builder.compile(checkpointer=checkpointer, store=store)
config = {"configurable": {"thread_id": "777"}}
graph.invoke({"username": "Alice", "user_input": "..."}, config=config)
# 节点中访问
def check_preference_node(state, runtime: Runtime):
namespace = ("users", state["username"])
item = runtime.store.get(namespace, "preferences")
if item:
return {"preferences": item.value}
return {}
74.3 总对比
|------|----------|----------| | 作用范围 | 单个 thread_id 内 | 跨 thread_id 共享 | | 底层对象 | State | Store | | 持久化组件 | Checkpointer | Store(put / get / search) | | 作用域标识 | thread_id | namespace → key → value | | 典型内容 | 对话历史、执行进度 | 用户偏好、画像、经验规则 | | 开发后端 | InMemorySaver | InMemoryStore | | 生产后端 | PostgresSaver | PostgresStore |
| 维度 | LangChain(Agent 层) | LangGraph(编排层) |
|---|---|---|
| 定位 | 高层 Agent,开箱即用 | 底层图编排 + Agent Runtime |
| 启用短期记忆 | create_agent(checkpointer=...) |
graph.compile(checkpointer=...) |
| 启用长期记忆 | create_agent(store=...) |
graph.compile(store=...) |
| 访问短期记忆 | state / agent.get_state() |
state / graph.get_state() |
| 访问长期记忆 | runtime.store(工具/中间件) |
runtime.store(节点函数) |
| 流程控制 | Agent 内置 ReAct 循环 | 自定义节点、边、条件路由 |
| 检查点高级用法 | 较少 | Replay、Fork、失败恢复 |
| 适用场景 | 快速搭建对话 Agent | 复杂工作流、精细控制记忆读写 |
LangChain v1.x Agent 底层即 LangGraph,短期记忆 =
State + Checkpointer + thread_id,长期记忆 =Store + namespace。
75. 两种中断机制
LangGraph 提供了两种中断机制:
-
动态中断 :在图的任意节点中调用
interrupt()函数实现它可以放在代码的任意位置,并且可以根据应用逻辑设置条件触发,所以是动态的。
动态中断提供了人机交互接口,使得调用者可以人为干预计算图的运行,是业务逻辑的一部分。
-
静态中断 :在编译或调用状态图时通过
interrupt_before和interrupt_after参数设置断点它是在运行前确定的,不能根据业务逻辑条件触发,所以是静态的。
静态中断主要用于调试,不是业务逻辑的一部分。