第 04 期 | 寻路连线 (Edges) 与终止符 (END)
🎯 本期学习目标
各位未来的 AI 架构师们,欢迎回到我们的《LangGraph 多智能体专家课》!上期我们搭建了 Agent 的“骨架”——节点 (Nodes),但光有骨架不行啊,还得有“经络”把它们连起来,让能量(数据)在其中流动。本期,我们将深入 LangGraph 的核心——寻路连线 (Edges) 和终止符 (END),学完你将:
- 掌握 LangGraph 中
add_edge的两种基本类型:学会如何构建固定流转和基于条件的动态流转,让你的 Agent 协作不再是简单的“排队”。 - 精通
add_conditional_edges的使用:理解其背后的决策逻辑,并能为我们的 AI 内容创作机构实现基于业务规则的智能流程分发。 - 理解
set_entry_point和set_finish_point(或END) 的精髓:构建一个有始有终、逻辑严谨的 Agent 工作流,不再让你的程序“无疾而终”。 - 将复杂的现实业务逻辑精准映射到 LangGraph 的寻路连线中:提升你将抽象需求转化为可执行代码的架构设计能力。
📖 原理解析
想象一下,你正在设计一个高度自动化的工厂,每个车间(Node)都有其独特的功能。那么,如何决定产品从哪个车间进入,经过哪些车间加工,最终又从哪个出口运出呢?这就是 Edges 和 END 要解决的问题。
在 LangGraph 里,节点 (Nodes) 是你定义好的 Agent 或工具函数,它们是工作流中的一个个“站点”。而 寻路连线 (Edges),顾名思义,就是连接这些站点的“路径”或“轨道”。它们定义了数据和控制流如何从一个节点转移到另一个节点。
1. 从 START 到 END:流程的生命周期
一个完整的 LangGraph 工作流,必然有起点和终点:
set_entry_point(node_name): 这是你工作流的“大门”。所有任务都从这里开始。就像你进入地铁站,总得有个入口吧?END: 这是你工作流的“终点站”。当流程走到这里,意味着任务已完成,计算停止。它是一个特殊的 LangGraph 关键字,明确地告诉系统:“嘿,伙计们,活儿干完了,收工!”
2. 寻路连线 (Edges):流程的动态骨架
Edges 分为两种核心类型:
a) 固定连线 (add_edge)
最简单直接的方式。如果你确定节点 A 执行完后,总是要将控制权和状态传递给节点 B,那就用 add_edge。
- 语法:
graph.add_edge("源节点名称", "目标节点名称") - 应用场景: 线性、顺序执行的步骤。比如:
Researcher完成研究后,一定要将结果交给Writer。
b) 条件连线 (add_conditional_edges)
这才是 LangGraph 真正强大的地方!它允许你的工作流根据当前的状态 (State) 做出决策,从而动态地选择下一个执行的节点。这就是我们 AI 内容创作机构中,Planner 智能决策的关键。
- 语法:
graph.add_conditional_edges("源节点名称", condition_function, mapping_dict)源节点名称: 哪个节点执行完后需要做决策?condition_function: 这是一个 Python 函数,它接收当前的state作为输入,并返回一个字符串(或字符串列表)。这个字符串就是你的“决策结果”。mapping_dict: 这是一个字典,将condition_function返回的决策结果映射到具体的“目标节点名称”。例如,{"research_needed": "researcher", "no_research": "writer"}。
- 应用场景: 任何需要根据业务逻辑动态调整流程的场景。比如:
Planner评估内容需求,决定是否需要Researcher介入。Editor检查初稿,决定是直接END发布,还是退回给Writer修改。
3. Mermaid 图解核心概念
让我们用一张图来直观感受一下我们 AI 内容创作机构的简化流程,特别是 Planner 如何通过条件连线决定走向:
graph TD
A[START] --> B(Planner Agent)
B -- "需要研究?" --> C{条件判断: research_needed?}
C -- "是 (True)" --> D(Researcher Agent)
C -- "否 (False)" --> E(Writer Agent)
D --> E
E --> F(Editor Agent)
F --> G[END]
style A fill:#D4EDDA,stroke:#28A745,stroke-width:2px,color:#28A745
style G fill:#F8D7DA,stroke:#DC3545,stroke-width:2px,color:#DC3545
style B fill:#E0F2F7,stroke:#17A2B8,stroke-width:2px,color:#17A2B8
style D fill:#FFF3CD,stroke:#FFC107,stroke-width:2px,color:#FFC107
style E fill:#D1ECF1,stroke:#007BFF,stroke-width:2px,color:#007BFF
style F fill:#FEEBFD,stroke:#E83E8C,stroke-width:2px,color:#E83E8C
style C fill:#FFE5CC,stroke:#FD7E14,stroke-width:2px,color:#FD7E14图解分析:
START:任务从这里启动。Planner Agent:这是我们的入口节点,它会接收初始请求并进行初步规划。条件判断: research_needed?:这就是Planner节点执行完后,我们将使用的condition_function。它会检查Planner在state中标记的research_needed字段。- 条件分支:
- 如果
research_needed为True,流程就导向Researcher Agent。 - 如果
research_needed为False,流程直接跳过Researcher,导向Writer Agent。
- 如果
Researcher Agent:执行研究任务。Writer Agent:执行内容撰写任务。无论是否经过Researcher,最终都会到达Writer。Editor Agent:执行内容编辑任务。END:任务最终完成。
看,这不就是把我们“AI 内容创作机构”的业务逻辑,用 LangGraph 的语言翻译了一遍吗?清晰、直观、可控!
💻 实战代码演练 (Agency项目中的具体应用)
好了,理论说得再多,不如上手敲代码。现在,我们将把上述的业务流程图,用 LangGraph 的代码实现出来。
项目背景回顾: 我们的 AI 内容创作机构接到一个内容创作请求。Planner 智能体首先评估这个请求,判断是否需要进行深入研究。如果需要,则将任务交给 Researcher;如果不需要,则直接交给 Writer。最终,内容由 Editor 完成审校后,整个流程结束。
我们将使用 LangGraph 的 StateGraph 来构建这个流程。
import operator
from typing import TypedDict, Annotated, List, Union
from langchain_core.messages import BaseMessage
from langchain_core.agents import AgentFinish
from langgraph.graph import StateGraph, END
from pprint import pprint # 用于美观地打印状态
# --- 1. 定义我们工作流的共享状态 (State) ---
# 这是所有 Agent 之间传递和共享信息的“画布”
class AgentState(TypedDict):
"""
代表多智能体协作中共享状态的 TypedDict。
任何智能体或工具都可以读取和更新此状态。
"""
topic: str # 内容创作的主题
research_needed: bool # 规划者判断是否需要研究
research_content: Annotated[str, operator.add] # 研究内容,使用 operator.add 进行聚合
draft_content: Annotated[str, operator.add] # 初稿内容,使用 operator.add 进行聚合
final_content: Annotated[str, operator.add] # 最终内容,使用 operator.add 进行聚合
# messages: Annotated[List[BaseMessage], operator.add] # 消息列表,用于Agent间的通信,本期暂时简化不使用
# --- 2. 定义各个智能体 (Agent) 作为节点 (Nodes) ---
# 为了本期聚焦 Edges 和 END,我们将 Agent 简化为普通的 Python 函数,
# 它们接收当前状态,模拟执行任务,并返回更新后的状态。
def planner_node(state: AgentState) -> AgentState:
"""
规划者智能体:根据主题判断是否需要研究,并生成初步指令。
"""
print("\n--- Planner Agent (规划者) 正在执行 ---")
topic = state["topic"]
# 模拟 Planner 的复杂决策逻辑
# 假设如果主题包含“深入”、“前沿”、“分析”等关键词,则需要研究
if "深入" in topic or "前沿" in topic or "分析" in topic:
research_needed = True
print(f"主题 '{topic}' 复杂,需要深入研究。")
else:
research_needed = False
print(f"主题 '{topic}' 相对简单,无需专门研究。")
# 更新状态
return {
"research_needed": research_needed,
"draft_content": f"Planner: 针对主题 '{topic}' 的初步构思。\n"
}
def researcher_node(state: AgentState) -> AgentState:
"""
研究者智能体:根据主题进行研究,并提供研究内容。
"""
print("\n--- Researcher Agent (研究者) 正在执行 ---")
topic = state["topic"]
# 模拟研究过程
simulated_research = f"Researcher: 为主题 '{topic}' 收集了以下关键信息:\n" \
f"- 事实1:关于 {topic} 的最新进展。\n" \
f"- 事实2:行业专家对 {topic} 的观点。\n" \
f"- 事实3:相关数据和统计。\n"
print("研究完成。")
# 更新状态
return {"research_content": simulated_research}
def writer_node(state: AgentState) -> AgentState:
"""
作者智能体:根据规划和研究内容撰写初稿。
"""
print("\n--- Writer Agent (作者) 正在执行 ---")
topic = state["topic"]
planner_draft = state["draft_content"]
research_content = state.get("research_content", "无研究内容。") # 如果没有研究者,则为空
# 模拟写作过程
full_draft = f"{planner_draft}\n" \
f"Writer: 整合了以下信息进行撰写:\n" \
f"--- 研究参考 ---\n{research_content}\n" \
f"--- 初稿正文 ---\n" \
f"尊敬的读者,欢迎阅读关于 '{topic}' 的文章...\n" \
f"这是文章的核心观点和初步阐述。\n" \
f"期待编辑的反馈。\n"
print("初稿撰写完成。")
# 更新状态
return {"draft_content": full_draft}
def editor_node(state: AgentState) -> AgentState:
"""
编辑者智能体:审校初稿,生成最终内容。
"""
print("\n--- Editor Agent (编辑者) 正在执行 ---")
draft_content = state["draft_content"]
# 模拟编辑过程
final_version = f"Editor: 对以下初稿进行了润色和校对。\n" \
f"========== 原始初稿 ==========\n{draft_content}\n" \
f"========== 最终版本 ==========\n" \
f"经过精心编辑,这篇关于 '{state['topic']}' 的文章已准备就绪,质量上乘,表达流畅。\n" \
f"确保了信息准确性和阅读体验。\n"
print("编辑审校完成,内容可发布。")
# 更新状态
return {"final_content": final_version}
# --- 3. 定义条件判断函数 (Condition Function) ---
# 这是 LangGraph 决定流程走向的关键!
def should_research(state: AgentState) -> str:
"""
根据 Planner 智能体在状态中设置的 'research_needed' 标志,
决定下一步是去 'researcher' 还是直接去 'writer'。
"""
print("\n--- 流程判断: 是否需要研究?---")
if state["research_needed"]:
print("判断结果:需要研究,将转向 Researcher Agent。")
return "research" # 返回一个字符串,与 mapping_dict 的键对应
else:
print("判断结果:无需研究,将转向 Writer Agent。")
return "write" # 返回一个字符串,与 mapping_dict 的键对应
# --- 4. 构建 LangGraph 工作流 ---
workflow = StateGraph(AgentState)
# 添加节点到图 (对应上期的内容)
workflow.add_node("planner", planner_node)
workflow.add_node("researcher", researcher_node)
workflow.add_node("writer", writer_node)
workflow.add_node("editor", editor_node)
# 设置入口点 (Entry Point)
# 所有的内容创作请求都从 planner 智能体开始
workflow.set_entry_point("planner")
# 添加条件连线 (Conditional Edges)
# 这是本期的核心!planner 执行完后,根据 should_research 函数的返回结果,
# 决定是连接到 researcher 还是 writer。
workflow.add_conditional_edges(
"planner", # 源节点:planner 执行完后做决策
should_research, # 条件函数:根据状态判断走向
{
"research": "researcher", # 如果 should_research 返回 "research",则去 researcher 节点
"write": "writer" # 如果 should_research 返回 "write",则去 writer 节点
}
)
# 添加固定连线 (Fixed Edges)
# researcher 完成后,总是将结果传递给 writer
workflow.add_edge("researcher", "writer")
# writer 完成后,总是将初稿传递给 editor
workflow.add_edge("writer", "editor")
# 设置终止点 (Finish Point)
# editor 完成后,整个内容创作流程结束
workflow.add_edge("editor", END) # 使用 END 关键字明确表示流程终止
# 编译工作流
app = workflow.compile()
# --- 5. 运行工作流并观察结果 ---
print("\n========== 运行场景 1: 需要深入研究的主题 ==========")
initial_state_1 = {"topic": "深入探讨大语言模型在多智能体协作中的前沿应用",
"research_needed": False, # Planner 会覆盖这个初始值
"research_content": "",
"draft_content": "",
"final_content": ""}
final_state_1 = app.invoke(initial_state_1)
print("\n--- 场景 1 最终状态 ---")
pprint(final_state_1)
print("\n----------------------------------------------------\n")
print("\n========== 运行场景 2: 相对简单的主题 ==========")
initial_state_2 = {"topic": "如何用Python实现一个简单的Web服务器",
"research_needed": False, # Planner 会覆盖这个初始值
"research_content": "",
"draft_content": "",
"final_content": ""}
final_state_2 = app.invoke(initial_state_2)
print("\n--- 场景 2 最终状态 ---")
pprint(final_state_2)
print("\n----------------------------------------------------\n")
# 可以将图可视化出来,帮助理解
# try:
# from IPython.display import Image, display
# display(Image(app.get_graph().draw_png()))
# except Exception as e:
# print(f"无法生成可视化图表:{e}")
# print("请确保安装了 'graphviz' 库和其系统依赖。")
代码解析:
AgentState: 我们定义了一个TypedDict来作为我们工作流的共享状态。Annotated和operator.add是 LangGraph 中处理状态聚合的强大机制,这里我们用它来模拟内容的逐步累加。*_node函数: 这就是我们的 Agent 节点。为了简化,它们只是普通的 Python 函数,接收state,打印一些模拟信息,然后返回一个更新state的字典。在实际项目中,这些会是调用 LLM 或工具的复杂 Agent 类。should_research函数: 这是本期的核心条件判断函数! 它接收当前的AgentState,并根据Planner智能体设置的research_needed字段返回"research"或"write"。这个返回值将作为add_conditional_edges的决策依据。StateGraph初始化: 创建我们的图实例。add_node: 将我们的 Agent 函数注册为图中的节点。set_entry_point("planner"): 明确指定planner是整个流程的起点。add_conditional_edges: 在planner之后,我们不再是简单地add_edge。我们告诉 LangGraph,planner之后要调用should_research函数来决定下一步去哪。- 如果
should_research返回"research",则流程转到researcher节点。 - 如果
should_research返回"write",则流程转到writer节点。 - 这完美地实现了我们业务中“是否需要研究”的决策逻辑。
- 如果
add_edge:researcher之后总是去writer,writer之后总是去editor,这些是固定的顺序流转。add_edge("editor", END): 这是另一个关键点!editor完成任务后,我们明确地将流程连接到END。这告诉 LangGraph,整个工作流已经完成,可以停止执行并返回最终状态了。app.invoke(initial_state): 运行我们的工作流。通过传入不同的initial_state(主要是topic),我们可以看到planner如何动态地引导流程走向不同的路径。
通过这两个运行场景,你将清晰地看到 add_conditional_edges 如何根据 Planner 的决策,让工作流自动选择是否经过 Researcher 节点,最终都汇聚到 Writer 和 Editor,并优雅地通过 END 结束。
坑与避坑指南
作为一名资深导师,我见过太多同学在 Edges 和 END 上犯的错误,来,我们把这些坑提前填平!
条件函数
condition_function返回值与mapping不匹配- 坑点:你的
condition_function返回了一个字符串,但这个字符串没有在add_conditional_edges的mapping_dict中作为键存在。 - 后果:LangGraph 会抛出
ValueError,表示无法找到对应的目标节点。 - 避坑指南:务必确保
condition_function的所有可能返回值都在mapping_dict中有明确的映射。 对于复杂的条件,可以考虑在mapping_dict中添加一个__default__键,将未明确映射的返回值导向一个默认节点(例如,一个错误处理节点或直接导向END)。# 错误示例 # def my_condition(state): return "unknown_path" # graph.add_conditional_edges("source", my_condition, {"path_a": "node_a"}) # "unknown_path" 没匹配 # 避坑示例:使用 __default__ # graph.add_conditional_edges("source", my_condition, { # "path_a": "node_a", # "path_b": "node_b", # "__default__": "error_handler_node" # 或者直接 END # })
- 坑点:你的
END的误解与缺失- 坑点:有些同学认为只要最后一个节点执行完了,流程就自然结束了。或者将
END作为一个普通的节点来处理。 - 后果:如果一个路径没有明确连接到
END,当该路径上的最后一个活动节点执行完毕后,LangGraph 会认为“没有更多的边可走”,从而隐式停止。这虽然不会报错,但不如显式使用END清晰,特别是在复杂的多分支流程中,容易造成误解或遗漏。 - 避坑指南:始终将
END作为一个明确的终止符来使用。 任何一个“成功”或“完成”的业务路径,都应该最终导向END。END是一个特殊的关键字,不是你定义的普通节点。它代表着图的执行边界,能让你的流程意图更加清晰。
- 坑点:有些同学认为只要最后一个节点执行完了,流程就自然结束了。或者将
循环依赖与无限循环
- 坑点:在设计条件连线时,不小心创建了循环,导致流程在某些条件下无限循环,例如
Editor总是将内容退回给Writer,而Writer总是写出让Editor不满意的版本。 - 后果:程序陷入死循环,资源耗尽,最终崩溃。
- 避坑指南:
- 引入迭代计数或状态变量:在
AgentState中添加一个revision_count字段。每次Editor将任务退回Writer时,revision_count加一。在Editor的条件判断中,检查revision_count是否超过预设阈值。如果超过,则强制流程导向END或一个“人工介入”节点。 - 可视化调试:使用
app.get_graph().draw_png()或draw_mermaid()来可视化你的工作流。复杂的条件连线和循环路径,肉眼很难直接看出问题,但图表会一目了然。
- 引入迭代计数或状态变量:在
- 坑点:在设计条件连线时,不小心创建了循环,导致流程在某些条件下无限循环,例如
set_entry_point的唯一性- 坑点:试图设置多个入口点,或者忘记设置入口点。
- 后果:LangGraph 要求一个图只能有一个
set_entry_point。忘记设置则无法启动。 - 避坑指南:一个图只能有一个
set_entry_point。 如果你的业务逻辑需要从不同的地方开始,那可能意味着你需要设计多个独立的 LangGraph 图,或者在入口点前加一个“调度器”节点,由它来决定实际的第一个业务节点。
条件函数中的状态修改
- 坑点:在
condition_function中修改了state。 - 后果:
condition_function的职责是读取状态并做出决策,而不是修改状态。虽然 Python 允许你修改传入的可变对象,但这会打破 LangGraph 的设计哲学,使得状态管理混乱,难以追踪问题。 - 避坑指南:条件函数应该是纯函数 (Pure Function),只读取
state并返回决策结果,绝不修改state。所有对state的修改都应该发生在节点 (add_node注册的函数) 内部。
- 坑点:在
掌握了这些,你对 LangGraph 工作流的控制力将大大增强,构建的系统也会更加健壮和可维护。
📝 本期小结
各位同学,这期我们深入探讨了 LangGraph 中连接各智能体生命线的关键——寻路连线 (Edges) 和终止符 (END)。我们学会了如何使用 add_edge 构建固定流转,以及如何通过 add_conditional_edges 和 condition_function 实现基于业务逻辑的动态流程分发。同时,我们也明确了 set_entry_point 和 END 在定义完整工作流生命周期中的重要作用。
通过在“AI 万能内容创作机构”项目中的实战演练,你现在应该能够:
- 清晰地将“是否需要研究”这样的业务决策,转化为 LangGraph 的条件连线。
- 构建一个从
START到END,有明确起点和终点的多智能体协作流程。 - 避免常见的陷阱,如条件不匹配、
END缺失或循环依赖等。
从现在开始,你的 LangGraph 工作流将不再是僵硬的线性执行,而是能像一个真正有“大脑”的智能系统一样,根据实时情况做出判断,灵活地调整流程。这意味着你的 AI Content Agency 能够更好地适应各种内容创作需求!
下期,我们将继续深入,探索如何让我们的 Agent 不仅仅是顺序执行,而是能够进行并行处理 (Parallelism),进一步提升我们机构的效率和处理复杂任务的能力!敬请期待!