LangGraph 入门到精通0x01:Graph 通讯机制

0 阅读6分钟

前言

前面了解了 LangGraph,以及写了第一个简单的 LangGraph 程序。今天继续深入学习 LangGraph,先从 LangGraph 的通讯机制开始。

Reducers

在 LangGraph 中当多个节点(Node)并行/串行修改同一状态时,Reducers 是定义“旧状态+新修改” 如何合并为新状态的纯函数。简单滴说就是一个状态的“合并规则引擎”。

LangGraph 中,我们通过 Annotated 标注预先定义“字段级合并规则”,框架自动按规则合并多节点修改。

以下面这行代码为例:

  • messages: Annotated[list[str], lambda old, new: old + new]
    • list[str]:字段类型-字符串列表
    • lambda old, new: old + new:这就是 Reducers——一个纯函数,定义了“旧状态(old)+新修改(new)”的合并逻辑(此处为“追加列表”)。
# 1. 定义状态与Reducers(关键!)
class State(TypedDict):
    messages: Annotated[list[str], lambda old, new: old + new]  # 追加合并对话
    count: Annotated[int, lambda old, new: old + new]          # 求和合并计数

Send

send 是 LangGraph 提供的动态消息触发 API。节点函数通过返回 Send 对象,主动向目标节点发送状态副本,触发其执行对应操作。其核心作用:替代静态边,实现“根据运行时数据动态选下一个节点”(比如用户意图变了,就换节点处理)。

  • 单播:返回{"send": Send(target, state)},向1个节点发送1份状态副本(类似“一对一派单”);

  • 广播:返回[Send(target, state_i) for i in N],向多个节点发送多份状态副本(类似“批量派单”)。

关于 Send 相关更多 case 详见 官方文档

State

  • messages:单播
  • jokes:广播
class State(TypedDict):
    messages: Annotated[list[str], operator.add]  # 对话消息
    jokes: Annotated[list[str], operator.add]  # 生成的笑话
    subject: Annotated[str, operator.add]

这里要特别注意:

  • query_order:这里消息发送只是发送 state 状态,并未产生新信息

  • generate_joke:这里在 Send 有新信息产生,但是 Send 新信息不会加入到 State 里,所以需要构造新State 传递

# 3. 核心节点:同时演示单播/广播(条件分支)
def demo_send(state: State) -> Union[Dict[str, Any], List[Send]]:
    last_msg = state["messages"][-1] if state["messages"] else ""

    # 🔹 单播写法:触发query_order节点
    if "订单" in last_msg:
        return {"send":
                    Send("query_order", {
                        "messages": state["messages"],
                        "jokes": state["jokes"],
                        "subject": ""})
                }  # 显式传递subject,避免字段缺失

    # 🔹 广播写法:触发generate_joke节点3次(并行)
    elif "笑话" in last_msg:
        sends = []
        for s in ["猫", "狗", "咖啡"]:
            # 复制一份 state,把 subject 写进去!
            new_state = {
                "subject": s  # 必须写在这里!
            }
            sends.append(Send("generate_joke", new_state))

        return {"send": sends}

    # 默认分支:触发通用回答
    else:
        return {"send": Send("general_answer", {"messages": state})}

节点

# 4. 单播目标节点:处理订单查询
def query_order(state: State) -> State:
    msg = state["messages"][0]
    return {
        "messages": [f"查订单结果:{msg}已发货"],
        "jokes": state.get("jokes", []),
        "subject": ""
    }

# 5. 广播目标节点:生成单个笑话
def generate_joke(state: State) -> State:
    subject = state.get("subject", "小毛蛋")
    joke = f"{subject}走进酒吧说“来杯牛奶”,老板问“变乖了?”,它说“医生说我得补钙!”"
    return {
        "messages": state.get("messages", []),
        "jokes": state.get("jokes", []) + [joke],
        "subject": ""
    }

条件边

  • 如果节点返回 {"send": [Send(), Send()]},那么条件边必须直接 return send
# 添加条件边,让demo_send的send指令驱动路由
def get_next_nodes(state):
    return state["send"]

# 配置demo_send的条件路由(关键:让Send指令生效)
builder.add_conditional_edges(
    "demo_send",
    get_next_nodes,  # 路由函数:返回要触发的目标节点
    {
        "query_order": "query_order",
        "generate_joke": "generate_joke",
        "general_answer": "general_answer"
    }
)

示例

# 示例1:单播(含“订单”)
res_single = graph.invoke({"messages": ["我的订单123没到"], "jokes": [], "subject": ""})
logger.info("=== 单播结果 ===")
logger.info("消息:" + str(res_single["messages"]))  # 输出:["我的订单123没到", "查订单结果:我的订单123没到已发货"]
logger.info("笑话:" + str(res_single["jokes"]))  # 输出:[]

# 示例2:广播(含“笑话”)
res_broadcast = graph.invoke({"messages": ["我想听笑话"], "jokes": [], "subject": ""})
logger.info("\n=== 广播结果 ===")
logger.info("消息:" + str(res_broadcast["messages"]))  # 输出:[]
logger.info("笑话:" + str(res_broadcast["jokes"]))  # 输出:3个关于猫、狗、咖啡的笑话

Running

image.png

Interrupt

Interrupt 是 LangGraph 内置的主动暂停机制。当流程执行到某个节点时,触发Interrupt冻结当前完整状态(包括所有变量、节点执行历史、上下文),返回控制权给调用方;待外部输入(如用户反馈、人工审核)到来后,再从暂停处无缝恢复执行

我们给出的 Demo 是这样一个 🌰:一个简单的 AI 自动处理 + 人类中断审核的 LangGraph 工作流。更多 case 详见 官方文档

  • interrupt():使用该函数制造中断
def conditional_human_review(state: State):
    """
    动态中断:
    只有 need_human_review = True 才会等待人类输入
    否则直接跳过
    """
    # ✅ 核心:条件中断
    if state["need_human_review"]:
        # 中断,等待人类输入
        human_input = interrupt({
            "ai_result": state["some_text"],
            "tip": "请输入修改意见(满足条件才会出现):"
        })
        return {"some_text": human_input}

    # 不满足条件 → 不中断,直接返回原内容
    logger.info("✅ 不满足中断条件,跳过人工审核")
    return state

Command

Command 是 LangGraph 框流程控制的“导航仪”。它封装了“下一步去哪、带什么数据、做什么操作”的指令,解决传统Agent“隐式流控易失控”的痛点。

  • 本质:结构体。
    • goto(目标节点名)
    • update(要修改的状态字段)
    • resume(恢复中断的节点)等关键信息,需要与 interrupt 结合使用
  • 作用:精确掌控工作流的跳转与状态变更
  • 应用场景
    • 多分支决策(如分类后跳不同节点)
    • 循环重试(如失败后跳回原节点)
    • 中断恢复(如等待用户输入后继续)。

State

class AgentState(TypedDict):
    question: str                          # 用户问题
    category: Literal["tech", "non-tech"] | None  # 分类:技术/非技术
    response: str | None                   # 最终回答
    need_interrupt: bool | None            # 是否需要中断(用于 resume 演示)

goto/update

  • Command
    • goto:跳去技术节点
    • update:更新内容
      • "category": "tech":更新分类
      • "need_interrupt": True:需要中断(演示 resume)
      • xxx:更多自定义信息
def classify_node(state: AgentState) -> Command:
    """
    核心演示:
    Command(goto=节点名) → 动态跳转
    Command(update=状态)  → 合并更新状态
    """
    question = state["question"]

    # 规则:包含代码、bug → 技术问题
    if "代码" in question or "bug" in question:
        return Command(
            goto="handle_tech",        # 跳去技术节点
            update={
                "category": "tech",    # 更新分类
                "need_interrupt": True # 需要中断(演示 resume)
            }
        )
    # 其他 → 非技术
    else:
        return Command(
            goto="handle_non_tech",
            update={
                "category": "non-tech",
                "need_interrupt": False
            }
        )

rusume

rusume 用于恢复 interrupt 带来的打断。首先需要定义 interrupt 代码。

def handle_tech(state: AgentState):
    """
    核心演示:
    interrupt() → 暂停流程
    Command(resume=xxx) → 恢复流程
    """
    # 如果需要中断,就暂停,等待人类输入
    if state["need_interrupt"]:
        # 中断!返回给前端/用户
        human_input = interrupt({
            "tip": "请输入人工修改意见",
            "ai_answer": f"已识别技术问题:{state['question']}"
        })

        # 恢复后,用人类输入更新结果
        return {
            "response": f"[技术+人工] {human_input}"
        }

    # 不需要中断 → 直接返回
    return {
        "response": f"[技术] 已处理:{state['question']}"
    }

在检测到中断,收到中断输入后再恢复中断继续执行。

# 检测到中断,才会有恢复
if result.get("__interrupt__"):
    logger.info("检测到技术问题,转人工")

    # 2. 用户输入 → 恢复流程(核心:resume)
    human_feedback = input("人工9527号修复意见:")
    resume_result = app.invoke(
        Command(resume=human_feedback),  # ✅ resume 恢复中断
        config=config
    )

    # 3. 恢复结果
    logger.info(f"🔹 恢复后最终结果:{resume_result['response']}")

更多 case 详见 官方文档

源码

github