让 AI 修复从故障复现走到发布验收
Reporter-to-Release 原型先脱敏扫描报告,在断网容器中用失败测试复现问题,再由智能体起草修复、人类审批合并,并核验发布提交和服务健康。团队报告了一次自建受控流程的检查结果,尚非真实客户故障验证。
AI 深度解读
AI 修 Bug 最容易跳过的一步,是确认自己修的就是用户遇到的问题。Vulcan 团队做的 Reporter-to-Release,把这件事拆成了一条有证据、有权限边界的流水线:先复现,再由报告者确认,AI 才能动手;人审核合并,线上发布核验通过后,还要回到报告者那里验收。
它最有参考价值的地方,是把「模型觉得修好了」变成一组可检查的状态。不过,漂亮的耗时数据来自一次团队自行提交、自行验收的受控运行,还不能代表真实客户场景。
先证明问题存在,才允许 AI 修改代码
这套系统围绕三个问题设计:报告者在提交后失联,公开文本可能夹带恶意指令,模型可能修掉一个相邻问题就宣布成功。
所以,报告首先经过确定性的输入过滤层。airlock.ts 使用 Unicode NFKC 规范化,移除零宽字符和标签字符,再检查八类指令模式,包括角色冒充、越权操作、编码载荷和多语言覆盖指令。命中后整份报告隔离,原始内容丢弃,工程 Agent 不接收任务。
通过检查的内容仍被标记为 UNTRUSTED REPORTER CONTENT,并脱敏个人信息、密钥,删除链接和 @mentions。原文只保留 SHA-256 哈希用于识别重复提交。规则只是启发式过滤,作者没有把它当成安全证明。
AI 分诊也没有决策权。服务端代理调用 Meta Model API 的 muse-spark-1.3,输出必须符合严格 JSON schema,并再次经过文本过滤。建议与正式严重程度、工作流状态分开存储,不能直接变成操作指令。
真正进入修复前,reproducer 先在没有 GitHub 凭证的情况下克隆公开 main,跑现有测试。基线已经失败,就交给人处理。基线通过后,模型只能编写 Vitest 回归测试。
下面这段测试证明,大额价格应当被拒绝:
import { describe, expect, it } from "vitest";
import { calculateTotal } from "@/lib/shoplite";
describe("ShopLite large price", () => {
it("rejects subtotal that cannot be safe integer cents", () => {
expect(() => calculateTotal(1e308, 1, "", false)).toThrow();
});
});
关键在 toThrow()。Number.isFinite(1e308) 为真,但乘以 100 后溢出为 Infinity,旧实现没有抛错,因而出现了真实断言失败。
系统要求退出码为 1、没有超时、输出含 AssertionError,且有完整测试文件结果。语法错误、导入失败都不算复现。第二次模型调用会检查测试是否符合报告者预期,但仍只是建议。
随后状态进入 awaiting_reporter,由原始报告者确认「这是不是你遇到的问题」。校验写在领域引擎里,前端无法绕过。回答否,就重新复现,旧测试和旧 PR 不再重放。
测试证明代码存在某种行为,报告者确认它是否对应自己的问题。
AI 可以交补丁,但不能替人批准
工程侧有 manager、reproducer、fixer、reviewer 四个 Switch 身份,每个 Bug 一个私有房间。任务交接要核对经过认证的发送回执,包括事件 ID、定向消息标记和完整任务文本。
fixer 的自由度很小:返回 JSON 格式的完整文件内容,不返回 shell 命令;只允许修改一到两个指定路径,每个最多 24,000 字符,拒绝符号链接和路径穿越。已经确认的失败断言原样复制,不能修改。
生成代码在独立 Docker 容器里执行,没有网络、凭证和可写根目录。容器使用 --network none、--read-only、--cap-drop ALL,再配合非 root 用户、资源限制和只读代码快照。补丁必须通过回归测试、原有测试、Prettier、ESLint 和 TypeScript,最多尝试两次,之后交给人。
模型给出的修复,是在金额计算后检查安全整数:
// src/lib/shoplite.ts
const subtotal = Math.round(price * 100) * quantity;
if (!Number.isSafeInteger(subtotal))
throw new Error("Use a valid price and a quantity between 1 and 99.");
要看第二行:输入是有限数,并不能保证计算结果仍是有效金额。这里验证的是以分为单位的最终小计。
该检查点的 113 个单元测试全部通过,服务随后打开 PR #4,只修改实现和新增测试。报告者确认后,生成经过测试的草稿 PR 用了 67 秒。
接下来,Agent 就没有继续操作的权力了。人批准的是确切 head SHA,新提交会清空批准;合并者必须是 GitHub User,不能是 bot,且至少存在一次人工批准。
风险路径还会升级门槛:文档是 tier 0,普通源码是 tier 1;auth、payments、billing、migrations、.github、security 属于 tier 2,需要两个不同的人通过 GitHub review 批准,其中一个是 code owner。
这不是写在提示词里的约定。工程服务的独立 bearer token 无权代替报告者确认、批准、合并或记录部署。生产验证得到的三次拒绝分别是 401 / 401 / 403。
合并、上线、用户确认,是三件不同的事
Vercel 构建完成,系统也不会立刻通知「已修复」。
独立 recorder 只在 main 的 CI 成功后运行,检查完整 commit SHA、项目、团队、production 目标和 READY 状态。不可变部署 URL、自定义域名都必须通过 /api/health 检查,Vercel alias API 还要证明域名确实指向同一个部署。
随后创建任务为 verify:vercel-production 的 GitHub deployment,由 GitHub 签名 webhook 驱动状态变化。账本再调用 GitHub compare API,只有 ahead 或 identical 才接受,确保部署包含这个 Bug 的合并提交。
这次运行中,自定义域名的 11/11 个 JavaScript、CSS 资源与不可变版本匹配。人工合并后,验证上线用了 100 秒,也就是 1 分 40 秒。
最终验收仍需分清证据类型:自动验收程序从已核验的生产版本提取源码,确认旧版本返回 Infinity,新版本抛出校验错误。线上 ShopLite 使用固定价格,因此溢出验证发生在生产源码的模块层,没有通过浏览器完成。
「报告者」也是团队自己的自动程序,并非真实客户。它使用原始私有链接确认后,状态才从 deployed 变为 verified。回答否则重新打开问题,清空全部确认,再次进入复现流程。
状态和通知靠同一份账本衔接。Neon Postgres 的 r2r_ledgers 为每个 workspace 保存一个带版本号的 JSON 文档,写入采用 compare-and-swap,冲突后重新读取并应用变更,最多重试 20 次。
通知任务与状态变化在同一事务写入 outbox。worker 使用 60 秒租约领取任务,失败指数退避,最长一小时。这保证的是至少一次投递:发送后、保存回执前崩溃,消息可能重复。
我的判断
我认可它把证据、权限和状态分开处理。常见的「给 Agent 仓库权限,再要求它谨慎操作」,很难保证模型不会越界;这里让令牌根本没有批准和合并能力,约束更具体。
但效率数字只能按演示理解。2026 年 11 月 7 日这次受控运行,从失败测试到核验发布用了 1 小时 44 分,其中 84 分钟在等待人工审查。合并提交通过了 166 项检查:124 项 TypeScript、21 项 Python、3 项 Docker、18 项浏览器检查。没有对照组,不能据此推导比人工修复快多少。
自动修复也只覆盖 src/lib/shoplite.ts 和每个 Bug 的测试文件。跨服务问题、复杂交互问题尚未得到验证。账本接收授权工程服务提供的测试证据,不会独立重跑;reviewer 与其他角色使用同一家模型提供商,也不构成独立审查。
部署后,main 和检出历史还发生过重写:合并 SHA 从 d81ba99 变为 3970b80,修复父提交从 496f9aa 变为 45624dc,文件树相同。作者保留了原始观察记录。它提醒我们,提交身份也是证据的一部分。
此外,共享 owner key 没有个人账号、SSO 或 MFA,审计记录没有密码学防篡改,多区域容灾也未实现。一个 Postgres 文档加单台 AWS 主机,适合验证流程,团队扩大后仍需重新评估。
能拿来做什么
- 给 AI 修复 Node 或前端工具函数设一个起点:先跑通基线,再提交能稳定失败的回归测试。不要允许模型通过修改断言来完成修复。
- 让 AI 输出指定文件补丁,并把执行环境、可写路径、尝试次数和测试要求写成程序约束;批准与合并使用独立的人类权限。
- 在缺陷跟踪里区分「已合并」「已上线」「已确认有效」。涉及发布时,同时核验提交身份、域名指向和健康检查,避免通知早于真实上线。
- 本地体验需要 Node 22 和 npm,依次运行
npm ci、npm run setup、npm run dev。用npm run demo:reproduce观察优惠券案例以退出码 1 失败,再用npm run demo:reproduce -- --fixed验证退出码 0;没有DATABASE_URL时,本地文件账本只支持一个 Node 进程。