我们最近完成了将 camelAI 智能体迁出虚拟机的工作。现在,这个智能体运行在 Cloudflare Durable Object 中,文件系统位于 SQLite 和 R2 里,并且它编写 JavaScript,而不是 bash。大多数团队会把编程智能体运行在完整的 Linux 虚拟机或容器沙箱中;我们以前也是如此。
我们想摆脱虚拟机,是因为为每位用户提供一台始终在线、挂载磁盘的机器,扩展起来成本太高。难点在于,编程智能体默认 Linux 环境:它们被训练得会本能地使用 bash,而我们最初采用的 harness 也需要完整的虚拟机。因此,走到今天经历了三次重构。代价是,智能体现在只能执行我们为其显式构建了方法的事情;听起来似乎受限,但这反而让产品变得更好。
我是 camelAI 的 CTO Miguel。我们的代码库最近已经开源,所以本文提到的一切,你都可以在 github.com/qaml-ai/camelAI 中读到对应代码。我会在下文链接相关文件。下面是整个演进过程。
第 0 步:虚拟机时代
我们的首版基于 Claude Code harness 构建,它需要一台完整虚拟机才能运行。我们尝试过几家虚拟机供应商,但没有一家同时满足持久化与性能要求,最终便自己构建了容器服务。那篇文章还在,但我们已经不再运行其中任何基础设施。
这个容器服务能用,但很重。为每位用户维持一台始终在线的虚拟机很贵,把每位用户的文件留在高速挂载磁盘上也很贵。要扩展,就意味着扩展真实机器和真实磁盘;以我们预期的用户规模来看,成本会高到无法承受。因此,我们没有继续在虚拟机编排上耍花样,而是开始围绕「根本不需要虚拟机」来设计。
第 1 步:把智能体移出虚拟机
Claude Code harness 与它的虚拟机不可分割,因此第一步是构建自己的 harness。我们以 Mario Zechner 的开源编程智能体 pi 为基础。pi 是一组分层的库:最高层假定存在一个普通操作系统,而底层则提供智能体循环、状态管理等原语,不关心它们实际运行在哪里。我们没有改动任何 pi 代码,只是导入这些底层模块,并在其上构建了自己的 harness;它运行在 Cloudflare Durable Object 中,而不是 Linux 环境里。
Durable Object 是一种小型有状态计算实例,会在 Cloudflare 边缘网络中、靠近创建它的用户处启动。每个聊天线程各有一个 Durable Object;相较于把一切路由到集中的虚拟机主机,这本身就降低了延迟。
在这个阶段,我们还保留着虚拟机,但智能体已经不住在其中。需要执行命令时,它会远程调用虚拟机。Anthropic 对其托管智能体也描述过同样的拆分:把大脑与双手分开。这带来了一些不错的特性:
- 虚拟机尚未唤醒时,智能体就能开始响应,因为它不必等待机器启动。
- 智能体继续工作时,虚拟机可以重新休眠;如果这轮不需要执行任何命令,它甚至完全不必唤醒。
- 一个大脑可以控制多双手:单个智能体可以同时操作多台虚拟机。
我们把这些「手」称为项目。每个项目都附带一台用于执行命令的虚拟机,以及一个通过 Cloudflare Artifacts 以编程方式创建的 Git 仓库;Artifacts 是与 Git 兼容的存储,可由 Worker 按需创建。智能体其实不知道自己运行在虚拟机外;它仍然拥有 bash,工作方式也和任何其他编程智能体一样。
问题是,这只解决了延迟,其他什么都没变。我们依然是每位用户一台虚拟机,因此依旧背负着原始设计中的全部成本与扩展问题。
第 2 步:移除虚拟机
下一版保留了相同的项目结构,但移除了其后的虚拟机。如今,每个项目由一个位于 Durable Object 中的文件系统支撑;较大的文件则存到 R2 中。
这不是我们的发明。Cloudflare 的 agents 团队构建了 Shell:一个面向 Workers 的实验性文件系统与执行运行时;我们大量复用了它的代码。其机制很简单:Durable Object 的存储是一个上限为 10 GB 的 SQLite 数据库,每一行也有最大尺寸。小文件直接放在 SQLite 行中;超过约 1.5 MB 的文件会写入 R2,而 SQLite 行只保留一个指针。对智能体来说,它看起来仍是普通文件系统;但在底层,它是数据库和对象存储,因此持久化的是数据,而不是我们必须始终维持的基础设施。
版本历史仍通过 Artifacts 运行,因此无需我们托管 Git 服务器,每个项目也能保留 Git 历史。
第 3 步:移除 bash
移除 bash 显得很激进。编程智能体受训练会去使用 bash,而这正是大家一开始都把它们放在虚拟机里的原因。问题也不只在成本:一个同时拥有 bash 和网络访问权限的智能体,想做任何有用的事就需要凭证;而我们尝试过的认证代理 URL 方案越来越别扭,也越来越难以强制执行。
所以我们把它移除了。智能体不再使用 bash,而是编写 JavaScript,通过 Code Mode 和 Cloudflare 的动态 Worker 加载器执行。每次执行都会运行在一个全新的 V8 isolate 中:它能在毫秒级启动,只占用几 MB 内存。沙箱预先加载了用户的数据连接,以及平台能做的一切对应的方法。凭证永远不会进入沙箱;智能体调用某个连接的方法,而认证在我们这一侧完成。
看看智能体实际用 bash 做什么,失去它的代价比你想象中小。大部分是文件操作,而智能体对此已有原生工具。我们提供读、写、编辑,以及自己实现的 grep 和 glob。这覆盖了 80/20 的那部分。其余是特定任务所需的特定命令,于是我们将它们变成显式方法:
- 通过代理执行的 wrangler deploy,变成了我们能完全控制的 deploy_project 方法。因为我们确切知道何时发生部署,便可以挂钩它并自动打开实时预览。以前,我们必须嗅探代理过的 wrangler 流量,猜测是哪个线程完成了部署。
- 构建用户应用和运行 Python notebook 也分别变成了专属方法,二者都由短生命周期容器支持。
我们仍为这两项工作保留容器,因为它们确实需要 Linux。用户应用使用 Vite、Tailwind 和 React Router 构建,而添加依赖意味着运行 bun install。我们曾考虑在 Worker 中运行构建,毕竟被构建的东西本身也是 Worker;但这条路径支持得并不好,而且 Workers 的内存限制为 128 MB,CPU 配额也很少。构建会很慢,许多项目也会超出内存上限。因此,构建会通过 Cloudflare Sandbox SDK 启动一个容器,复制项目进去,执行任务、返回结果,再关闭容器。Notebook 的运行方式相同。我们仍会使用完整的 Linux,但只用于真正需要它的那几秒工作。
坦率的缺点是,我们必须预判智能体需要什么。使用 bash 时,它可以自行想办法;现在,如果缺少某项能力,就得由我们添加。不过实践中,这种压力对产品有益:它迫使我们思考用户究竟在做什么,并为此构建一条一等支持的路径,而不是让智能体临场发挥。
还有一个意外收益。Bash 是开放式的,而廉价模型在开放式环境中表现很吃力。使用一组更小、显式的方法后,它们的表现明显更好;这很重要,因为这套架构的目的就是让 camelAI 的运行成本保持低廉。
这让我们走到了哪里
如今的技术栈是:Durable Objects 承载智能体及其文件系统,R2 存放大文件,Artifacts 保存 Git 历史,pi 作为 harness,Code Mode 与动态 Workers 负责执行。它和其他 Cloudflare 应用一样部署,不需要管理任何外部容器服务。
动态 Workers 按执行次数计费,而不是按在线秒数计费。数千次执行的成本,大致只相当于我们曾评估过的那些服务中几分钟容器时间的成本。由于一切都运行在靠近用户的边缘网络,延迟很低;扩展则成了 Cloudflare 的问题,不是我们的。
用户仍可构建并部署完整的全栈应用到线上 URL,智能体仍能读取、写入、grep 和部署。从用户视角看,没有任何变化。
简而言之
我们起初将 Claude Code harness 运行在自建虚拟机服务上,成本高且难以扩展。随后,我们先把智能体移入 Cloudflare Durable Object,让它远程控制虚拟机;这解决了延迟,却没有解决成本。接着,我们基于 Cloudflare 的 Shell 项目,用存储在 Durable Object SQLite 与 R2 中的文件系统彻底替代虚拟机,并通过 Cloudflare Artifacts 保留 Git 历史。最后,我们移除了 bash,借助 Code Mode 和动态 Workers 为智能体提供 JavaScript 沙箱,并为部署、构建和 notebook 提供显式方法。结果是:成本低了几个数量级、延迟更低、运维更简单,小模型也更容易驱动。所有代码都已在 github.com/qaml-ai/camelAI 开源。