文章目录
- 节点
- 边
节点
1.节点的输入输出
1.1 节点输入
在LangGraph中,一般来讲,节点都是Python函数(可以是同步的,也可以是异步的),它们接受以下参数:
- state:图的状态,代表了具体的业务数据;
- config:一个RunnableConfig对象,包含诸如thread_id之类的配置信息,在调用图时,也可以传递用户定义的其他配置;
- runtime:一个Runtime对象,包含运行时context(可自定context,在调用图时传入即可)以及其他信息,如store和stream_writer等;
以上参数,会在运行过程中,自动被LangGraph运行时注入。
节点定义config和runtime入参如下所示,在运行时,这两个参数LangGraph名称会通过关键字传参方式进行传参,因此两个参数位置可以调换。
具体示例如下:
py
# 测试节点输入的参数
from typing import TypedDict
from langchain_core.runnables import RunnableConfig
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.runtime import Runtime
# 定义状态
class UserState(TypedDict):
query: str
token: str
user_id: int
user_name: str
# 定义配置信息
conf = {
"configurable": {
"token": "abcdefg_123456_zzzzzzz"
}
}
# 定义上下文信息
context = {
"user_id": 1,
"user_name": "admin"
}
# 定义节点
def get_user_info(state: UserState, config: RunnableConfig, runtime: Runtime):
# 获取配置信息
token = config["configurable"]["token"]
# 获取上下文信息
# 获取用户id
user_id = runtime.context.get("user_id")
# 获取用户名称
user_name = runtime. context.get("user_name")
return {"token":token,"user_id":user_id,"user_name":user_name}
# 创建状态图实例
state_graph = StateGraph(state_schema=UserState)
# 添加节点
state_graph.add_node(get_user_info)
# 添加边
state_graph.add_edge(START,"get_user_info").add_edge("get_user_info",END)
# 编译
copile_state_graph = state_graph.compile()
# 执行
res = copile_state_graph.invoke({"query":"获取用户信息"},config=conf,context=context)
print(res)
结果
bash
{'query': '获取用户信息', 'token': 'abcdefg_123456_zzzzzzz', 'user_id': 1, 'user_name': 'admin'}
1.2 节点输出
节点的输出为当前节点对状态的增量更新,而不能将接收到的整个状态实例返回出去。
LangGraph底层会将所有节点输出的状态,都作为增量状态,并尝试和当前的全局状态做一次合并操作。
如果将整个状态都输出,对于没有配置任何reducer的状态键,langgraph底层无法完成合并,会抛出异常;对于配置了reducer的状态键,如果节点输出了不属于该节点更新的状态,也会导致数据产生问题。
示例
py
# 测试节点输出返回整个状态的问题
from typing import TypedDict
from langgraph.constants import START, END
from langgraph.graph import StateGraph
# 定义状态
class State(TypedDict):
id: int
name: str
age: int
# 定义节点
def add_name(state: State):
# 修改全局状态中name的值
# state["name"] = "齐飞"
# return state
return {"name":"齐飞"}
# 定义节点
def add_age(state: State):
# 修改全局状态中age的值
# state["age"] = 18
# return state
return {"age": 24}
# 创建状态图实例
state_graph = StateGraph(state_schema= State)
# 添加节点
state_graph.add_node(add_name)
state_graph.add_node(add_age)
# 添加边(测试节点串行)
# state_graph.add_edge(START, "add_name").add_edge("add_name","add_age").add_edge("add_age",END)
# 添加边(测试节点并行,如果每个节点返回的是整个state,会报错)
state_graph.add_edge(START, "add_name").add_edge(START,"add_age").add_edge("add_age",END).add_edge("add_name",END)
# 编译
compile_state_graph = state_graph.compile()
# 执行
res = compile_state_graph.invoke({"id":1})
print(res)
结果
bash
{'id': 1, 'name': '齐飞', 'age': 24}
2.特殊节点
START和END节点都是LangGraph当中的特殊节点。START它代表着将用户输入发送到图中的节点。引用此节点的主要目的是确定应首先调用哪些节点。END节点是一个特殊节点,代表终止节点。当想表示哪些边在完成后没有动作时,会引用这个节点。
START和END节点本质上是一个字符串,如下所示:
py
END = sys.intern("__end__")
"""The last (maybe virtual) node in graph-style Pregel."""
START = sys.intern("__start__")
"""The first (maybe virtual) node in graph-style Pregel."""
START作为一个特殊节点,在图执行完START节点之后,也会创建一个checkpoint。
3.节点缓存
LangGraph支持基于节点输入对节点进行缓存。对于配置了缓存的节点,且缓存结果没有过期,以相同的输入再次调用节点时,可直接从缓存当中读取结果,不需要再进行节点计算。
使用缓存的方法如下:
- 编译图时指定缓存存储后端:langgraph.cache包当中提供了内存级别的缓存后端,Redis缓存后端,和Sqlite缓存后端的具体实现,需要构造相关实例,并在图编译时传入。
- 为节点指定缓存策略。每个缓存策略需配置:
- key_func:用于根据节点的输入生成缓存键,默认情况下是使用pickle对输入进行hash运算的结果。
- ttl:缓存的生存时间(以秒为单位)。如果未指定,缓存将永不过期。

InMemoryCache
py
# 测试节点缓存
import time
from typing import TypedDict
from langgraph.cache.memory import InMemoryCache
from langgraph.cache.redis import RedisCache
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import CachePolicy
# 定义状态
class State(TypedDict):
query: str
final_result: str
# 定义节点
def get_info_node(state: State):
print("get_info_node执行")
# 获取用户的提问
query = state["query"]
return {"final_result": f"根据用户的提问:{query},查询到了结果"}
# 创建状态图
state_graph = StateGraph(state_schema= State)
# 添加节点并设置缓存策略
state_graph.add_node(get_info_node, cache_policy=CachePolicy(ttl=3))
# 添加边
state_graph.add_edge(START, "get_info_node").add_edge("get_info_node",END)
# 编译时传入缓存存储后端和检查点
# 创建缓存存储后端
cache = InMemoryCache()
compile_state_graph = state_graph.compile(cache=cache,checkpointer=InMemorySaver())
# 第一次执行
res = compile_state_graph.invoke({"query":"中国的帝都是那个城市"},config={
"configurable":{
"thread_id": "qf123"
}
})
print(res)
# 睡3秒,让缓存过期
# time.sleep(3)
# 第二次执行
res2 = compile_state_graph.invoke({"query":"中国的帝都是那个城市"},config={
"configurable":{
"thread_id": "sgg"
}
})
print(res2)
响应结果
bash
get_info_node执行
{'query': '中国的帝都是那个城市', 'final_result': '根据用户的提问:中国的帝都是那个城市,查询到了结果'}
{'query': '中国的帝都是那个城市', 'final_result': '根据用户的提问:中国的帝都是那个城市,查询到了结果'}
RedisCache
py
# 测试节点缓存
# 使用redis
import time
from typing import TypedDict
from langgraph.cache.memory import InMemoryCache
from langgraph.cache.redis import RedisCache
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import CachePolicy
import redis
# 定义状态
class State(TypedDict):
query: str
final_result: str
# 定义节点
def get_info_node(state: State):
print("get_info_node执行")
# 获取用户的提问
query = state["query"]
return {"final_result": f"根据用户的提问:{query},查询到了结果"}
# 创建状态图
state_graph = StateGraph(state_schema= State)
# 添加节点并设置缓存策略
# state_graph.add_node(get_info_node, cache_policy=CachePolicy())
state_graph.add_node(get_info_node, cache_policy=CachePolicy(ttl=3))
# 添加边
state_graph.add_edge(START, "get_info_node").add_edge("get_info_node",END)
# 编译时传入缓存存储后端和检查点
# 创建缓存存储后端
# cache = InMemoryCache()
# 创建Redis
redis = redis.Redis(host="127.0.0.1", port=6379, password="123456",
protocol=2 # 因为redis版本较低,强制使用 RESP2
)
print(redis.ping())# 如果返回 True,则连接正常
cache = RedisCache(redis=redis)
compile_state_graph = state_graph.compile(cache=cache,checkpointer=InMemorySaver())
# 第一次执行
res = compile_state_graph.invoke({"query":"中国的帝都是那个城市"},config={
"configurable":{
"thread_id": "qf1"
}
})
print(res)
# 睡3秒,让缓存过期
# time.sleep(3)
# 第二次执行
res2 = compile_state_graph.invoke({"query":"中国的帝都是那个城市"},config={
"configurable":{
"thread_id": "qf2"
}
})
print(res2)
响应结果
bash
get_info_node执行
{'query': '中国的帝都是那个城市', 'final_result': '根据用户的提问:中国的帝都是那个城市,查询到了结果'}
{'query': '中国的帝都是那个城市', 'final_result': '根据用户的提问:中国的帝都是那个城市,查询到了结果'}
在redis中的key-value

4.节点重试
在很多使用场景中,有些节点由于客观原因限制,导致其执行过程是不稳定的。因此,我们可能希望节点拥有自定义的重试策略,例如在调用API、查询数据库或调用大语言模型(LLM)等情况下。
为节点添加重试策略,需要在add_node中设置retry_policy参数。retry_policy参数接受一个RetryPolicy命名元组对象。RetryPolicy对象有两个属性:max_attempts和retry_on参数,前者定义了总共重试次数,后者定义了对于哪些异常类型进行重试。
示例代码如下:
py
# 测试节点重试
from typing import TypedDict
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import RetryPolicy
# 定义状态
class State(TypedDict):
try_count: int
final_result: str
# 定义一个变量设置重试的次数
try_count = 0
# 定义节点
def retry_node(state: State):
# 对重试次数加1
global try_count
try_count += 1
print(f"第{try_count}次调用")
if try_count < 5:
raise Exception("重试")
else:
print("重试成功")
return {"final_result": f"重试成功","try_count":try_count}
# 创建状态图实例
state_graph = StateGraph(state_schema= State)
# 添加节点并设置重试策略
state_graph.add_node(retry_node,retry_policy=RetryPolicy(max_attempts=5,#最大重试次数
max_interval=1.0 #最大重试间隔
))
# 添加边
state_graph.add_edge(START, "retry_node").add_edge("retry_node",END)
# 编译
compile_state_graph = state_graph.compile()
# 执行
res = compile_state_graph.invoke({"try_count":1})
print(res)
响应结果
bash
第1次调用
第2次调用
第3次调用
第4次调用
第5次调用
重试成功
{'try_count': 5, 'final_result': '重试成功'}
5. 图内外数据传递
默认情况下,通过graph.invoke调用图时,仅在整个图的执行过程都结束之后,我们才能够拿到最终的状态,那么如果想要在图的执行过程当中,想要获取到图内部所产生的数据,应该如何实现?
考虑Agent的一个场景:首先,我们希望在LLM输出时,就能够拿到LLM所产生的token,在前端进行展示;其次,如果Agent的流程较长,我们希望能够拿到,当前正在执行的节点或者流程是什么。
由于LangGraph实现了langchain_core当中的Runnable接口,其为我们提供了stream和astream方法,也即流式输出方法,通过流式输出,就能解决前面所说到的问题。
Stream方法提供了多种不同的模式,如下表所示:
| 模式 | 描述 |
|---|---|
| values | 每一个执行后,流式输出完整的状态 |
| updates | 图执行过程中,每一步执行后流式输出增量更新。如果在同一个步当中产生了多个增量更新,这些增量更新会分别流式输出。 |
| custom | 流式输出节点内部的自定义数据。 |
| messages | 在任何调用了LLM的节点当中,流式输出两元组数据:(LLM Token,metadata) |
| debug | 流式输出所有能输出的信息 |
| 混合模式 | 流模式传入列表,在列表当中添加多种不同的模式,可以得到多种流式输出 |
具体代码如下所示:
py
import time
from typing import TypedDict, Annotated, List
import operator
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.types import StreamWriter
from langgraph.runtime import Runtime
# 创建大模型实例
llm = ChatOpenAI(
# 配置api_key
api_key="sk-01c903a6402548e5ada16386aae71fe7",
# 配置base_url
base_url="https://api.deepseek.com",
# 配置调用的模型
model="deepseek-v4-flash"
)
# --- 1. 定义状态 ---
class State(TypedDict):
messages: Annotated[List[BaseMessage], operator.add]
current_step: str
# --- 2. 定义节点 ---
def node_input(state: State):
"""模拟接收用户输入"""
return {
"messages": [HumanMessage(content="请帮我写一段Python代码")],
"current_step": "input_received"
}
def node_processing(state: State, runtime: Runtime):
"""
模拟中间处理过程,并使用 writer 输出自定义流式数据
这对应 stream_mode="custom"
"""
steps = ["正在分析意图...", "正在检索知识库...", "正在构建Prompt..."]
writer = runtime.stream_writer
for i, step in enumerate(steps):
time.sleep(0.5) # 模拟耗时操作
# 使用 writer 发送自定义数据 (不影响图的状态)
# 这些数据只能通过 stream_mode="custom" 接收到
writer({
"step_index": i + 1,
"description": step,
"timestamp": time.time()
})
return {"current_step": "processing_complete"}
def node_generation(state: State):
"""
模拟 LLM 生成过程
LangGraph 的 stream_mode="messages" 会自动捕获 LLM 的流式输出
"""
# 调用 LLM
response = llm.invoke(state["messages"])
return {
"messages": [response],
"current_step": "generation_complete"
}
# --- 3. 构建图 ---
builder = StateGraph(State)
builder.add_node("input", node_input)
builder.add_node("process", node_processing)
builder.add_node("generate", node_generation)
builder.add_edge(START, "input")
builder.add_edge("input", "process")
builder.add_edge("process", "generate")
builder.add_edge("generate", END)
graph = builder.compile()
# --- 4. 演示不同的流式输出模式 ---
def run_demo():
initial_state = {"messages": [], "current_step": "start"}
print(f"\n{'=' * 20} 1. Mode: values (输出完整状态) {'=' * 20}")
print("描述: 每执行完一个节点,输出当前的完整 State")
for event in graph.stream(initial_state, stream_mode="values"):
# event 就是当前的 State 字典
print(f"State: keys={list(event.keys())}, step={event.get('current_step')},event={event}")
print(f"\n{'=' * 20} 2. Mode: updates (输出增量更新) {'=' * 20}")
print("描述: 每执行完一个节点,输出该节点返回的增量数据")
for event in graph.stream(initial_state, stream_mode="updates"):
print(f"Update: {event}")
print(f"\n{'=' * 20} 3. Mode: custom (输出自定义数据) {'=' * 20}")
print("描述: 仅输出节点内部通过 writer() 发送的数据")
for event in graph.stream(initial_state, stream_mode="custom"):
# event 就是 writer() 中传入的对象
print(f"Custom Data: {event}")
print(f"\n{'=' * 20} 4. Mode: messages (输出 LLM Token) {'=' * 20}")
print("描述: 输出 LLM 生成的消息片段 (Token)")
# 注意:FakeListChatModel 默认是一次性返回,但在 messages 模式下会被包装成 chunk
for chunk, metadata in graph.stream(initial_state, stream_mode="messages"):
# chunk 是 BaseMessageChunk 对象
# metadata 包含 node 信息
node_name = metadata.get('langgraph_node', 'unknown')
print(f"[{node_name}] Token: {chunk.content!r}")
# print(chunk.content,end="")
time.sleep(0.2)
print(f"\n{'=' * 20} 5. Mode: debug (调试模式) {'=' * 20}")
print("描述: 输出所有详细的执行信息,包括任务调度、输入输出等")
# debug 模式输出量很大,这里只打印前几条示意
count = 0
for event in graph.stream(initial_state, stream_mode="debug"):
if count < 3:
print(f"Debug Event: {event['type']} - {event.get('payload', {}).get('name')}")
count += 1
print("... (省略后续 debug 信息)")
print(f"\n{'=' * 20} 6. Mixed Mode (混合模式) {'=' * 20}")
print("描述: 同时获取 updates 和 custom 数据")
# 返回的是 (mode, data) 元组
for mode, data in graph.stream(initial_state, stream_mode=["updates", "custom"]):
if mode == "updates":
print(f"[Updates] 来自节点 {list(data.keys())[0]}")
elif mode == "custom":
print(f"[Custom] {data['description']}")
if __name__ == "__main__":
run_demo()
响应结果
bash
==================== 1. Mode: values (输出完整状态) ====================
描述: 每执行完一个节点,输出当前的完整 State
State: keys=['messages', 'current_step'], step=start,event={'messages': [], 'current_step': 'start'}
State: keys=['messages', 'current_step'], step=input_received,event={'messages': [HumanMessage(content='请帮我写一段Python代码', additional_kwargs={}, response_metadata={})], 'current_step': 'input_received'}
State: keys=['messages', 'current_step'], step=processing_complete,event={'messages': [HumanMessage(content='请帮我写一段Python代码', additional_kwargs={}, response_metadata={})], 'current_step': 'processing_complete'}
State: keys=['messages', 'current_step'], step=generation_complete,event={'messages': [HumanMessage(content='请帮我写一段Python代码', additional_kwargs={}, response_metadata={}), AIMessage(content='您好!您的请求"写一段Python代码"比较宽泛,不知道您具体想要实现什么功能?比如:文件操作、数据分析、网络爬虫、GUI界面、算法练习等。为了更准确地帮助您,请您补充具体需求。\n\n在您提供详细信息之前,我准备了一个常用的Python代码模板------一个**简单计算器**(支持加减乘除)以及**读取CSV文件**的示例,您可以参考并修改:\n\n```python\n# 简单计算器函数\ndef calculator(a, b, operator):\n """\n 执行基本运算\n :param a: 第一个操作数(数字)\n :param b: 第二个操作数(数字)\n :param operator: 运算符号 \'+\'、\'-\'、\'*\'、\'/\'\n :return: 计算结果或错误信息\n """\n if operator == \'+\':\n return a + b\n elif operator == \'-\':\n return a - b\n elif operator == \'*\':\n return a * b\n elif operator == \'/\':\n if b == 0:\n return "错误:除数不能为零"\n return a / b\n else:\n return "不支持的运算符"\n\n# 使用示例\nif __name__ == "__main__":\n x = 10\n y = 3\n print("10 + 3 =", calculator(x, y, \'+\'))\n print("10 - 3 =", calculator(x, y, \'-\'))\n print("10 * 3 =", calculator(x, y, \'*\'))\n print("10 / 3 =", calculator(x, y, \'/\'))\n print("10 / 0 =", calculator(x, 0, \'/\'))\n```\n\n```python\n# 读取CSV文件(假设文件名为data.csv,包含表头)\nimport csv\n\ndef read_csv(file_path):\n try:\n with open(file_path, \'r\', encoding=\'utf-8\') as f:\n reader = csv.DictReader(f)\n for row in reader:\n print(row) # 每一行是一个字典,键为列名\n except FileNotFoundError:\n print(f"文件 {file_path} 未找到。")\n except Exception as e:\n print(f"读取错误:{e}")\n\n# 使用示例(取消注释下方代码并替换文件路径即可运行)\n# read_csv("data.csv")\n```\n\n如果您有更具体的场景,请告诉我,我会为您编写针对性的代码。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 989, 'prompt_tokens': 10, 'total_tokens': 999, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 476, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 10}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '2786850c-e129-4976-bedb-40c3af24b9ba', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f46e2-c792-7703-860f-83b319590f0a-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 10, 'output_tokens': 989, 'total_tokens': 999, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 476}})], 'current_step': 'generation_complete'}
==================== 2. Mode: updates (输出增量更新) ====================
描述: 每执行完一个节点,输出该节点返回的增量数据
Update: {'input': {'messages': [HumanMessage(content='请帮我写一段Python代码', additional_kwargs={}, response_metadata={})], 'current_step': 'input_received'}}
Update: {'process': {'current_step': 'processing_complete'}}
Update: {'generate': {'messages': [AIMessage(content='当然可以帮你写Python代码!不过,由于你的描述比较宽泛,我需要更多信息才能写出符合你需求的代码。请问你想实现什么功能呢?例如:\n\n- 数据处理(读取CSV、清洗数据、统计等)\n- 爬虫(抓取网页信息)\n- 自动化(文件整理、邮件发送等)\n- 机器学习(分类、回归等)\n- 简单工具(计算器、猜数字游戏等)\n- 其他具体任务\n\n你只需描述一下需要解决的问题或场景,我会为你生成相应的Python代码并提供解释。如果暂时没有明确需求,也可以先告诉我你想练习或学习的内容。😊', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 248, 'prompt_tokens': 10, 'total_tokens': 258, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 114, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 10}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'e93bb4c5-cb34-4684-883c-75c40789682f', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f46e2-f613-7d00-8e87-87092cc59be7-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 10, 'output_tokens': 248, 'total_tokens': 258, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 114}})], 'current_step': 'generation_complete'}}
==================== 3. Mode: custom (输出自定义数据) ====================
描述: 仅输出节点内部通过 writer() 发送的数据
Custom Data: {'step_index': 1, 'description': '正在分析意图...', 'timestamp': 1783600710.9778183}
Custom Data: {'step_index': 2, 'description': '正在检索知识库...', 'timestamp': 1783600711.4782324}
Custom Data: {'step_index': 3, 'description': '正在构建Prompt...', 'timestamp': 1783600711.9788284}
==================== 4. Mode: messages (输出 LLM Token) ====================
描述: 输出 LLM 生成的消息片段 (Token)
[input] Token: '请帮我写一段Python代码'
[generate] Token: ''
[generate] Token: ''
[generate] Token: ''
[generate] Token: '您的'
[generate] Token: '请求'
[generate] Token: '非常'
[generate] Token: '开放'
[generate] Token: '------'
[generate] Token: '我可以'
[generate] Token: '帮'
[generate] Token: '您'
[generate] Token: '写'
[generate] Token: '各种'
[generate] Token: '类型的'
[generate] Token: 'Python'
[generate] Token: '代码'
[generate] Token: ','
[generate] Token: '但'
[generate] Token: '不知道'
[generate] Token: '您'
[generate] Token: '具体'
[generate] Token: '需要'
[generate] Token: '什么'
[generate] Token: '功能'
[generate] Token: '。'
[generate] Token: '为了'
[generate] Token: '更'
[generate] Token: '高效'
[generate] Token: '地'
[generate] Token: '帮助'
[generate] Token: '您'
[generate] Token: ','
[generate] Token: '请'
[generate] Token: '告诉我'
[generate] Token: ':\n\n'
[generate] Token: '1'
[generate] Token: '.'
[generate] Token: ' **'
[generate] Token: '您'
[generate] Token: '想要'
[generate] Token: '解决'
[generate] Token: '什么问题'
[generate] Token: '?'
[generate] Token: ''
[generate] Token: ''
==================== 5. Mode: debug (调试模式) ====================
描述: 输出所有详细的执行信息,包括任务调度、输入输出等
Debug Event: task - input
Debug Event: task_result - input
Debug Event: task - process
... (省略后续 debug 信息)
==================== 6. Mixed Mode (混合模式) ====================
描述: 同时获取 updates 和 custom 数据
[Updates] 来自节点 input
[Custom] 正在分析意图...
[Custom] 正在检索知识库...
[Custom] 正在构建Prompt...
[Updates] 来自节点 process
[Updates] 来自节点 generate
6.人工审核节点
Agent在完成用户设定的任务时,有时候我们希望用户参与当中部分重要决策过程。LangGraph为此提供了一个非常方便的原语:interrupt。
其原理如下图所示:

可以在节点内部任意位置引入interrupt,当节点执行到interrupt所在位置时,就会停止执行,并可以得到从图内传递出来的,需要用户审核的数据,在用户进行相关审核完成后,又可通过graph.invoke,让图继续往下执行。
具体示例如下:
py
# 测试人工审核
from typing import TypedDict
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
from torch.utils.checkpoint import checkpoint
# 定义状态
class State(TypedDict):
# 请假人
name: str
# 请假天数
days: int
# 审批结果
approved: bool
# 定义节点
def approved_node(state: State):
print("进入审批节点")
# 获取用户输入的信息
name = state["name"]
days = state["days"]
# 待审批的信息
approved_info = interrupt({
"请假人": name,
"请假天数": days,
"审批结果": "待审批"
})
# 判断是否审批通过
if approved_info:
print(f"{name}请假{days}天,审批通过")
return {"name":name,"days":days,"approved":approved_info}
else:
print(f"{name}请假{days}天,审批未通过")
return {"name":name,"days":days,"approved":approved_info}
# 创建状态图实例
state_graph = StateGraph(state_schema= State)
# 添加节点
state_graph.add_node(approved_node)
# 添加边
state_graph.add_edge(START,"approved_node").add_edge("approved_node",END)
# 创建checkponter和config
checkpointer = InMemorySaver()
# 编译
compile_state_graph = state_graph.compile(checkpointer=checkpointer)
config = {
"configurable":{
"thread_id": "qf123"
}
}
# 执行
res = compile_state_graph.invoke({"name":"王飞龙","days":7},config=config)
print(res)
# # 审批通过
res1 = compile_state_graph.invoke(Command(resume=True),config=config)
# 审批拒绝
# res1 = compile_state_graph.invoke(Command(resume=False),config=config)
print(res1)
响应结果
bash
进入审批节点
{'name': '王飞龙', 'days': 7, '__interrupt__': [Interrupt(value={'请假人': '王飞龙', '请假天数': 7, '审批结果': '待审批'}, id='68e17c4a925b377ae4de6b7b08c7768e')]}
进入审批节点
王飞龙请假7天,审批通过
{'name': '王飞龙', 'days': 7, 'approved': True}
需要注意的是,使用interrupt时,在第二次调用graph.invoke(command)继续执行时,有interrupt的函数,会从函数起点往下执行,如果在函数当中,有更新数据、调用API接口等相关操作时,会造成多次重复执行的情况。因此,一定要保证这种类型操作的幂等性,或者是将其封装到一个单独的节点当中,避免在恢复时再次执行。
边
1.边的本质
定义边,本质上是定义了,Pregel当中节点订阅的状态通道和节点执行之后,需要更新的状态通道。
边有几种关键类型:
- Normal Edges: 普通边。直接从一个节点连接到下一个节点。
- Conditional Edges: 条件边。调用函数以确定接下来要前往哪个(哪些)节点。
2.条件边
条件边的本质是一个路由函数,其根据当前状态动态决定下一个要执行的节点。
py
# 测试条件边
from typing import TypedDict
from langgraph.constants import START, END
from langgraph.graph import StateGraph
# 定义状态
class State(TypedDict):
value: int
result: str
# 定义节点
def node_a(state: State):
print("进入节点a")
# 在节点a中对用户输入的value加1之后返回
return {"value": state["value"] + 1, "result":"对value进行了加1操作"}
def node_b(state: State):
print("进入节点b")
return {"value":state["value"],"result":"value是一个偶数"}
def node_c(state: State):
print("进入节点c")
return {"value":state["value"],"result":"value是一个奇数"}
# 定义路由函数
def route_fun(state: State):
# 获取value的值
value = state["value"]
if value % 2 == 0:
return "b"
else:
return "c"
# 创建状态图实例
state_grap = StateGraph(state_schema=State)
state_grap.add_node("node_a",node_a)
state_grap.add_node("node_b",node_b)
state_grap.add_node("node_c",node_c)
# 添加边
state_grap.add_edge(START, "node_a")
# 添加条件边
state_grap.add_conditional_edges(
"node_a",
route_fun,
{
"b": "node_b",
"c": "node_c"
}
)
state_grap.add_edge("node_b", END)
state_grap.add_edge("node_c", END)
# 编译
compile_state_graph = state_grap.compile()
# 执行
res = compile_state_graph.invoke({"value":12})
print(res)
# 获取图
graph = compile_state_graph.get_graph()
# 打印图
print(graph.draw_ascii())
响应结果
bash
进入节点a
进入节点c
{'value': 13, 'result': 'value是一个奇数'}
+-----------+
| __start__ |
+-----------+
*
*
*
+--------+
| node_a |
+--------+
. .
.. ..
. .
+--------+ +--------+
| node_b | | node_c |
+--------+ +--------+
* *
** **
* *
+---------+
| __end__ |
+---------+
3.可控循环
通过条件边,我们可以构建带有循环结构的图,例如在典型的REACT模式下,工具调用和大模型总结生成结果形成了一个循环,如下图所示:

需要注意的是,这种带循环的图结构,有一个隐藏的问题:图执行过程当中,可能因为某些原因,导致一直在循环内循环往复执行,因此LangGraph也提供了一个强制使图的执行终止的递归限制参数。
递归限制设定了图在抛出错误之前允许执行的超级步骤数量,默认值25,在graph.invoke的config参数中指定。在经过指定数量的超级步骤后,图还没有自然停止执行时,LangGraph会抛出异常GraphRecursionError。
示例
py
# 测试可控循环
from typing import TypedDict
from langgraph.constants import END, START
from langgraph.errors import GraphRecursionError
from langgraph.graph import StateGraph
class State(TypedDict):
count: int
max_count: int
result: str
# 定义节点a
def node_a(state: State):
# 获取count的值
count = state["count"]
# 对count加1
count += 1
print(f"第{count}次调用node_a,count的值是:{count}")
return {"count": count, "result": f"第{count}次调用"}
# 定义节点b
def node_b(state: State):
# 获取max_count的值
max_count = state["max_count"]
print(f"进入节点b,max_count的值是:{max_count}")
return {"result": f"最大次数为{max_count}"}
# 路由函数
def route_fun(state: State):
# 获取value的值
value = state["count"]
# 获取最大值
max_count = state["max_count"]
# 判断是否达到了最大的循环次数
if value >= max_count:
return END
else:
return "node_b"
# 创建状态图实例
state_graph = StateGraph(state_schema=State)
# 添加节点
state_graph.add_node(node_a)
state_graph.add_node(node_b)
# 添加边
state_graph.add_edge(START, "node_a")
# 添加条件边
state_graph.add_conditional_edges(
"node_a",
route_fun
)
state_graph.add_edge("node_b", "node_a")
# 编译
compile_state_graph = state_graph.compile()
try:
# 执行,需要传入最大循环次数(不能大于25)
# res = compile_state_graph.invoke({"count": 0, "max_count": 5},config={'recursion_limit': 5})
# res = compile_state_graph.invoke({"count": 0, "max_count": 5},config={'recursion_limit': 6})
# res = compile_state_graph.invoke({"count": 0, "max_count": 5},config={'recursion_limit': 10})
# res = compile_state_graph.invoke({"count": 0, "max_count": 100},config={
# 'recursion_limit': 200 # 设置递归限制
# })
res = compile_state_graph.invoke({"count": 0, "max_count": 100})
print(res)
# 获取图
graph = compile_state_graph.get_graph()
print(graph.draw_ascii())
except GraphRecursionError as e:
print(f"循环次数超限,异常信息是:{e}")
响应结果
bash
第1次调用node_a,count的值是:1
进入节点b,max_count的值是:100
第2次调用node_a,count的值是:2
进入节点b,max_count的值是:100
第3次调用node_a,count的值是:3
进入节点b,max_count的值是:100
第4次调用node_a,count的值是:4
进入节点b,max_count的值是:100
第5次调用node_a,count的值是:5
进入节点b,max_count的值是:100
第6次调用node_a,count的值是:6
进入节点b,max_count的值是:100
第7次调用node_a,count的值是:7
进入节点b,max_count的值是:100
第8次调用node_a,count的值是:8
进入节点b,max_count的值是:100
第9次调用node_a,count的值是:9
进入节点b,max_count的值是:100
第10次调用node_a,count的值是:10
进入节点b,max_count的值是:100
第11次调用node_a,count的值是:11
进入节点b,max_count的值是:100
第12次调用node_a,count的值是:12
进入节点b,max_count的值是:100
第13次调用node_a,count的值是:13
循环次数超限,异常信息是:Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.
For troubleshooting, visit: https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
LangChain:
LangGraph: