Agent开发 2.1 - 控制流方案横向对比:Pipeline、DAG、ReAct 与有限状态机

2026-10-20 | 63分钟 | yrobot | Agent开发, 控制流, Pipeline, DAG, ReAct, 有限状态机, LangGraph

在构建基于大语言模型的应用时,许多开发者容易陷入一个直觉陷阱:以为只要写好提示词、接上几个外部工具,系统就能自然而然地像人类一样解决复杂业务。

然而在真实生产中,决定一个系统能否稳定交付、算力账单会不会瞬间失控的,往往不是模型本身,而是底层的控制流(Control Flow)。

控制流回答的是最底层的物理运行机制:

  • 究竟由谁决定下一步执行什么?
  • 外部工具报错后如何恢复?
  • 循环推演在什么条件下必须强制终止?

本文将从技术词源的常识认知切入,横向拆解工业界最主流的 4 种控制流方案,并深入分析扁平推演循环的崩溃临界点与防爆门设计。


名字背后的常识:破除专有名词死记硬背

很多架构选型之所以令人困惑,很大程度上是因为技术名词被包装得过于抽象。还原它们被创造时的物理机制,其实概念非常朴素:

1. DAG:Directed Acyclic Graph(有向无环图)

  • 有向(Directed):任务之间存在明确的前后依赖关系。比如必须先完成“提取数据”,才能进行“格式转换”。
  • 无环(Acyclic):执行链路单向向前推进,严禁回头打转,绝不出现死循环。
  • 图(Graph):在计算机科学中,由一组节点(Nodes)和连接节点的连线(Edges)组成的数据结构统称为“图”。
  • 工程类比:DAG 就像工程施工中的甘特图,或是做饭流水线:淘米与洗菜可以并行展开,随后汇总下锅烹饪,流程始终单向向前,绝不产生自旋。

2. ReAct:Reason + Act(推理 + 行动)

  • 词源:出自 2022 年普林斯顿大学与 Google 联合发表的经典论文,由 Reason(推理思考)与 Act(采取行动)拼接而成。
  • 物理机制:过去的大语言模型要么只空想(纯生成长文本),要么机械调用 API(单步执行)。ReAct 让模型在每一次行动前显式输出一段自言自语(Thought),随后触发环境调用(Action),拿到环境结果(Observation)后,再进入下一轮自言自语。
  • 控制流本质:在原生实现中,ReAct 本质上是一个没有任何物理护栏的裸奔 while 循环,循环何时终止、中间调用什么工具,完全取决于模型自身的自回归判断。

3. 有限状态机 (FSM) 与 StateGraph(状态图)

  • 有限状态机(Finite State Machine):系统在任意时刻只能处于有限个预先定义好的“状态”之一(例如交通红绿灯只有红、黄、绿三种状态;地铁闸机只有锁定与开闸两种状态),且状态之间的跳转必须遵循严格的触发规则。
  • StateGraph:以 LangGraph 为代表的框架将状态机的概念工程化,每个执行节点是一个具体的操作函数,连线是带条件的跳转边,整个系统在图拓扑中流转一个强类型的全局状态(State)。
  • 工程类比:如果说裸奔的 ReAct 是让一个全能选手在空地上自由发挥,容易跑偏甚至打转;StateGraph 则是为推演过程修筑了带轨道和信号灯的车间:数据只能在预设工位之间按规则流转,代码用硬边界锁死了模型的失控半径。

自主权让渡光谱:从 Workflow 到 Autonomous Agent

从最死板的代码流水线到完全自由探索的智能体,并不是非黑即白的对立,而是一条连续的控制流自主权让渡光谱:

完全由代码控制 ----------------------- 局部让渡分支权 ----------------------- 完全由模型自主循环
Pipeline (串行流水线)             DAG / Router (路由工作流)               Flat ReAct (扁平推演)
  0% 让渡                          局部选择分支路径                          100% 运行时自主推演

在这条光谱中,核心权衡始终是:用系统的确定性与低成本,去换取探索的灵活性与泛化能力。

方案类别 核心机制 控制权归属 核心优势 致命代价 典型适用场景
Pipeline(流水线) 单向无分支线性管道 100% 程序员硬编码 时延极低、成本最低、100% 可控 毫无变通能力,遇异常即中断 结构化数据抽取、固定格式排版
DAG / Router(路由) 预设分支拓扑,LLM 充当路口分流器 程序员定拓扑,LLM 定路由 路径清晰可控,支持局部并发 无法自愈,一旦误判分支无法回退 意图分类客服、任务派发网关
StateGraph(状态机) 预设带轨道的节点图,LLM 决定状态跳转 程序员定轨道,LLM 在节点内决策 既能闭环重试,又绝不脱轨,每步可审计 架构复杂度较高,开发与定义繁琐 企业级严肃审批、合规长程推演
Flat ReAct(扁平循环) 无约束开放式 while 循环 100% 运行时让渡给 LLM 自由度最高,可解未知开放问题 极易陷入死循环、上下文爆炸 开放式网络研究、未知代码排查

这里经常会引发一个普遍的工程疑问:在 LangChain 等框架中常见的简单路由分流(Router),到底算不算 Agent?

从行业实践和 Anthropic《Building effective agents》指南的划分来看,更严谨的定义是将其划分为 Agentic Workflow(智能体工作流)。虽然大语言模型获得了分支选择权,但系统缺乏“根据环境反馈进行多轮自适应自愈”的闭环能力。在严格的软件架构分层中,只有引入了基于环境反馈的推演循环,系统才具备了自主智能体的根本特征。


4 种控制流的极简代码骨架对比

为了彻底看清这 4 种方案在代码层面的物理差异,以下展示去除所有防御性边角料后的核心机制骨架(TypeScript)。

1. Pipeline(串行流水线)

没有任何分支,数据单向流淌。

async function runPipeline(userInput: string) {
  const keywords = await llmExtractKeywords(userInput); // 1. 抽取关键词
  const rawData = await database.search(keywords);      // 2. 查询数据
  const formatted = await llmFormatReply(rawData);      // 3. 组织成大白话
  return formatted;
}

2. DAG / Router(路由工作流)

代码固定路径分支,大语言模型仅在分水岭节点做选择。

async function runRouter(userInput: string) {
  const route = await llmDecideRoute(userInput, ["query_db", "direct_chat"]);
  
  if (route === "query_db") {
    return await queryDatabaseAndReply(userInput);
  } else {
    return await directChatReply(userInput);
  }
}

3. Flat ReAct(扁平循环)

把控制权全盘交出,依靠模型自己决定是继续调工具还是完结。

async function runReAct(userInput: string, tools: Record<string, Function>) {
  const messages = [{ role: "user", content: userInput }];

  while (true) {
    const decision = await callLLM(messages);
    if (decision.type === "finish") return decision.content; // 模型主动退出

    const toolResult = await tools[decision.toolName](decision.args);
    messages.push({ role: "tool", content: JSON.stringify(toolResult) });
  }
}

4. StateGraph(受控有限状态机)

节点执行逻辑与分支跳转规则分离,大语言模型只能在预设的状态轨道内流转。

const workflow = new StateGraph<AgentState>()
  .addNode("planner", planNextStep)      // 只能做任务规划
  .addNode("executor", callSelectedTool)  // 只能执行已批准的工具
  .addNode("reviewer", verifyResult)      // 只能做质检查验
  .addConditionalEdge("planner", (state) => state.needTool ? "executor" : "reviewer")
  .addConditionalEdge("reviewer", (state) => state.isPassed ? END : "planner");

扁平 ReAct 的缺陷与典型故障

当我们将控制权完全让渡给一个未受约束的 Flat ReAct 循环时,在真实的生产环境中,最常见的是以下两类故障:

                    [Flat ReAct 故障路径]
                             |
         +-------------------+-------------------+
         |                                       |
    [工具返回错误]                          [长程多步推演]
         |                                       |
  [大模型惯性重试]                         [上下文无节制累积]
         |                                       |
[相同错误参数连续调用]                    [关键指令被长文本稀释]
         |                                       |
[陷入死循环阻塞]                          [超出 Token 窗口上限报错]

问题 1:工具报错引发的死循环重试

当外部工具发生错误(例如查询某个用户时返回 { error: "User not found, connection timeout" }),未受干预的大语言模型很容易陷入重试死循环。

模型往往倾向于假设“是我参数没传对”,而不是理性判断该服务是否已下线。在接下来的推演中,它会以完全相同或轻微变形的参数连续重试多次。如果代码层没有拦截手段,系统将直接陷入死循环,耗尽算力并严重阻塞业务线程。

问题 2:长程推演导致的上下文膨胀与注意力稀释(Lost in the Middle)

在多步推演中,每次工具调用都会返回大段 JSON 结构体或 HTML 片段,这些信息全量累加在 messages 数组中:

  1. 硬性超限报错:无节制的记录累积使得上下文体积迅速增大,最终超出模型上下文窗口上限(如 128k Token),触发云端接口直接返回 400 Bad Request 报错。
  2. 注意力稀释:在上下文被大量长文本占用后,自注意力机制出现稀释,即“迷失在中间(Lost in the Middle)”。模型容易遗忘最开始由用户输入的关键约束(例如“必须输出简体中文”、“限定预算在 1 万元以内”)。

控制流防爆门:生产环境的最简最小防御

要治理上述两大崩溃点,生产级代码绝不能放任 while (true) 裸奔运行。最简有效的防护方案是在循环内嵌入两道硬性“防爆门”:

  1. 最大步数硬熔断(Max Turns Failsafe):强制设置单次推演的最大循环步数(例如 5 步),到达阈值后坚决掐断循环并返回失败警报。
  2. 上下文配额守卫(Token Budget Guard):在每次向模型发起请求前,估算当前历史消息的 Token 消耗;一旦逼近模型额定容量的安全水位(如 90%),立刻中止追加并触发降级处理。
// 具备双重防爆门的安全 ReAct 运行时骨架
async function safeReAct(
  userInput: string, 
  tools: Record<string, Function>,
  maxTurns: number = 5,
  maxContextTokens: number = 8000
) {
  const messages = [{ role: "user", content: userInput }];
  let turnCount = 0;

  while (turnCount < maxTurns) { // 防爆门 1:步数硬熔断
    turnCount++;

    // 防爆门 2:上下文容量守卫(超出 90% 立即熔断)
    const currentTokens = estimateTokens(messages);
    if (currentTokens > maxContextTokens * 0.9) {
      throw new Error(`[ContextOverflow] 上下文消耗接近阈值 90%,强制终止避免崩溃`);
    }

    const decision = await callLLM(messages);
    if (decision.type === "finish") return decision.content;

    const toolResult = await tools[decision.toolName](decision.args);
    messages.push({ role: "tool", content: JSON.stringify(toolResult) });
  }

  throw new Error(`[MaxTurnsExceeded] 达到最大循环步数 ${maxTurns},系统强制熔断`);
}
flowchart TD
    Start([用户输入]) --> CheckBudget{Token 消耗 > 90%?}
    CheckBudget -- 是 --> ErrContext[触发 ContextOverflow 熔断退出]
    CheckBudget -- 否 --> CheckTurn{当前步数 >= MAX_TURNS?}
    CheckTurn -- 是 --> ErrTurn[触发 MaxTurnsExceeded 熔断退出]
    CheckTurn -- 否 --> CallLLM[调用大语言模型]
    CallLLM --> NeedTool{模型要求调用工具?}
    NeedTool -- 否 --> Finish([正常输出最终结果])
    NeedTool -- 是 --> ExecTool[执行外部工具]
    ExecTool --> AppendHistory[结果写入上下文, 步数 + 1]
    AppendHistory --> CheckBudget

选型决策矩阵:遵循奥卡姆剃刀法则

在面对真实业务需求时,选型的第一准则始终是奥卡姆剃刀:若无必要,勿增实体。严禁为了追求“技术前沿感”而强行在规则穷举场景引入自主推演智能体。

业务决策路径:
1. 业务步骤是固定的吗?
   ├── 是 -> 选用 Pipeline(无分支)或硬编码代码
   └── 否 -> 继续判断

2. 分支选项是已知的,只需根据用户意图选择走哪一条路吗?
   ├── 是 -> 选用 DAG / Router 工作流(低成本、强确定)
   └── 否 -> 继续判断

3. 业务需要多步循环探索,但每一步的边界和合规要求极其严格吗?
   ├── 是 -> 选用 StateGraph(有限状态机,受控循环)
   └── 否 -> 选用带熔断防爆门的 Flat ReAct(开放探索)

掌握了从 Pipeline 到 StateGraph 的控制流底座,才能在后续的架构设计中,为复杂业务挑选出满足需求的最简方案组合。