这条在讲 Info Inlet 的作者把 agent 流水线自动化到合并生产,九天后凌晨两点的客户电话把他打回手工——发在 dev.to 上,10 月 7 号,标题《I let my AI agents merge to production. Once.》,那个“Once.”先记住。
我差点关了。前面几段讲他过去一年怎么自动化:一个 agent 写变更初稿,第二个 agent 审 diff,测试、类型检查、lint、staging 部署、对接近真实数据库的 smoke check,全自动。然后把最后一步合并也交了出去。他自己说那九天感觉像未来。我读到这儿以为是又一篇“AI 自动化一切很爽”的记录。
bug 成形那段我才决定推。一条写路径:代码先向客户端返回确认,再持久化数据库行。客户端收到确认、重试时写入没落库——系统对没发生的事做出了确信。这个 diff 是 agent 写的,审查 agent 批的。同一个 sprint 那个审查 agent 还发现过两个真实问题,但没发现这个。作者原话是「这个 bug 看起来像完成的工作」。这句我想抄下来。
五个自动 yes 全过了。测试全过因为只测 happy path。staging 正常因为没人在 staging 用错误毫秒重试请求。我读完这串把自家流水线过了一遍,也没有。
我读到一段他在拆“审查代码”和“拥有合并”。前者是判断正确性、惯用性、边界情况的技能,后者是对影响真实用户且不可逆的决定承担后果的责任。判断可以自动化,问责不能——agent 没法 on-call,没法道歉,也没法把凌晨两点那个电话带进下一次决策。他把“不能自动化”放对了位置:不是审查,是问责。我见过太多方向反着的。
代码审查是可逆的。合并到生产不可逆、面向外部。作者说许多团队保留人工审查而自动化合并,方向反了。他没往下展开太多,就一句:一次顺利的合并什么都产不出来,一次坏合并产出事故、postmortem、被锁的客户、电话。合并者坐在一个只有下行风险的位置上。偏偏这位置“看起来最容易”,所以最急着交出去。
我顺手把那个 bug 重放了一遍。写个小脚本模拟“先确认后落库”:
def write_key(key, value):
ack(key) # 客户端拿到 200
db.set(key, value) # 真正落库
在 ack() 和 db.set() 之间插 os._exit(1) 模拟崩溃,跑十次,三次客户端收到确认但库里没写进去。复现不复杂,但跑完发现一个事:它缺的不是更多场景覆盖,是一个专门把确认和落库拆开打碎的故障场景。我自己的 e2e 测试也一样,只断言最终响应,没断言“确认和落库之间发生了什么”。
作者的改法不回头。他澄清说不是主张每一步都人类在环,让人类监督每个生成 diff 只是表演。生成和审查仍然自动化,审查改得更严格。人只保留生产合并的签字权——不重新审 diff,看的是影响范围、谁会被 page、重试故事、凌晨两点风险。加了一条必答:每条写路径合并前,回答「如果客户端重试会发生什么」。就这一句。他说这问题会捕获那个 bug。我信。
评论区有 50 条 top comments,最值钱的来回是 Marcus Kim 和作者那段。Marcus 建议在提交边界附近中断写入、重发相同请求、检查持久状态而非响应。作者直接采纳了,修订规则:每条写路径合并前增加故障注入重试用例,断言持久状态而非响应。检查持久状态而非响应——这句话我打算直接抄进自家 agent 代码审查的 prompt 里。上礼拜我还在纠结审查 agent 该从哪个层面看 diff,这条给的方向比我自己试出来的清楚。
部署保持自动,部署的决定不自动。管道是机器人式的,发送到生产的那个不可逆外部步骤要有人署名。这条我是认同的。不是保守,是把人放对了位置——人不该去盯着 agent 写出来的每行 diff,那是表演性监督;人该坐的是那个只有下行风险的位置,出事了得能打通电话。
不点链接也能带走的一句:把你流水线里“看起来最容易自动化”的那个不可逆按钮找出来——合并到生产、发布开关、支付入口——先把“谁在凌晨两点负责这个决定”写清楚,再决定要不要交出去。agent 可以生成,可以怀疑,可以五层 yes 全过,但那个按钮背后得站着一个能被叫醒的人。
这条我会收进夹子里,标成“重试语义相关的推荐”下周再看一遍。作者后来没再更新,评论区 Marcus 那条被作者采纳之后也停在原处。如果要
