Node,Edge进阶

Node 负责做事,Edge 负责决定去哪,Send / Command / Runtime 负责处理普通边不够用的场景。先把普通边和条件边跑明白,再看动态分发、状态更新并跳转、运行时上下文。读完后能说清“这一步为什么不是固定下一跳”,就抓住了进阶 API 的用处。

Node(节点) 可以看作:图中的一个可执行步骤。它通常就是一个 Python 函数,可以是同步函数,也可以是异步函数。图运行时,框架会按边的连接关系,依次或并行地调度这些节点执行

Node 的本质不是“图上的一个点”,而是:

  • 一段明确的处理逻辑
  • 一次对当前 State 的读取
  • 一次对 State 的局部更新

 

为什么节点很重要:
Node 是图真正“干活”的地方。一般主要负责

    • 调用大模型
    • 调用工具或外部 API
    • 做检索、重排、格式化
    • 做路由判断前的中间计算
    • 记录某一步的结果、状态标记或错误信息

节点参数:

  • state:图当前这一步看到的共享状态。"业务数据”
  • config:本次运行的配置与元数据,类型通常是 RunnableConfig
  • runtime:运行时对象,可访问 contextstorestream_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__ -. &nbsp;condition_1&nbsp; .-> node1;
        __start__ -. &nbsp;condition_2&nbsp; .-> node2;
        __start__ -. &nbsp;condition_3&nbsp; .-> 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__ -. &nbsp;farewell&nbsp; .-> farewell_node;
        __start__ -. &nbsp;greeting&nbsp; .-> greeting_node;
        __start__ -. &nbsp;question&nbsp; .-> 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:更适合“这个节点本身就是决策点,离开时把状态和去向一起交代清楚”
posted @ 2026-05-18 15:38  幻影之舞  阅读(6)  评论(0)    收藏  举报