AI Agent 编排演进录:从野生 ReAct 到企业级 Pydantic Graph

当极客助手走向生产级应用,我们如何平衡灵活性与确定性?

March 23, 2026·3 min read·Yimin
#AI Agent#Orchestration#Pydantic Graph#架构设计#ReAct

当你发现 LLM 开始忘记你写在 Prompt 里的 10 步执行计划时,你就知道,是时候放弃纯粹的文本魔法,投入工程化的怀抱了。

🎯 编排(Orchestration)的核心冲突

在构建单步问答机器人时,一切都很简单。但当我们希望 Agent 能够完成“查询日程 -> 规划路线 -> 预订酒店 -> 发送邮件通知”这类涉及多个外部系统、可能长达数分钟甚至需要等待用户中途授权的复杂任务时,**编排(Orchestration)**就成了核心难题。

这里存在一个根本性的冲突:高度灵活的 LLM 大脑 vs 需要绝对稳定和确定性的业务流程。

我们既希望 LLM 能像人一样聪明地应对突发状况,又害怕它在执行关键业务操作时“随性发挥”。在探索最佳实践的过程中,我们经历了三种典型的编排流派。


🏛️ 流派一:隐式编排(大模型驱动路由)

这是很多极客工具和早期个人助手最喜欢采用的方式。

核心思想: 将整个执行进度和状态总结成一段文本(比如 Active Plan Step Tracker),当作“备忘录”动态塞进每轮对话的 System Prompt 尾部,完全交由 LLM 根据这段文本来决定下一步该调用什么工具。

// 塞给 LLM 的备忘录示例
✅ Step 1: 查询机票
   Expected: 获得航班列表
   Actual: 查到两班可用航班

→ Step 2: 预订最便宜的航班
   Expected: 获得预订确认号

Please read the actual result and decide next tool to call.

高光时刻:极佳的容错弹性与原生 Agent 体验

这种模式最大的魅力在于异常处理极其自然。因为 LLM 每一轮都在“阅卷”,如果 Step 2 调用的预订 API 报错“余额不足”,LLM 会在下一轮直接“看到”这个报错。它可能不需要你写任何 try-catch 代码,就会自己决定“那我换一张信用卡重试”或者“给用户发消息求助”。这种见机行事的灵活性,是最符合人类直觉的“真 Agent”体验。

事实上,如果底层模型的 Context Window 足够大、Attention 机制足够强(比如顶配的 GPT-4 或 Claude 3.5 Opus),能保证不跑偏,这其实是目前最好用、开发最敏捷的模式。

翻车现场:上下文污染与失控

但现实往往很骨感。当流程涉及十几步,或者某一步的 API 返回了巨量的内容(如长网页 HTML),这段可怜的“备忘录”常常被挤出 LLM 的注意力焦点(Attention)。LLM 可能会开始“无视”备忘录,胡乱跳过步骤,甚至陷入死循环。对于要求 100% 确定性的生产环境来说,这种不可控性是巨大的隐患。


🏛️ 流派二:静态规划与顺序执行

为了解决不可控的问题,很多业务系统转向了传统的代码控制流。

核心思想: 先让 LLM 一次性规划好所有步骤,生成一个完整的 Plan 列表(JSON 格式),然后引擎接管,用一个死板的 for 循环按顺序依次执行这些步骤。

痛点分析:当静态流水线遇到动态世界

为什么这种模式在面对复杂业务时会捉襟见肘?从数据结构的本质来看,这种规划生成的是一个线性表(List)或最多是有向无环图(DAG)。它的执行逻辑是 for step in plan:,水永远往低处流,没有“回头路”。

痛点场景线性控制流的尴尬
脆弱的异常处理(缺乏回头路)执行到 Step 3 发生网络超时,由于 for 循环是单向向前的,极难优雅地中断当前循环,把现场数据打包扔回给 LLM 让它“针对当前残局重新规划”。通常只能粗暴地标记整个任务失败。
复杂的中断恢复假设 Step 4 需要等待用户授权后才能继续。在单线程线性循环里处理这种异步挂起(Suspend),往往需要手写非常繁琐的状态机:手动将当前执行上下文序列化存入 Redis,稍有不慎就会导致状态不一致。

这种模式把非结构化的世界强行拉平,导致业务代码中充满了为了应对各种 Edge Case 而打的补丁。


🧠 终极进化:基于 Graph 的数据驱动编排

当业务逻辑复杂度达到中台级别时,我们开始引入基于有向图(如 Pydantic Graph 或 LangGraph)的编排架构。这不是简单的工具替换,而是一次范式跃迁

为什么必须是 Graph(图)?

在计算机科学中,Graph(图)相比于线性表和树,最核心的特权在于它允许存在环(Cycles)和复杂的条件分支。 在过去的线性编排中,LLM 在运行时动态生成一条一次性的线性“轨道”,代码顺着跑完就结束了。 而在 Graph 框架中,程序员在编译时预先铺设好一个四通八达的“铁路网”,然后让 LLM 在关键节点充当“扳道工”,根据当前上下文决定数据流向。这带来了三个降维打击:

1. 结构化接力(Type-Safe Orchestration)

规划节点(Planner)不再返回长篇大论的 Markdown 文本,而是被 Pydantic 强制约束,每次只能输出一个极其精准的“下一步指令”对象:

# 静态检查期就能发现错误!
class StepCommand(BaseModel):
    is_finished: bool
    tool_name: str
    tool_params: dict

执行节点(Executor)拿到指令,跑完代码,再把严谨的 StepResult 对象扔回给 Planner。数据在节点间像接力棒一样传递,彻底消除了路由层的幻觉

2. 原生中断与恢复(Suspend & Resume)

图(Graph)天生就是状态机。在这个架构下,当执行节点发现没有 Auth Token 时,它不需要你自己去写 Redis 保存逻辑。它只需向外抛出一个 FlowSuspendSignal(挂起信号)。 图引擎会自动冻结当前的调用栈上下文,完成持久化,并优雅地结束本次请求。当拿到外部授权回调时,引擎注入 FlowResumeInput 唤醒图,代码会丝滑地从断点继续往下跑。复杂的“人机协同”变得像写同步代码一样简单。

3. 理想丰满与现实骨感的“重规划(Re-plan)”

在图的架构设计中,我们通常会画出一条优美的“错误回退边”:当执行节点抛出异常时,将其包装成带有错误信息的对象流回 Planner Node,让 LLM 重新下发指令。

然而,在实际工程中,这种 Re-plan 往往“不好用”或“很难工作好”

  1. 上下文割裂导致“管中窥豹”:为了保持节点独立,流回 Planner 的往往只有干瘪的报错信息(如 {"error": "timeout"})。Planner 因为没有完整参与到前面的“沉浸式对话”中,缺乏全局语境,它的重规划往往很盲目。
  2. 陷入“死循环”的深渊:正因为 Planner 是个“只看指令状态”的瞎子,它极易陷入“生成相同指令 -> 执行报错 -> 再次生成相同指令”的死循环。

这正是 Graph 架构为了获得安全性而必须付出的灵活性代价。你用铁轨把 LLM 关在了确定的笼子里,就不能指望它还能像隐式编排里那样纵观全局、游刃有余地兜底。


📝 总结:场景决定架构

架构没有绝对的好坏,只有最适合当下的选择:

  1. 实验性工具 / 极客玩具:拥抱纯提示词驱动的隐式编排(如通过 Prompt 维护 Tracker),你可以享受极速的开发体验和 LLM 自身的超强纠错能力,非常敏捷。
  2. 高并发 / 强监管的生产级服务:当任务失败成本极高,或者需要复杂的权限中转时,请尽早用 Pydantic Graph 等框架铺设类型安全的“轨道”。把非结构化的 LLM 关在结构化的笼子里,用引擎接管路由与状态,这是目前通往工程化的必经之路。