Skip to content
EngineeringOctober 27, 2026

智能体如何部署 AnyRouter:一次一个 Worker

AnyRouter 运行在一组 Cloudflare Worker 上,智能体从多台机器上大约每小时部署一次。现在每个 Worker 都记录自己正在运行的提交,一次部署只发布自该提交以来代码有变化的 Worker,而比生产环境更旧的检出无法覆盖生产。本文介绍它如何运作,以及它现在的成本。

一个仓库,多个 Worker

AnyRouter 是一个 pnpm monorepo,发布为多个 Cloudflare Worker。一次完整部署包括 17 个主要 Worker 和两个小型代理 Worker。API、网站、控制台、文档、博客和 MCP 服务器都是独立的 Worker,其中大多数共享同一批包里的代码。

智能体全天合并改动,大约每小时部署一次。本系列第一篇介绍了这个循环。本文讲其中的一条命令 pnpm ops deploy,以及它每次运行都要回答的三个问题。

  1. 每个 Worker 现在运行的是什么?
  2. 这次改动可能影响哪些 Worker?
  3. 这个检出比生产环境新吗?

为什么我们不再全部部署

直到 10 月初,只要共享包有改动,一次部署就会发布整个 Worker 集群,而几乎每个改动都会碰到共享包。完整部署的中位数约为 1,130 秒,统计自部署日志中 10 月 6 日到 9 日最近 5 次成功的完整运行。

最大的阶段耗时 385 秒。它把模型目录爬取成静态 HTML,然后带着这些页面重新部署 web Worker。只有 web Worker 会发布这些 HTML。即使只改文档,也要付出这次爬取,耗时约 513 秒,其中 425 秒生成的页面从未被部署。

每个 Worker 记录自己的提交

第一个修复是不再猜测生产环境在运行什么。ops 执行的每次 wrangler deploy 都会传入 --message anyr-sha=<commit>。Cloudflare 把这条消息保存在 Worker 版本上,任何拥有 API 令牌的机器之后都能读取。它不花钱,也不需要新的数据库。

普通的 pnpm ops deploy 会并行读取每个 Worker 的线上版本。我们在 10 月 12 日测量时,这次读取耗时 1.8 秒。更新密钥会产生一个没有消息的新版本,所以读取程序会跳过这些版本,找到其下的代码版本。

然后每个 Worker 的 diff 是 <它的线上提交>..HEAD,再加上未提交的改动。我们以前与 origin/main 比较。合并一推送,那个 diff 就是空的;10 月 11 日,一次 --since HEAD~1 的部署漏掉了一个压在后续文档提交下面的合并。线上提交是唯一与实际运行内容一致的基准。

读不到提交的 Worker 一律部署。这包括从未打过标记的 Worker、从有未提交改动的工作区部署的版本,以及 API 错误。部署多了是安全的,部署少了不安全。

由导入图选出目标

接下来,scripts/lib/worker-graph.ts 把改动的文件映射到 Worker。对每个 Worker,它收集其依赖的 workspace 包和源码实际导入的 @anyrouter/* 包,再对每个包套用同样的规则。测试文件不会增加依赖边。

共享 UI 包按文件处理。对每个 Worker,规划器从该 Worker 自己的源码出发,沿着导入走进 packages/ui。改动一个 UI 文件,只会部署能到达它的 Worker,dry run 会写出导入它的文件。如果遇到无法追踪的导入,例如带模板字符串的动态 import(),就把该 Worker 视为能到达整个 UI 包。

有些输入可能改变所有 bundle,所以会强制走完整路径。包括 package.json、lockfile、根目录的 TypeScript 和 Vite 配置、部署脚本本身、数据库迁移,以及规划器不认识的任何路径。只改 docs/kb 下的 kb 笔记,本身不会触发部署。

没有受影响的

根输入有改动

部分 Worker

被其他机器持有

已获得

是

否

读取每个 Worker 的线上 anyr-sha

把每个线上 sha 与 HEAD 比较

通过导入图映射改动的文件

计划

跳过,不部署

完整路径

只部署这些 Worker

部署租约空闲?

等待,然后中止

HEAD 比某个线上 sha 旧?

中止:先 pull 再重试

构建、上传、标记 anyr-sha

一次运行如何决定发布什么,以及能在任何上传之前叫停它的两项检查。

pnpm ops deploy --dry-run 会打印计划:每个 Worker 一行、每个目标的原因,以及预计耗时。下面是规划器对几类改动的估算,基于部署日志中各阶段的中位数:

改动部署估算之前
只改控制台dashboard约 65 秒约 1,130 秒
只改文档应用docs约 95 秒约 513 秒
首页区块web 及其运行时依赖约 620 秒约 1,130 秒
只改一条 kb 笔记无0 秒0 秒

过时的检出无法让生产回退

10 月 12 日,两台机器同时运行了 pnpm ops deploy。其中一台在另一台运行中途部署了更新的提交,于是前者的最终检查失败。那次没有造成损害,因为更新的构建已经包含了较旧的提交。如果顺序相反,生产环境就会回到更旧的构建。

现在在第一次上传或迁移之前会运行两道防护。

  • 部署租约。部署会向 Workers KV 写入一个租约键,内容包括机器、提交、开始时间和一个随机 nonce,有效期 25 分钟。KV 没有 compare-and-set,所以写入方等待 1 秒后再读回该键,只有存储的 nonce 属于自己的机器才拥有租约。其他机器会等待,每 15 秒检查一次,默认 15 分钟后放弃。构建期间,一个后台进程每分钟续一次租约。
  • 回退防护。拿到租约后,部署会把 HEAD 与每个目标的线上 anyr-sha 比较。如果 HEAD 是某个线上提交的严格祖先,就中止,并提示生产环境更新、请先 pull。git 无法解析的线上提交永远不会阻止部署。

第三项检查来自另一次事故。一次部署要在几分钟内持续读取共享检出,而有一次另一个会话在构建过程中在那里运行了 git pull。那次部署用更新的脚本爬取了基于更旧 bundle 的页面,导致 4 个模型页面退回到服务端渲染。现在 ops deploy 在开始时记录 HEAD 和受跟踪文件的状态,并在每次构建、上传和爬取之前重新检查两者。任何一个变化,它都会中止,并列出已经发布的 Worker。

爬取有了缓存,也更快了

每次 web 部署都会整体替换该 Worker 的静态文件,所以每次都必须带上预渲染页面。丢掉它们会让目录退回到服务端渲染。因此规划器从不跳过爬取,而是复用已有的爬取结果。

缓存键是构建产物的哈希,而不是源码的哈希,因为 HTML 中嵌入了构建的资源文件名。对同一构建重试失败的部署时,会复用爬取结果,完全跳过爬取。不涉及 web Worker 的部署则完全不爬取。

爬取本身也变快了。它以前一次爬一个页面,每次暂停 800 毫秒,这是为 Workers Free 计划选的参数。在付费计划上,它现在一次爬 2 个页面,暂停 100 毫秒。在一台机器记录的 10 月 25 日和 26 日的 18 次成功部署中,同一步骤(爬取加重新部署 web Worker)耗时 84 到 129 秒,中位数约 100 秒。以前是 385 秒。

现在的成本

下面是一台机器在 10 月 25 日和 26 日的部署日志。其他机器也在部署,所以这只是样本,不是总数。

  • 按改动文件模式,成功 18 次,失败 3 次。
  • 18 次从开始到结束的中位数为 522 秒;到生产环境提供新代码并通过 5 秒关键冒烟测试的中位数为 419 秒。完整爬取和检查在此之后运行。
  • 18 次中有 11 次走了完整路径,中位数 530 秒;其余 7 次发布了 11 到 17 个 Worker,中位数 493 秒。

最后这个数字最重要。智能体自己经常修改部署脚本、lockfile 和共享包,每一项都会强制一次大范围部署。按 Worker 选择目标,在小而局部的改动上省得最多,比如一次控制台修复或一次文档编辑。所以完整路径部署从约 1,130 秒降到 530 秒,主要来自另外两项改动。爬取从 385 秒降到约 100 秒,上传现在也与下一次构建同时进行。这是我们对日志中各阶段耗时的解读,不是对照实验。

接下来,我们希望减少强制走完整路径的输入。控制台就是一个例子。10 月 10 日统计时,控制台 Worker 能到达的 126 个控制台 UI 文件中,有 102 个也通过一棵旧的路由树被打包进 web Worker,所以修改其中任何一个也会部署 web Worker。删除那棵旧树后,这些修改就只会部署控制台。

两分钟发第一笔请求

用自己的 key 免费开始,或充值按 token 计费。Go 每月 $4 额度 — $2/月,或捐一把 provider key 免费开通。

开始使用