跳到主要内容

从第一性原理设计智能体运行时

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数智能体(Agent)框架在早期都会犯一个关键性错误:将智能体视为一个函数。你调用它,它循环运行,然后返回。这种思维模型在演示(Demo)中行得通。但当一个现实世界的任务运行了 45 分钟,在第 23 步遇到速率限制(Rate Limit),而你却没有任何可以恢复的内容时,它就会崩溃。

生产环境的智能体运行时(Runtime)不是一个函数运行器。它是一个执行基座(Execution Substrate)——比起 Python 函数,它更接近于进程调度器或分布式工作流引擎。从一开始就理清这一区别,决定了你的智能体系统是能够优雅地处理故障,还是需要人工点击重试。

破坏简单循环的三个问题

在构建任何东西之前,你需要准确理解为什么一个天真的 while agent_not_done: step() 循环会在生产环境中失败。这里有三个复合问题。

延迟。 LLM 很慢。一个包含 10 次工具调用的多步骤任务可能在每次 LLM 调用上花费 4 到 8 秒。如果其中任何调用可以并行完成,那么按顺序阻塞每个调用就会浪费数秒的时间。真实用户会注意到两秒钟不必要的等待。循环没有并行的概念——它默认将一切串行化。

可靠性。 长时间运行的任务会失败。网络会掉线。API 会触发速率限制。服务会宕机。你的智能体运行时间越长,被中断的可能性就越高。一个单纯的循环不提供恢复路径。如果它在 25 个步骤中的第 18 步抛出异常,你只能从第 1 步重新开始。每一次 LLM 调用都要付费两次。更重要的是,任何已经产生的副作用——写入的文件、发送的电子邮件、修改的记录——都可能执行两次。

非确定性。 LLM 在不同运行中会产生不同的输出,即使在 temperature 为 0 时也是如此(实际上)。你无法可靠地回放历史。你能做的是通过检查点(Checkpoint)保存状态,以便恢复时从保存的快照开始,而不是重新运行前面的步骤。但循环没有检查点模型,因此重新运行是你唯一的选择。

这三个问题叠加在一起。一个在负载下会失败、需要重启、以非确定性方式回放,并且延迟极高的任务,绝不是生产系统。它只是一个穿着西装的原型。

为什么是图,而不是链

在一个严肃的智能体运行时中,第一个架构决策是如何表示执行结构。天真的答案是步骤的顺序列表。更好的答案是有向图(Directed Graph)。

图可以让你表达列表无法表达的两件事:并行和循环。

并行之所以重要,是因为独立的子任务——从两个 API 获取数据,或者调用两个互不依赖的工具——应该并发执行。在列表中,你将它们串行化。在图中,你将它们连接为兄弟节点,运行时会同时执行它们。

循环之所以重要,是因为智能体不是流水线。它们会循环。智能体根据当前状态决定是继续还是终止,而不是预定的步骤计数。列表如果不通过索引跳转(Index-jumping)之类的变通手段就无法表示这一点。图则能自然地表示它:一个节点可以有一条指向先前节点的边。

这就是为什么原本为分布式数据处理开发的基于图的执行引擎,能出奇地契合智能体工作流。那些使其在处理数十亿条边时表现出色的特性(并行、容错、迭代收敛),正是智能体运行时所需要的。

执行模型:超级步与确定性并发

一旦你采用了图表示,你就需要一个能正确处理并行节点的执行模型。天真的方法——使用共享的可变状态并行运行一切——会产生竞态条件(Race Conditions)。两个节点同时更新同一个字段会根据谁先写入而产生非确定性的结果。

解决方案来自一个有着数十年历史的大规模并行图计算算法。核心思想是将执行结构化为离散的、同步的轮次,称为超级步(Super-steps)。

这里是它的工作原理:

  1. 规划(Plan):识别哪些节点有资格执行。资格由订阅(Subscription)决定——当一个节点读取的通道(Channel)更新时,该节点就会运行。
  2. 执行(Execute):并行运行所有符合条件的节点。关键在于,每个节点在超级步开始时都会获得一份独立的状态副本。在执行期间,任何节点都看不到其他节点的写入。
  3. 协调(Reconcile):收集所有已完成节点的更新,并按确定的顺序将其应用到共享状态。不存在写入时间竞争——协调发生在所有执行完成后。
  4. 检查点(Checkpoint):序列化最终状态。这个快照现在就是恢复点。

第 2 步中的隔离是关键洞察。通过给每个节点一份私有的状态副本,你完全消除了节点间的数据竞争。两个同时向同一字段写入的节点,其写入将在第 3 步中确定性地合并——由运行时定义合并语义,而非执行时机。

这种模型为你提供了无需锁的正确并行执行。它还为你提供了一个干净的检查点:在每个超级步之后,状态都是完全确定的且可序列化的。

检查点机制并非可选

许多团队将检查点机制(Checkpointing)视为一种“锦上添花”的功能。直到他们第一个运行了 30 分钟的 Agent 任务在第 28 分钟失败时,才发现它是整个系统的“承重墙”。

在 Agent 运行时中,完善的检查点机制意味着在每个超级步(Super-step)之后序列化完整的执行状态。“完整”不仅包括 Agent 的工作内存,还包括完整的图状态:哪些节点已运行、产出了什么输出、通道(Channels)中包含什么。检查点是一个足以从该确切点恢复执行的快照。

这种设计的性能影响至关重要。加载检查点在历史长度上应该是 O(1) 的——你只需加载最新的快照,而不是重放所有之前的事件。历史查询是另一个独立的问题;它们扫描快照日志,但不应影响热路径(Hot-path)的执行延迟。

这种有界的恢复成本使检查点机制变得实用。如果恢复需要重放所有之前的步骤,长运行任务将面临不断增长的恢复时间,最终导致恢复比重启还要慢。基于快照的方法使得恢复时间保持恒定,无论 Agent 已经完成了多少步骤。

在实践中,区别在于:一个在第 199 步失败的 200 步任务,其恢复时间与一个在第 4 步失败的 5 步任务相同。步骤数不会累加进恢复成本中。

人在回路(Human-in-the-Loop)是一等原语

大多数 Agent 框架将人工审核视为一种事后补救——一个告诉循环打印消息并等待标准输入(stdin)的标志位。这在任何实际部署中都会失效。你无法为了等待一个可能在 5 分钟或 5 小时后才回复的人而让一个进程无限期地存活。

正确的模型将人工干预视为预期事件,而非异常事件。当 Agent 需要人工输入时,它会持久化检查点状态,发出暂停信号,并终止其进程。没有任何东西被保持在线。当人类回复时,运行时从检查点恢复——Agent 继续运行,就好像从未停止过一样。

这种关注点分离至关重要。Agent 进程不管理人工延迟。Agent 不需要知道人类是在 10 秒还是 10 小时后回复的。检查点保证了上下文在间隙中得以保留。恢复过程与从服务器崩溃中恢复并无二致。

这种模型还支持有意义的监督模式。与其进行二选一的“批准或拒绝”,你可以建模部分状态编辑——人类审查当前状态,纠正特定字段,Agent 携带纠正后的值继续运行。或者是升级(Escalation)——Agent 暂停,人类将其升级到不同的工作流,原始 Agent 带着注入的额外上下文恢复运行。

设计这些交互模型需要在构建审批流之前思考状态模式(State Schema)。检查点就是接口。人工审核者需要查看、理解和修改的所有内容,都必须在检查点状态中有所体现。

从一开始就考虑流式传输与可观测性

Agent 系统的另一种失败模式是不透明。Agent 启动,运行 90 秒,然后产生输出或报错。你完全不知道这期间它在做什么。

基于超级步构建的 Agent 运行时具有天然的可观测性钩子。每个步骤边界都是一个事件:运行了哪些节点、产生了哪些状态变化、最终状态是什么。这些事件是流式输出和分布式追踪的基石。

流式输出不仅是一个 UX 功能。在生产系统中,流式传输是建立可靠信心的手段,让你确信长运行任务正在取得进展。一个能随进度流式传输部分结果的 Agent,可以让下游系统——仪表盘、监控工具、人类操作员——尽早发现停滞,而不是等到超时触发。

步骤边界也能清晰地映射到追踪跨度(Trace spans)。每个节点的执行都是一个具有定义好的开始、结束、输入和输出的 Span。节点内的 LLM 调用是子 Span,工具调用也是子 Span。图结构映射到追踪层级。这为你提供了针对非确定性 Agent 行为的分布式追踪级调试能力,否则这种行为几乎无法诊断。

事后再补上可观测性是痛苦的。围绕步骤边界设计的运行时使其成为结构性的特征。

哪些东西不应内置

生产环境中运行时设计中一个不太明显的选择是:有意地排除掉什么。

特定的 LLM 供应商、工具模式、内存后端——这些都会改变。供应商 API 在进化,新模型在出现,内存架构因用例而异。一个内置了这些假设的运行时会迅速过时。

运行时的职责是执行语义:节点如何调度、状态如何在它们之间流动、如何提取检查点、如何处理失败。该层之上的所有内容——特定的 Agent、它们调用的工具、它们调用的 LLM——都属于用户的代码,而非运行时。

这种分离使得运行时在技术栈的其他部分进化时保持稳定。它还使运行时可以被隔离测试。你可以为调度逻辑、检查点/恢复行为和状态协调编写确定性的单元测试,而无需模拟(Mocking)LLM 调用。

这一原则也延伸到了 API。如果执行引擎与面向开发者的图构建 API 解耦,你可以独立迭代两者。废弃一个图构建的便捷方法不需要更改底层的调度器。优化调度器也不会破坏现有的图定义。

为尚未到来的规模而构建

反对早期构建完善运行时的理由是,在达到真实规模之前,这些开销并不划算。这通常是错误的。

使运行时具备可扩展性的属性——O(1) 检查点加载、隔离的节点执行、确定性并发——同样也是使其在小规模下可调试且可靠的因素。你可以立即获得调试方面的好处。而当负载增加时,扩展性的优势也会随之而来。

不预先构建这些属性的代价是,你以后必须在生产环境的压力下对其进行改造。将检查点语义改造进一个运行中的系统,需要对所有现有状态进行仔细的迁移。将确定性并发改造进原本假设为顺序执行的代码中,需要仔细检查以前隐藏的竞态条件。

这些问题在负载下很难修复。但如果从一开始就做对,它们是很简单的,因为那时图结构仍然灵活,状态模式尚未被生产数据冻结。

智能体系统仍处于早期阶段,现在的运行时选择将决定两年后哪些事情可行,哪些不可行。生产级执行模型并不是过早优化——它是使上层一切保持可靠的基础。

References:Let's stay in touch and Follow me for more thoughts and updates