智能体运行框架
目的
为什么一个运行五轮仍能正常工作的智能体循环,在任其运行五百轮时就会失败?
智能体原型是围绕模型调用构建的循环,对于短任务,这个循环已经足够。但当它连续运行数小时,同一个循环就会遇到无法处理的事件。某个工具挂起,整条轨迹也随之停滞。模型不断提出重试,每次重试的形式都正确,却没有一次能改变任何情况,于是费用不断增加,任务却停滞不前。操作人员需要停止运行,但循环只监听模型自身发出的完成信号。一个删除生产数据的提议一经提出便立即执行,因为提议与实际效果之间没有任何阻拦。这四种失败有着共同的原因。关于轨迹正在做什么、已经花费多少以及正在等待什么的唯一记录,都存在于循环和模型上下文内部,外部无法读取或修改。运行框架将这份记录移到模型之外,并利用它来暂停、审批、约束和停止轨迹,无须要求模型配合。它是运行时针对 H·S·A 中时间跨度暴露的首个闭环控制机制,因为每增加一轮,就多一次陷入循环、停滞或超支的机会;同时,它也是权限暴露遭遇最棘手情形的地方:任何沙箱都无法撤销的不可逆操作。
学习目标
- 解释为什么智能体原型的循环无法在没有模型配合的情况下暂停、约束或停止轨迹
- 设计轨迹记录,将状态、预算、授权和待处理工作保存在模型上下文之外
- 构建闭合的轨迹状态机,使其守卫条件能够拒绝迟到的工具结果和非法的恢复操作
- 利用停止原因和流式取消,在轮次边界执行取消、暂停、恢复和强制终止
- 为不可逆的工具调用设计审批关卡,包含有效期、前置条件复查和拒绝执行的观测结果
- 根据多维预算计算轨迹的支出,并选择由引导、限制到停止的逐级控制策略
- 根据实测的成本与成功率曲线评估预算上限,而非采用固定的经验法则
从 While 循环到运行框架
设想共享节点上的一条轨迹:它已经执行了四十轮,而任务原本预计五轮即可完成。在同一分钟内,四个事件接踵而至。操作人员希望暂停轨迹以便检查;另一个租户的交互式任务正在等待同一模型的速率限额;轨迹的下一个提议将删除一张生产表;而它已经花掉了大部分预算,却仍未通过任何测试。闭环轨迹 中的智能体循环以围绕模型调用的 while 循环编写,无法对这些事件中的任何一个采取行动。它唯一的退出途径是模型结束回复时给出的停止原因(调用契约)。轨迹的阶段、支出、待处理工具调用和权限,仅存在于消息列表和解释器调用栈中。如果进程死亡,轨迹也随之死亡,而它在沙箱中启动的任何进程都会继续运行,无人回收。
这四个事件中的每一个都要求提供模型无法提供的能力。暂停需要在模型之外有一个位置,能够接收停止请求,并在安全时机执行该请求。向交互式租户让出执行权,需要一个能够查看哪些轨迹正在等待、哪些正在运行的调度器。暂缓删除表的操作,需要在提议与工具之间设置一道关卡。停止无效开销,需要一个模型既无法读取也无法重置的计量器。不变量闭合(原则 不变闭包原则)已经说明了这类约束应当位于何处:位于模型之下的运行时中,无论模型提出什么,都必须强制执行。第二部分将这一规则应用于下一次调用可见的内容,第三部分将其应用于单个动作可以触及的范围(工具调用、智能体沙箱)。第四部分将其应用于循环本身,而承载这一规则的组件就是智能体运行框架。
Definition 0.1: 智能体运行框架
智能体运行框架 是负责管理单条轨迹的智能体循环的运行时组件。它组装每次模型调用、解释模型回复、决定每个提议的工具调用是否以及何时派发,并维护记录,使循环之外的任何主体都能通过这些记录检查、暂停、约束或停止该轨迹。
- 重要性:所有必须跨轮次成立的保证(轮次上限、以美元计的费用上限、不可逆调用前的审批、收到请求后有序停止)都只能在这里强制执行,否则便无处执行,因为模型只能看到其上下文,不能依赖它来计数、让出执行权或停止。
- 区别:工具层决定单次调用是否有效、是否获得授权以及是否已完成结算,沙箱则限定该调用可以触及的范围;运行框架决定调用究竟何时发生、以何种顺序发生,以及轨迹是否继续。
- 常见误区:将运行框架视为围绕模型的胶水代码,导致状态、开销和待处理工作都存放在局部变量与提示文本中,任何调度器、运维人员或恢复路径都无法读取它们。
图 1 对比了这两种设计。在面板 (a) 中,原型循环调用模型,执行返回的任何工具调用,并追加结果,因此循环之外的任何主体都无法介入,也没有任何内容能在进程结束后保留下来。在面板 (b) 中,同一个循环运行于运行框架内部。模型仍然读取上下文并返回提议,但提议会交给运行框架;在任何操作发生之前,运行框架都会查阅其维护的记录。获准的调用进入第三部分构建的工具层和沙箱层,观测结果则先经由运行框架返回,再进入下一次上下文。
运行框架的一个轮次可以具体说明这种职责分工。代码 1 展示了运行框架围绕单次模型调用所作的决策。在调用模型之前,它检查是否存在待处理的控制请求,以及预算是否已经耗尽。它调用模型时设置输出词元上限和截止时间。它根据提供方的用量报告计入此次调用的费用,绝不依据模型自身的陈述。随后,它根据停止原因进入不同分支。结束本轮的回复会进入完成检查,被截断的回复绝不会被派发,而包含工具调用的回复则会在审批关卡等待,或通过工具层派发。
def run_turn(rec: TrajectoryRecord) -> None:
if rec.pending_control: # cancel, pause, kill
return apply_control(rec) # applied at a turn boundary
if budget_exhausted(rec):
return finalize(rec, reason="BUDGET_EXCEEDED")
transition(rec, "CALLING_MODEL")
reply = call_model(rec.context(),
max_tokens=rec.limits.max_output_tokens,
deadline=rec.limits.call_deadline)
charge(rec, reply.usage) # metered, not self-reported
if reply.stop_reason == "end_turn":
return finalize(rec, reason="MODEL_ENDED") # completion check
if reply.stop_reason == "max_tokens":
return observe(rec, truncation_notice(reply)) # never dispatched
calls = reply.tool_calls
if not calls: # refusal, stop sequence, text
return observe(rec, reply) # handled by policy, then READY
if any(needs_approval(c, rec) for c in calls):
return hold_for_approval(rec, calls)
for c in calls:
dispatch(rec, c) # validated, granted, keyed
transition(rec, "WAITING_TOOL")代码清单中的每一行都会读取或写入 rec,本章其余部分将依次围绕这些读写操作展开。首先介绍记录本身,因为运行框架无法对其无法描述的轨迹施加任何约束。接下来介绍状态和带有守卫条件的 transition 函数,然后介绍驱动它们的控制动作与停止原因、对工具和人员的等待、选择运行哪条就绪轨迹的调度器,最后介绍 charge 和 budget_exhausted 背后的预算。
简单的字符串拼接 while 循环与经过架构设计的智能体运行框架之间,存在显著的性能差异;这种差异不仅是理论上的,也可以在复杂推理环境中通过实证测量。如 图 2 所示,在 ARC-AGI 交互式推理基准 (Chollet 2019年) 上对完全相同的基础模型策略进行评测,揭示了运行框架敏感性定律。在使用标准 while 循环 ReAct 框架 (Yao 等 2023年)、传递非结构化文本帧和未受管理的暂存存储器笔记时,模型在不同搜索预算下仅能解决 35.2% 至 62.7% 的任务。相比之下,将相同的神经网络权重封装在有状态的提供方适配器运行框架中——该框架维护原生键值缓存前缀树,强制使用经过模式验证的状态帧,并将工具执行与内存滞留解耦 (Yang 等 2024年)——可使任务准确率跃升至 99.95%。这一高达 37.24 个百分点的能力跃升证明,智能体在现实世界中的能力和运行效率受其运行框架状态机的架构保真度约束,而非仅由神经网络参数规模的扩展决定。
这一实证差异说明了为什么运行框架状态机是一种一等系统原语。未受管理的 while 循环在复杂推理任务上表现退化,原因在于三种相互叠加的失效模式:交互记录膨胀(\(O(T^2)\) 次 token 重传导致前缀缓存被驱逐)、未经验证的模式漂移(允许幻觉产生的参数被静默执行),以及缺乏事务回滚(使环境中的瞬时故障永久污染轨迹上下文)。相比之下,经过架构设计的运行框架在模型策略之下强制维持不变量的闭包性质。
造成这种性能与成本差异的架构机制汇总于 表 1。简单的 while 循环脚手架不断重传日益增长的上下文字符串,并维持未受管理的工具执行,而经过架构设计的智能体运行框架通过确定性的系统原语管理内存、状态表示和执行不变量。
| 系统维度 | 临时 while 循环脚手架 | 有状态的提供商适配器运行框架 |
|---|---|---|
| 上下文管理 | 朴素的字符串重发(\(O(T^2)\) token 膨胀) | 原生 KV 缓存前缀复用(共享提示词块) |
| 状态表示 | 非结构化、自由形式的暂存文本 | 带类型的轨迹记录(智能体控制块) |
| 工具挂起 | 同步阻塞的远程调用(加速器内存被锁定) | 协作式异步让出执行权与驱逐 |
| 错误恢复 | 遇到致命执行故障时中止进程 | 确定性的检查点回滚与修复 |
| 动作不变量 | 未经检查便分派工具调用提议 | 执行前的能力与模式验证 |
轨迹记录
一个节点运行着五百条轨迹,运维人员需要知道哪些轨迹正在等待人工响应,哪些已消耗超过 90% 的预算,以及哪些仍持有沙箱。交互记录无法快速回答这些问题,因为它记录的是说过什么,而不是轨迹持有什么、尚需履行什么,或正在等待什么。同样的缺口也出现在运行框架内部的每一轮中。当工具结果到达时,运行框架必须先知道哪些调用尚未完成,才能接受该结果;当取消请求到达时,它必须知道应释放哪些作业和租约。因此,运行框架为每条轨迹维护一份小型结构化记录,本章中的每种机制都通过这份记录运作。
Definition 0.2: 轨迹记录
轨迹记录 是由运行框架拥有、模型从不写入的每条轨迹的控制状态:轨迹标识、当前状态、单次调用限制、预算与实测消耗、授权与租约、待处理工作,以及指向其上下文、计划、日志和工作区的指针。
- 意义:它使整条轨迹成为可调度、分配预算、暂停、批准和停止的单位,而非单次模型调用,并使这些操作的耗时与轨迹数量成正比,而非与交互记录的长度成正比。
- 区别:交互记录供模型读取;轨迹记录供运行框架读取。持久执行 中的持久日志是可用于重建记录的历史;轨迹记录则是运行框架每一轮都会查阅的实时投影。
- 常见陷阱:将控制状态存储在提示词中(例如,“你已使用 50 轮中的 40 轮”),或允许模型自身的声明更新控制状态,这会使每一项限制都变成模型可以忽略的请求。
表 2 按照各组字段使运行框架能够执行的工作,对记录中的字段进行分组。大多数字段组对应的机制由本书其他部分负责;运行框架在这份记录中保留对这些机制的访问句柄。
| 字段组 | 内容 | 使运行框架能够执行的工作 |
|---|---|---|
| 标识 | 轨迹 ID、父级 ID、租户、任务契约(任务规范契约) | 路由控制请求、向正确的租户计费、将重试或子智能体关联到其来源 |
| 状态 | 当前状态、退出原因、轮次计数 | 无需读取交互记录即可回答“这条轨迹正在做什么” |
| 单次调用限制 | 模型版本、采样参数、最大输出 token 数、调用截止时间(调用契约) | 限制并复现每次模型调用 |
| 预算与消耗 | 轮次、token 数、实际经过时间和美元支出的上限;各项的实测用量 | 强制执行模型无法查看或重置的上限 |
| 授权与租约 | 工具授权(可互操作的工具发现)、工作区租约(智能体工作区)、凭证句柄 | 在轨迹执行过程中收缩权限,并在退出时释放所有资源 |
| 待处理工作 | 尚未完成的工具调用 ID 及其截止时间、作业句柄(异步工具分派)、暂存的审批、待处理的控制请求 | 仅接受匹配的结果,并准确取消仍未完成的工作 |
| 指针 | 上下文存储(上下文工程)、计划(作为可修订状态的计划)、日志位置、工作区快照 | 访问体量较大的状态,而无需将其复制到记录中 |
指针组解释了为什么记录能保持较小的体量。它引用交互记录、计划和工作区,而不是包含这些内容,因此一次状态查询、预算检查或调度决策只需访问几百字节,而非数十万 token。同样的约束也适用于服务侧。轨迹在服务系统中的注意力状态(KV 缓存管理)保留成本高,而引用成本低;轨迹等待期间该状态是否继续驻留,则是 保留、驱逐、重计算或卸载 中作出的服务决策。记录保存引用,运行框架提供预期等待时间,但记录绝不会成为存放该状态的地方。
第二项约束涉及由谁写入。模型不具备任何隐式权限,因此它输出的任何内容都不会直接改变记录。模型在输出中声称任务已完成、剩余预算很少,或某次调用是安全的,这些都只是供运行框架评估的观察信息,而不是应直接应用的更新。消耗字段来自提供商为每次调用报告的用量,状态只能通过运行框架验证的转换而改变,授权也只能由运行框架决定收缩。这是轨迹级封装(原则 11)的具体体现。整条轨迹是运行时进行调度、分配预算和保护的单位,其控制状态位于模型无法触及之处。
现在,记录已包含运行框架描述一条轨迹所需的全部信息。然而,描述的可信程度取决于改变它的规则;如果任何代码路径都能覆盖状态字段,就只是在多加一层间接访问的情况下重现原型循环的失效。下一节将把状态字段的变化限定在一组规模较小、带守卫条件的状态转换之内。
将控制元数据与注意力内存解耦
为了支持数千个并发智能体会话而不耗尽主机资源,运行框架将轻量级轨迹控制元数据与占用大量内存的 Transformer 推理缓冲区解耦。对于一条长度为 \(L = 32{,}768\) 个 token 的智能体轨迹,若模型具有 \(H = 64\) 层、\(N_{\text{kv}} = 8\) 个键值头,且维度为 \(D = 128\): \[ M_{\text{KV}} = 2 \times H \times N_{\text{kv}} \times D \times L \times \text{sizeof(fp16)} = 2 \times 64 \times 8 \times 128 \times 32{,}768 \times 2 \approx 8.59 \text{ GB} \] 上下文 token 缓冲区占用: \[ M_{\text{tokens}} = L \times 4 \text{ bytes} = 32{,}768 \times 4 = 131{,}072 \text{ bytes} \approx 128 \text{ KB} \] 与之形成鲜明对比的是,智能体控制块(ACB)元数据结构需要紧凑且固定大小的存储空间: \[ \text{SizeOf}(\text{ACB}) = 256 \text{ bytes} \] 在内存中维护 10,000 条并发智能体轨迹仅需要: \[ M_{\text{control}} = 10{,}000 \times 256 \text{ bytes} = 2.56 \times 10^6 \text{ bytes} \approx 2.44 \text{ MB} \]
ACB 作为描述符,持有指向外部物理资源的间接引用句柄,从而实现这种彻底的内存解耦,详见 表 3。
| 指针引用 | 目标子系统 / 内存层级 | 物理存储位置 | 间接引用与权限衰减不变量 |
|---|---|---|---|
context_buf_ref |
逻辑词元缓冲区 | 主机系统 RAM | 单调增长的词元 ID 序列与角色边界;在预填充批次之间共享。 |
kv_lease_id |
物理 PagedAttention 表 | GPU 高带宽内存(HBM) | 绑定到推理引擎的内存租约;可迁移至主机 DRAM,或在资源争用时被驱逐。 |
wal_stream_offset |
仅追加的预写日志 | 本地 NVMe 闪存 | 单调递增的字节偏移量,确保在轮次派发前完成持久状态恢复。 |
cap_token_ref |
权限衰减后的能力令牌 | 监督器安全内存 | 经 Ed25519 密码学签名的令牌,确保各沙箱均无环境隐式权限。 |
表 4 比较了每个智能体一个线程、异步协程与 ACB 进程表之间的架构权衡。
| 系统维度 | 每个智能体一个同步线程 | 协作式事件驱动让出执行权 |
|---|---|---|
| 工作单元并发数 | 受 OS 线程数上限约束(\(10^2\text{--}10^3\) 个线程) | 仅受主机内存中描述符容量约束(\(10^4\text{--}10^5\) 个 ACB) |
| 空闲内存占用 | 每条空闲轨迹占用 \(2\text{--}8\text{ MiB}\)(线程栈 + 内核结构体) | 每条空闲轨迹占用 \(64\text{--}128\text{ KiB}\)(RAM 中序列化的 ACB) |
| 信号截获 | 阻塞于内核系统调用;需要 EINTR 重试处理 |
即时响应;在线程释放前的检查点检查信号位 |
| 加速器服务槽位 | 工具执行期间持续占用或泄漏;GPU 利用率低 | 立即释放;GPU 资源为活跃的解码队列提供服务 |
| 故障影响范围 | 工作单元崩溃会摧毁线程栈,并使沙箱成为孤立实例 | ACB 的生命周期长于工作单元;崩溃恢复需要持久日志 |
轨迹状态
两个故障案例展示了未受保护的状态字段会允许什么发生。在第一个案例中,一条轨迹派发了一个测试套件,截止时间为十分钟后。截止时间已过,运行框架追加一条超时观测,模型随后提出另一种方法。一分钟后,原先的运行完成,其结果到达。如果运行框架追加该结果,下一次调用就会看到针对同一次调用的两条相互矛盾的观测:一条说它已超时,另一条说它已通过。这种迟到结果越过较新决策的竞态称为观测交叉。在第二个故障案例中,一条轨迹正在等待人工批准删除数据库,而运行框架其他位置的一条重试路径看到轨迹处于空闲状态,便再次调用模型,并派发模型提出的任何操作。这两个故障都源于代码直接设置状态,而不检查这一变更是否合法。
运行框架通过为轨迹定义一个封闭的状态集合,并且只允许设计中明确规定的状态转换,来防止这两类故障。表 5 按各状态持有的内容及其下一步可转入的状态列出了这些状态。其中六个为活动状态,三个为终止状态。
| 状态 | 含义 | 轨迹持有的内容 | 合法的后续状态 |
|---|---|---|---|
READY |
已排队,等待下一次模型调用 | 仅记录 | CALLING_MODEL, PAUSED, FINALIZING |
CALLING_MODEL |
一次模型调用正在进行 | 一个带有输出上限和截止时间的未完成请求 | READY, WAITING_TOOL, AWAITING_APPROVAL, FINALIZING |
WAITING_TOOL |
工具调用已派发,结果尚未返回 | 待完成的调用 ID、截止时间、作业句柄 | READY, FINALIZING |
AWAITING_APPROVAL |
一个不可逆调用被暂缓,等待决策 | 被暂缓的调用、其审批请求、到期时间 | WAITING_TOOL, READY, FINALIZING |
PAUSED |
在轮次边界暂停,等待人工介入 | 仅记录 | READY, FINALIZING |
FINALIZING |
退出前的清理 | 待取消的未完成作业、待释放的租约 | COMPLETED, FAILED, CANCELED |
COMPLETED, FAILED, CANCELED |
终止状态 | 无 | 无 |
图 3 绘出了同一个状态机。主循环从 READY 经 CALLING_MODEL 到达 WAITING_TOOL,再返回起点。没有可执行内容的回复,例如纯文本或被截断的回复,会直接返回 READY,并附带供下一次调用使用的观测。不可逆调用会转入 AWAITING_APPROVAL;若该调用获批且仍然有效,则从此状态转向 WAITING_TOOL,否则携带一条拒绝观测返回 READY。无论是模型结束了自己的轮次,还是操作员、终止指令或预算耗尽结束了运行,每一条退出路径都经过 FINALIZING。
PAUSED 只能从 READY 进入,即使暂停是强制触发而非主动请求的,这一点也成立。当自动恢复无法继续时,例如本应用于撤销先前步骤的操作已彻底失败,故障可能在任一活动状态中显现,通常是 WAITING_TOOL。随后,智能体运行框架分两步将问题升级交由操作员处理。它首先处理完所有正在进行的工作:取消尚未结束的模型调用,撤回暂扣的批准,并取消每个未完成的工具调用;若无法确认取消,则将其标记为状态存疑。这会使轨迹返回 READY,且不再有任何未完成事项。只有此后,它才通过常规的受守卫条件约束的转换暂停轨迹,并撤销其授权。因此,升级处理不会为状态机增加任何路径,操作员接手的轨迹也不会包含任何尚未完成的工作。枢轴操作的不可逆性 描述了导致这种情况的故障。
完成路径需要谨慎处理。停止原因为 end_turn 表示模型声称自己已经完成,而智能体运行框架将其视为运行任务完成检查的请求,而非成功。FINALIZING 运行任务契约规定的检查,并采用契约要求的闭环证据级别(闭合证据等级),且仅在检查通过时记录 COMPLETED。一条已完成的轨迹是否也算作已验收的任务,以及某个给定智能体达到这一结果的频率,属于 智能体评估 讨论的度量问题。
四项不变量使该状态机成为守卫机制,而不只是一张图:
- 工具结果仅在
WAITING_TOOL中被接受,且其调用 ID 必须仍处于待处理集合中。截止时间一过,智能体运行框架会先从待处理集合中移除该调用的 ID,再追加超时观测,因此,上述第一种故障中的迟到结果找不到匹配的 ID,便会被丢弃并记入日志。这就堵住了观测跨界的漏洞。 - 被暂扣的调用只有获得通过关卡复查的批准,才能离开
AWAITING_APPROVAL进入派发。其他任何路径,包括发现轨迹处于空闲状态的重试路径,都不能派发该调用,从而堵住了第二种故障的漏洞。 - 每次退出都经过
FINALIZING。该状态是清理屏障。它会取消未完成的作业,释放工作区租约和凭证,记录退出原因,并运行完成检查。直接跳转到终止状态的轨迹会遗留孤立进程和仍然有效的凭证。 - 终止状态具有吸收性。任何迟到结果、操作员操作或重试都无法使已结束的轨迹重新运行。重试是一条具有新 ID 的新轨迹,其记录将旧轨迹标记为父轨迹,因此每次尝试的历史都能完整保留,以供评估和调试。
代码 2 实现了这一守卫机制。智能体运行框架中的每次状态变更都经过同一个函数:检查该转换是否属于合法集合,求值附加于该转换的所有守卫条件,并记录变更。所有这些操作都在持有该记录的锁时执行,确保两个并发事件无法同时成功。
LIVE = {"READY", "CALLING_MODEL", "WAITING_TOOL",
"AWAITING_APPROVAL", "PAUSED"}
LEGAL = {("READY", "CALLING_MODEL"), ("READY", "PAUSED"),
("CALLING_MODEL", "READY"), ("CALLING_MODEL", "WAITING_TOOL"),
("CALLING_MODEL", "AWAITING_APPROVAL"),
("WAITING_TOOL", "READY"), ("AWAITING_APPROVAL", "WAITING_TOOL"),
("AWAITING_APPROVAL", "READY"), ("PAUSED", "READY")}
LEGAL |= {(s, "FINALIZING") for s in LIVE}
LEGAL |= {("FINALIZING", t) for t in ("COMPLETED", "FAILED", "CANCELED")}
def transition(rec, new_state, event):
with rec.lock:
key = (rec.state, new_state)
guard = GUARDS.get(key, lambda rec, event: True)
if key not in LEGAL or not guard(rec, event):
raise IllegalTransition(rec.state, new_state, event)
rec.history.append((rec.state, new_state, event.id))
rec.state = new_state
def on_tool_result(rec, result):
with rec.lock:
if rec.state != "WAITING_TOOL" or result.call_id not in rec.pending:
audit(rec, "late_or_unknown_result", result.call_id)
return # dropped, never appended
rec.pending.remove(result.call_id)
append_observation(rec, result)这一防护机制的开销包括一次加锁、一次集合查找和一次追加操作,相比耗时以秒计的模型调用,这些开销可以忽略不计,因此没有性能方面的理由可以跳过它。状态机现在规定了哪些转换是合法的。但它尚未规定谁可以从循环外部请求状态转换,也未规定当请求在模型调用流式输出期间到达时,该请求何时生效。
Checkpoint 0.1: 轨迹记录与状态
形式化的状态转换不变量
形式上,轨迹状态空间被定义为以下有限集合: \[ \mathcal{S} = \{\text{INIT}, \text{RUNNABLE}, \text{RUNNING}, \text{WAIT\_IO}, \text{WAIT\_ESCROW}, \text{PREEMPTED}, \text{COMPLETED}, \text{FAILED}, \text{TERMINATED}\} \] 状态转换依据显式的状态转移函数进行: \[ s' = \delta(s, e, g) \quad \text{where} \quad g(\text{ACB}, e) = \mathbf{true} \] 其中,\(e\) 是传入的生命周期事件,\(g\) 是用于判定 ACB 不变量是否成立的守卫谓词。代码 3 实现了严格的守卫检查,以防止非法的状态跳转。
def transition_acb(acb: AgentControlBlock, new_state: State, event: Event) -> None:
"""Validate and apply state transition on Agent Control Block."""
valid_transitions = {
State.INIT: {State.RUNNABLE},
State.RUNNABLE: {State.RUNNING},
State.RUNNING: {State.WAIT_IO, State.WAIT_ESCROW, State.PREEMPTED, State.COMPLETED, State.FAILED},
State.WAIT_IO: {State.RUNNABLE, State.FAILED, State.TERMINATED},
State.WAIT_ESCROW: {State.RUNNABLE, State.FAILED, State.TERMINATED},
State.PREEMPTED: {State.RUNNABLE, State.TERMINATED},
}
if new_state not in valid_transitions.get(acb.state, set()):
raise InvalidStateTransitionError(f"Illegal transition from {acb.state} to {new_state} via {event}")
acb.state = new_state
acb.last_event = event
acb.transition_epoch_ms = current_epoch_ms()控制操作
模型正在流式输出一个工具调用,其当前已输出的参数为 {"action": "drop_table", "target": "cust,此时操作员按下停止按钮。立即停止会留下一个不构成有效调用的片段;如果运行框架将其记录为轨迹的最后一个提案,之后解析日志的恢复路径可能会尝试补全该片段。等待调用完成,则意味着等待模型输出操作员正试图阻止的破坏性请求的剩余部分。人员和策略随时都可能发出控制请求,而轨迹只能在特定时刻安全地变更,因此运行框架需要一条规则来协调两者。
这条规则将请求到达与请求生效分开处理。控制请求到达时,运行框架将其写入记录中的待处理控制字段,随后返回。它在下一个轮次边界应用该请求;在这一时刻,没有正在进行的模型调用,也没有派发到一半的工具调用。轨迹会在以下时刻到达轮次边界:每次模型调用之前;模型回复已解析、但尚未派发任何内容之后;工具结果被接受之后;以及进入审批等待时。由于不能指望轨迹自行让出执行权或停止,这种控制必须置于模型之外(原则 12),而轮次边界就是运行框架实施控制的位置。控制操作列出了运行框架所需的请求。
| 操作 | 通常由谁发出 | 生效时机 | 产生的状态 |
|---|---|---|---|
| 取消 | 用户、操作员、父轨迹 | 下一个轮次边界;中止正在进行的调用 | FINALIZING,随后为 CANCELED |
| 中断 | 用户 | 下一个轮次边界,以一条新用户消息的形式生效 | READY,消息纳入上下文 |
| 暂停 | 操作员、调度器 | 下一个轮次边界 | PAUSED |
| 恢复 | 操作员 | 立即 | READY |
| 强制终止 | 操作员、安全策略 | 立即,即使轮次正在执行 | FINALIZING,随后为 FAILED |
| 超出预算 | 运行框架自身的计量器 | 下一个轮次边界 | FINALIZING,随后为 FAILED |
大多数请求都要等待轮次边界,这就引出了一个问题:模型调用需要多久才能到达这一边界。回复只有在输出最后一个 token 后才能结束,而解码时间会随输出 token 数量增长(加速器服务延迟)。因此,最坏情况下的等待时间等于输出 token 数上限乘以每个输出 token 所需的时间。对于允许输出数千个 token 的调用,这一等待时间可能达到一分钟甚至更久;对于试图停止破坏性操作的操作员而言,这实在太长。有两种机制可以缩短等待时间。第一种是输出 token 数上限本身,它在限制成本的同时,也限制了等待时间。第二种是流式取消:运行框架以流式方式处理每次模型调用,当有取消、暂停或强制终止请求待处理时,中止流、丢弃部分回复,并以取消事件代替提案写入记录。部分回复绝不会被解析为工具调用。被截断或中止的生成结果无法通过验证,因此不能被派发(原则 隔离不变量)。中止前已输出的 token 仍会计费,运行框架会像处理其他 token 一样将其计入费用。
每条模型回复还会报告其结束原因。归一化调用状态信封中的状态封装已经规定了循环可以如何处理调用的每种结果;运行框架只补充每种结果会使轨迹进入何种状态。轮次结束回复使轨迹转入 FINALIZING,以进行完成检查。包含工具调用的回复,如果其中任何调用不可逆,就使轨迹转入 AWAITING_APPROVAL;否则转入 WAITING_TOOL。因达到输出上限而被截断、因停止序列而结束,或表示拒绝的回复,会使轨迹返回 READY,并为下一次调用提供一条观测,除非任务策略要求升级处理或以拒绝结束任务。失败或超时的调用,在预算仍允许时使轨迹返回 READY以重试;预算不再允许时,则使其转入 FINALIZING。
强制终止是唯一无需等待的请求。它用于完成当前轮次本身就会造成危险的情形,例如尝试逃逸沙箱,或失控的工具正在写入共享存储。运行框架中止所有模型调用,通过 后台进程治理所述的进程组清理机制终止轨迹沙箱中的每个进程,并进入 FINALIZING,同时记录原因。打断轮次是有代价的。恰好在强制终止前派发的工具调用可能已经生效,也可能尚未生效,此时其结果无法确定。运行框架将这类调用标记为结果存疑,而不是失败,并将其交给 幂等动作执行中的结算机制处理;该机制使用调用的幂等键来确定其效果是否已经发生。当轨迹已派生子智能体时,取消和强制终止请求也必须传递给它们;取消级联将同一规则扩展到智能体之间。
控制操作使人员和策略能够在明确定义的时刻介入循环。然而,轨迹的大部分实际耗时并非用于模型调用,而是用于等待工具响应;在这一等待期间到达的请求,必须遇到处于可应用该请求的状态的轨迹。
安全检查点与信号评估
异步信号不得任意中断执行。在非原子文件系统操作进行到一半时终止智能体,会破坏工作区状态。运行框架定义了确定性的检查点,以便安全地评估待处理信号,详见 安全检查点。
| 检查点符号 | 周期边界 | 静止状态与不变量保证 | 监督拦截机制 |
|---|---|---|---|
| \(C_{\text{obs}}\) | 观测之后 | 沙箱执行已完成;观测封装已根据待处理 I/O 描述符完成验证。 | 在追加上下文之前拦截暂停或强制终止信号;丢弃无效的观测载荷。 |
| \(C_{\text{pre}}\) | 推理之前 | 上下文已组装并序列化;推理引擎上没有待处理的张量操作。 | 在派发成本高昂的预填充批次之前拦截信号;若暂停,则逐出 KV 租约。 |
| \(C_{\text{post}}\) | 解码之后 | 自回归解码已完成;语法已验证;提议的操作尚未解析。 | 在修改主机状态或验证工具参数之前捕获取消或暂停请求。 |
| \(C_{\text{tool}}\) | 工具派发之前 | 工具参数已验证;安全能力已检查;尚未派发 RPC。 | 最后一道安全关卡;防止沙箱或外部系统中产生尚未提交的副作用。 |
信号派发生命周期与检查点评估流程如 figure所示。
SIGPAUSE被缓存在 ACB 信号位掩码中。在有界子检查点 \(C_{\text{post}}\)(\(K=16\) 个 token,延迟 \(\le 533\text{ ms}\)),监督器拦截执行,将 ACB 持久化到持久存储,通过 PCIe Gen5 将 2.50 GB 的 KV 缓存逐出至主机 DRAM,并将智能体置于静止的挂起状态。收到 SIGRESUME后,监督器重新装载内存并恢复工具派发,且不会造成上下文撕裂。
Evaluating Supervisory Signals at Checkpoints实现了检查点评估,使轨迹能够干净地暂停或终止。
def evaluate_signal_checkpoint(acb: AgentControlBlock, checkpoint: Checkpoint) -> bool:
"""Evaluate pending signals at deterministic trajectory checkpoints."""
if not acb.pending_signals:
return True
sig = acb.pending_signals.popleft()
if sig == Signal.SIGPAUSE:
acb.state = State.PREEMPTED
persist_trajectory_checkpoint(acb)
return False
elif sig == Signal.SIGKILL:
acb.state = State.TERMINATED
release_sandbox_lease(acb.sandbox_id)
return False
return True等待工具
编码智能体的一个回合通常将耗时几秒的模型调用与耗时长得多的测试套件运行、构建或搜索配对。轨迹时长核算 中的轨迹时长核算将每个回合划分为模型时间、工具时间和等待时间;对于大量使用工具的任务,工具时间占主导地位。如果智能体运行框架为每条轨迹分配一个工作线程,并让该线程阻塞直到工具返回,那么几乎所有工作线程都会处于等待状态。在一千条轨迹并发运行时,它会占用一千个阻塞的工作线程,而阻塞的工作线程只有在工具调用返回或超时后,才能察觉取消请求已经到达。
解决办法是用记录而非阻塞的工作线程来表示等待中的轨迹。在派发时,智能体运行框架将每次调用的 ID 和截止时间写入待完成集合,将轨迹转移到 WAITING_TOOL,并释放工作线程以处理另一条轨迹。工具的完成通知随后以事件形式到达。智能体运行框架应用 代码 2 中的守卫条件,追加观测,并在待完成集合为空时将轨迹转移到 READY。任何事件驱动的 I/O 运行时都能以很低的开销实现这一点。这里的关键不在于选择哪种事件循环,而在于等待中的轨迹只占用一条记录,无须占用其他资源。因此,等待期间到达的取消请求会发现轨迹处于智能体运行框架能够立即采取行动的状态,框架可以取消尚未完成的调用,并将轨迹转移到 FINALIZING。
三项改进使等待机制更加完善。运行数分钟或数小时的工具返回作业句柄,而不是结果(异步工具分派)。智能体运行框架将句柄存入待完成集合,轮询作业状态或订阅其完成事件,并在轨迹被取消时使用该句柄取消作业。包含多个并行工具调用的回复会使轨迹保持在 WAITING_TOOL,直到每次调用都已返回或超过截止时间,从而让下一次模型调用看到完整的观测集合。每次派发的调用还都带有截止时间。超过截止时间后,智能体运行框架从待完成集合中移除该调用的 ID,请求工具层取消调用,并追加一条结构化的超时观测。如果该调用产生了副作用,其结果就会处于不确定状态,与被终止信号中断的调用一样,并由同一套结算机制加以解决。
轨迹等待期间,模型服务系统中缓存的上下文状态不由智能体运行框架管理。保留、驱逐、重新计算或卸载该状态,是在占用内存与付出重新计算成本之间进行权衡;这一决策在 保留、驱逐、重计算或卸载 中根据预计等待时长作出。智能体运行框架提供只有它才掌握的一项输入,即根据调用截止时间和工具历史推导出的预计等待时长。短暂的 lint 检查调用适合保留状态以便快速复用。耗时较长的测试套件运行、暂停或审批等待则适合释放状态。
工具等待在工具作出响应或超过截止时间时结束。下一类等待在机器端没有这样的时间界限,因为轨迹等待的是由人决定某个不可逆调用是否应当执行。
协作式进程让出机制
模型的 token 生成以毫秒为时间尺度(\(10 - 50\ \text{ms}\)/token),而外部工具调用,例如编译作业、数据库查询和网页检索,往往需要数秒到数分钟。如果智能体线程在等待外部工具完成时同步阻塞,就会占用宝贵的主机并发槽位和 GPU 工作单元容量。
协作式进程让出通过在派发工具请求后立即将轨迹转移到 WAIT_IO 来解耦执行,从而释放执行线程,供其他活跃轨迹使用。这一执行过程如 图 5 所示。
epoll 数据通路,其中固定工作线程池中的线程在检查点 \(C_{\text{tool}}\) 立即被释放,将非阻塞文件描述符注册到内核事件多路复用器,并以可忽略的内存开销将并发规模扩展到 \(10^4\text{--}10^5\) 条轨迹。
将操作系统网络轮询(epoll)与 ACB 派发连接起来的事件驱动唤醒机制概述于 表 6。
WAITING_IO 转移到 RUNNABLE,无须线程轮询,也不会导致计算资源饥饿。
| 阶段 | 子系统组件 | 运行机制与状态转换不变量 |
|---|---|---|
| 1. 事件通知 | 内核多路复用器(epoll/kqueue) |
沙箱进程向套接字描述符写入 JSON-RPC 完成通知时,触发边沿触发式唤醒。 |
| 2. 观测提取 | 监督层事件循环 | 读取观测缓冲区 \(\mathbf{o}_k\),在检查点 \(C_{\text{obs}}\) 将载荷绑定到 ACB,并记录持续时间。 |
| 3. 活性与信号检查 | ACB 信号验证器 | 检查待处理信号位掩码;若在等待期间被取消,则转移到 TERMINATING,否则推进到 RUNNABLE。 |
| 4. 重新进入运行队列 | 调度器优先队列 | 将 ACB 描述符插入活跃优先队列;为后续预填充批次准备好上下文。 |
代码 5 实现了采用协作式让出的有界 token 生成。
async def execute_bounded_decode(acb: AgentControlBlock, client: LLMClient) -> GenerationResult:
"""Execute token decoding bounded by cooperative step quotas."""
tokens_generated = 0
while tokens_generated < acb.step_token_quota:
token = await client.decode_next_token(acb.trajectory_id)
tokens_generated += 1
acb.tokens_total += 1
if token.is_tool_call_delimiter:
acb.state = State.WAIT_IO
return GenerationResult(status=Status.TOOL_PENDING, tokens=tokens_generated)
return GenerationResult(status=Status.QUOTA_EXHAUSTED, tokens=tokens_generated)审批关卡
一个清理智能体提出执行 drop_table(db="prod", table="orders_staging")。该工具已列入其工具目录,参数通过了验证,且 可互操作的工具发现 的授权允许更改暂存表的模式。智能体沙箱 的沙箱限定了智能体代码能够访问的范围,但这次调用作用于一个共享的外部系统,任何沙箱快照都无法恢复该系统的状态。按照 H·S·A 暴露面 的术语,它属于 \(A_3\) 操作。在枢轴边界下(原则 14),不可逆操作只能向前完成,因此阻止错误操作的唯一时机是在派发之前。运行框架需要一种机制,将此类调用暂时扣留,直到具有审批权限的人作出决定,同时在等待期间不占用其他资源。
Definition 0.3: 暂存审批
暂存审批 是指运行框架将拟议的不可逆工具调用暂时扣留,不予派发,直到获授权的审核者批准;被扣留的调用设有有效期,在派发前会依据记录的前置条件重新检查;若调用被拒绝、过期或已不再适用于当前状态,则向模型返回拒绝观测,以取代该调用。
- 重要性:对于无法撤销的操作,这是唯一可用的闭合机制,因为其他所有运行时机制要么限定操作能够触及的范围,要么在事后修复其影响。
- 区别:授权决定调用是否被允许;暂存审批决定一个已获许可的不可逆调用是否在此刻、针对当前世界状态、经某人明确同意后执行。
- 常见误区:根据模型自身对调用的描述来决定哪些调用需要审批,或批准调用后延迟派发,却不重新检查世界状态是否仍与审核者看到的一致。
运行框架依据其掌握的事实来决定哪些调用进入审批关卡。工具的副作用注解(工具接口模式)将调用标记为破坏性或不可逆,轨迹的权限级别规定其在无人监督时可以执行到何种程度,而策略则明确列出始终需要审核的目标,例如生产数据或超过阈值的付款。模型自己声称某个调用安全,并不参与这一判断。审批关卡的适用范围也应保持有限。如果要求审核者每小时批准数十次常规调用,他们就会养成不阅读便批准的习惯,而一个总是获得批准的关卡无法提供闭合机制。授权与沙箱负责处理可逆及低权限调用,审批关卡则专门用于处理这些机制无法使之安全的调用。
图 6 展示了审核者收到的内容,以及审批关卡的运行过程。面板 (a) 是审批请求。它展示的是规范形式的精确调用,而非产生该调用的交互记录;还包括目标及其权限级别、试运行结果或受影响行数等预览、运行框架在提出调用时观察到的前置条件、规范调用的摘要、有效期,以及发起请求的轨迹的身份和剩余预算。批准与摘要绑定,因此审核者批准的是一个具体操作。如果后续轮次更改了哪怕一个参数,该批准就不再适用。
面板 (b) 展示了协议。运行框架扣留调用,并将轨迹转入 AWAITING_APPROVAL,在该状态下仅保留轨迹记录。相对于机器的时间尺度,这种等待很长:每轮执行只需数秒,而等待可能持续数分钟到数小时。根据利特尔定律(容量的解析表达式),同时等待的轨迹数量等于审批请求到达率乘以平均审核时间。因此,缓慢的审核队列会成倍增加等待中的轨迹数量,这就是等待中的轨迹必须仅占用其记录所需资源的原因。等待时间计入请求的有效期,而不计入轨迹的计算预算,因为人的审核时间并不能说明轨迹是否正在取得进展。较长的等待也会增加等待期间主机重启的可能性,因此,被扣留的调用及其请求必须在进程终止后仍然保留;持久执行 提供了这种持久性。
批准是针对编写请求时的世界状态作出的决定,而当批准到达时,世界状态可能已经发生变化。暂存表可能新增了行,另一个进程可能更改了其模式,或者授权可能已被撤销。因此,在派发之前,运行框架会重新读取请求中记录的前置条件,确认摘要仍与被扣留的调用匹配,并检查授权和预算是否仍然有效。只有当这些条件全部满足时,它才会派发调用,并附带使重试派发安全的幂等键(原则 9)。要求由不同角色的两名审核者审批的策略,只会改变决策步骤:该步骤会等待两份批准均到达后,再进行重新检查。
拒绝、过期以及前置条件失效,最终都以相同方式结束。运行框架丢弃被扣留的调用,将结构化拒绝作为该调用的观测返回给模型,然后将轨迹转入 READY,使下一轮能够根据拒绝结果调整计划、针对当前状态请求新的批准,或在不执行该操作的情况下结束:
{
"status": "REFUSED",
"reason": "APPROVAL_EXPIRED",
"call": "drop_table(db=\"prod\", table=\"orders_staging\")",
"message": "No approval was recorded before the request expired."
}未收到答复即视为拒绝。如果审批关卡在没有收到答复时默认派发,那么每一条丢失的通知都会导致一次未经审查的不可逆操作。审批关卡约束的是一条轨迹中的单个步骤。节点上的各条轨迹仍会相互竞争,共享同一组模型速率限制和沙箱,而运行框架必须决定下一条运行的就绪轨迹。
Checkpoint 0.2: 控制操作与审批关卡
密码学托管清单与法定人数不变量
当智能体提出一项不可逆的外部操作(\(A_3\))时,运行框架会冻结执行,并将一份不可变的托管清单序列化。Cryptographic Escrow Manifest Verification展示了如何依据操作员授权令牌对清单进行密码学验证。
def verify_escrow_payload(manifest: EscrowManifest, auth_token: AuthToken) -> bool:
"""Verify cryptographic signature and policy validity of an escrow manifest."""
if not crypto_verify(auth_token.signature, manifest.payload_hash, auth_token.public_key):
return False
if auth_token.expiration_epoch_ms < current_epoch_ms():
return False
return manifest.action_type in auth_token.permitted_actions托管清单的生命周期转换详见 表 7。
| 当前状态 | 事件 / 守卫条件 | 下一状态 | 操作 / 转换副作用 |
|---|---|---|---|
RUNNING |
拟议操作超出影响范围阈值(\(\mathbf{a}_k \in \mathcal{A}_{\text{escrow}}\)) | WAITING_ESCROW |
停止分派,解绑 OS 工作线程,将 KV 缓存检查点保存到主机 DRAM,将 ACB 持久化到 NVMe 存储。 |
WAITING_ESCROW |
墙钟时间 \(\tau_{\text{current}} > \tau_{\text{expire}}\)(租约到期) | READY |
使清单 \(\mathcal{M}\) 失效,合成 REFUSED 观测 \(\mathbf{o}_k^{\text{synth}}\),将 ACB 重新入队。 |
WAITING_ESCROW |
有效的密码学签名集合满足策略 \(\mathcal{P}_{\text{quorum}}\) | READY |
验证摘要,从 NVMe 恢复 ACB,绑定 OS 工作线程,将 \(\mathbf{a}_k\) 分派给沙箱驱动程序。 |
对于高风险企业工作流,根据 表 8 中的法定人数不变量,必须由多名不同的审批者进行审批。
| 不变量 | 形式化约束 | 系统验证规则 |
|---|---|---|
| 签名者唯一性 | \(\forall j \neq k,\; u_j \neq u_k\) | 每个记录的签名都必须来自不同的密码学身份。单个操作员不能使用不同的凭据子密钥签名两次来满足 \(m=2\) 法定人数要求。 |
| 角色正交性 | \(\bigcup_{i=1}^m \text{Role}(u_i) \supseteq \mathcal{C}_{\text{roles}}\) | 签名必须涵盖不同的组织职能领域。例如,将基础设施变更部署到生产环境需要来自 role:secops 和 role:site-reliability 的独立签名。 |
| 时间有效性 | \(\forall i,\; \tau_{\text{issued}} \le \tau_i \le \tau_{\text{expire}}\) | 所有 \(m\) 签名都必须在有效租约窗口内登记。如果所需签名在到期后才到达,则整个清单失效;部分审批结果不会在租约续期后保留。 |
并发轨迹调度
一个节点运行四十条批处理轨迹,在夜间执行代码仓库维护,同时还运行少量交互式轨迹,每条交互式轨迹都有一位用户在等待。所有轨迹共享同一个提供方的每分钟 token 数和请求数速率限制,以及一个沙箱池(沙箱池与重置)。采用先到先服务时,交互式轮次必须排在一队批处理轮次之后等待。采用贪心服务策略时,节点会超出提供方的速率限制,提供方会拒绝请求;按相同时间安排进行退避的轨迹会同时重试,并同时被拒绝。
智能体运行框架调度的是轮次,而分配的单位是一条 READY 轨迹的下一次模型调用。三条规则可以解决其中大部分问题。第一,按权重比例在不同优先级类别之间分配轮次,并根据每个轮次的估计 token 数计费,使一个大型批处理轮次无法饿死一个小型交互式轮次;亏空轮询等加权公平排队方案能够以每次决策恒定的工作量实现这一点。第二,使用一个设定速率低于提供方速率限制的本地令牌桶来控制轮次准入,从而由运行框架决定谁应等待,避免通过请求被拒绝才发现限制。第三,仅在分派工具调用时获取沙箱,并在调用返回时释放沙箱,使沙箱不会在模型调用期间闲置。
处于 WAITING_TOOL、AWAITING_APPROVAL 或 PAUSED 状态的轨迹不获得轮次;从长时间等待中返回的轨迹也不会携带累积额度,以免凭借这些额度独占节点。剩余的风险是优先级反转:交互式轨迹等待的沙箱被批处理轨迹的长时间构建任务占用。为交互式类别预留一部分沙箱池容量可以消除这一问题,代价是部分容量可能闲置。如何为节点集群设置速率限制、资源池规模和优先级通道,是 轨迹容量 中讨论的容量问题。
公平调度限制了一条轨迹在任意时间窗口内占用节点资源的程度。但它不限制一条轨迹的资源占用总量,因为一条轨迹可能在数百个轮次中每轮都表现完全合规,却始终未完成任务。
多维资源争用向量
并发智能体轨迹沿四个不同的物理硬件维度争用资源,汇总见 表 9。
| 争用向量 | 物理 / 虚拟子系统 | 瓶颈指标 | 饱和时的系统故障模式 | 监督强制执行机制 |
|---|---|---|---|---|
| 沙箱内存 | 物理主机 RAM | 常驻集大小(\(\text{RSS}\)) | 触发 Linux OOM killer;主机内核抖动 | 对活跃容器池施加硬容量限制的信号量 |
| 工具执行 | 主机 CPU 调度器 | CPU 线程利用率(\(\%\)) | 监督器事件循环饥饿;看门狗超时 | Cgroup CPU 配额与有界工作线程池 |
| 推理带宽 | 模型网关配额 | 每分钟 token 数(\(\text{TPM}\)) | HTTP 429 级联;指数退避引发的队列成批停滞 | 通过本地令牌桶进行速率整形的准入门控 |
| 磁盘带宽 | 主机存储(NVMe/SSD) | 每秒 I/O 操作数(\(\text{IOPS}\)) | SQLite/WAL 快照停滞;文件系统锁定延迟 | 直接 I/O 速率限制与临时 tmpfs 挂载 |
亏空轮询分派架构
为防止具有大型上下文的贪心轨迹独占 GPU 内存带宽和主机线程池,智能体运行框架采用亏空轮询(Deficit Round-Robin,DRR)调度。节点调度层级如 图 7 所示。
每轮调度配额的分派机制如 图 8 所示。
代码 7 实现了活跃轨迹之间的亏空轮询分派。
def dispatch_round(active_acbs: list[AgentControlBlock], q_base: int, bucket: TokenBucket) -> None:
"""Dispatch active trajectories using Deficit Round-Robin scheduling."""
for acb in active_acbs:
if acb.state != State.RUNNABLE:
continue
acb.deficit += q_base
cost = acb.estimated_step_cost
if acb.deficit >= cost and bucket.consume(cost):
acb.deficit -= cost
acb.state = State.RUNNING
schedule_vcpus(acb)预算与上限
一个被要求诊断数据流水线故障的智能体执行了一条查询,但查询超时了。模型认为查询格式有误,于是尝试一个变体,再尝试另一个,接着进行范围更广的诊断,却同样失败。每次失败都会向上下文追加一段堆栈跟踪,而每个新提议都是格式正确且满足所有单次调用限制的工具调用。这条轨迹在每次工具调用时都让出执行权,没有超出分配给它的速率限制份额,并在调用之间释放沙箱,因此在调度器看来,它表现得十分规矩。数小时、数百轮之后,它已经花费了真金白银,却没有改变环境中的任何状态。失控循环及终止它的预算展示了这一循环,以及终止它的唯一机制。
模型无法自行终止这一循环。它看到的是上下文,而不是计量器,因此轮次数、token 数和价格并不在其输入之中;随着错误观测不断累积,它们会挤占原先界定任务的指令(**上下文污染动力学**)。诸如“花费不要超过五美元”这样的指令会降低超支的概率,却无法为支出设定硬性上限,而且随着上下文被失败信息填满,其约束力会越来越弱。陷入循环的模型仍然保持活跃,输出格式正确,也满足每项单次调用限制,却没有取得任何进展,因此,整条轨迹的上限必须与其他控制措施一样,设置在模型之外(原则 12)。
智能体运行框架实施两层上限。单次调用上限,即每个请求所设置的最大输出 token 数和调用截止时间(调用契约),约束一次调用。每条轨迹的预算则通过一个上限向量约束整个运行过程,
\[\mathbf{B} = \langle H_{\max},\ N_{\max},\ T_{\max},\ C_{\max} \rangle\]
其维度包括轮次、token 数、实际经过时间和美元费用。轮次上限 \(H_{\max}\) 是最简单、应用最广泛的限制,它统计的轮次与 \(H\)(H·S·A 暴露面中的时域)所统计的相同。但仅有这一限制还不够。即使轨迹未达到轮次上限,只要每轮都重新发送很长的上下文,仍然可能耗尽一笔很大的美元预算。仅设置实际经过时间上限会终止缓慢但廉价的工作,例如等待长时间构建的轨迹;而仅设置美元费用上限,则无法捕捉使用廉价模型却占用沙箱数小时的失控运行。每个维度都能捕捉其他维度遗漏的故障,部署时还可以增加沙箱小时数等维度,以便在这些维度构成实际约束时加以限制。
支出通过计量获得,而非估算。每次调用后,智能体运行框架都会读取提供商报告的用量,将其分为未缓存的输入 token、已缓存的输入 token 和输出 token,并更新轨迹的费用:
\[C^{(k)} = C^{(k-1)} + p_{\text{in}}\, n_{\text{in}}^{(k)} + p_{\text{cache}}\, n_{\text{cache}}^{(k)} + p_{\text{out}}\, n_{\text{out}}^{(k)} + \sum_{j} c_{\text{tool}}(t_{k,j})\]
其中,\(n_{\text{in}}^{(k)}\)、\(n_{\text{cache}}^{(k)}\) 和 \(n_{\text{out}}^{(k)}\) 分别表示轮次 \(k\) 的未缓存输入 token 数、已缓存输入 token 数和输出 token 数,\(p\) 各项表示它们的单价,\(c_{\text{tool}}\) 则表示工具调用产生的任何直接费用,例如付费搜索的费用。价格结构来自 加速器服务延迟。输出 token 的费用是输入 token 的数倍,但轨迹每轮都会重新发送不断增长的上下文,因此输入通常占据轨迹账单的大部分,而对这些重复发送的上下文进行前缀缓存(在 编排下一次调用 中为复用而组织,由 跨轮次前缀缓存 的机制提供服务)是影响费用的最有力的单项手段。下文的计算示例展示了这一手段对预算的影响。
粗略估算 0.1: 按预算计量轨迹的资源消耗
计量读数:该轨迹已处理 898,500 个 token,其中几乎全部是重复发送的上下文。在上下文仅追加的情况下,只有首次提示和后续每轮新增的 token 按未缓存价格计费,该轨迹的费用为 $0.50。如果每轮缓存均未命中,相同 token 的费用为 $2.77。
预算使用情况:启用缓存时,该轨迹已使用轮数预算的 75 percent、token 预算的 44.9 percent、墙钟时间预算的 51.1 percent 和美元预算的 14.2 percent,因此轮数上限构成约束。未启用缓存时,美元预算使用比例跃升至 79.1 percent,美元费用成为构成约束的维度。两种情况下的 token 预算使用比例相同,因为缓存改变的是 token 的费用,而非处理的 token 数量。
系统启示:同一条轨迹可能远未耗尽预算,也可能已接近美元费用上限,这取决于其前缀缓存是否命中。破坏缓存的改动,例如每轮都重新排列工具列表(编排下一次调用),首先表现为更早接近美元费用上限,这也是预算对美元费用和 token 分别计量的原因之一。缓存与推理预算 对节省的费用本身进行了量化。
运行框架在每轮将预算归结为一个数,即最紧上限的已用比例,
\[\rho_k = \max_{d} \frac{u_{k,d}}{B_d}\]
其中,\(u_{k,d}\) 表示维度 \(d\) 在第 \(k\) 轮结束后的实测用量。仅在 \(\rho_k = 1\) 处强制停止会造成浪费,因为轨迹在达到上限时突然停止,会丢弃原本再用几轮就能整理成可用结果的工作。因此,运行框架会分阶段升级管控措施,概述见 分阶段预算管控升级 和页边说明。
| 预算已用比例(\(\rho_k\)) | 阶段 | 运行框架的操作 | 保证内容 |
|---|---|---|---|
| 低于 0.70 | 常规 | 无 | 仅有常规的单次调用上限 |
| 0.70 至 0.90 | 引导 | 在下一次调用中追加预算通知;压缩上下文 | 无保证;降低继续探索的概率 |
| 0.90 至 1.00 | 限制 | 将授权缩小至只读工具;禁用子智能体创建;要求给出最终结果 | 不再对环境产生新的影响 |
| 1.00 或以上 | 终止 | 进入 FINALIZING;记录 BUDGET_EXCEEDED 以及构成约束的维度;保留部分工作成果 |
不再产生费用 |
每个阶段都使用了前面章节介绍的机制。引导阶段追加一条通知,说明构成约束的维度及剩余额度,并触发上下文压缩(上下文压缩),因为反复发送的长上下文通常是最大的成本驱动因素。该通知是一项请求,因此它会降低继续探索的概率,但不提供任何保证。限制阶段则提供保证,因为它改变的是授权而非提示。将轨迹限制为仅使用只读工具,会使其转入 \(A_0\);禁用子智能体创建则可防止其通过委派工作来规避预算。更改工具列表会使已缓存的前缀失效一次(编排下一次调用),此时付出这一成本是值得的。终止阶段进入 FINALIZING,取消尚未结束的作业,为调用方保留工作区快照和所有部分产物,并记录哪个维度的预算已耗尽,使轨迹记录既能表明失败,也能说明失败原因。
分阶段预算管控升级 中的阈值仅用于说明,上限本身应通过测量确定,而非凭空猜测。具体方法是:在充裕的预算下运行一组有代表性的任务,记录每个通过验收的任务完成时所需的轮数和美元支出,并绘制在预算 \(b\) 内通过验收的任务比例随 \(b\) 变化的曲线。曲线先陡峭上升,随后趋于平缓。将上限设在远超拐点的位置,只能带来少量额外的成功,却会在那些本就不可能完成的轨迹上耗费大量资金。由于不同任务类别的曲线各不相同,上限也应有所区别。统计评估的严谨性 提供了根据有限次试验解读此类曲线所需的统计方法,每项通过验收的任务的成本 则将结果折算为每个通过验收任务的成本。
预算知道一条轨迹已经花了多少钱,却不知道它是否取得进展,因此,陷入循环的轨迹只有在触及上限时才会被发现。要通过识别重复的相同调用或不再变化的测试结果来提前发现这类循环,就需要根据环境状态判断进展;语义看门狗定时器 将这种检测作为故障恢复的一部分展开讨论。父智能体及其子智能体之间必须统筹预留的资金,以及整个集群的支出速率限制,由 支出治理 规定。在单条轨迹内,预算使智能体运行框架完整成形。记录描述轨迹,状态机约束轨迹如何变化,控制动作和审批关卡让人们能够在安全的时机介入,调度器负责共享节点资源,而预算保证循环最终结束。
谬误与陷阱
构建智能体运行框架时的错误,源于将其视为模型外围的基础管线,而非落实所有跨轮次保证的地方。
谬误: 将智能体循环作为异步协程运行,就能使其成为智能体运行框架。
将原型循环迁移到事件循环上,解决了一个缺陷:等待工具的协程不再占住工作线程。但除此之外,它没有解决任何问题。轨迹的状态、待处理调用、支出和授权仍然保存在协程的局部变量中,调度器或运维人员若不恢复协程,就无法读取这些信息,也没有守卫在状态转换发生前进行检查。迟到的工具响应会恢复等待它的那个协程,而这恰恰是待处理调用 ID 守卫所拒绝的观测跨界。取消任务会在模型调用恰好执行到的位置引发异常,从而可能将流式工具调用从中截断。协程还会随其进程一同终止,且不留下任何关于其原定意图的记录。异步 I/O 使智能体运行框架能够高效等待;轨迹记录、带守卫的状态机以及轮次边界控制,才是使其成为智能体运行框架的关键。
谬误: 在系统提示中声明预算,就能限制轨迹的支出。
“支出不得超过五美元”这样的指令,使支出上限取决于模型是否遵从指令,而不变量闭包性(原则 不变闭包原则)排除了这种做法。模型看不到自身的词元数量或提供商的价格,因此,即使这条指令位于上下文末尾附近,它也无法计算自己的支出。最可能超支的轨迹,是那些陷入错误恢复循环的轨迹;其上下文不断被失败信息填满,将这条指令推得离当前轮次越来越远。图 9 中的失控循环在每一轮都格式良好,其上下文中也没有任何内容告诉它停止。预算是智能体运行框架根据用量报告计量,并通过更改授权或终结轨迹来强制执行的上限,而非供模型阅读的一句话。
陷阱: 根据被截断或已取消的模型响应派发工具调用。
因达到输出上限而停止的回复,或因取消而中止的流,可能在工具调用尚未完成时结束。有些解析器会通过补齐括号并提供默认值来修复片段,从而将一个只写了一半的 drop_table 变成完整调用,其中包含模型从未生成过的参数。隔离不变式(原则 隔离不变量)禁止派发任何未正常结束的输出。智能体运行框架将截断通知作为观测返回,将反复发生的截断计入预算;如果截断再次发生,则提高该调用类型的输出上限,或要求模型将输出拆分到多个轮次中。
陷阱: 批准不可逆调用时,未设置有效期或未重新检查前置条件。
没有有效期的审批请求,会在通知丢失或审核人员换班时使轨迹无限期停滞;被遗弃的请求不断积累,同时仍占用工作区租约和凭据。未重新检查的审批则更糟。审核人员在周一批准删除某张表时,批准的是该表在周一的状态;如果智能体运行框架在周三才派发调用,而其他进程已在此期间写入新行,那么它执行的就是一个无人审核过的操作。每个请求都需要设置到期后默认拒绝的有效期、将审批绑定到一个确切调用的摘要,以及在派发前立即重新检查已记录的前置条件、授权和预算。
陷阱: 凭直觉设置轮次上限,而不是依据成本与成功率曲线。
因为“五十轮听起来很充裕”而选定的轮次上限,会在两个方向上失效。对于简单任务,它会让失败轨迹在已通过验收的任务所对应的曲线趋于平坦后,仍继续消耗资源很长时间。对于困难任务,它会提前终止某些轨迹,而实测曲线本会表明这些轨迹再经过几轮就能成功完成。由于不同任务类别的曲线各不相同,统一的全局上限对大多数类别都不合适。应根据使用 统计评估的严谨性 的统计数据测得的各类别曲线,在其拐点处确定上限,并在模型、工具或上下文策略发生变化时重新评估。
总结
原型智能体循环将轨迹的所有重要信息(状态、开销、正在等待的事项以及允许执行的操作)都保存在循环内部和模型上下文中,因此外部机制无法暂停它、约束它或停止它。智能体运行框架将这些控制状态移入由自身管理的轨迹记录,并且仅通过封闭状态集合上的受保护转换来修改记录。保护条件会拒绝迟到的工具结果,防止暂扣的调用在未经批准时被派发,让每条退出路径都经过清理屏障,并确保终态不可再改变。
智能体运行框架基于这份记录构建长时间运行的轨迹所需的控制机制。控制请求在到达时被记录,并在轮次边界生效;流式取消和输出上限限制等待时间,而强制终止则保留给完成当前轮次本身就会造成危险的情况。停止原因告诉智能体运行框架,每条回复使轨迹处于何种状态。等待中的轨迹只保留一份记录,而不占用工作进程;不可逆调用则被暂存,等待审批,该审批有有效期、绑定到一个确切调用,并在派发前接受重新检查。调度器在各条轨迹之间公平分配节点资源,而经过计量的多维预算则对无法完成的轨迹进行引导、限制,并最终使其停止;预算上限依据实测的成本与成功率曲线确定。
要点回顾: 控制位于模型之外
- 轨迹以记录为准,而非对话记录:状态、开销、授权和待处理工作都保存在智能体运行框架管理的一份小型记录中,因此每次控制决策只需读取几个字段,无须读取对话记录,模型生成的任何内容也都不能直接改变这些字段。
- 受保护的转换赋予状态明确含义:带有保护条件的封闭状态集合会拒绝迟到的工具结果,阻止在等待审批期间派发调用,让每条退出路径都经过清理,并将每次重试都变成一条新轨迹。
- 请求随时到达,在轮次边界生效:取消、暂停和预算操作会等待一个没有任何工作处于半完成状态的时刻;流式取消和输出上限使这种等待保持短暂;只有强制终止会打断轮次,将结果未定的调用留待后续结算处理。
- 不可逆调用暂存等待,并绑定到一个确切操作:审批关卡会暂扣调用,到期后将其拒绝,在派发前重新检查已记录的前置条件,并返回拒绝观测,使模型能够据此规划。
- 预算是依据成功率曲线确定并进行计量的上限:轮次、词元、实际经过时间和美元费用均依据使用情况报告计量;缓存可能使一条轨迹在美元费用中的占比发生数倍变化;引导、限制和停止三个阶段逐步从提出请求升级为强制保证。
智能体运行框架从不要求模型自行停止、让出执行权或计数。控制请求在轮次边界生效,每次调用的上限约束各次模型调用,预算则约束整条轨迹。这将带外抢占(原则 12)落实为运行时能够执行的机制。每种机制都读取或写入同一份模型无法触及的记录,这正是轨迹级封装(原则 11)的具体形式。审批关卡在仍可应用枢轴边界(原则 14)的最后时刻,即派发之前,加入其只能向前推进的步骤。这些机制共同消除了在单台在线主机上运行的轨迹所面临的执行跨度风险暴露。

