我们把 agent 重写成了完全运行在 Durable Object 里,用 Pi、Agents SDK 和 Code Mode 驱动

我们最近把 camelAI agent 从虚拟机上迁了出来。现在,agent 运行在 Cloudflare Durable Object 里,文件系统放在 SQLite 和 R2 上,而且它写的是 JavaScript,不再是 bash。大多数团队会把 coding agent 放在完整的 Linux VM 或容器沙箱里跑,我们以前也是这么做的。

我们想摆脱 VM,是因为给每个用户配一台常驻机器、再挂上磁盘,成本高到很难规模化。难点在于,coding agent 默认就假设自己在 Linux 里。它们训练时就习惯调用 bash,而我们最早上线用的 harness 也要求完整 VM,所以走到今天一共经历了三次重设计。代价是,现在 agent 只能做那些我们已经显式封装出方法的事。听起来受限,但对产品反而是好事。

我是 Miguel,camelAI 的 CTO。我们的代码库最近已经开源,所以这篇文章里讲到的所有代码都能在 github.com/qaml-ai/camelAI 读到。下面会一路附上相关文件链接。先看整个演进过程。

第零步:VM 时代

我们最早上线时用的是 Claude Code harness,它需要一台完整虚拟机才能运行。我们试过好几家 VM 服务商,没有一家能满足我们的持久化和性能要求,最后只能自己搭了一套容器服务。那篇文章现在还在,但那套基础设施我们已经不用了。

容器服务能跑,但太重。每个用户一台常驻 VM 很贵,把每个用户的文件都放在高速挂载磁盘上也很贵。要扩容,就意味着扩真实机器和真实磁盘;按我们目标用户规模来看,成本会高到不可接受。所以我们没有继续在 VM 编排上抠技巧,而是开始围绕“根本不需要 VM”来重新设计。

第一步:把 agent 从 VM 里拿出来

Claude Code harness 和 VM 绑定得太死,所以第一步是做自己的 harness。我们基于 pi 来做,这是 Mario Zechner 开源的 coding agent。pi 是一组库组成的栈。最高层假设自己运行在普通操作系统里,但更底层会给你 agent loop、状态管理这类 agent 基础能力,而且不关心它们到底跑在哪里。我们没有改任何 pi 代码,而是把这些底层库引进来,在它们之上构建了自己的 harness,让它运行在 Cloudflare Durable Object 里,而不是 Linux 环境里。

Durable Object 是一个小型有状态计算实例,会在 Cloudflare 边缘网络上启动,位置靠近创建它的用户。每个聊天线程都有自己的 Durable Object;光是这一点,相比把所有请求都绕到一个中心化 VM 主机,延迟就降下来了。

这个阶段我们还保留了 VM,但 agent 不再住在 VM 里。需要运行命令时,它会远程调用 VM。Anthropic 在描述自己的托管 agent 时也提到过这种拆分:大脑和双手分离。它给我们带来了一些不错的性质:

  • agent 不用等机器启动,所以 VM 还没醒,它就已经可以开始响应。
  • VM 可以在 agent 继续工作的同时重新休眠;如果这一轮不需要任何命令,它甚至可以完全不用醒来。
  • 一个大脑可以控制多双手。一个 agent 可以同时操作好几台 VM。

我们把这些“手”叫作项目(project)。每个项目都配一台用于执行命令的 VM,以及一个通过 Cloudflare Artifacts 以编程方式创建的 git repo;Artifacts 是一种兼容 git 的存储,可以直接从 Worker 里按需创建。agent 其实不太知道自己已经跑在 VM 外面。它仍然有 bash,用起来也和其他 coding agent 一样。

问题是,这只解决了延迟,其他都没变。我们仍然是每个用户一台 VM,所以原始设计里的成本和扩展问题还都在。

第二步:移除 VM

下一版保留了同样的项目结构,但去掉了背后的 VM。现在,每个项目都由一个运行在 Durable Object 里的文件系统支撑,较大的文件则放到后端的 R2 里。

这不是我们发明的。Cloudflare 的 agents 团队做过 Shell,一个面向 Workers 的实验性文件系统和执行运行时,我们大量复用了他们的代码。机制很简单。Durable Object 的存储是一个 SQLite 数据库,上限 10 GB,而且每一行也有最大大小限制。小文件直接放在 SQLite 行里。超过大约 1.5 MB 的文件会写到 R2,SQLite 行里只保留一个指针。对 agent 来说,它看起来像一个普通文件系统;但底层其实是数据库和对象存储,所以持久化变成了存下来的数据,而不是一套必须一直活着的基础设施。

版本历史仍然走 Artifacts,所以每个项目都能保留 git 历史,而我们不用自己托管 git server。

第三步:移除 bash

移除 bash 一开始听起来很激进。coding agent 训练时就习惯调用 bash,而 bash 也是大家一开始把它们放进 VM 的原因。但问题不只是成本。有了 bash 和网络访问能力的 agent,如果要做有用的事,就需要凭证;而我们尝试过的认证代理 URL 越来越像临时拼出来的方案,也越来越难严格执行。

所以我们把 bash 移除了。现在 agent 不写 bash,而是写 JavaScript,并通过 Code Mode 和 Cloudflare 的动态 Worker 加载器执行。每次执行都会跑在一个全新的 V8 isolate 里,毫秒级启动,只占几 MB 内存。这个沙箱会预先加载用户的数据连接,以及平台能做的每件事对应的方法。凭证永远不会进入沙箱。agent 调用某个连接的方法,认证则发生在我们这一侧。

如果看一看 agent 实际上都拿 bash 做什么,就会发现失去 bash 的代价比想象中小。大部分只是文件操作,而 agent 已经有原生工具可以做。我们给它提供 read、write、edit,再加上我们自己的 grep 和 glob 实现。这覆盖了 80/20。剩下的是针对具体任务的具体命令,于是它们变成了显式方法:

  • 通过代理执行的 wrangler deploy,变成了我们完全可控的 deploy_project 方法。因为我们精确知道什么时候发生 deploy,就可以接上 hook,并自动打开实时预览。以前我们必须嗅探代理过的 wrangler 流量,去猜到底是哪条聊天线程部署了东西。
  • 构建用户应用和运行 Python notebook,也变成了各自独立的方法,背后都由短生命周期容器支撑。

这两个任务我们仍然保留了容器,因为它们确实需要 Linux。用户应用用 Vite、Tailwind 和 React Router 构建,添加依赖意味着要跑 bun install。我们考虑过在 Worker 里构建,因为被构建出来的东西本身也是 Worker,但这条路支持得不好,而且 Workers 只有 128 MB 内存限制,CPU 也只有一小部分。构建会很慢,很多项目也会直接超过内存上限。所以现在构建时会通过 Cloudflare Sandbox SDK 启动一个容器,把项目拷进去,跑完任务,返回结果,然后关闭容器。notebook 运行也是同样的方式。我们仍然会用完整 Linux,但只在真正需要它的那几秒钟里用。

老实说,缺点是我们必须提前预判 agent 需要什么。有 bash 的时候,它可以自己想办法。现在如果缺一个能力,我们就得把它加进去。实际做下来,这种压力对产品是好事,因为它逼着我们思考用户到底在做什么,并为这些事做出一等支持路径,而不是让 agent 临场发挥。

还有一个意外收获。bash 是开放式环境,低成本模型在开放式环境里表现会吃力。方法集合变小、变明确之后,它们的表现明显更好;这一点很重要,因为让 camelAI 保持低运行成本,正是这套架构的目标。

现在我们走到了哪里

现在这套栈是:Durable Objects 负责 agent 和它的文件系统,R2 存大文件,Artifacts 存 git 历史,pi 作为 harness,Code Mode 加动态 Workers 负责执行。它像其他 Cloudflare 应用一样部署,也没有外部容器服务需要管理。

动态 Workers 按执行次数计费,而不是按在线秒数计费。几千次执行的成本,大概相当于我们以前评估过的那些服务上几分钟容器时间。延迟低,是因为所有东西都跑在靠近用户的边缘;扩容则成了 Cloudflare 的问题,而不是我们的问题。

用户仍然可以构建并部署全栈应用到真实 URL,agent 也仍然能读、写、grep 和部署。从用户视角看,什么都没变。

TL;DR

我们一开始是在自建 VM 服务上跑 Claude Code harness,成本高,也很难扩展。第一步,我们把 agent 本身搬进 Cloudflare Durable Object,让它远程控制 VM;这解决了延迟,但没有解决成本。接着,我们基于 Cloudflare 的 Shell 项目,用存放在 Durable Object SQLite 和 R2 里的文件系统完全替代 VM,并通过 Cloudflare Artifacts 保留 git 历史。最后,我们移除了 bash,用 Code Mode 和动态 Workers 给 agent 提供一个 JavaScript 沙箱,并为部署、构建和 notebook 提供显式方法。结果是成本低了几个数量级,延迟更低,运维更简单,小模型也更容易驱动。全部代码都已经开源在 github.com/qaml-ai/camelAI