从ReAct到Graph编排:构建复杂AI工作流的新范式
1. 从“链式”到“图式”:为什么我们需要超越ReAct?
如果你最近在折腾AI Agent或者大模型应用开发,大概率已经对ReAct(Reasoning + Acting)这个范式不陌生了。它让大模型学会了“思考一步,行动一步”,通过循环调用工具来解决复杂问题,这无疑是AI应用开发史上的一大步。但当你真正上手,想把一个稍微复杂点的业务流程自动化时,很快就会发现ReAct的局限性:它本质上是一条单链。
想象一下,你要开发一个智能客服Agent,它需要同时处理用户查询、查询知识库、检查订单状态、并可能调用外部API生成优惠券。在ReAct的循环里,这些步骤只能一个接一个地发生。如果“查询知识库”和“检查订单状态”之间没有依赖关系,理论上它们可以并行执行以提升效率,但ReAct做不到。更棘手的是,如果流程中出现了分支判断(比如,如果是老用户就走A路径,新用户就走B路径),或者需要循环处理一个列表中的每一项,用单纯的ReAct来实现就会变得异常臃肿和难以控制。你的提示词(Prompt)会变成一个充满复杂条件判断的“怪物”,可维护性急剧下降。
这就是“Graph编排”概念开始受到关注的核心原因。它不再将AI的工作流视为一条单一的、不可分割的链条,而是将其解构为一个有向无环图。在这个图里,每个节点(Node)代表一个独立的、可复用的功能单元(比如调用一次大模型、执行一个工具、做一个条件判断),节点之间的边(Edge)则定义了数据流动和执行的依赖关系。DAG(Directed Acyclic Graph,有向无环图)确保了流程不会陷入死循环。
所以,“Graph编排:不只是ReAct的通用DAG”这个标题,精准地指出了一个演进方向:ReAct是Graph的一个特例(一个简单的、线性的图),而Graph编排是一个更通用、更强大的范式,能够描述和驱动任意复杂的、非线性的AI工作流。它把工作流的控制逻辑从晦涩难懂的提示词中剥离出来,用清晰的、可编程的图结构来定义,这极大地提升了复杂Agent的可构建性、可观测性和可维护性。最近社区里出现的LangGraph、Snap Graph Builder等工具和graph engineering的热议,正是这一趋势的体现。
2. Graph编排的核心构件:节点、边与状态
要理解Graph编排,我们必须先拆解它的几个核心构件。这就像搭积木,只有清楚了每一块积木的形状和作用,才能构建出稳固而复杂的结构。
2.1 节点:工作流的功能单元
在Graph中,节点是执行具体任务的单元。它可以是:
- LLM调用节点:接收输入,调用大模型,返回文本结果。这是最基础的节点类型。
- 工具调用节点:执行一个具体的函数,比如调用搜索引擎API、查询数据库、运行一段代码。
- 条件判断节点:根据当前工作流的状态(State),决定下一步该走哪条分支。这是实现分支逻辑的关键。
- 人工审核节点:将流程暂停,等待人工输入或确认后再继续。这在关键业务场景中必不可少。
- 自定义函数节点:任何你可以用代码实现的逻辑,都可以封装成一个节点。
节点的设计追求“单一职责”和“可复用性”。一个设计良好的“查询天气”节点,既可以被“出行规划”工作流使用,也可以被“穿衣建议”工作流使用。
2.2 边:控制流与数据流
边定义了节点之间的连接关系,它决定了两件事:执行顺序和数据传递。
- 顺序边:最简单的边,表示一个节点执行完后,紧接着执行下一个节点。这模拟了链式调用。
- 条件边:从一个条件判断节点出发,根据判断结果的不同,指向不同的下游节点。例如,
if user_is_vip then node_A else node_B。 - 并行边:让多个没有依赖关系的节点同时开始执行,最后汇聚到一个节点进行结果合并,这能显著提升整体效率。
数据通过边在节点间流动。通常,整个Graph会维护一个共享的“状态”(State)对象,每个节点读取状态的一部分作为输入,并将自己的输出写回状态。下一条边上的节点,读取的就是更新后的状态。
2.3 状态:工作流的“记忆体”
状态是Graph编排的灵魂,它是一个在节点间共享和传递的上下文对象。你可以把它想象成一份不断被填写和修改的“工单”或“病历”。
- 状态的结构:通常是一个字典(Dictionary)或类似的结构,包含诸如
messages(对话历史),intermediate_results(中间结果),user_query(用户问题)等字段。 - 状态的更新:每个节点执行后,都有机会修改这个状态对象。例如,一个工具调用节点可能会在状态中新增一个
weather_info字段。 - 状态的持久化:对于长时间运行或需要中断恢复的工作流,状态需要能够被序列化存储,并在下次执行时加载。这是构建可靠生产级系统的关键。
理解了这三个构件,我们就能看出Graph相对于ReAct的优势:ReAct将“思考”、“行动”、“更新状态”这几个动作压缩在一个不可分割的循环内,而Graph将它们拆分开,并通过显式的边和状态来管理,从而获得了极大的灵活性和表现力。
3. 实战对比:用ReAct vs. Graph实现一个智能查询Agent
理论说得再多,不如看一个具体的例子。假设我们要构建一个智能查询Agent,它的逻辑是:
- 接收用户一个问题。
- 判断问题类型:如果是事实性问题(如“珠穆朗玛峰多高”),直接调用网络搜索工具;如果是需要复杂分析或创作的问题(如“写一首关于秋天的诗”),则直接调用大模型生成。
- 将得到的结果格式化后返回给用户。
我们用两种方式来实现它。
3.1 ReAct 实现方式:提示词中的“隐形”逻辑
在ReAct范式下,我们通常需要编写一个非常精巧的提示词,试图让大模型自己学会这个判断逻辑。提示词可能长这样:
你是一个智能助手。请遵循以下步骤: 1. 思考:分析用户的问题属于以下哪一类: - A类:事实性问题,有明确答案,需要查询最新信息。例如:“...多高”、“...是谁”、“...最新新闻”。 - B类:分析性、创作性或主观性问题,不需要查询外部信息。例如:“写一首诗”、“分析一下...”、“你觉得...”。 2. 行动:根据你的思考,执行相应操作: - 如果认为是A类,你必须且只能使用 `search_web` 工具。 - 如果认为是B类,你必须直接回答,不能使用任何工具。 3. 最终,将你的行动结果整理成友好的回复。 当前问题:{user_question}然后,我们实现一个ReAct循环,不断调用LLM,让它输出“Thought: ... Action: ...”,并解析它的Action来调用工具。这个方法的弊端非常明显:
- 控制脆弱:大模型可能不严格按照你设定的格式输出,或者做出错误的分类判断,导致流程走偏。
- 逻辑耦合:判断逻辑和工具调用逻辑都糅杂在提示词和模型的“黑箱”推理中,难以调试和优化。
- 难以扩展:如果想增加第三种问题类型(比如需要查询数据库),就必须重写整个提示词,破坏原有的逻辑。
3.2 Graph 实现方式:显式的、可编程的流程图
现在我们用Graph的方式来重构这个Agent。我们使用LangGraph这个流行的框架来示意。
首先,我们定义三个节点函数:
# 节点1:路由判断节点 def route_question(state): question = state["question"] # 这里可以用一个简单的规则,也可以用一个小型分类模型 if any(keyword in question for keyword in ["多高", "是谁", "最新", "定义"]): return {"next_node": "search_node"} else: return {"next_node": "generate_node"} # 节点2:搜索节点 def search_node(state): question = state["question"] search_result = call_search_tool(question) # 假设的工具函数 return {"answer": f"根据搜索,结果是:{search_result}"} # 节点3:生成节点 def generate_node(state): question = state["question"] llm_response = call_llm(question) # 假设的LLM调用 return {"answer": llm_response}然后,我们定义Graph的结构:
from langgraph.graph import StateGraph, END # 定义状态结构 from typing import TypedDict, Literal class AgentState(TypedDict): question: str next_node: Literal["search_node", "generate_node", "__end__"] answer: str # 创建图 workflow = StateGraph(AgentState) # 添加节点 workflow.add_node("router", route_question) workflow.add_node("search", search_node) workflow.add_node("generate", generate_node) # 设置入口 workflow.set_entry_point("router") # 根据路由结果,动态决定下一个节点 workflow.add_conditional_edges( "router", lambda state: state["next_node"], # 根据`next_node`字段的值决定路由 { "search_node": "search", "generate_node": "generate", } ) # 搜索和生成节点执行完后,都直接结束 workflow.add_edge("search", END) workflow.add_edge("generate", END) # 编译图 app = workflow.compile()最后,运行这个Graph:
# 初始化状态 initial_state = AgentState(question="珠穆朗玛峰多高?", next_node="", answer="") # 执行图 final_state = app.invoke(initial_state) print(final_state["answer"]) # 输出:根据搜索,结果是:8848.86米... # 另一个问题 initial_state2 = AgentState(question="写一首关于秋天的五言诗", next_node="", answer="") final_state2 = app.invoke(initial_state2) print(final_state2["answer"]) # 输出大模型生成的诗歌对比与优势:
- 控制权明确:判断逻辑完全由
route_question函数控制,是确定性的、可调试的代码,不再依赖大模型的“自觉”。 - 结构清晰:整个工作流被可视化为一幅图:
router -> (search | generate) -> END。任何开发者一眼就能看懂业务逻辑。 - 易于扩展:如果想增加一个“查询数据库”的节点和路径,只需要添加一个新节点,并在路由函数中增加一个判断分支和边即可,原有结构不受影响。
- 可观测性强:每个节点的输入输出(状态变更)都可以被记录和监控,便于排查问题。
这个简单的例子揭示了Graph编排的核心价值:它将AI工作流的“控制逻辑”从提示词的“魔法”中解放出来,变成了可编程、可调试、可维护的软件工程问题。
4. 高级模式与工程实践:构建健壮的AI工作流
当我们掌握了Graph的基本用法后,就可以探索更复杂的模式,并将其应用于工程实践,以构建真正健壮、可投入生产的系统。
4.1 循环与聚合:处理列表任务
很多业务场景需要处理列表。例如,分析一份产品评论列表,对每一条评论进行情感分析和要点提取,最后生成一份总结报告。用ReAct实现这种循环非常别扭,而Graph可以很优雅地处理。
思路是创建一个“循环体”子图。主图节点将评论列表放入状态,然后进入循环子图。循环子图每次处理一条评论,更新进度,并判断是否还有下一条。处理完所有评论后,退出循环,进入一个“聚合节点”来生成总结报告。LangGraph通过Pregel引擎支持这种循环,你可以通过检查状态中的index或list是否已处理完来控制边的走向。
4.2 并行与竞争:提升效率与鲁棒性
Graph编排最强大的能力之一是支持并行执行。
- 并行处理:当多个子任务间没有依赖时,可以同时执行。例如,在准备一份市场报告时,可以同时并行执行“爬取竞品价格”、“获取社交媒体声量”、“查询行业趋势”三个节点,最后一起汇总,效率远高于串行。
- 竞争模式:有时我们不确定哪种方法最好,可以让多个节点同时处理同一个问题,然后通过一个“裁判”节点来选择最佳结果。例如,对于一个复杂问题,同时让
GPT-4和Claude生成答案,再用一个轻量级模型或规则来判断哪个答案更优。
实现并行需要在定义边时,让一个节点同时指向多个下游节点,并设置一个“汇聚”节点来等待所有并行分支完成。这需要框架支持异步执行和状态合并。
4.3 错误处理与持久化:生产级的必须品
任何线上系统都必须考虑失败。在Graph中,错误处理可以做得非常精细。
- 节点级重试:对于可能因网络波动失败的节点(如调用外部API),可以内置重试机制。
- 子图级回退:如果一条路径失败,可以沿着条件边切换到备用的“降级”路径。例如,如果搜索最新信息失败,就改为从缓存数据库中获取历史信息。
- 状态持久化与断点续跑:这是Graph编排相比传统脚本的巨大优势。每一次Graph运行都有一个唯一的
run_id,每个节点执行后的完整状态都可以序列化保存到数据库(如Redis、PostgreSQL)。如果流程因任何原因中断(服务器重启、节点崩溃),我们可以根据run_id加载中断时的状态,从下一个节点继续执行,而不是从头开始。这对于处理耗时很长的业务流程(如订单审核流程)至关重要。
4.4 可观测性与调试:给工作流装上“仪表盘”
当工作流变得复杂,调试就成了挑战。Graph的显式结构天生有利于可观测性。成熟的框架(如LangGraph)通常提供:
- 可视化:自动生成工作流的拓扑图,直观展示所有节点和边。
- 执行追踪:记录每个节点的开始时间、结束时间、输入状态快照、输出状态快照以及任何错误信息。这相当于一份详细的“飞行数据记录仪”。
- 中间状态检查:你可以在任何节点执行前后,插入钩子(hooks)来检查或修改状态,这对于调试复杂的数据流转问题非常有用。
将这些追踪数据与像LangSmith这样的LLM应用监控平台结合,你就能获得一个功能强大的“AI工作流运维中心”,可以监控耗时、分析错误、优化性能。
5. 主流框架选型与“Graph Engineering”的崛起
随着Graph编排模式走红,相关的工具和框架也如雨后春笋般出现。选择哪个工具,取决于你的具体需求和技术栈。
5.1 框架对比
- LangGraph:目前生态最活跃、功能最全面的选择之一。它深度集成在LangChain生态中,但也可以独立使用。优势在于其强大的状态管理、对循环和并行的原生支持、优秀的可观测性工具(与LangSmith集成),以及活跃的社区。它定义了
StateGraph、MessageGraph等抽象,适合构建复杂的、有状态的Agent工作流。如果你的项目已经在用LangChain,或者需要构建生产级复杂Agent,LangGraph是首选。 - Snap Graph Builder:这是一个较新的工具,从其名称“Builder”可以看出,它更侧重于可视化、低代码/无代码的方式构建Graph。用户可以通过拖拽节点、连接边来设计工作流,降低了使用门槛,非常适合产品经理、业务分析师或不想写太多代码的开发者快速原型设计。它的后端可能仍然依赖于某个Graph执行引擎。
- Dagster / Airflow:这两个是传统数据工程领域的知名工作流编排器。它们本身并不是为AI Agent设计的,但其核心的DAG理念是相通的。如果你的AI工作流需要与复杂的数据管道(ETL)、调度、依赖管理深度集成,特别是工作流中混合了AI节点和传统数据处理节点,那么使用这些成熟的、久经考验的编排器可能更合适。它们需要更多的“胶水代码”来封装AI节点。
- 自定义实现:对于简单或特定的场景,你也可以用任何编程语言(Python的
networkx库,甚至直接用字典和函数)自己实现一个轻量级的Graph执行引擎。这给了你最大的灵活性,但也需要自己处理状态管理、并行、持久化等所有问题,不推荐用于复杂项目。
5.2 “Graph Engineering”成为新技能
“Graph Engineering”这个词开始流行,正说明构建AI工作流正在从“调提示词的艺术”转变为“设计系统的工程”。一个合格的Graph工程师需要具备以下能力:
- 系统设计能力:能够将一个模糊的业务需求,分解为清晰的、模块化的节点和边,设计出高效、健壮的数据流和控制流。
- 状态建模能力:如何设计状态对象的结构,使其既能承载必要信息,又不过于臃肿,是一项关键设计决策。
- 框架精通:深入理解所选框架(如LangGraph)的API、执行模型和最佳实践。
- 运维意识:必须考虑错误处理、日志、监控、性能优化和部署,这与开发任何后端服务没有区别。
Graph编排不是要完全取代ReAct。对于简单的、线性的任务,一个精心设计的ReAct提示词可能更快捷。但对于任何有分支、循环、并行、人工介入需求的复杂业务流程,Graph编排是通向可维护、可扩展、可观测的AI应用的必经之路。它标志着AI应用开发正逐步走出“脚本”和“提示词工程”的早期阶段,迈向真正的“软件工程”范式。
