Node,Edge进阶
Node 负责做事,Edge 负责决定去哪,Send / Command / Runtime 负责处理普通边不够用的场景。先把普通边和条件边跑明白,再看动态分发、状态更新并跳转、运行时上下文。读完后能说清“这一步为什么不是固定下一跳”,就抓住了进阶 API 的用处。
Node(节点) 可以看作:图中的一个可执行步骤。它通常就是一个 Python 函数,可以是同步函数,也可以是异步函数。图运行时,框架会按边的连接关系,依次或并行地调度这些节点执行
Node 的本质不是“图上的一个点”,而是:
- 一段明确的处理逻辑
- 一次对当前 State 的读取
- 一次对 State 的局部更新
为什么节点很重要:
Node 是图真正“干活”的地方。一般主要负责
-
- 调用大模型
- 调用工具或外部 API
- 做检索、重排、格式化
- 做路由判断前的中间计算
- 记录某一步的结果、状态标记或错误信息
节点参数:
state:图当前这一步看到的共享状态。"业务数据”config:本次运行的配置与元数据,类型通常是RunnableConfigruntime:运行时对象,可访问context、store、stream_writer等
这里再补一个很实用的点:config 和 runtime 这两个参数,通常是由 LangGraph 运行时通过关键字方式自动注入的。所以在大多数场景里,你不需要手动去“组装”它们;节点函数里是否声明、声明顺序如何,更多是为了表达“这个节点需要哪些运行时信息”。
还有一个非常容易踩的坑,也非常值得在这里提前讲清楚:节点应该返回“增量更新”,而不是把接收到的整个 State 原样或修改后整份返回出去。
原因很简单:
- LangGraph 会把节点返回值当成“本节点对状态的局部更新”
- 然后再按字段对应的 Reducer 规则,把这些更新合并回全局 State
推荐的写法是:
def node(state: MyState) -> dict:
return {"result": "new_value"}
而不推荐把整份 state 直接修改后再整体 return。因为那样很容易带来两个问题:
- 没配置特殊 Reducer 的字段,会出现本不该被当前节点改动却被一起覆盖的情况
- 配置了 Reducer 的字段,如果节点把“不属于本节点职责的状态”也一并返回,后续合并结果会更难预测
你可以把这条规则记成一句话:节点只返回自己真正负责更新的字段。
节点缓存:
节点缓存解决的问题很实际:如果某个节点很贵、很慢、但相同输入经常重复出现,能不能不要每次都重新跑?
LangGraph 要真正命中缓存,至少需要两处配置:
- 节点层:给节点配置
CachePolicy - 编译层:
compile(cache=...)提供具体缓存实现,例如InMemoryCache()
再往工程化一点理解,缓存其实还有第三个维度:缓存后端放在哪里。LangGraph 文档和相关实现里,常见可以看到:
- 内存缓存
- Redis 这类外部缓存
- SQLite 这类本地持久化缓存
所以完整一点看,缓存通常分三层:
- 节点声明缓存策略:例如
CachePolicy(ttl=...) - 图编译时选择缓存后端:例如
InMemoryCache() - 运行时按 key_func + ttl 判断是否命中
如果你先只学最基础的一层,记成“节点声明支持缓存,图编译时选择具体缓存后端”就够了。
在真实项目里,缓存很适合这些节点:
- 纯计算节点
- 解析、格式化、清洗类节点
- 成本高但输入重复率高的外部调用节点
-
""" 【案例】节点缓存(Node Caching):为节点配置 CachePolicy(ttl=8),编译时传入 InMemoryCache(),相同输入在 ttl 秒内直接返回缓存结果,避免重复执行耗时逻辑。 知识点速览: - add_node(..., cache_policy=CachePolicy(...)) 是“节点声明自己支持缓存”;compile(cache=...) 则是“图编译时选择具体缓存后端”。 - `ttl` 决定缓存保留多久;如果还需要更精细地控制“什么样的输入算同一次结果”,可以再配合 `key_func`。 - 本例用 set_entry_point / set_finish_point 构成单节点图,目的是把“缓存行为”本身看清楚,不让流程结构分散注意力。 """ import time from typing_extensions import TypedDict from langgraph.graph import StateGraph from langgraph.cache.memory import InMemoryCache from langgraph.types import CachePolicy class State(TypedDict): x: int result: int builder = StateGraph(State) def expensive_node(state: State) -> dict[str, int]: """模拟耗时计算(sleep 3 秒),用于观察缓存命中时不再执行。""" time.sleep(3) return {"result": state["x"] * 2} # 为该节点配置缓存,ttl=8 秒 builder.add_node( node="expensive_node", action=expensive_node, cache_policy=CachePolicy(ttl=8), ) builder.set_entry_point("expensive_node") builder.set_finish_point("expensive_node") # 编译时指定使用内存缓存 app = builder.compile(cache=InMemoryCache()) # 第一次执行:无缓存,耗时约 3 秒 print("第一次执行(无缓存,耗时 3 秒):") print(app.invoke({"x": 5})) # 第二次执行:命中缓存,立即返回 print("\n第二次运行利用缓存并快速返回:") print(app.invoke({"x": 5})) # 等待 ttl 过期后再次执行,将重新计算 print("\n等待 8 秒,缓存过期...") time.sleep(8) print("8 秒后第三次执行(重新计算,耗时 3 秒):") print(app.invoke({"x": 5})) """ 【输出示例】 第一次执行(无缓存,耗时 3 秒): {'x': 5, 'result': 10} 第二次运行利用缓存并快速返回: {'x': 5, 'result': 10} 等待 8 秒,缓存过期... 8 秒后第三次执行(重新计算,耗时 3 秒): {'x': 5, 'result': 10} """
错误处理与重试机制:
节点失败了,是不是应该立刻整个图报错,还是可以重试一下?
LangGraph 用 RetryPolicy 来描述这件事。官方参考文档里,RetryPolicy 主要包含这些常见参数:
max_attempts:最多尝试执行多少次,包含第一次正式执行。initial_interval:第一次重试前先等多久,通常以秒为单位。backoff_factor:每次重试后,等待时间按多少倍增长,用来做退避。max_interval:单次重试等待时间的上限,避免退避时间无限变长。jitter:是否在等待时间上加入一点随机扰动,降低“同时重试把服务再次打爆”的风险。retry_on:指定哪些异常值得重试;不符合条件的异常会直接抛出,而不是继续重试。
其中最关键的不是把参数全背下来,而是先分清楚两层语义:
- 时间策略:多久重试一次,是否退避,是否加抖动
- 异常策略:哪些异常应该重试,哪些异常不该重试
在真实项目里,一个很重要的经验是:
- 网络抖动、临时超时 这类问题通常适合重试
- 参数错误、类型错误、业务逻辑错误 通常不适合盲目重试
所以 retry_on 的真正价值是:让重试更像工程策略,而不是“失败了就无脑再跑一遍”。
"""
【案例】节点重试策略(RetryPolicy):默认重试、自定义 retry_on 仅对特定异常重试、以及「不可重试异常」直接失败,演示 add_node(..., retry_policy=RetryPolicy(...)) 的用法。
对应教程章节:第 24 章 - LangGraph API:节点、边与进阶 → 1、Graph API 之 Node(节点)
知识点速览:
- RetryPolicy 不只是“重试几次”,而是两层策略组合:一层是时间策略(重试次数、间隔、退避),一层是异常策略(哪些错误值得重试)。
- RetryPolicy(max_attempts=5) 适合先观察默认行为;RetryPolicy(..., retry_on=custom_retry_on) 则更贴近真实项目里的精细控制。
- 本例最值得关注的是:不是所有异常都应该重试,像 ValueError 这类逻辑/参数错误通常应直接失败。
"""
from typing import Dict, Any
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import RetryPolicy
# 定义状态类型
class DiliState(TypedDict):
result: str
# 全局计数器:记录API尝试次数
attempt_counter = 0
# 工具函数
def build_retry_graph(node_name: str, node_func, retry_policy: RetryPolicy):
builder = StateGraph(DiliState)
# 为节点添加重试策略,需要在add_node中设置retry_policy参数。
# retry_policy参数接受一个RetryPolicy命名元组对象。
# 默认情况下,retry_on参数使用default_retry_on函数,该函数会在遇到任何异常时重试
builder.add_node(node_name, node_func, retry_policy=retry_policy)
builder.add_edge(START, node_name)
builder.add_edge(node_name, END)
return builder.compile()
# 模拟不稳定的API调用,使用全局变量跟踪尝试次数
def unstable_api_call(state: DiliState) -> Dict[str, Any]:
"""模拟不稳定API:前2次失败,第3次成功(全局计数器记录尝试次数)"""
global attempt_counter
attempt_counter += 1
# 纯文本打印尝试次数
print(f"尝试调用API,这是第 {attempt_counter} 次尝试")
# 模拟失败/成功逻辑:前2次抛异常,第3次返回结果
if attempt_counter < 3:
raise Exception(f"模拟API调用失败abcd (尝试 {attempt_counter})")
return {"result": f"API调用成功,经过 {attempt_counter} 次尝试"}
# 自定义重试条件判断函数
def custom_retry_on(exception: Exception) -> bool:
"""自定义重试规则:只对包含「模拟API调用失败」的异常重试"""
print("########################: " + str(exception))
err_msg = str(exception)
if "模拟API调用失败" in err_msg:
print(f"捕获到可重试异常: {err_msg}")
return True
print(f"捕获到不可重试异常: {err_msg}")
return False
# 模拟抛出 ValueError 的节点
def value_error_call(state: DiliState) -> Dict[str, Any]:
"""模拟抛出ValueError:默认重试策略对这类异常不重试"""
print("调用会抛出 ValueError 的节点")
raise ValueError("模拟 ValueError 异常")
# 测试方法1:默认重试策略
def test_default_retry():
global attempt_counter
print("1. 使用默认重试策略:")
print(" 默认策略会对除特定异常外的所有异常进行重试")
print(" 不会重试的异常包括: ValueError, TypeError, ArithmeticError, ImportError,")
print(" LookupError, NameError, SyntaxError, RuntimeError,")
print(
" ReferenceError, StopIteration, StopAsyncIteration, OSError\n"
)
print("测试默认重试策略:")
attempt_counter = 0 # 重置计数器
default_graph = build_retry_graph(
node_name="unstable_api",
node_func=unstable_api_call,
retry_policy=RetryPolicy(max_attempts=5), # 最多5次尝试,足够重试成功
)
try:
result = default_graph.invoke({"result": ""})
print(f"最终结果: {result}\n")
except Exception as e:
print(f"最终失败: {type(e).__name__}: {e}\n")
# 测试方法2:自定义重试策略(输出完全匹配要求)
def test_custom_retry():
global attempt_counter
print("2. 使用自定义重试策略:")
print(" 自定义策略只对特定错误进行重试\n")
print("测试自定义重试策略:")
attempt_counter = 0 # 重置计数器
custom_graph = build_retry_graph(
node_name="custom_retry_api",
node_func=unstable_api_call,
retry_policy=RetryPolicy(max_attempts=5, retry_on=custom_retry_on),
)
try:
result = custom_graph.invoke({"result": ""})
print(f"最终结果: {result}\n")
except Exception as e:
print(f"最终失败: {type(e).__name__}: {e}\n")
# 测试方法3:不可重试异常演示,测试 ValueError(默认策略不会重试)
def test_no_retry_exception():
print("3. 测试不会重试的异常类型:")
print("测试 ValueError(默认策略不会重试):")
no_retry_graph = build_retry_graph(
node_name="value_error_node",
node_func=value_error_call,
retry_policy=RetryPolicy(max_attempts=3),
)
try:
result = no_retry_graph.invoke({"result": ""})
print(f"最终结果: {result}\n")
except Exception as e:
print(f"最终失败: {type(e).__name__}: {e}\n")
# 主演示函数
def run_demo():
print("=== LangGraph 节点重试策略完整演示===")
print("-" * 80 + "\n")
# test_default_retry()
# test_custom_retry()
test_no_retry_exception()
print("-" * 80)
print("=== 演示结束 ===")
# 程序入口
if __name__ == "__main__":
run_demo()
"""
【输出示例】
=== LangGraph 节点重试策略完整演示===
--------------------------------------------------------------------------------
3. 测试不会重试的异常类型:
测试 ValueError(默认策略不会重试):
调用会抛出 ValueError 的节点
最终失败: ValueError: 模拟 ValueError 异常
--------------------------------------------------------------------------------
=== 演示结束 ===
"""
边 Graph
Edge(边) 决定的就是:这一站做完之后,下一步去哪里。所以边的本质不是装饰性的连线,而是图的流程控制规则.
LangGraph 里最基础的两类边是:
- 普通边(Normal Edge):固定从 A 到 B
- 条件边(Conditional Edge):根据当前状态决定下一步去哪
围绕这两类边,又会延伸出:
- 入口点(Entry Point)
- 条件入口点(Conditional Entry Point)
1.普通边:执行完当前节点后,无条件进入下一个节点
builder.add_edge("node_a", "node_b")
2. 条件边:根据当前状态决定下一步去哪”,就需要条件边
graph.add_conditional_edges("node_a", route_fn, mapping)
这里可以拆成三部分理解:
"node_a":从哪个节点出发做路由route_fn:根据当前状态返回路由结果mapping:把路由结果映射到具体目标节点
"""
【案例】条件边(Conditional Edges):根据状态(如 x 的奇偶)在多个后继节点中选一个执行,使用 add_conditional_edges(节点名, 路由函数, 映射)。
知识点速览:
- add_conditional_edges(source, route_fn, mapping):route_fn(state) 的返回值作为 key,在 mapping 中查到下一节点名;若为 bool,常用 {True: "node_a", False: "node_b"}。
- 条件边的重点是“让边负责分流,而不是把所有 if/else 都塞回节点里”;路由函数在 source 节点执行后被调用,根据当前 state 决定下一跳。
- 本例顺手演示了 State 也可以用 Pydantic BaseModel 定义,这更适合需要默认值和校验的场景。
"""
from typing import Optional
from langgraph.constants import START, END
from langgraph.graph import StateGraph
from loguru import logger
from pydantic import BaseModel
class MyState(BaseModel):
"""
定义状态模型,用于在图节点之间传递数据
Attributes:
x (int): 输入的整数
result (Optional[str]): 处理结果,可为"even"或"odd"
"""
x: int
result: Optional[str] = None
# 检查输入状态的节点函数
def check_x(state: MyState) -> MyState:
"""
检查输入状态的节点函数
Args:
state (MyState): 包含输入数据的状态对象
Returns:
MyState: 返回原始状态对象,未做修改
"""
logger.info(f"[check_x] Received state: {state}")
return state
# 判断状态中x值是否为偶数的条件函数
def is_even(state: MyState) -> bool:
"""
判断状态中x值是否为偶数的条件函数
Args:
state (MyState): 包含待判断数值的状态对象
Returns:
bool: 如果x是偶数返回True,否则返回False
"""
return state.x % 2 == 0
# 处理偶数情况的节点函数
def handle_even(state: MyState) -> MyState:
"""
处理偶数情况的节点函数
Args:
state (MyState): 包含偶数输入的状态对象
Returns:
MyState: 返回更新后的状态对象,result设置为"even"
"""
logger.info("[handle_even] x 是偶数")
return MyState(x=state.x, result="even")
# 处理奇数情况的节点函数
def handle_odd(state: MyState) -> MyState:
"""
处理奇数情况的节点函数
Args:
state (MyState): 包含奇数输入的状态对象
Returns:
MyState: 返回更新后的状态对象,result设置为"odd"
"""
logger.info("[handle_odd] x 是奇数")
return MyState(x=state.x, result="odd")
builder = StateGraph(MyState)
# 添加节点
builder.add_node("check_x", check_x)
builder.add_node("handle_even", handle_even)
builder.add_node("handle_odd", handle_odd)
# 添加条件边,根据is_even函数的返回值决定流向哪个节点
builder.add_conditional_edges(
"check_x", is_even, {True: "handle_even", False: "handle_odd"}
)
# 添加起始边,从START节点流向check_x节点
builder.add_edge(START, "check_x")
# 添加结束边,从处理节点流向END节点
builder.add_edge("handle_even", END)
builder.add_edge("handle_odd", END)
# 编译图结构
graph = builder.compile()
# 打印图的可视化结构
print(graph.get_graph().print_ascii())
# 测试用例:输入偶数4
logger.info("输入 x=4(偶数)")
graph.invoke(MyState(x=4))
# # 测试用例:输入奇数3
# logger.info("输入 x=3(奇数)")
# graph.invoke(MyState(x=3))
"""
【输出示例】
+-----------+
| __start__ |
+-----------+
*
*
*
+---------+
| check_x |
+---------+
... ..
. ..
.. ..
+-------------+ +------------+
| handle_even | | handle_odd |
+-------------+ +------------+
*** **
* **
** **
+---------+
| __end__ |
+---------+
None
2026-03-23 16:38:23.954 | INFO | __main__:<module>:108 - 输入 x=4(偶数)
2026-03-23 16:38:23.955 | INFO | __main__:check_x:40 - [check_x] Received state: x=4 result=None
2026-03-23 16:38:23.955 | INFO | __main__:handle_even:65 - [handle_even] x 是偶数
"""
"""
【案例】条件边另一种写法:路由函数返回字符串 key(如 "condition_1"),在 add_conditional_edges 的 mapping 中映射到不同节点;可从 START 直接根据 state 分支到多个节点之一。
知识点速览:
- add_conditional_edges(START, route_fn, {"condition_1": "node1", "condition_2": "node2", ...}):路由函数返回的字符串与 mapping 的 key 匹配,决定从 START 进入哪个节点。
- 适合「多分支入口」:根据初始 state 的某个字段(如 x)决定第一跳,再各自到 END。
- 它和上一份条件边案例的区别不在 API 本身,而在于这里强调的是“字符串路由键 + mapping”的多分支入口写法。
"""
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, List, Annotated
# 定义状态
class DiliState(TypedDict):
x: int
def addition1(state):
"""
执行加法运算的节点函数
参数:
state (dict): 包含输入数据的状态字典,必须包含键"x"
返回:
dict: 返回更新后的状态字典,其中"x"的值增加1
"""
print(f"加法节点addition1收到的初始值:{state}")
return {"x": state["x"] + 1}
def addition2(state):
print(f"加法节点addition2收到的初始值:{state}")
return {"x": state["x"] + 2}
def addition3(state):
print(f"加法节点addition3收到的初始值:{state}")
return {"x": state["x"] + 3}
def route_by_sentiment(state: DiliState) -> str:
# 路由逻辑...返回最终的条件
flag = state["x"]
if flag == 1:
return "condition_1"
elif flag == 2:
return "condition_2"
else:
return "condition_3"
graph = StateGraph(DiliState)
graph.add_node("node1", addition1)
graph.add_node("node2", addition2)
graph.add_node("node3", addition3)
# 添加路由函数,参数:当前节点,路由函数,路由函数返回的条件与node的映射
graph.add_conditional_edges(
START,
route_by_sentiment,
{"condition_1": "node1", "condition_2": "node2", "condition_3": "node3"},
)
# 所有处理节点都连接到END
graph.add_edge("node1", END)
graph.add_edge("node2", END)
graph.add_edge("node3", END)
app = graph.compile()
# 定义一个初始状态字典,包含键值对"x": 具体数字
initial_state = {"x": 3}
# 调用graph对象的invoke方法,传入初始状态,执行图计算流程
result = app.invoke(initial_state)
print(f"最后的结果是:{result}")
# 打印图的边和节点信息
# print(graph.edges)
# print(graph.nodes)
# 打印图的ascii可视化结构
print(app.get_graph().print_ascii())
print("=================================")
print()
# 打印图的可视化结构,生成更加美观的Mermaid 代码,通过processon 编辑器查看
print(app.get_graph().draw_mermaid())
"""
【输出示例】
加法节点addition3收到的初始值:{'x': 3}
最后的结果是:{'x': 6}
+-----------+
| __start__ |
+-----------+..
... . ...
... . ...
.. . ..
+-------+ +-------+ +-------+
| node1 |* | node2 | | node3 |
+-------+ *** +-------+ **+-------+
*** * ***
*** * ***
** * **
+---------+
| __end__ |
+---------+
None
=================================
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
node1(node1)
node2(node2)
node3(node3)
__end__([<p>__end__</p>]):::last
__start__ -. condition_1 .-> node1;
__start__ -. condition_2 .-> node2;
__start__ -. condition_3 .-> node3;
node1 --> __end__;
node2 --> __end__;
node3 --> __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
"""
入口点与条件入口:
- 用
set_entry_point/set_finish_point指定固定入口出口 - 从
START上直接挂条件边,按初始输入决定进入哪条处理链
"""
【案例】入口点与出口点:用 set_entry_point / set_finish_point 指定图的第一个和最后一个节点,等价于 add_edge(START, node) 与 add_edge(node, END),写法更简洁。
知识点速览:
- set_entry_point(node_id):图从该节点开始执行,底层等价于 add_edge(START, node_id)。
- set_finish_point(node_id):执行到该节点后图结束,底层等价于 add_edge(node_id, END)。
- 适合线性链或单入口单出口的图,减少重复写 START/END 边。
- 本例重点是理解“入口/出口的声明方式”,不是引入新类型的边;它本质上仍然是在配置普通边。
"""
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
# 定义状态
class DiliState(TypedDict):
value: int
step: str
# 定义节点函数
def node_a(state: DiliState) -> dict:
"""节点A"""
print("执行节点A")
print("state[value]:" + str(state["value"]))
print("state[step]:" + str(state["step"]))
return {"value": state["value"] + 1, "step": "A执行完毕"}
def node_b(state: DiliState) -> dict:
"""节点B"""
print("执行节点B")
return {"value": state["value"] * 2, "step": "B执行完毕"}
def main():
"""演示入口点"""
print("=== 入口点演示 ===")
# 创建图
builder = StateGraph(DiliState)
# 添加节点
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
# set_entry_point / set_finish_point 是更简洁的入口出口配置方式,本质上仍然是在帮你建立 START/END 的边
builder.set_entry_point("node_a")
builder.add_edge("node_a", "node_b")
builder.set_finish_point("node_b")
# 编译图
graph = builder.compile()
# 执行图
result = graph.invoke({"value": 0, "step": "hello"})
print(f"执行结果: {result}\n")
if __name__ == "__main__":
main()
"""
【输出示例】
=== 入口点演示 ===
执行节点A
state[value]:0
state[step]:hello
执行节点B
执行结果: {'value': 2, 'step': 'B执行完毕'}
+-----------+
| __start__ |
+-----------+
*
*
*
+--------+
| node_a |
+--------+
*
*
*
+--------+
| node_b |
+--------+
*
*
*
+---------+
| __end__ |
+---------+
None
=================================
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
node_a(node_a)
node_b(node_b)
__end__([<p>__end__</p>]):::last
__start__ --> node_a;
node_a --> node_b;
node_b --> __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
"""
"""
【案例】条件入口点:从 START 开始就根据状态分支,使用 add_conditional_edges(START, route_fn, mapping),根据初始输入(如 user_input)决定进入哪个处理节点。
知识点速览:
- add_conditional_edges(START, route_input, {"greeting": "greeting_node", ...}):invoke 传入的 state 先交给 route_input,返回值作为 key 在 mapping 中查下一节点,实现「不同输入走不同入口」。
- 与「条件边」区别:条件边是“某节点执行完后”再分支;条件入口点是“图一启动”就分支,常用于做一级路由。
- 本例重点是理解“图从哪里开始”可以由输入动态决定;至于问候、告别、问题这三类文案本身,只是为了帮助观察路由效果。
"""
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. 定义简单的状态
class SimpleState(TypedDict):
user_input: str
response: str
node_visited: str
# 2. 路由函数 - 决定从START去哪
def route_input(state: SimpleState) -> str:
"""根据用户输入决定去哪个节点"""
text = state["user_input"].lower()
if "hello" in text or "hi" in text:
return "greeting" # 返回路由键
elif "bye" in text or "exit" in text:
return "farewell" # 返回路由键
else:
return "question" # 返回路由键
# 3. 各个处理节点
def handle_greeting(state: SimpleState) -> SimpleState:
"""处理问候"""
state["response"] = "你好!很高兴见到你!"
state["node_visited"] = "greeting_node"
return state
def handle_farewell(state: SimpleState) -> SimpleState:
"""处理告别"""
state["response"] = "再见!祝你有个美好的一天!"
state["node_visited"] = "farewell_node"
return state
def handle_question(state: SimpleState) -> SimpleState:
"""处理问题"""
state["response"] = "我听到了你的问题,需要更多帮助吗?"
state["node_visited"] = "question_node"
return state
# 4. 创建图
def create_simple_graph():
"""创建一个简单的图"""
stateGraph = StateGraph(SimpleState)
# 添加节点
stateGraph.add_node("greeting_node", handle_greeting)
stateGraph.add_node("farewell_node", handle_farewell)
stateGraph.add_node("question_node", handle_question)
# 条件入口点:图从 START 进入后,先调用 route_input,再根据 mapping 决定第一跳去哪个业务节点
stateGraph.add_conditional_edges(
START, # 起点
route_input, # 路由函数
# 路由映射(可选):路由函数的返回值 -> 节点名
{
"greeting": "greeting_node", # route_input返回"greeting"时,去greeting_node
"farewell": "farewell_node", # route_input返回"farewell"时,去farewell_node
"question": "question_node", # route_input返回"question"时,去question_node
},
)
# 所有节点都到END
stateGraph.add_edge("greeting_node", END)
stateGraph.add_edge("farewell_node", END)
stateGraph.add_edge("question_node", END)
return stateGraph.compile()
# 5. 使用示例
def run_example():
# 创建图
graph = create_simple_graph()
# 测试不同的输入
test_inputs = ["Hello everyone!", "Goodbye now", "What time is it?"]
for user_input in test_inputs:
print(f"\n输入: {user_input}")
print("-" * 30)
# 创建初始状态
initial_state = SimpleState(user_input=user_input, response="", node_visited="")
# 执行图
result = graph.invoke(initial_state)
print(f"路由决策: {route_input(initial_state)}")
print(f"访问的节点: {result['node_visited']}")
print(f"响应: {result['response']}")
print()
# 打印图的ascii可视化结构
print(graph.get_graph().print_ascii())
# 运行示例
if __name__ == "__main__":
print("简单条件入口点示例")
print("=" * 40)
run_example()
"""
【输出示例】
简单条件入口点示例
========================================
输入: Hello everyone!
------------------------------
路由决策: greeting
访问的节点: greeting_node
响应: 你好!很高兴见到你!
输入: Goodbye now
------------------------------
路由决策: farewell
访问的节点: farewell_node
响应: 再见!祝你有个美好的一天!
输入: What time is it?
------------------------------
路由决策: question
访问的节点: question_node
响应: 我听到了你的问题,需要更多帮助吗?
+-----------+
| __start__ |.
.....+-----------+ .....
.... . ....
..... . .....
... . ...
+---------------+ +---------------+ +---------------+
| farewell_node | | greeting_node | | question_node |
+---------------+**** +---------------+ ***+---------------+
**** * ****
***** * *****
*** * ***
+---------+
| __end__ |
+---------+
None
=================================
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
greeting_node(greeting_node)
farewell_node(farewell_node)
question_node(question_node)
__end__([<p>__end__</p>]):::last
__start__ -. farewell .-> farewell_node;
__start__ -. greeting .-> greeting_node;
__start__ -. question .-> question_node;
farewell_node --> __end__;
greeting_node --> __end__;
question_node --> __end__;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
"""
条件边也可以构成循环:
条件边不只能做分支,也能做循环。
- 某个节点先做一次判断
- 如果条件满足,就继续走下一个处理节点
- 处理完后再回到前一个判断节点
- 直到某个终止条件满足,再走向
END
这类结构在 LangGraph 里很常见,尤其是:
- Agent 的 ReAct 循环
- “检索不够就继续补检索”的循环
- 多步规划执行里的“继续 / 停止”判断
但循环结构有一个隐藏风险:如果终止条件设计得不对,图可能一直循环下去。
send | command | Runtime上下文
send: 动态多路分发与map-ruduce:
Send 最适合解决的问题是:上游节点产出了一批任务,任务数量运行时才知道,而我想把这批任务分发给同一个下游节点分别处理。
这正是典型的 Map-Reduce 思路:
- Map:先把大任务拆成很多小任务
- Reduce:小任务各自完成后,再把结果汇总
LangGraph 里,条件边函数可以返回 Sequence[Send]。每个 Send 都包含两部分:
- 目标节点名
- 要传给该节点的那份状态
案例:
"""
【案例】Send 与 Map-Reduce 模式:条件边函数返回 Sequence[Send],每个 Send(节点名, 状态) 触发一次该节点的执行,LangGraph 并行执行后按 Reducer 汇总(如列表合并),适合「动态数量子任务」并行再汇总。
知识点速览:
- 条件边若返回 List[Send](或 Sequence[Send]),每个 Send 指定「下一节点 + 传入该节点的 state」,框架会并行执行这些分支并合并结果。
- Map 阶段:生成主题列表 → 为每个主题构造 Send("make_joke", {"subject": 主题});Reduce 阶段:jokes 字段用列表合并 Reducer,多路结果合并成一条列表。
- 适合「一批输入拆成多份、并行处理、再汇总」的流程。
- 本例最值得观察的是:每个 Send 分支拿到的是“自己的那份状态”,而最终能不能顺利汇总,取决于下游字段有没有设计好对应的 Reducer。
"""
from typing import Annotated, List, Sequence
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import Send
# 定义状态
class DiliState(TypedDict):
subjects: List[str]
jokes: Annotated[List[str], lambda x, y: x + y] # 使用列表合并的方式
# 第一个节点:生成需要处理的主题列表
def generate_subjects(state: DiliState) -> dict:
"""生成需要处理的主题列表"""
print("执行节点(第一个节点:生成需要处理的主题列表): generate_subjects")
subjects = ["猫", "狗", "程序员"]
print(f"生成主题列表: {subjects}")
return {"subjects": subjects}
# Map节点:为每个主题生成笑话
def make_joke(state: DiliState) -> dict:
"""为单个主题生成笑话"""
subject = state.get("subject", "未知")
print(f"执行节点: make_joke,处理主题: {subject}")
# 根据主题生成相应笑话
jokes_map = {
"猫": "为什么猫不喜欢在线购物?因为它们更喜欢实体店!",
"狗": "为什么狗不喜欢计算机?因为它们害怕被鼠标咬!",
"程序员": "为什么程序员喜欢洗衣服?因为他们在寻找bugs!",
"未知": "这是一个关于未知主题的神秘笑话。",
}
joke = jokes_map.get(subject, f"这是一个关于{subject}的即兴笑话。")
print(f"生成笑话: {joke}")
return {"jokes": [joke]}
# 条件边函数:根据主题列表生成Send对象列表
def map_subjects_to_jokes(state: DiliState) -> List[Send]:
"""将主题列表映射到joke生成任务"""
print("执行条件边函数: map_subjects_to_jokes")
subjects = state["subjects"]
print(f"映射主题到joke任务: {subjects}")
# 为每个主题创建一个Send对象,指向make_joke节点
# 每个Send对象包含节点名称和传递给该节点的状态
send_list = [Send("make_joke", {"subject": subject}) for subject in subjects]
print(f"生成Send对象列表: {send_list}")
return send_list
def main():
"""演示Map-Reduce模式"""
print("=== Map-Reduce 模式演示 ===\n")
# 创建图
builder = StateGraph(DiliState)
# 添加节点
builder.add_node("generate_subjects", generate_subjects)
builder.add_node("make_joke", make_joke)
# 添加边
builder.add_edge(START, "generate_subjects")
# 添加条件边,使用Send对象实现map-reduce
builder.add_conditional_edges(
"generate_subjects", # 源节点
map_subjects_to_jokes, # 路由函数,返回Send对象列表
)
# 从make_joke到结束
builder.add_edge("make_joke", END)
# 编译图
graph = builder.compile()
print(graph.get_graph().print_ascii())
# 执行图
initial_state = {"subjects": [], "jokes": []}
print("初始状态:", initial_state)
print("\n开始执行图...")
result = graph.invoke(initial_state)
print(f"\n最终结果: {result}")
print("\n=== 演示完成 ===")
if __name__ == "__main__":
main()
"""
【输出示例】
=== Map-Reduce 模式演示 ===
+-----------+
| __start__ |
+-----------+
*
*
*
+-------------------+
| generate_subjects |
+-------------------+
*
*
*
+---------+
| __end__ |
+---------+
None
初始状态: {'subjects': [], 'jokes': []}
开始执行图...
执行节点(第一个节点:生成需要处理的主题列表): generate_subjects
生成主题列表: ['猫', '狗', '程序员']
执行条件边函数: map_subjects_to_jokes
映射主题到joke任务: ['猫', '狗', '程序员']
生成Send对象列表: [Send(node='make_joke', arg={'subject': '猫'}), Send(node='make_joke', arg={'subject': '狗'}), Send(node='make_joke', arg={'subject': '程序员'})]
执行节点: make_joke,处理主题: 猫
生成笑话: 为什么猫不喜欢在线购物?因为它们更喜欢实体店!
执行节点: make_joke,处理主题: 狗
生成笑话: 为什么狗不喜欢计算机?因为它们害怕被鼠标咬!
执行节点: make_joke,处理主题: 程序员
生成笑话: 为什么程序员喜欢洗衣服?因为他们在寻找bugs!
最终结果: {'subjects': ['猫', '狗', '程序员'], 'jokes': ['为什么猫不喜欢在线购物?因为它们更喜欢实体店!', '为什么狗不喜欢计算机?因为它们害怕被鼠标咬!', '为什么程序员喜欢洗衣服?因为他们在寻找bugs!']}
=== 演示完成 ===
"""
Command: 更新状态+ 指定下一跳
Command 解决的是另一个很常见的需求:某个节点在做完判断后,既想更新状态,又想直接决定下一步去哪
最常见的两个参数是:
update:当前节点希望写回 State 的局部更新内容,仍然会按字段对应的 Reducer 规则合并。goto:当前节点执行完后,希望图下一步跳转到哪个节点;也可以直接跳到END结束流程。
这和条件边的边界要分清楚:
- 条件边:通常更适合“节点做完了,再单独根据状态决定去哪”
- Command:更适合“这个节点本身就是决策点,离开时把状态和去向一起交代清楚”

浙公网安备 33010602011771号