← 返回文章

启动 13 次、切换 1 次:我怎样让智能体安全执行发布

发布执行器启动了 13 次,前 12 次停止都不是待发布版本本身的缺陷导致的。五项机制让协调智能体、验证工具和网络造成的问题停在生产切换之前,文中附有实现片段。

迁移后半程要上传约 2,000 个文件、创建映射、部署,然后切换域名。我把这些步骤放进一个由 systemd 托管的长脚本,由 AI 协调智能体驱动。执行器一共启动了 13 次,最后一次才完成切换。

前 12 次停止,没有一次是待发布版本本身的缺陷导致的:五次来自协调智能体,四次来自我们自己的验证工具,两次来自网络,还有一次是时间预算检查按设计拒绝继续。它们都没有损害生产环境。下面拆开说明让失败停在安全位置的五项机制。

12 次停止的原因

停止原因:协调智能体 5 次,验证工具 4 次,网络 2 次,时间预算检查 1 次,待发布版本自身缺陷 0 次
fig 01 停止原因:协调智能体 5 次,验证工具 4 次,网络 2 次,时间预算检查 1 次,待发布版本自身缺陷 0 次
原因 次数 例子
协调智能体的状态或修改 5 遗留上次尝试的构建回执;sed 修改失败后未检查就重启;复制了与生产数据不一致的演练数量;某个脚本缺少 Python 路径
我们的验证工具 4 拒绝已知无害的页面;Python 哈希随机化导致台账重建顺序不同;某一步缺少允许列表;拒绝保留查询字符串的 CDN 重定向
网络 2 上传到第 767 个文件时,连接 R2 的 TLS 握手超时;部署成功后,Vercel CLI 最后一次获取结果失败
时间预算检查 1 剩余窗口不足以完成后续阶段,因此按设计停止
待发布版本本身的缺陷 0
周五和周六的 13 次启动,按停止原因着色;第 13 次完成域名切换
fig 02 周五和周六的 13 次启动,按停止原因着色;第 13 次完成域名切换

要判断能不能让智能体驱动发布,这张表给出了实际执行中的阻力。多数阻力来自智能体和它使用的工具。执行器遇到不满足条件的情况就拒绝继续,才让这些阻力没有变成生产事故。

执行流程:检查恢复条件,等待 HOLD 1,按精确授权写入,等待 HOLD 2,部署但不切换域名,逐个验证 URL,等待 HOLD 3 和 HOLD 4,准备好回滚后再切换
fig 03 执行流程:检查恢复条件,等待 HOLD 1,按精确授权写入,等待 HOLD 2,部署但不切换域名,逐个验证 URL,等待 HOLD 3 和 HOLD 4,准备好回滚后再切换

机制一:批准必须绑定证据哈希

执行器会在四个检查点暂停。每到一处,它先将通过程序检查的证据写进 hold 文件,再等待 go 文件。go 文件必须比 hold 文件更新,而且包含 hold 文件的 SHA-256。旧批准或提前写好的批准,因此无法放行新的证据。

hold() {  # hold N title evidence-file
  local n=$1 sha
  cp "$3" "$S/HOLD-$n.txt"; sha=$(sha256sum "$S/HOLD-$n.txt" | cut -c1-64)
  until [[ -f $S/GO-$n && $S/GO-$n -nt $S/HOLD-$n.txt ]] && grep -q "$sha" "$S/GO-$n"; do
    [[ -f $S/STOP ]] && fail "STOP at HOLD-$n"; sleep 10
  done
}
# reviewer releases it:  echo "reviewed $(sha256sum HOLD-3.txt | cut -c1-64) <note>" > GO-3

日志里的每次批准,都能追溯到它具体批准了哪份证据。智能体以这种方式放行了 14 次检查点,我没有手工放行任何一次;无论由谁批准,留下的审计记录相同。

机制二:按完整内容精确授权

工具先输出 JSON 摘要,再询问 Proceed? [y/N]。一个小型伪终端包装器代替人回答。它严格核对整份摘要及其数据类型,而且只对第一次提示作出肯定回答:

def strict_eq(a, b):
    if type(a) is not type(b): return False          # 0 != False, 1 != True
    if isinstance(a, dict): return a.keys() == b.keys() and all(strict_eq(a[k], b[k]) for k in a)
    if isinstance(a, list): return len(a) == len(b) and all(map(strict_eq, a, b))
    return a == b

# on "Proceed? [y/N]": answer "y" only if this is prompt #1 and strict_eq(last_plan_json, EXPECTED); else "n"

EXPECTED 来自已经封存的证据,包括计划摘要值(digest)、数量、目标和对象键。这套授权机制也要像生产代码一样测试:先输入工具真实输出的确认内容,再改动一个字段,确认包装器回答的是 n。

机制三:每个阶段都能恢复,并重新核实状态

重跑时,执行器不会相信本地的“已完成”标记,而是读取数据存储,重新分类:

  • 上传: 预留 ID 由键和哈希生成。重跑会跳过已经确认完成的文件,对上次回复未知的那一个文件,使用同一个预留 ID 重新上传。恢复后实际上传了 1,180 个文件,跳过 767 个,失败数为 0。
  • 映射: 区分“已按预期应用”“尚不存在”和“不匹配”。授权执行器只接收尚不存在的映射;只要发现不匹配,立即停止。
  • 恢复入口: 即使指定 RESUME_AT=mappings,也要重新执行所有启动检查,之后才允许跳到映射阶段。

机制四:只重试已经理解的失败

for try in 1 2 3 4 5 6; do
  run_upload > "$log"; rc=$?
  [[ $rc -eq 0 ]] && break
  transient=$(grep -cE '^unready [^ ]+: (direct PUT reply unknown|PUT outcome unknown)' "$log")
  other=$(grep -cE '^(refused|Traceback|Error)|requires audited reconciliation' "$log")
  [[ $rc -eq 2 && $transient -ge 1 && $other -eq 0 ]] || break     # anything else: stop
  sleep 20
done
[[ $rc -eq 0 ]] || fail "upload failed after $try tries"

如果不区分原因,一律重试,那一周早些时候遇到的两个真实问题就会被掩盖。这里仅重试我们已经证明可以安全重试的那一类失败,其他情况都停止。

机制五:验证器本身也需要测试

域名切换前,检查程序会验证新构建生成的每个 URL,约 80,000 个,并核对全部媒体字节。这也是代码,也会出错。12 次停止中有四次由验证工具造成,代价最大的一次涉及重定向:

Vercel 的 308 重定向会保留请求的查询字符串。 /assets/x.m4a?v=123 会转到 https://media.example/x.m4a?v=123。我们的检查却要求 Location 与不含查询字符串的媒体 URL 完全相等,于是在网站正常工作的情况下,一轮报告了 4,758 个失败。

修复检查器前后的误报:模拟检查 1,835→0,基线采集 17→0,候选部署检查 4,759→0
fig 04 修复检查器前后的误报:模拟检查 1,835→0,基线采集 17→0,候选部署检查 4,759→0

由此补了两条规则:

  1. 如果旧系统也通不过同一检查,先调查检查器。 直接请求旧部署一次,就看到了相同的重定向行为。
  2. 离线模拟必须先复现线上失败,通过结果才可信。 我们的模拟桩按检查器期待的方式返回重定向,没有复现 Vercel 的实际行为。修复时必须先让旧检查器报告失败,再证明新检查器通过。候选部署检查的 4,759 个失败,包括上述 4,758 个重定向误报和 1 个回滚检查失败。

Vercel 发布时需要注意的行为(截至 2026 年 10 月)

  • vercel deploy --prod --skip-domain 会创建生产部署,但不会移动自定义域名,也不会改变项目的“当前生产部署”指针。定时任务跟随当前生产部署,不跟随域名别名。 每次部署前后,我们都核实了这点。
  • 项目默认的 *.vercel.app 别名会随着部署移动。
  • 在伪终端里执行 npx <cli>,如果本地没有相应软件包,会先出现 Ok to proceed? 安装提示。无人值守执行可能卡在这里,应该将 CLI 安装到本地并固定版本。审查模型在它卡住上传之前发现了这个问题。

我接受的取舍

严格的启动检查有时会因为并非真实故障的原因拒绝继续,例如某个预期数量误用了演练数据。每次拒绝都要修正再启动,通常花 5–15 分钟。这个代价我可以接受;让执行器靠猜测继续,代价更难控制。后续应该让每个预期值都来自封存证据,避免沿用上一轮的状态。

下一篇:哪些交给智能体,哪些仍由我决定,以及独立审查究竟值在哪里。