📚 第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_node和format_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_concurrency、recursion_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_symptoms、diagnosis_result、treatment_plan、error_flag 字段。节点 :symptom_analysis(LLM 分析症状)、disease_matching(规则匹配)、treatment_generation、error_handler。普通边 :disease_matching → treatment_generation。条件边 :symptom_analysis 后根据置信度判断走向 disease_matching 或 ask_more_info。Checkpointer :PostgresSaver 持久化每个会话的 thread_id。错误处理 :条件边检测 error_flag 为 True 时路由到 error_handler 节点。 |
| 当节点需要从多个数据源聚合结果时,LangGraph 提供了哪些机制? | ① Reducer 机制 :为列表字段配置 operator.add,多个并行节点的返回结果自动合并;② Send API 并行分发 :在节点内返回 Send 列表,框架并行执行多任务,用 Reducer 汇总;③ 子图(Subgraph):将复杂逻辑封装为子图,在主图中作为单个节点调用,内部维护独立状态。 |
💡 学习收获总结
通过本节课的学习,你已经能够掌握:
- Node 的本质:一个处理 State、返回部分更新的函数------没有副作用,不关心被谁调用
- Edge 的本质:一个决定"下一步做什么"的函数------节点执行完毕后的后置逻辑
- 条件边的本质:将路由决策权交给 State 中的动态数据,让工作流具备"看情况决定"的能力
- 路由函数的本质:普通的 Python 函数,接收 State,返回节点名称------纯粹的输入输出逻辑
- 工作流构建公式 :
State(数据容器)+Node(执行单元)+Edge(流程控制)=Graph(工作流图)