我们最近完成了将 camelAI Agent 从虚拟机迁移的工作。现在 Agent 运行在 Cloudflare Durable Object 内部,其文件系统存储在 SQLite 和 R2 中,并且它编写 JavaScript 而不是 bash。大多数团队在完整的 Linux 虚拟机或容器沙箱中运行编码 Agent,我们过去也是如此。
我们想要摆脱虚拟机,因为给每个用户提供一个始终在线的机器并附带磁盘,成本太高,难以扩展。难点在于编码 Agent 默认使用 Linux。它们被训练去使用 bash,而我们最初启动的框架需要一个完整的虚拟机,因此我们经过了三次重新设计才达到现在的状态。权衡的结果是,Agent 现在只能执行我们明确构建了方法的事情,这听起来有限制,但对产品却是有益的。
我是 Miguel,camelAI 的 CTO。我们的代码库最近开源了,因此这篇文章中的所有内容你都可以在 github.com/qaml-ai/camelAI 上看到代码。我会在讲解过程中链接到相关文件。以下是我们的演进过程。
第零步:虚拟机时代
我们最初基于 Claude Code 框架启动,该框架需要一个完整的虚拟机来运行。我们尝试了几家虚拟机提供商,但没有一家能满足我们对持久性和性能的要求,最终我们构建了自己的容器服务。那篇文章还在,但我们不再运行那套基础设施了。
容器服务是可行的,但很笨重。为每个用户提供始终在线的虚拟机成本高昂,同时在快速附加磁盘上保存每个用户的文件也很昂贵。扩展意味着要扩展真实的机器和真实的磁盘,对于我们目标用户数量来说,这将是极其昂贵的。因此,我们不再在虚拟机编排上耍小聪明,而是开始设计根本不需要虚拟机的方案。
第一步:将 Agent 从虚拟机中剥离
Claude Code 框架与其虚拟机密不可分,因此第一步是构建我们自己的框架。我们基于 pi 构建,这是 Mario Zechner 的开源编码 Agent。pi 是一个库栈。最高层假设一个正常的操作系统,但较低层提供了 Agent 的基本要素,比如 Agent 循环和状态管理,而不关心它们在哪里运行。我们没有修改任何 pi 代码。我们导入了那些较低层,并在其之上构建了我们自己的框架,运行在 Cloudflare Durable Object 内部,而不是 Linux 环境中。
Durable Object 是一个小型有状态计算实例,在 Cloudflare 的边缘节点上启动,靠近创建它的用户。每个聊天线程都有自己的 Durable Object,这本身就比将所有流量路由到集中的虚拟机主机降低了延迟。
在这个阶段,我们保留了虚拟机,但 Agent 不再运行在虚拟机内部。当需要执行命令时,它会远程调用虚拟机。Anthropic 为其管理型 Agent 描述了同样的拆分方式:大脑与双手分离。这给我们带来了一些好处:
- Agent 在虚拟机启动之前就开始响应,因为它不需要等待机器启动。
- 虚拟机可以在 Agent 继续工作时进入休眠状态,或者如果该轮次不需要任何命令,根本不需要启动。
- 一个大脑可以控制多个双手。一个 Agent 可以同时操作多个虚拟机。
我们将这些双手称为项目。每个项目都有一个用于执行命令的虚拟机,以及一个通过 Cloudflare Artifacts 以编程方式创建的 git 仓库,这是一种兼容 git 的存储,可以从 Worker 即时配置。Agent 实际上并不知道它运行在虚拟机之外。它仍然拥有 bash,并且像其他编码 Agent 一样工作。
问题是,这仅仅解决了延迟问题,并没有解决其他问题。我们仍然为每个用户保留了一个虚拟机,因此我们仍然面临最初设计中的所有成本和扩展问题。
第二步:移除虚拟机
下一个版本保持了相同的项目结构,但移除了背后的虚拟机。每个项目现在由一个存储在 Durable Object 内部的文件系统 支持,对于较大的文件则使用 R2。
这不是我们发明的。Cloudflare 的 Agent 团队构建了 Shell,一个实验性的 Worker 文件系统和执行运行时,我们大量复用了他们的代码。原理很简单。Durable Object 的存储是一个 SQLite 数据库,容量上限为 10 GB,每一行都有最大大小限制。小文件直接存储在 SQLite 行中。超过大约 1.5 MB 的文件会被写入 R2,SQLite 行中只保存一个指针。对 Agent 来说,它看起来像一个正常的文件系统,但底层是数据库和对象存储,因此持久化是存储的数据,而不是我们需要保持运行的基础设施。
版本历史仍然通过 Artifacts 运行,因此每个项目都保留有 git 历史,而我们无需托管 git 服务器。
第三步:移除 bash
移除 bash 感觉是一次剧烈的改变。编码 Agent 被训练去使用 bash,而 bash 正是为什么大家最初都在虚拟机中运行它们的原因。这也不仅仅是成本问题。一个拥有 bash 和网络访问权限的 Agent 需要凭证才能做任何有用的事情,而我们尝试使用经过身份验证的代理 URL 的方法变得越来越 hacky 且难以执行。
因此我们移除了它。Agent 不再使用 bash,而是编写 JavaScript,通过 Code Mode 和 Cloudflare 的 动态 Worker 加载器 来执行。每次执行都在一个全新的 V8 隔离环境中运行,该环境毫秒级启动,内存占用仅几兆字节。沙箱预先加载了用户的数据连接,以及平台能做的所有事情的方法。凭证永远不会进入沙箱。Agent 调用连接的方法,身份验证在我们这边进行。
当你观察 Agent 实际使用 bash 做什么时,失去它带来的损失比你想象的要小。大部分是文件操作,Agent 有原生工具来处理。我们提供了读取、写入和编辑,以及我们自己的 grep 和 glob 实现。这覆盖了 80% 的情况。其余的是特定作业的特定命令,这些变成了明确的方法:
- 通过代理的 wrangler deploy 变成了一个我们完全控制的 deploy_project 方法。由于我们确切知道何时部署,我们可以 hook 它并自动打开实时预览。以前,我们必须嗅探代理的 wrangler 流量来猜测哪个线程部署了某些东西。
- 构建用户的应用和运行 Python 笔记本变成了它们自己的方法,两者都由短生命周期的容器支持。
我们保留了这两个作业的容器,因为它们确实需要 Linux。用户的应用使用 Vite、Tailwind 和 React Router 构建,添加依赖意味着运行 bun install。我们考虑过在 Worker 内部运行构建,因为正在构建的东西本身就是一个 Worker,但这条路径支持不够好,并且 Worker 有 128 MB 的内存限制和一小部分 CPU。构建会很慢,而且很多项目会超出内存限制。因此,我们改为通过 Cloudflare Sandbox SDK 启动一个容器,将项目复制进去,运行作业,返回结果,然后关闭容器。笔记本运行也是如此。我们仍然使用完整的 Linux,但仅限于实际需要它的那几秒钟的工作。
一个诚实的缺点是我们必须预测 Agent 需要什么。有了 bash,它可以自己解决问题。现在,如果缺少某个功能,我们必须添加它。在实践中,这种压力对产品是有益的,因为它迫使我们去思考用户正在做什么,并为此构建一个一流的路径,而不是让 Agent 即兴发挥。
还有一个意想不到的好处。bash 是开放式的,而廉价模型在开放环境中表现不佳。使用一组更小的明确方法,它们的表现明显更好,这很重要,因为保持 camelAI 运行成本低廉正是这个架构的重点。
我们现在的位置
现在的技术栈是:Durable Objects 用于 Agent 及其文件系统,R2 用于大文件,Artifacts 用于 git 历史,pi 作为框架,以及 Code Mode 和动态 Worker 用于执行。它像任何其他 Cloudflare 应用一样部署,无需管理外部容器服务。
动态 Worker 按执行次数计费,而不是按运行时间秒数计费。数千次执行的成本大约相当于我们之前评估的容器服务中几分钟的容器时间。延迟很低,因为一切都在靠近用户的边缘运行,扩展是 Cloudflare 的问题,而不是我们的问题。
用户仍然可以构建和部署全栈应用到实时 URL,Agent 仍然可以读取、写入、grep 和部署。从用户的角度来看,什么都没有改变。
总结
我们从基于自建虚拟机服务的 Claude Code 框架开始,这套方案昂贵且难以扩展。首先,我们将 Agent 本身移入 Cloudflare Durable Object,并让它远程控制虚拟机,这解决了延迟问题,但没有解决成本问题。然后,我们基于 Cloudflare 的 Shell 项目,用存储在 Durable Object SQLite 和 R2 中的文件系统完全取代了虚拟机,并通过 Cloudflare Artifacts 保留 git 历史。最后,我们移除了 bash,通过 Code Mode 和动态 Worker 为 Agent 提供了一个 JavaScript 沙箱,并为部署、构建和笔记本提供了明确的方法。结果是成本降低了几个数量级,延迟更低,操作更简单,并且更容易被小型模型驱动。所有这些都在 github.com/qaml-ai/camelAI 开源。





