Contents

Agent 部署方式:中心服务与 Sandbox

Agent 接收到任务后,会多次调用模型和工具,直到任务结束。组织这个过程的程序称为运行时,也就是 Runtime。

本文讨论两种部署方式:由中心服务运行 Agent,通过远程接口操作工程;或者把 Codex 放进 Sandbox,由沙箱内的引擎直接操作工程。下面分别介绍它们的组成、数据存储、执行过程,以及中断后的恢复方法。

1. 总体结构

先说明本文中几个组件的含义:

  • Runtime:组织模型调用、工具执行和上下文更新的程序;
  • Sandbox:隔离的执行环境,包含工程目录,以及读写文件、运行命令所需的程序;
  • 模型 API:接收上下文,返回文本或工具调用请求;
  • 平台服务:接收用户需求,管理项目、任务、权限、预算和交付结果。

这几个组件可以有不同的部署关系,如下图所示。

中心 Runtime 与 Sandbox 内 Codex 的部署关系
图 1:两种部署方式

中心服务方案中,Runtime 调用模型,解析模型提出的工具请求,再通过远程接口让 Sandbox 执行。模型拿到工具结果后,继续决定下一步操作。Runtime 可以部署多个实例,分别执行不同任务。

沙箱内 Codex 方案中,平台把任务交给 Codex,由它完成这段循环。文件和命令就在 Codex 所在的环境中,可以直接访问。这里放进沙箱的是执行程序,模型推理仍然可以使用远端 API。

两种方案都需要平台,也都可以扩容。它们主要区别在于:模型调用与工具执行之间的循环,由中心进程维护,还是由沙箱内进程维护。

运行时是否自研,是另一个选择。中心服务可以接入现成引擎,沙箱里也可以运行自研 Agent。本文用我们现有的中心 Runtime 说明第一种方式,用沙箱内 Codex 说明第二种方式;后者按参考架构讨论。

2. 中心服务

2.1. 接收任务与分配执行

假设用户提交一个需求:“给游戏增加暂停菜单”。接入服务需要先确定用户、项目和工作区,检查权限与预算,再创建运行记录。

我们将一次用户需求对应的运行称为 Run。Runtime 的工作实例称为 Worker。接入服务把 run_id 交给任务队列,空闲 Worker 领取任务后,从数据库读取运行信息,开始执行。

任务从接入服务经队列交给 Runtime,并访问数据库和工具
图 2:中心服务的组成与调用关系

数据库写入和队列派发之间,存在一个需要处理的间隙:任务已经保存,但进程在发送队列消息前退出,任务就可能一直无人执行。

一种实现方法是在同一事务中保存 Run 和待派发记录,再由后台程序将待派发记录送进队列,这就是 Outbox。发送失败时重新发送;Worker 收到重复通知时,通过数据库条件更新领取任务,避免多个实例同时取得执行权。

用户重复点击提交,也需要单独处理。接入请求可以携带稳定的请求编号,相同请求再次到达时返回原 Run。这个编号表示用户的一次提交,与 Worker 后续重试的编号不同。

2.2. 运行记录

中心服务首先要区分三种对象:

  • Thread:会话,关联多轮对话及其工程;
  • Run:一次运行,对应本轮用户需求;
  • Attempt:一次执行尝试,记录这次由哪个 Worker 执行,以及如何结束。

例如,同一个会话里“增加暂停菜单”和“修改菜单颜色”是两个 Run。第一个 Run 执行到一半,Worker 重启后由另一实例接管,则会产生新的 Attempt,但仍属于原 Run。

我们的中心运行时使用 threads、agent_runs、agent_run_attempts 和 messages 保存相应记录。运行配置和恢复所需变量也随 Run 保存,恢复时使用原任务的输入。

围绕一次运行,持久存储至少需要覆盖以下内容。这里按用途列出逻辑记录,实际实现可以合并或拆分表。

记录保存内容与写入时机
会话创建时保存用户、项目、工作区和默认配置;配置变化时更新。
运行接收需求时保存原始输入、本次配置、预算和状态;结束时补充结果与原因。
执行尝试领取或接管任务时登记执行者,执行中更新心跳,结束时记录退出原因。
消息按顺序追加完整的用户输入、模型输出、工具调用及工具结果。
工具操作调用前保存参数和操作编号,返回后保存状态、远端任务或命令会话编号。
检查点上下文压缩完成后,保存压缩结果及覆盖到哪条消息。
执行事件保存需要补发给客户端的进度、事件顺序和所属执行尝试。
交付记录保存代码或文件检查点、产物地址、运行版本和验证结果。

这些记录可以使用关系数据库保存。编号、状态、归属和时间使用固定字段;模型消息、工具参数等可变内容使用 JSON 字段。按会话、Run 和消息顺序建立索引,便于查询和恢复。

工程文件放在独立的工作区,通过持久数据盘或文件快照保存。工作区包含未提交到 Git 的修改,所以仅保存 Git 提交号并不够。图片、截图、完整日志和构建产物一般进入对象存储,数据库保存引用和校验信息。

Worker 内存中仍然会缓存当前上下文和执行状态,但进程重启后,继续任务所需的数据应当能从这些存储中取回。

2.3. 消息与上下文

消息历史和模型上下文有不同的用途。

消息历史记录已经发生的对话和操作,用于展示、排查与恢复;上下文是下一次实际发给模型的输入,除了消息,还包括本轮指令、工程规则和工具定义。

工具定义描述名称、用途和参数格式。例如命令工具需要命令与工作目录,文件工具需要路径。模型根据这些定义提出调用,Runtime 再交给对应的执行代码。

消息字段、历史顺序、上下文检查点及模型输入
图 3:消息与上下文的逻辑结构,字段为示意

图中的消息包含以下信息:

  • message_id:消息编号;
  • run_id:所属运行;
  • role:用户、模型或工具等消息角色;
  • tool_call_id:工具调用与结果的关联编号,普通消息可以没有;
  • content:文本或结构化内容。

上下文长度有限,不能无限追加全部历史。任务较长时,可以把较早的历史整理成摘要,保留继续执行所需的原文,再追加新的消息。压缩完成后保存的记录,称为上下文检查点。

图中较早的消息已整理进检查点,后续新消息仍单独保存。恢复时先加载检查点,再按顺序追加它尚未覆盖的消息。因而检查点既要保存整理后的历史,也要保存覆盖位置;只留下摘要,无法确定哪些消息已经包含在内。

工具调用和结果还需要符合所用模型的消息协议。压缩时保留必要的关联,尚未确认完成的操作也要保留真实状态,避免后续把“已提交”理解成“已完成”。

我们的实现中,prompt_checkpoints 保存压缩后的历史及覆盖边界,runtime_context_snapshots 按会话和 Agent 保存最新的模型可见上下文。前者用于接续,后者便于检查当前输入。

两者分开还有存储上的原因。假设每一步都向历史表追加一整份上下文,前面的内容就会反复写入。原始消息顺序追加、压缩时保存检查点、最新快照覆盖更新,可以减少这种重复。

2.4. 工具调用过程

Runtime 收到模型输出后,需要区分普通文本和工具调用。以运行测试为例,一次工具调用的过程如下:

  1. 接收完整的工具名称和参数;
  2. 检查工具是否可用、参数是否合法,以及任务是否有操作权限;
  3. 保存本次工具操作,分配可查询的调用编号;
  4. 调用 Sandbox 的执行接口;
  5. 保存执行结果,与原工具调用关联;
  6. 将结果加入上下文,发起下一次模型请求。

工具参数可能通过流式响应逐段到达。执行层需要等完整参数形成后再调用,不能把半段命令提前交给沙箱。

工具也有两种常见去向。读文件、改代码、运行命令进入 Sandbox;素材生成、扣费、版本发布进入各自的业务服务。后者的操作状态由业务服务保存,Runtime 留下查询它们所需的编号。

2.5. 命令执行会话

读取文件通常很快完成,命令则可能持续很久。测试运行几十秒,开发服务器还可能一直不退出。工具接口等待一段时间后先返回时,需要同时说明命令是否仍在执行。

命令执行会话的启动、继续读取及结束
图 4:通过同一执行会话访问长时间运行的命令

命令启动后,执行服务返回会话标识,通过它找到对应的沙箱和进程。后续请求使用同一标识,继续读取新增输出、发送输入,或者中断进程。进程结束时再返回退出码。

我们现有的 exec_command 由中心 Runtime 调用 SandboxProxy,再由沙箱中的执行服务启动命令。后续通过 write_stdin 等接口访问原会话。它们采用 Codex 风格的工具接口,这条链路中的 Agent 主循环仍在中心 Runtime。

输出量也需要控制。短输出可以直接进入工具结果;长日志分段返回,并注明是否截断,必要时附上完整日志引用。否则一次构建就可能把后续模型输入占满。

2.6. 中断与恢复

Worker 重启时,Sandbox 中的命令可能还在运行。新 Worker 读取运行、消息和工具记录后,应当先查询原执行会话:

  • 进程仍在运行:继续读取输出,等待结果;
  • 进程已经结束:取回退出码和剩余输出;
  • 会话或沙箱已经丢失:根据操作性质决定是否重新执行;
  • 状态暂时无法确认:保留未知状态,继续查询或安排核对。

测试一般可以重跑,发布和付费操作则需要查询原操作、确认幂等条件,不能统一按失败重试。

接管还需要处理旧 Worker 恢复连接的情况。任务可以使用带有效期的租约表示执行权,由 Worker 定期更新心跳。接管时生成新的执行编号,后续数据库写入校验这个编号,拒绝旧执行者的更新。

但数据库的校验不会自动停止旧进程修改工程。同一个工作区需要单独保证写入者唯一,或者为不同执行尝试使用独立副本。交给新执行者之前,要确认旧进程已经停止或失去写入权限。

至此,中心运行时需要维护的主要部分就包括:运行记录、模型上下文、工具协议、远程进程,以及这些状态的恢复。

3. Sandbox 内运行 Codex

3.1. 接入结构

把 Codex 放进 Sandbox 后,平台仍然接收用户需求、创建 Run,并保存项目、权限和预算。执行时由平台分配沙箱,再将任务交给其中的 Codex。

平台调度多个包含 Runner 和 Codex 的 Sandbox
图 5:平台与沙箱内执行程序的关系

平台与引擎之间通常还需要一段接入程序。这里称为 Runner,主要负责:

  • 启动引擎,传入任务与运行配置;
  • 继续原会话,传入用户的补充输入;
  • 接收引擎事件,回传进度和结果;
  • 处理中断,管理引擎及相关命令进程。

Codex 提供 SDK 和命令行入口,可以启动、继续或恢复会话,也可以输出结构化执行事件。具体采用哪种方式,需要与选定版本一起验证。Codex SDK、codex exec。

普通文件读写和命令执行在沙箱内部完成。平台原来围绕模型维护的上下文组织、工具调用循环,转由引擎处理;平台与执行侧的主要接口变为任务、补充输入、事件和控制指令。

3.2. 数据保存

执行程序移入沙箱后,数据也会分布在不同位置。

保存位置保存内容
平台数据库Run、Attempt、用户与项目、预算、沙箱和会话映射、已确认事件、交付引用。
Codex 状态目录固定版本恢复会话所需的文件和索引,以及对应的版本和运行配置。
工程持久卷或快照源码、未提交修改、必要配置和文件检查点。
业务服务已提交操作的编号、状态、费用和结果。
对象存储需要留存的日志、截图、图片和构建产物。
沙箱临时目录可重新安装的依赖、缓存和临时构建文件。

平台需要把 run_id、attempt_id、sandbox_id、工作区编号和 Codex 会话编号关联起来。用户继续修改时,据此找到原工程与会话;沙箱替换时,据此确定恢复哪份状态。

Codex 状态目录需要按照固定版本的实际要求保存。聊天文本导出未必包含恢复所需的全部索引和执行状态。不同项目还应使用隔离的状态目录与工作区,避免引擎找到另一项目的文件或会话。

3.3. 任务执行与事件回传

一项任务进入执行阶段时,平台先创建或复用沙箱,挂载工作区与引擎状态,准备依赖和范围受限的访问权限。Runner 接收启动指令后,创建新会话或继续原会话。

这里需要分别处理启动指令和执行事件的重试。

平台发送启动指令后没有收到确认,Runner 可能已经启动了引擎。启动指令因此需要稳定编号,Runner 保存已接受的指令,平台重连后查询原执行。否则一次网络超时就可能启动两个 Codex 进程。

事件回传也可能断线。Runner 可以按 Run 和 Attempt 为事件编号,暂存尚未确认的事件;平台保存后返回确认,重连后从最后确认位置继续传递。平台按照事件编号去重,客户端也可以从已接收位置补读。

这里重传的是执行记录,接收方不会因此再执行一遍工具。沙箱本地暂存只能应对短暂断线;如果连磁盘一起丢失,未同步出去的事件仍然会丢失,关键恢复记录需要及时保存到外部存储。

3.4. 沙箱回收与恢复

沙箱重新创建后,文件、会话和进程需要分别处理。

利用文件快照、引擎状态和业务记录恢复沙箱任务
图 6:沙箱重建后的恢复过程

继续用暂停菜单的例子:Codex 修改了源码并启动测试,随后沙箱丢失。恢复时先准备新环境,取回源码和未提交修改,再加载原会话。原测试进程通常已经不存在,确认可以安全重跑后,重新启动测试。

文件与会话的保存位置不同,保存时间也可能不同。会话已记录“修改完成”,文件快照却来自修改前,后续执行就会基于错误的工程状态。可以在相关工具动作结束后,为工程快照、会话状态和事件确认位置保存关联检查点;无法保证一致时,恢复流程需要检查实际差异。

开发服务器、终端和网络连接属于运行中的进程状态。即使会话与文件都恢复了,也需要重新检查这些进程是否存在,并按需启动。

我们的沙箱代码中,有一套可选的本地文件读写方案:依赖与构建尽量使用本地磁盘,源码保存在持久工作区,最终产物再写回。该方案默认关闭,需要配置启用。这里区分的正是可重建数据与需要保留的任务成果。

沙箱分配方式也影响恢复成本。按项目保留环境,可以复用依赖和预览进程,同时需要处理空闲资源、残留进程和并发写入;按任务新建环境,清理比较直接,但每次要准备依赖并恢复工作区。

引擎与镜像版本同样要固定并记录。升级前验证原会话能否继续、工具行为是否变化。重新分配执行者时,仍需处理旧租约、旧进程及工作区写入权限。

4. 业务工具

4.1. 请求已执行,但响应丢失

两种部署方式都会调用素材、费用和发布等业务服务。这类调用存在一种情况:服务已经接受请求,Agent 却没有收到响应。

例如素材服务创建了背景图任务并登记费用,网络在返回结果时断开。Agent 恢复后若新建一次生成请求,就可能产生第二份任务和费用。

通过稳定操作编号查询或重试同一次素材任务
图 7:业务操作的查询与幂等重试

Agent 在第一次调用前保存业务操作标识,素材服务接单时,将这个标识与创建的素材任务关联。这样,即使回复丢失,双方仍然能够找到同一次操作。

同一操作编号和相同参数再次提交时,服务返回原任务,不再执行第二次业务动作,这就是幂等。相同编号携带不同参数则应拒绝或报冲突。编号的去重范围还要结合用户、项目等归属确定。

恢复时按原操作标识查询,可以确认原任务仍在执行、已经结束,还是没有成功提交。不支持查询、也不支持幂等的接口,在断线后可能暂时无法确定结果,应保留未知状态,再按操作性质处理。

4.2. 取消、权限与预算

用户取消任务时,中心 Runtime 或 Runner 可以停止后续模型调用,并中断当前命令。已经交给素材或发布服务的工作,需要另外发送取消请求或确认最终结果。因此,“收到取消指令”和“执行已经停止”需要分别记录。

权限与预算由平台和业务服务校验。平台向执行环境提供有有效期、范围受限的访问权限,下游服务校验项目、操作和额度。恢复任务可以使用原运行配置,但已经撤销的权限仍以当前业务校验为准。

沙箱内程序可访问的文件、网络与凭证,由运行环境限制。长期、跨项目的高权限凭证进入沙箱后,生成的代码也可能读取到。Codex 自身的命令限制和审批策略需要与外层沙箱共同配置。Codex 安全与审批说明。

5. 方案比较

中心 RuntimeSandbox 内 Codex
中心运行时维护模型循环与上下文,也可以接入现成引擎。沙箱中的 Codex 维护模型循环与上下文。
文件和命令经过远程工具协议执行,需要维护执行会话。基础工程操作在本地执行,平台维护引擎托管接口。
恢复中心记录后,查询远端工具和业务操作。恢复平台映射、引擎状态与工作区,再核对外部操作。
管理 Worker,以及实际执行代码的沙箱。管理沙箱内的引擎版本、会话、依赖和进程。
项目、权限、费用和交付由业务系统管理。项目、权限、费用和交付仍由业务系统管理。

任务以调用业务 API 为主、流程比较明确时,中心服务便于集中组织。任务需要长时间在工程中修改、构建和调试时,沙箱内 Codex 可以复用已有的编码执行能力,减少基础工具适配。

这只是按工作形态做出的判断。比较具体方案时,还需要固定任务、模型和工具条件,检查正常完成率,以及 Worker 重启、沙箱丢失、响应超时和用户取消时的恢复情况。等待时间、重复操作与有效任务成本,应当一起记录。

任务结束时,还需要明确交付了什么。下面是暂停菜单任务的一份简化结果结构。

运行结果关联源码、构建、预览和功能验证记录
图 8:运行结果与交付记录的逻辑关系

源码记录说明改了哪些文件;构建记录说明构建是否通过、使用哪份产物;预览记录说明哪个版本正在运行;验证记录说明暂停与继续功能是否符合要求。

我们的一次游戏实验中,部署和健康检查通过以后,多人玩法仍然需要用多个客户端验证。执行器的结束、部署的完成和需求的实现,有各自对应的记录。无论采用哪种部署方式,平台都需要把这些结果关联到原任务。