19.3 langgraph的工作节点和路由函数

📚 第26课核心知识点梳理:工作节点与路由函数

在 LangGraph 的工作流引擎中,节点(Nodes)和边(Edges)是其最核心的两个构建块。如果将工作流比作一条流水线,State 是承载数据的托盘,Node 就是每个工位(工人从托盘取材料、加工、放回),Edge 则是连接工位的传送带。本次课程将带你深入理解这两大核心概念,并掌握如何通过路由函数实现条件控制。


一、节点(Node):工作流的基本执行单元

节点是 LangGraph 中最基本的执行单元,负责完成一个具体的子任务。每个节点本质上是一个普通的 Python 函数。

1. 节点的三大类型
节点类型 说明 典型场景
模型调用节点 封装 LLM 推理逻辑,负责调用大模型生成内容 路由决策、内容生成、意图识别
工具调用节点 集成外部 API 或数据库操作 查询天气、搜索知识库、获取实时信息
逻辑处理节点 执行纯代码逻辑,不涉及外部调用 数据转换、条件判断、结果格式化
2. 节点函数的标准签名
python 复制代码
def node_name(state: StateType) -> dict:
    """
    节点函数签名
    
    Args:
        state: 当前全局状态(只读访问,不应直接修改)
    
    Returns:
        一个表示状态更新的字典,LangGraph 会自动合并到全局 State 中
    """
    # 读取 state 中的必要信息
    user_input = state.get("messages", [])[-1].content
    
    # 执行业务逻辑(如调用 LLM 或 API)
    result = some_processing(user_input)
    
    # 返回需要更新的字段
    return {"messages": [result], "step": "completed"}
3. 节点的核心原则
  • 单一职责:每个节点只完成一个明确的任务,便于复用和维护
  • 增量更新:节点只返回需要修改的字段(partial state),而不是整个 State
  • 无状态函数:节点不应依赖或存储外部变量,所有需要的上下文都应通过 State 传递

二、边(Edge):工作流的控制纽带

边定义了节点之间的连接关系,它决定了一个节点执行完毕后,下一步该去哪个节点。LangGraph 中的边主要分为普通边和条件边两大类。

1. 普通边(静态边/固定流转)

add_edge 实现两个节点之间的固定连接,即 A 执行完毕后无条件流向 B。

python 复制代码
builder.add_edge("step_a", "step_b")   # A 执行完必走 B
builder.add_edge("step_b", "step_c")   # B 执行完必走 C
builder.add_edge("step_c", END)        # C 执行完后结束

适用场景:流程固定的顺序执行,如"数据清洗 → 特征提取 → 模型预测"。

2. 条件边(动态路由)

add_conditional_edges 实现根据当前状态(State)动态决定下一个节点。

python 复制代码
builder.add_conditional_edges(
    "classifier",           # 参数1:起点节点(从哪个节点出发)
    routing_function,       # 参数2:路由函数(决定下一步走向)
    {                       # 参数3:路径映射字典
        "intent_a": "node_a",
        "intent_b": "node_b",
        "__end__": END
    }
)

三、路由函数:控制流的决策核心

路由函数是条件边的灵魂,负责"站在十字路口指路"。

路由函数的三要素
要素 说明 示例
输入 必须接收当前 State def router(state: StateType)
输出 必须返回节点名称字符串 return "agent"return END
映射表 将返回值翻译为目标节点 {"agent": "agent", "__end__": END}
路由函数设计原则
  • 保持无状态性:路由函数不应产生副作用,只读取 State 并返回决策
  • 执行时间控制在 100ms 内:避免成为流程瓶颈
  • 使用 Literal 类型注解:限制返回值范围,防止拼写错误
python 复制代码
from typing import Literal

def should_continue(state: AgentState) -> Literal["next_node", "__end__"]:
    """路由函数:根据状态决定下一步"""
    if state.get("iterations", 0) >= 3:
        return "__end__"      # 超过3次迭代,结束
    return "next_node"        # 继续执行

四、执行时序铁律(面试必考点)

节点函数先跑,边函数后跑,边永远后置于当前节点执行,是节点完成后的后置调度逻辑。

复制代码
1. 框架选中当前节点
2. 调用节点函数 → 执行业务逻辑 → 返回状态更新
3. 当前节点执行完毕后,自动触发该节点对应的边函数(普通边或条件边)
4. 边函数计算出"下一跳节点名"
5. 判断下一跳是否为 END → 是则结束,否则以新节点重复循环

这个时序永不改变,边不会在节点执行前被调用!


🪐 星系案例:宇宙深空科研任务智能体

下面构建一个宇宙深空科研任务智能体,模拟深空探测的工作流程。该智能体根据用户的探索意图(观测/采样/分析/预警),动态路由到不同的任务处理节点。

案例整体架构图

#mermaid-svg-sf4cM7mpkGm56gYU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sf4cM7mpkGm56gYU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sf4cM7mpkGm56gYU .error-icon{fill:#552222;}#mermaid-svg-sf4cM7mpkGm56gYU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sf4cM7mpkGm56gYU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sf4cM7mpkGm56gYU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sf4cM7mpkGm56gYU .marker.cross{stroke:#333333;}#mermaid-svg-sf4cM7mpkGm56gYU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sf4cM7mpkGm56gYU p{margin:0;}#mermaid-svg-sf4cM7mpkGm56gYU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sf4cM7mpkGm56gYU .cluster-label text{fill:#333;}#mermaid-svg-sf4cM7mpkGm56gYU .cluster-label span{color:#333;}#mermaid-svg-sf4cM7mpkGm56gYU .cluster-label span p{background-color:transparent;}#mermaid-svg-sf4cM7mpkGm56gYU .label text,#mermaid-svg-sf4cM7mpkGm56gYU span{fill:#333;color:#333;}#mermaid-svg-sf4cM7mpkGm56gYU .node rect,#mermaid-svg-sf4cM7mpkGm56gYU .node circle,#mermaid-svg-sf4cM7mpkGm56gYU .node ellipse,#mermaid-svg-sf4cM7mpkGm56gYU .node polygon,#mermaid-svg-sf4cM7mpkGm56gYU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sf4cM7mpkGm56gYU .rough-node .label text,#mermaid-svg-sf4cM7mpkGm56gYU .node .label text,#mermaid-svg-sf4cM7mpkGm56gYU .image-shape .label,#mermaid-svg-sf4cM7mpkGm56gYU .icon-shape .label{text-anchor:middle;}#mermaid-svg-sf4cM7mpkGm56gYU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sf4cM7mpkGm56gYU .rough-node .label,#mermaid-svg-sf4cM7mpkGm56gYU .node .label,#mermaid-svg-sf4cM7mpkGm56gYU .image-shape .label,#mermaid-svg-sf4cM7mpkGm56gYU .icon-shape .label{text-align:center;}#mermaid-svg-sf4cM7mpkGm56gYU .node.clickable{cursor:pointer;}#mermaid-svg-sf4cM7mpkGm56gYU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sf4cM7mpkGm56gYU .arrowheadPath{fill:#333333;}#mermaid-svg-sf4cM7mpkGm56gYU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sf4cM7mpkGm56gYU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sf4cM7mpkGm56gYU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf4cM7mpkGm56gYU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sf4cM7mpkGm56gYU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf4cM7mpkGm56gYU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sf4cM7mpkGm56gYU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sf4cM7mpkGm56gYU .cluster text{fill:#333;}#mermaid-svg-sf4cM7mpkGm56gYU .cluster span{color:#333;}#mermaid-svg-sf4cM7mpkGm56gYU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sf4cM7mpkGm56gYU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sf4cM7mpkGm56gYU rect.text{fill:none;stroke-width:0;}#mermaid-svg-sf4cM7mpkGm56gYU .icon-shape,#mermaid-svg-sf4cM7mpkGm56gYU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sf4cM7mpkGm56gYU .icon-shape p,#mermaid-svg-sf4cM7mpkGm56gYU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sf4cM7mpkGm56gYU .icon-shape .label rect,#mermaid-svg-sf4cM7mpkGm56gYU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sf4cM7mpkGm56gYU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sf4cM7mpkGm56gYU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sf4cM7mpkGm56gYU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 观测任务
采样任务
分析任务
预警任务
未知指令
START
agent_node: 分析指令
route_decision
observe_node
sample_node
analyze_node
alert_node
unknown_node
log_node
format_node
END

完整代码

python 复制代码
import os
import sys
import asyncio
from typing import TypedDict, Annotated, Literal
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END, add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
from operator import add

load_dotenv()

if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

llm = ChatOpenAI(
    model="qwen-plus",
    temperature=0.7,
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

# ========== 1. 定义 State(工作流中的数据容器) ==========
class SpaceState(TypedDict):
    # 对话历史 - add_messages 负责追加合并
    messages: Annotated[list[BaseMessage], add_messages]
    
    # 任务执行日志 - 使用 operator.add 实现列表累积
    task_log: Annotated[list[str], add]
    
    # 任务状态 - 无 reducer,默认覆盖
    current_task: str      # 当前任务类型
    target_object: str     # 探测目标(如"仙女座星系"、"黑洞")
    processing_status: str # 处理状态(进行中/已完成/失败)
    final_result: str      # 最终结果

# ========== 2. 节点定义(每个节点是独立的工作单元) ==========

def agent_node(state: SpaceState):
    """智能体节点:解析用户指令,提取任务目标和类型"""
    last_msg = state["messages"][-1].content
    updates = {}
    
    # 提取目标天体/星系
    for target in ["仙女座星系", "猎户座星云", "黑洞", "白矮星", "超新星"]:
        if target in last_msg:
            updates["target_object"] = target
            print(f"🎯 锁定目标: {target}")
            break
    
    # 识别任务类型(关键:这里决定了路由走向)
    if "观测" in last_msg or "observe" in last_msg:
        updates["current_task"] = "observe"
        updates["task_log"] = ["🎯 已解析任务类型: 天文观测"]
    elif "采样" in last_msg or "sample" in last_msg:
        updates["current_task"] = "sample"
        updates["task_log"] = ["🧪 已解析任务类型: 样本采集"]
    elif "分析" in last_msg or "analyze" in last_msg:
        updates["current_task"] = "analyze"
        updates["task_log"] = ["📊 已解析任务类型: 光谱分析"]
    elif "预警" in last_msg or "warning" in last_msg:
        updates["current_task"] = "alert"
        updates["task_log"] = ["⚠️ 已解析任务类型: 深空预警"]
    else:
        updates["current_task"] = "unknown"
        updates["task_log"] = ["❓ 无法识别任务类型,进入帮助模式"]
    
    updates["processing_status"] = "analyzing"
    return updates

def observe_node(state: SpaceState):
    """观测节点:执行天文观测任务"""
    target = state.get("target_object", "目标天体")
    print(f"🔭 正在对 {target} 执行天文观测...")
    
    prompt = f"作为天文学家,请详细描述对{target}进行天文观测的主要步骤、所需设备和预期发现。"
    response = llm.invoke([HumanMessage(content=prompt)])
    
    return {
        "messages": [response],
        "task_log": [f"🔭 观测任务已完成,目标: {target}"],
        "processing_status": "completed"
    }

def sample_node(state: SpaceState):
    """采样节点:执行样本采集任务"""
    target = state.get("target_object", "目标天体")
    print(f"🛸 准备对 {target} 执行样本采集...")
    
    prompt = f"作为航天工程师,请详细说明在{target}执行样本采集任务的技术方案、仪器设备和操作流程。"
    response = llm.invoke([HumanMessage(content=prompt)])
    
    return {
        "messages": [response],
        "task_log": [f"🧪 样本采集任务已完成,目标: {target}"],
        "processing_status": "completed"
    }

def analyze_node(state: SpaceState):
    """分析节点:执行光谱分析任务"""
    target = state.get("target_object", "目标天体")
    print(f"📈 正在对 {target} 进行光谱数据分析...")
    
    prompt = f"作为天体物理学家,请分析{target}的光谱数据,解释其主要化学成分和物理特性。"
    response = llm.invoke([HumanMessage(content=prompt)])
    
    return {
        "messages": [response],
        "task_log": [f"📊 光谱分析任务已完成,目标: {target}"],
        "processing_status": "completed"
    }

def alert_node(state: SpaceState):
    """预警节点:执行深空预警任务"""
    target = state.get("target_object", "异常信号源")
    print(f"🚨 正在对 {target} 生成深空预警报告...")
    
    prompt = f"作为空间安全专家,请根据{target}的观测数据,评估其风险等级并生成预警报告。"
    response = llm.invoke([HumanMessage(content=prompt)])
    
    return {
        "messages": [response],
        "task_log": [f"⚠️ 深空预警已完成,目标: {target}"],
        "processing_status": "completed"
    }

def unknown_node(state: SpaceState):
    """未知指令节点:处理无法识别的任务"""
    response = AIMessage(content="抱歉,未能识别您的任务指令。请尝试包含以下关键词:观测、采样、分析、预警")
    return {
        "messages": [response],
        "task_log": ["❓ 已进入帮助模式,等待用户重新输入"],
        "processing_status": "completed"
    }

def log_node(state: SpaceState):
    """日志节点:记录任务执行摘要"""
    print(f"📝 [日志节点] 任务类型: {state.get('current_task')}, 状态: {state.get('processing_status')}")
    # 不修改状态,仅用于日志记录
    return {}

def format_node(state: SpaceState):
    """格式化节点:组装最终输出"""
    result = state.get("final_result", "")
    if not result and state.get("messages"):
        # 如果还没有 final_result,从 messages 中提取最新回复
        for msg in reversed(state["messages"]):
            if isinstance(msg, AIMessage) and msg.content:
                return {"final_result": msg.content}
    return {"final_result": result or "任务已完成"}

# ========== 3. 路由函数(条件边的核心) ==========

def route_decision(state: SpaceState) -> Literal["observe", "sample", "analyze", "alert", "unknown", "log"]:
    """
    路由函数:根据 current_task 字段决定下一个节点
    这是 LangGraph 动态路由的核心实现
    """
    task = state.get("current_task", "unknown")
    
    # 路由映射:读取 State 中的任务类型,返回对应的目标节点名
    route_map = {
        "observe": "observe",
        "sample": "sample",
        "analyze": "analyze",
        "alert": "alert",
    }
    
    if task in route_map:
        return route_map[task]   # 正常任务路由到对应处理节点
    return "unknown"             # 未知任务路由到帮助节点

def route_after_task(state: SpaceState) -> Literal["log", "__end__"]:
    """任务执行后的路由:固定流向日志节点(体现 add_edge 的优势)"""
    return "log"

def route_after_log(state: SpaceState) -> Literal["format", "__end__"]:
    """日志节点后的路由:固定流向格式化节点,然后结束"""
    return "format"

# ========== 4. 构建图(用边连接节点,形成工作流) ==========

checkpointer = MemorySaver()
builder = StateGraph(SpaceState)

# 注册节点
builder.add_node("agent", agent_node)
builder.add_node("observe", observe_node)
builder.add_node("sample", sample_node)
builder.add_node("analyze", analyze_node)
builder.add_node("alert", alert_node)
builder.add_node("unknown", unknown_node)
builder.add_node("log", log_node)
builder.add_node("format", format_node)

# 设置入口
builder.set_entry_point("agent")

# 条件边:agent 节点后,由 route_decision 决定走向
builder.add_conditional_edges(
    "agent",
    route_decision,
    {
        "observe": "observe",
        "sample": "sample",
        "analyze": "analyze",
        "alert": "alert",
        "unknown": "unknown"
    }
)

# 普通边:各任务节点执行完后固定走向 log 节点
builder.add_edge("observe", "log")
builder.add_edge("sample", "log")
builder.add_edge("analyze", "log")
builder.add_edge("alert", "log")
builder.add_edge("unknown", "log")

# 固定流向:log → format → END
builder.add_edge("log", "format")
builder.add_edge("format", END)

# 编译图
graph = builder.compile(checkpointer=checkpointer)

# ========== 5. 运行测试 ==========

async def main():
    print("🪐 欢迎来到宇宙深空科研任务智能体")
    print("=" * 60)
    
    thread_id = input("请输入会话 ID: ").strip() or "space_001"
    config = {"configurable": {"thread_id": thread_id}}
    
    print("\n💡 提示:请输入您的科研任务指令")
    print("   示例:观测仙女座星系 / 采集黑洞样本 / 分析白矮星光谱 / 预警超新星风险\n")
    
    while True:
        user_input = input("请输入任务指令: ")
        if user_input.lower() in ["quit", "exit"]:
            break
        
        # 执行工作流
        result = graph.invoke(
            {"messages": [HumanMessage(content=user_input)]},
            config=config
        )
        
        # 输出最终结果
        print(f"\n✅ 任务完成")
        print(f"📋 最终结果: {result.get('final_result', '无结果')}\n")
        print(f"📊 任务日志摘要: {result.get('task_log', [])}")
        print("=" * 60)

if __name__ == "__main__":
    asyncio.run(main())

好的!下面给出在 PyCharm 中运行这段"宇宙深空科研任务智能体"代码的详细交互步骤 ,帮助你充分验证节点路由条件边状态管理等核心知识点。


🧪 测试步骤(按顺序执行)

准备工作

  • 确保 .env 文件中已配置阿里云百炼 API Key。

  • 运行代码,程序会提示:

    复制代码
    请输入会话 ID: 

    你可以输入任意 ID,例如 space_test,或者直接回车使用默认值 space_001。 同一个 ID 可以跨多次运行恢复对话历史(Checkpointer 记忆),但本测试侧重路由演示,输入任意 ID 均可。


第 1 步:观测任务(测试观测节点)

输入:

复制代码
观测仙女座星系

预期行为:

  • agent_node 解析出 current_task = "observe"target_object = "仙女座星系"
  • 条件边 route_decision 返回 "observe" → 进入 observe_node
  • observe_node 调用 LLM 生成观测方案。
  • 经过 log_nodeformat_node,最终输出。

控制台输出特征:

复制代码
🎯 锁定目标: 仙女座星系
🔭 正在对 仙女座星系 执行天文观测...
✅ 任务完成
📋 最终结果: (LLM 生成的详细观测步骤)
📊 任务日志摘要: ['🎯 已解析任务类型: 天文观测', '🔭 观测任务已完成,目标: 仙女座星系']

第 2 步:采样任务(测试采样节点)

输入:

复制代码
采集黑洞样本

预期行为:

  • current_task = "sample" → 路由到 sample_node

输出特征:

复制代码
🎯 锁定目标: 黑洞
🛸 准备对 黑洞 执行样本采集...
✅ 任务完成
📋 最终结果: (LLM 生成的采样方案)
📊 任务日志摘要: ['🧪 已解析任务类型: 样本采集', '🧪 样本采集任务已完成,目标: 黑洞']

第 3 步:分析任务(测试分析节点)

输入:

复制代码
分析白矮星光谱

预期行为:

  • current_task = "analyze" → 路由到 analyze_node

输出特征:

复制代码
🎯 锁定目标: 白矮星
📈 正在对 白矮星 进行光谱数据分析...
✅ 任务完成
📋 最终结果: (LLM 生成的光谱分析报告)
📊 任务日志摘要: ['📊 已解析任务类型: 光谱分析', '📊 光谱分析任务已完成,目标: 白矮星']

第 4 步:预警任务(测试预警节点)

输入:

复制代码
预警超新星风险

预期行为:

  • current_task = "alert" → 路由到 alert_node

输出特征:

复制代码
🎯 锁定目标: 超新星
🚨 正在对 超新星 生成深空预警报告...
✅ 任务完成
📋 最终结果: (LLM 生成的预警报告)
📊 任务日志摘要: ['⚠️ 已解析任务类型: 深空预警', '⚠️ 深空预警已完成,目标: 超新星']

第 5 步:未知指令(测试 unknown 节点)

输入:

复制代码
探测月球

注意探测 不属于预设任务关键词,且目标 月球 不在预设目标列表中(预设:仙女座星系、猎户座星云、黑洞、白矮星、超新星)。

但目标提取失败不影响任务类型识别。因为 current_task 仍然是 unknown(没有"观测/采样/分析/预警"关键词),因此路由到 unknown_node

输出特征:

复制代码
❓ 无法识别任务类型,进入帮助模式
✅ 任务完成
📋 最终结果: 抱歉,未能识别您的任务指令。请尝试包含以下关键词:观测、采样、分析、预警

第 6 步:多轮对话(验证 State 的持久化与累加)

继续在同一会话中输入指令(使用相同的 thread_id)。

先输入:

复制代码
我叫小明,我喜欢摄氏度,观测猎户座星云

此时会记录 user_name但是 注意当前 State 中没有定义 user_name 字段,所以不会记录。只是为了演示 add 归约器对 task_log 的累积效果。

输出特征:

复制代码
🎯 锁定目标: 猎户座星云
🔭 正在对 猎户座星云 执行天文观测...
✅ 任务完成
📊 任务日志摘要: [
    '🎯 已解析任务类型: 天文观测', 
    '🔭 观测任务已完成,目标: 猎户座星云'
]

再输入:

复制代码
采样黑洞

输出特征:

复制代码
🎯 锁定目标: 黑洞
🛸 准备对 黑洞 执行样本采集...
✅ 任务完成
📊 任务日志摘要: [
    '🎯 已解析任务类型: 天文观测',      # 第一次任务的日志
    '🔭 观测任务已完成,目标: 猎户座星云',
    '🧪 已解析任务类型: 样本采集',      # 第二次任务的日志追加
    '🧪 样本采集任务已完成,目标: 黑洞'
]

验证点task_log 列表不断累积,体现了 add 归约器的效果。


📝 完整输入顺序参考表

步骤 输入指令 预期路由节点 关键验证点
1 观测仙女座星系 observe_node 路由到观测节点,日志含"天文观测"
2 采集黑洞样本 sample_node 路由到采样节点,日志含"样本采集"
3 分析白矮星光谱 analyze_node 路由到分析节点,日志含"光谱分析"
4 预警超新星风险 alert_node 路由到预警节点,日志含"深空预警"
5 探测月球 unknown_node 路由到未知节点,返回帮助消息
6 观测猎户座星云 observe_node 查看 task_log 累积
7 采样黑洞 sample_node 再次查看 task_log 追加效果

🔧 调试技巧

如果你想更直观地看到路由决策过程,可以在 route_decision 函数开头加一行打印:

python 复制代码
def route_decision(state: SpaceState):
    print(f"[DEBUG] current_task = {state.get('current_task')}")
    ...

这样每次路由时控制台会输出当前任务类型,便于跟踪分支走向。

按照以上步骤逐一测试,你就能完整掌握节点和条件边的实际工作方式。如果遇到任何输出不符合预期,请检查 API Key 是否正确,或 LLM 是否正常响应。

🎯 高频面试题汇总

一、基础概念题(必考)

面试问题 专业回答要点
LangGraph 中的 Node 和 Edge 分别扮演什么角色? Node 是业务逻辑的载体,代表工作流中的执行单元 ;Edge 是流程控制的载体,代表节点间的连接关系。Node 负责"做什么"(调用 LLM、查询数据库、数据处理),Edge 负责"下一步做什么"(静态流转或动态分支)。
普通边和条件边有什么区别? 普通边(add_edge)是固定连接 ,A 执行完必定走向 B。条件边(add_conditional_edges)是动态连接,通过路由函数根据当前 State 动态决定下一步,支持分支和循环。
路由函数的输入输出是什么?有什么设计约束? 输入必须是当前 State,输出必须是代表目标节点名称的字符串(或 END)。约束:必须使用 Literal 类型注解限制返回值范围;保持无状态,不产生副作用;执行时间控制在 100ms 内。
LangGraph 的执行时序是怎样的?为什么理解它很重要? 节点函数先跑,边函数后跑,边是节点完成后的后置调度逻辑。这一时序永不改变。理解它能避免常见设计错误,确保节点依赖状态正确传递。

二、进阶实战题(拉开差距的关键)

面试问题 专业回答要点
LangGraph 与 LangChain 的 Chain 模式相比,解决了什么核心问题? Chain 模式本质是线性有向无环图(DAG),难以处理循环、分支和并发。LangGraph 通过 Node + Edge 显式建模控制流,支持循环 (Agent 可多次调用工具)、条件分支 (根据不同意图分流)、并行处理(Send API 实现扇出并行)。其核心突破在于状态图对复杂流程的表达能力。
如何设计一个可维护的节点?请给出具体原则。 单一职责 :每个节点只完成一个明确任务;② 增量更新 :只返回需要修改的 State 字段;③ 函数式无状态 :不在节点内存储状态,所有依赖通过 State 传递;④ 错误隔离 :独立 try-except,避免单点故障扩散;⑤ 命名清晰:节点名称反映其功能。
如何在条件边中实现多分支路由?请举例说明。 条件边默认支持单返回值。实现多分支:让路由函数返回分支标识符(如 "branch_a"),在映射字典中配置多个分支目标;或在节点内使用 Send API 返回 [Send("node1"), Send("node2")] 列表,框架会并行执行多个目标节点,并用 Reducer 合并结果。
条件边和 Send API 实现并行分发有什么区别?各自适用什么场景? 条件边:路由函数返回 单一 目标节点,适用于单路径决策;Send API:可返回 Send 对象列表,框架对列表中每个对象创建并行任务,适用于动态扇出并行(如同时处理多个订单)。
LangGraph 工作流中如何实现"错误处理"和"失败重试"? ① 在条件边中增加错误状态分支:route_after_task 检测到错误时返回 "error_node";② 在节点内使用 try-except,返回 {"status": "error", "retry_count": state.get("retry_count", 0) + 1};③ 使用 RunnableConfig 中的 max_concurrencyrecursion_limit 参数控制重试机制。
节点与状态(State)的交互中,什么是"增量更新"?它为什么重要? 增量更新指节点只返回需要修改的 State 字段(partial state),而不是整个 State 对象。它重要是因为:① 减少冗余数据传输,提高性能;② 支持多个节点并发执行时通过 Reducer 合并更新,避免冲突;③ 保持节点职责单一。

三、生产环境经验题(工程能力展示)

面试问题 专业回答要点
在实际项目中,如何验证和调试 LangGraph 工作流? ① 使用 graph.get_graph().draw_ascii() 打印 ASCII 图可视化流程;② 启用 stream_mode="values" 观察节点执行过程;③ 集成 LangSmith 追踪完整执行轨迹;④ 使用 Checkpointer 实现状态快照回溯调试;⑤ 为关键节点添加结构化日志,记录 State 的关键字段变化。
当工作流包含大量节点时,如何优化性能? 并行化 :使用 Send API 将独立任务并行执行;② 缓存 :为纯逻辑节点配置缓存策略;③ 懒加载 :按需加载大模型实例;④ 分治 :将子工作流封装为子图,降低主图复杂度;⑤ 异步优先 :对于 I/O 密集型节点,使用异步版本(async def)提升并发能力。
条件边的 return mapping 不写会怎样?有什么风险? 不写 mapping 时,LangGraph 会自动将路由函数的返回值作为目标节点名进行跳转 。风险:① 缺乏类型校验,字符串拼写错误会导致"未知节点"错误;② 难以支持多路动态路由;③ 代码可读性和可维护性下降。最佳实践 :始终提供显式的 mapping 字典并用 Literal 注解返回值。

四、综合设计题

面试问题 专业回答要点
请设计一个跨多节点的工作流,要求包含普通边、条件边、Checkpoint 持久化和错误处理。 示例:医疗诊断 Agent。State :包含 patient_symptomsdiagnosis_resulttreatment_planerror_flag 字段。节点symptom_analysis(LLM 分析症状)、disease_matching(规则匹配)、treatment_generationerror_handler普通边disease_matching → treatment_generation条件边symptom_analysis 后根据置信度判断走向 disease_matchingask_more_infoCheckpointerPostgresSaver 持久化每个会话的 thread_id错误处理 :条件边检测 error_flag 为 True 时路由到 error_handler 节点。
当节点需要从多个数据源聚合结果时,LangGraph 提供了哪些机制? Reducer 机制 :为列表字段配置 operator.add,多个并行节点的返回结果自动合并;② Send API 并行分发 :在节点内返回 Send 列表,框架并行执行多任务,用 Reducer 汇总;③ 子图(Subgraph):将复杂逻辑封装为子图,在主图中作为单个节点调用,内部维护独立状态。

💡 学习收获总结

通过本节课的学习,你已经能够掌握:

  1. Node 的本质:一个处理 State、返回部分更新的函数------没有副作用,不关心被谁调用
  2. Edge 的本质:一个决定"下一步做什么"的函数------节点执行完毕后的后置逻辑
  3. 条件边的本质:将路由决策权交给 State 中的动态数据,让工作流具备"看情况决定"的能力
  4. 路由函数的本质:普通的 Python 函数,接收 State,返回节点名称------纯粹的输入输出逻辑
  5. 工作流构建公式State(数据容器)+ Node(执行单元)+ Edge(流程控制)= Graph(工作流图)
相关推荐
梓䈑1 小时前
C++ 接入 SQLite 数据库:环境搭建、API 详解 与 两种执行方式对比
数据库·c++·sqlite
曹牧1 小时前
Oracle:CHR
数据库·oracle
小小小小宇1 小时前
TypeScript类型体操
前端
TechWayfarer1 小时前
IP精准定位服务在保险行业的接入实践:区域需求洞察与精准服务
数据库·python·tcp/ip·flask
KKKlucifer1 小时前
数据分类分级产品排名解析:场景定制、规则联动、增量更新成核心能力
大数据·数据库·人工智能
minji...1 小时前
MySQL数据库 (七) MySQL表的基本查询(上),insert、replace、select、where、order by
数据库·mysql·select·replace·insert·order by·where
SWAGGY..1 小时前
Linux系统编程:(十三)环境变量
java·linux·算法
金融RPA机器人丨实在智能1 小时前
数据库运维Agent比价指南:国产自研产品适配国产数据库兼容性更好吗?
运维·数据库·人工智能·ai
IT策士1 小时前
Redis 从入门到精通:持久化RDB 与 AOF
数据库·redis·缓存