链式编排(Chain)的本质与极限

什么是链式编排

早期的 LLM 应用框架,如 LangChain,强调通过“链”把提示、模型、工具等组件串联起来。链式编排类似于函数式管道:每一步接受前一步的输出作为输入并产生新的输出。官方文档在比较 LangChain 和 LangGraph 时指出,LangChain 的工作流通常是 线性或有向无环图(DAG),执行路径预先确定、主要是正向推进 。这种模式非常适合处理静态或顺序明确的任务,例如对文档执行一系列预定操作、调用单个工具或模型等。

链式编排在实际项目中的优势明显:

  1. 易于理解和调试:链条的每一步都清晰可见,适合快速原型开发和简单需求。
  2. 配合良好的建模语言:LangChain 提供了 LCEL(LangChain Expression Language)这样的 DSL,使得链式操作如数据管道一样直观。 LangChain 投入大量精力使开发者能够轻松创建自定义链。
  3. 适用于严格的“管道”任务:例如 ETL(抽取‑转换‑加载)、固定的 RAG 流程等,DAG 可以准确描述数据流并被调度器高效执行。

链式编排的局限性

然而,链式编排的局限也十分突出,尤其在构建具有决策和循环的代理时暴露无遗:
• 无法表示循环:DAG 必须是无环的,因此任何需要“再试一次”的业务逻辑都难以表达。LangChain 官方博客指出,虽然可以用链模拟一些复杂流程,但很难引入循环逻辑 。
• 分支控制困难:虽然 LangChain 支持简单的条件分支,但多路决策和嵌套循环会使链式结构变得复杂且难以维护。DataCamp 的对比表提到,LangChain 控制流能力有限,只有受限的分支和重试原语 。
• 隐式状态管理:链式框架往往把状态隐藏在各个组件内部,或者仅在短期内传递少量信息。这种隐式状态难以在多个步骤之间共享,对于需要长记忆的对话或需要多次迭代的问题来说,这是一大障碍 。
• 难以调试复杂决策:当链条里需要依据 LLM 输出来决定下一步时,开发者必须把决策逻辑硬编码在提示里,缺乏显式的流程可视化和回溯手段。
在日益复杂的代理系统中,这些限制迫使我们寻找新的编排模式。

线性(DAG)结构示例

本示例构建了一个只有一个节点的有向无环图:节点执行一次计数自增操作,然后直接结束。由于没有循环边,即便设置较大的上限,计数器也只会增加一次。

from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END

# 定义状态,包含计数器和最大迭代次数
class CounterState(TypedDict):
    count: Annotated[int, operator.add]
    max_iters: int

# 节点:将 count 加 1
def increment_once(state: CounterState) -> dict:
    return {"count": state["count"] + 1}

# 构建图:起点是 increment_once,然后直接结束
builder = StateGraph(CounterState)
builder.add_node("increment_once", increment_once)
builder.set_entry_point("increment_once")
builder.add_edge("increment_once", END)

graph = builder.compile()

# 执行:即使 max_iters=5,count 也只会增加一次
final_state = graph.invoke({"count": 0, "max_iters": 5})
print(final_state["count"])  # 输出 1

上述代码代表了一个典型的线性流程,没有循环、没有条件判断,适用于简单的数据管道或串行任务 。

为什么 Agent 天然是「图结构」

代理的循环与决策

智能代理的核心在于“思考‑行动‑观察‑再思考”。例如,经典的 ReAct 算法在执行工具调用后根据结果调整策略,可能多次反复,直到达到目标。这种 循环 与 条件决策 是代理与简单流水线之间最大的区别。LangChain 团队在介绍 LangGraph 时指出,早期的链式应用往往缺乏引入循环的机制,因此无法实现复杂的代理逻辑 。
LearnWithParam 的文章形象地对比了标准 DAG 与代理的循环图,并指出:
“大多数数据管道是 DAG,数据像水一样单向流动。但代理需要 循环。例如“起草→评审→回退重新起草”这样的循环。你无法在 DAG 中建模循环,需要支持循环的图结构” 。
代理需要在执行链中反复检查条件、修正参数、重试工具调用,甚至中途暂停等待人工输入,这显然超出了单向链条的能力。这也是为什么 LangGraph 将代理描述为“具有循环的图”:它允许节点之间形成环,且每个节点能够读取和更新全局状态,从而实现动态决策和记忆。

LLM 的不确定性与动态流程

大型语言模型具有生成性和不确定性。当模型调用工具返回的结果不符合预期时,需要重新检索数据或调整 prompt,这是所谓的 自适应循环。LangGraph 博客举了 RAG 的例子:在检索增强生成中,如果第一次检索返回的文档无用,代理应当让 LLM 重新构造检索查询 。这种动态迭代无法在固定的链式流程中表达,但图结构可以自然地描述这种“循环直到满意”的逻辑。

多 Agent 协作与人机协同

在企业场景或复杂任务中,往往需要多个角色(或代理)相互协作。例如,AuxilioBits 的博客指出,LangGraph 的共享状态允许不同代理在同一个工作流中学习并相互构建,避免了传统工作流引擎中冗余的任务和误沟通 。这种多主体协作会产生多条分支路径,决策和信息流非线性、非单向;只有图结构才能容纳这种复杂的互动模式。
同时,LangGraph 提供了人机协同机制。执行过程可以在某个节点暂停,等待人工审批,然后继续运行 。暂停点与恢复点都会记录在全局状态中,确保流程在恢复后能正确继续。这种机制需要在图中显式建模暂停和恢复边,也加强了图结构的必要性。

图结构与循环示例

为了实现第 2.2 节讨论的循环逻辑,可以使用条件边在节点满足条件时回到自身。下面的代码将计数器递增直到达到上限:

from langgraph.graph import StateGraph, END

# CounterState 定义同上

def increment(state: CounterState) -> dict:
    # 每次将 count + 1
    return {"count": state["count"] + 1}

def should_continue(state: CounterState):
    # 如果未达到最大迭代次数,则继续调用 increment;否则结束
    return "increment" if state["count"] < state["max_iters"] else END

builder = StateGraph(CounterState)
builder.add_node("increment", increment)
builder.set_entry_point("increment")
builder.add_conditional_edges("increment", should_continue)

graph = builder.compile()
final_state = graph.invoke({"count": 0, "max_iters": 3})
print(final_state["count"])  # 输出 3

在此示例中,条件函数根据状态中的 count 和 max_iters 决定下一步是否继续执行 increment 节点,体现了循环控制。这正是 DAG 无法直接表达但代理通常需要的能力 。

图、DAG 与状态机

图与 DAG

图(Graph)是由节点和边组成的数学结构,可以有环,也可以无环。DAG(有向无环图)是图的子集,其边方向固定且不存在回路。传统的数据管道、ETL 流程和调度器(如 Airflow、Prefect)采用静态 DAG:任务节点在编排前就确定好顺序和依赖,运行时按照固定顺序执行。Sai 的文章明确指出,静态 DAG 支持数据持久化,但不为运行时的决策和动态逻辑设计。
对于代理系统而言,DAG 的“无环”限制是最大的障碍。代理的决策会根据模型输出、外部环境或用户反馈动态变化;可能需要回到之前的步骤或重新调用某个工具。LearnWithParam 的文章就强调:“你无法在 DAG 中建模循环,需要支持循环的图结构” 。

状态机

状态机是一种特殊的图,它强调节点代表状态,边代表状态转移,转移发生依据事件或条件。相比普通图,状态机更关注“系统当前处于何种状态”和“发生事件后如何迁移”。LangGraph 团队在博客中将代理视为一种“有指导的人为构建的状态机”,用于更精细地控制 AgentExecutor 的循环 。换言之,LangGraph 并非简单的图拼接工具,而是让你把代理的每个思考步骤变成显式的状态转换,从而可理解、可调试、可回溯。
Sai 的对比文章把 LangGraph 称为 动态有限状态机,并指出:
“动态 FSM 在运行时根据输入或输出决定状态转移,支持循环、记忆和条件跳转,特别适合基于 LLM 的代理工作流” 。
因此,图(Graph)提供了结构,DAG 提供了确定性,状态机提供了决策机制。LangGraph 则综合三者:通过有向图建立节点和边,通过允许环实现循环,通过全局状态维护记忆并驱动状态转移。

控制流:分支、循环与并行

除了环路,代理还需要复杂的条件分支和并行执行。在链式结构中,分支会导致“链条爆炸”:每条路径都需要单独定义;而并行任务难以协调共享状态和同步。LangGraph 通过三种边(普通边、条件边、并行发送边)解决这一问题。

在实例中,一个常见的模式是“思考→行动→评估→循环”,如图所示:
在这里插入图片描述
左边展示了链式 (DAG) 执行路径,数据只能顺序流动;右边是代理图,具有循环边和终止边。只有当边允许回到上一步时,代理才能在错误发生后重新思考和修正,这正是图结构和状态机的核心价值。

状态机示例

状态机强调“状态值驱动转移”。下面的例子定义了三个状态(INIT、WORKING、DONE),通过路由函数根据 status 决定下一步:

from typing import Literal, TypedDict
from langgraph.graph import StateGraph, END

class FSMState(TypedDict):
    status: Literal["INIT", "WORKING", "DONE"]
    log: str

def init_node(state: FSMState) -> dict:
    # 初始化完成后转为 WORKING
    return {"status": "WORKING", "log": state["log"] + "初始化完成\n"}

def work_node(state: FSMState) -> dict:
    # 工作完成后转为 DONE
    return {"status": "DONE", "log": state["log"] + "任务完成\n"}

def route(state: FSMState):
    # 根据 status 决定下一个节点或结束
    if state["status"] == "INIT":
        return "init"
    elif state["status"] == "WORKING":
        return "work"
    else:
        return END

builder = StateGraph(FSMState)
builder.add_node("init", init_node)
builder.add_node("work", work_node)
builder.set_entry_point("init")
builder.add_conditional_edges("init", route)
builder.add_conditional_edges("work", route)

graph = builder.compile()
result = graph.invoke({"status": "INIT", "log": ""})
print(result["log"])

该示例中,状态字段 status 代表有限状态机的当前状态,路由函数根据其值选择进入不同节点,最终通过 DONE 状态结束流程。这展示了状态机的典型模式 。

为什么 LangGraph 不是 BPMN 或传统工作流引擎

传统工作流引擎的定位

BPMN(业务流程模型与符号)和工作流引擎,如 Camunda、Power Automate 或 RPA 系统,主要面向 确定性、流程驱动的业务流程。它们通常使用可视化工具设计流程图,然后根据预定义的规则执行。AuxilioBits 的博文指出,传统流程自动化假定工作流是线性的:输入→处理→输出 。这种工具擅长处理表单审批、数据迁移等业务规则明确的场景。

建模复杂性:BPMN 的痛点

当业务流程需要大量条件分支、循环和人工干预时,BPMN 图会迅速膨胀,变成难以维护的“意大利面图”。在处理跨多系统、跨多角色的事件管理流程时,“使用 Camunda 或 Power Automate 建模会迅速变得混乱——条件路径爆炸,异常处理难以阅读,任何变更都要重新梳理整张图” 。对于需要动态决策和记忆的代理来说,这种静态图根本无法表达其复杂性。
此外,传统工作流引擎的节点多是 任务调用,无内置的语言模型推理。传统 DAG 把任务视为孤立的黑箱,缺乏对 LLM 动态决策、记忆更新和多次尝试的支持 。即便可以通过持久化中间状态和重试机制勉强实现,但整体工程量大且缺乏框架级抽象。

LangGraph 的差异化定位

AuxilioBits 的文章明确指出:“不要把 LangGraph 归类为 Airflow、Step Functions 或 BPMN 工具。LangGraph 并不是围绕任务或 DAG 构建的,而是围绕会话状态转换、记忆和代理行为” 。这种设计带来了几个关键差异:
• 状态与记忆优先:在 LangGraph 中,节点不是单纯的调用,而是拥有“角色、上下文和推理能力”的自治演员 。全局状态允许不同代理共享记忆并根据历史做出决策 。
• 决策驱动而非流程驱动:流程工具强调预定义路径,而代理强调在运行时根据模型输出和环境变化选择下一步。这就是图中条件边和循环的重要性 。
• 内置 LLM 与工具调用:LangGraph 将 LLM 调用视为节点类型之一,并提供持久化、流式输出、人机协同等高级能力,这是一般工作流引擎不具备的。
• 代码优先,而非纯可视化:LangChain 创始人 Harrison 在博客《Not Another Workflow Builder》中解释了为何团队没有开发可视化“拖拉式”工作流构建器。他认为高复杂度问题还是需要在代码中实现,并借助 LangGraph 控制流实现可靠代理;可视化适合简单或教学场景 。

综上所述,LangGraph 是构建智能代理的底层框架,而不是通用的流程建模工具。它针对 LLM 生成任务的动态性和不确定性设计,强调状态管理和循环控制,这正是 BPMN 和传统工作流工具无法提供的。

LangGraph 的“最小抽象集合”

LangGraph 自称为“低层编排框架”,其核心API非常简洁。一位开发者初看 LangGraph 的源码时,可能会惊讶于它抽象的少:

  1. StateGraph:代表整个图及其执行环境。你初始化它时需要指定一个全局状态的 TypedDict 或 Pydantic 模型,它定义了在执行过程中需要存储的所有字段。LangGraph 博客中解释,StateGraph 接收状态定义并在运行时维护该状态的值 。
  2. Node:节点是一个函数或 LangChain LCEL runnable,接收当前状态并返回一个“补丁”字典,框架会根据补丁更新全局状态。节点可以调用 LLM、外部工具或者执行逻辑处理。官方“Thinking in LangGraph”指南强调,节点是纯粹的函数,它们读状态、做工作、返回更新。
  3. Edge:边定义了控制流。包括普通边、条件边以及发送(并行)边。条件边由路由函数决定下一步走向,并根据返回值映射到不同节点 。
  4. State:全局状态对象,贯穿整个执行过程,是代理的记忆来源。它支持字段覆盖和累加两种更新方式 。设计好的 State 既要包含必要的上下文,又要避免存储格式化文本,这在“Thinking in LangGraph”中也被多次强调 。

为了更直观地理解这种最小抽象,可以查看下图:
在这里插入图片描述
中央圆形表示共享的全局状态,三个矩形代表不同的节点。实线箭头表示从状态到节点的读取(节点获取上下文),虚线箭头表示节点对状态的写入(返回补丁)。下方的普通箭头则表示控制流:节点 A → 节点 B → 节点 C → End。只有当状态、节点和边同时存在时,LangGraph 才能完整地表达代理逻辑。

节点、边、状态:缺一不可

节点:具体工作者

每个节点就是代理的一步“动作”。它可以是 LLM 调用、数据检索、数据库写入或任意 Python 函数。关键是节点必须:
• 纯函数化:节点输入是当前状态,输出是状态补丁和(可选的)下一个节点的指示,避免副作用导致调试困难。 明确指出节点应当只做一件事,并返回明确的更新。
• 可测试:由于节点是独立的函数,开发者可以单独测试它们的逻辑,保证大颗粒代理流程的可靠性。 讨论了将不同操作分拆为多个节点的好处:隔离外部服务调用、提供中间可视化、配置不同的重试策略等。

边:控制流的灵魂

边决定了执行顺序和条件跳转。LangGraph 支持三种边:
• 普通边:无条件从一个节点到另一个节点,如“工具节点→LLM 节点”。
• 条件边:由路由函数决定下一步走向。路由函数检查状态,根据内容返回一个标签,然后映射到具体节点。比如,根据评估结果决定是结束流程还是重新调用工具 。
• 并行发送边(Send API):创建多个子线程并行执行子图,将结果汇聚到父图。这在需要并行调用多个工具或同时处理多个子任务时非常实用。

通过边的组合,可以表达复杂的控制流,包括循环、分支、并行和同步。在第后面的文章中我们将会看到更多模式。

状态:共享记忆

状态是代理能够进行连贯对话和决策的基础:
• 全局共享:所有节点都可以读取和更新同一个状态对象,实现信息共享 。
• 结构明确:通过 TypedDict 定义字段类型和更新方式,框架自动合并补丁,保持类型安全 。
• 持久化与回溯:配合检查点(Checkpointer),状态可以在每个节点边界持久化,当执行中断或失败时从相同状态恢复 。这种“时间旅行”能力使 Agent 具有容错性。

如果没有状态,节点之间只能通过参数传递简单的字符串,很难实现复杂的记忆更新;如果没有边,节点之间缺乏控制流指导;没有节点,状态也无法被修改。因此,节点、边和状态缺一不可,构成了 LangGraph 的最小闭环。

节点、边和状态的协作示例

最后,通过一个问候机器人演示节点如何读取和更新状态、边如何驱动控制流。该机器人读取初始消息并生成回复,然后更新状态:

from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END

class GreetingState(TypedDict):
    messages: Annotated[list[str], operator.add]
    responded: bool

def greet_node(state: GreetingState) -> dict:
    # 读取用户的最后一条消息并生成回复
    last_message = state["messages"][-1]
    reply = f"Hello, {last_message}!"
    return {"messages": [reply], "responded": True}

def should_end(state: GreetingState):
    # 如果已经回复则结束,否则继续调用 greet
    return END if state["responded"] else "greet"

builder = StateGraph(GreetingState)
builder.add_node("greet", greet_node)
builder.set_entry_point("greet")
builder.add_conditional_edges("greet", should_end)
graph = builder.compile()

initial_state = {"messages": ["World"], "responded": False}
final_state = graph.invoke(initial_state)
print(final_state["messages"])  # ['World', 'Hello, World!']

该例子体现了 LangGraph 的三大抽象如何协同工作:节点读取状态并返回补丁,边决定流程走向,全局状态用于在不同节点之间传递信息 。
在上一篇中,我概述了 LangGraph 的总体设计、基本用法以及它在语言代理系统中的地位。当我着手深入研究其背后的思想时,一个直观的感受是:传统链式编排不足以承载真正的智能代理,而图结构才是代理的自然形态。这一章,我将从工程和理论两个层面剖析「链」到「图」的演进之路,既结合官方文档和源码细节,也融入个人经验与思考。

本章小结

这一章从多个角度阐述了为什么在构建现代 LLM 代理时需要从传统的链式编排转向图式编排。我结合官方文档、博客和社区文章,总结出以下几点要点:

  1. 链式编排属于线性或 DAG 结构,适合简单、确定的任务,但难以表达循环和复杂条件 。随着代理需求的复杂化,链条不可避免地暴露出缺乏记忆和控制流的局限。
  2. Agent 天然需要循环和决策,例如 ReAct 算法中的“反思‑行动‑再反思”。这种循环无法在 DAG 中建模,需要支持环的图结构 。此外,多代理协作和人机协同更是图结构的天然场景 。
  3. 图、DAG 与状态机各自的适用场景不同:图是广义容器,DAG 强调无环和确定性,状态机强调状态与事件驱动。LangGraph 结合三者,通过有向图定义节点和边,通过允许环实现循环,通过状态驱动条件转移 。
  4. LangGraph 不等同于 BPMN 或传统工作流引擎。后者旨在建模确定性业务流程,而 LangGraph 专注于对话式、记忆化、多主体的智能代理。它围绕状态、循环和 LLM 推理构建,而非简单的任务序列 。 亦强调,高复杂度问题应在代码中实现,由 LangGraph 提供细粒度控制,而非通过可视化拖拽解决。
  5. LangGraph 的核心抽象只有四个:StateGraph、Node、Edge、State。这种简约设计让开发者既能灵活建模复杂逻辑,又能保持可理解性。共享状态、纯函数节点和显式的控制边构成了可靠的执行机制 。

下一篇文章中将进一步剖析状态的设计技巧、节点的实现模式以及条件边和循环如何在实践中运作,为大家提供具体的代码示例和工程经验。通过深入理解这些细节,我们才能真正发挥 LangGraph 的强大能力,构建稳定可靠的智能代理系统。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐