作者分享如何用 AI Agent 在每晚自动扫描、提交和合并代码维护 PR,包含分级授权、预算上限和紧急杀死开关的设计。深入讲述了大规模自动化的工程实践与安全考量。
我一个人维护着大约二十个生产和支持类 repo——一个带有精选数据源的内容网站、几个定时 scraper、一些 Cloudflare Workers,以及围绕这些项目逐渐堆积起来、处于半休眠状态的常见工具。这里没有运维团队。一个人维护的技术资产,其维护债务不会主动跳出来提醒你;它只会悄无声息地不断累积,直到某一天,你突然需要那个一直没有维护的东西。
于是,我造了一个机器人来负责维护,而且——这正是人们反应最大的地方——我给了它 merge 权限。
每天午夜,一个 GitHub Actions job 会自动唤醒,检查我组织中的每一个 repo,并尽可能修复问题:创建 pull request、自行 merge、为任何需要人工处理的事项创建 issue,最后给我写一份报告。我把这个 repo 命名为 make-me-better,这大概已经说明了我如何看待自己的自律能力。
在你关掉这个标签页之前,先听我说完:它受到预算、分级制度和 kill switch 的约束。这三样东西就是本文的全部重点。Agent 本身反而是最简单的部分。
prompt 将允许它执行的操作分成三个层级:
Tier 1——机械性操作。补齐缺失的配置、添加 memory 目录,以及只有一种显而易见正确写法的 boilerplate。它可以自行创建并 merge 这些变更。
Tier 2——需要判断的操作。任何涉及业务逻辑的内容都只能创建 issue,不能创建 PR。prompt 中的规则是:“绝不能猜测业务逻辑。”
Tier 3——自我修改。允许它修改自己的代码——包括观察脚本、workflow,甚至它自己的 prompt。每晚最多一次。
除了分级制度,还有硬性上限:每晚最多三个修复 PR、三个 issue,以及一次自我修改。它绝不会删除任何东西,也绝不会 force-push。kill switch 则被刻意设计得非常简单:在 repo 根目录提交一个名为 PAUSE 的文件,它就会停止。我不想使用 dashboard,也不想设置一个凌晨两点出问题时还得费劲回忆放在哪里的配置开关。
还有一个设计决策很快就证明了自己的价值:prompt 采用“报告优先”原则。即使运行在中途失败,报告也会照常写入并提交——commit 步骤的运行条件是 if: ${{ !cancelled() }}。即使某次运行崩溃,它仍会告诉你自己在崩溃之前看到了什么。当你调试一个会在你睡觉时工作的机器人,残缺的证据也胜过一座沉默的坟墓。
第一个晚上,简直像是在逐层剥开我自己判断失误的全过程。
Run 1 死在了观察脚本里一个由我亲手写下的 shell bug 上。我到处都这样写:
last_commit=$(gh api "repos/$OWNER/$r/commits?per_page=1" -q '...' 2>/dev/null || echo "")
看上去没问题。其实并不是。如果 gh api 在失败之前输出了一部分 JSON 错误响应——比如空 repo 返回的 409——那么 || echo 不会用 fallback 替换这部分输出,而是把 fallback 追加在这部分输出后面。最终得到的是 {"message":"Git Repository is empty."} 与 fallback 拼接起来的内容:它既不是有效的 JSON,也不是你预期的 fallback,所有下游处理都会因此崩溃。正确做法是先赋值,只有在 command substitution 本身失败时才设置 fallback:
last_commit=$(gh api "..." -q '...' 2>/dev/null) || last_commit=""
从上世纪九十年代开始,我就断断续续地写 shell 脚本。结果我还是把这种代码发布了出去。而且是在同一个文件里,分别写了三次。
Run 2 在 token exchange 环节失败了——该 action 试图使用一个我并未配置的 OIDC flow。显式传入我的 PAT 后,问题解决了。
Runs 3 和 4 才是有意思的部分:Agent 完美地完成了运行,写出了一份完整报告,却无法将它 push 上去。403。我的 fine-grained PAT 对几乎所有内容都只有只读权限——创建它时,我故意把权限收得很紧,后来却忘了这回事。这里还有一个值得记录的判断错误:当 Agent 随后尝试修改自己的 workflow 文件时,基于多年使用传统 GitHub token 的经验,我“知道”它需要 workflow scope,于是开始到处寻找这个 scope。但 fine-grained PAT 根本没有 workflow scope。它通过一个名为 Workflows 的 repository permission 来控制 workflow 文件的写入权限——而且修改 fine-grained PAT 的权限不会改变 token 值,因此不需要去任何地方重新粘贴 token。二十五年积累下来的知识确实是一笔宝贵资产,除了其中那些已经悄无声息地过期的部分。
第一个晚上,这个 job 还六次撞上了自己的 turn 上限,而 GitHub 每次都尽职尽责地重启了它。每次重启后,它只能通过一种方式检查自己是否仍在当晚的额度之内:统计当前处于 open 状态的修复 PR。但已经 merge 的 PR 会从这个列表里消失。因此,每个新实例看到的都是一张白纸。到了早上,当晚累计产生了四个修复 PR 和两次自我修改——两项都超过了上限。它 merge 的内容没有任何错误。但预算机制仍然没有完成自己唯一的使命:它要限制的是一整晚,而不是单次运行。崩溃后会重置的上限根本不叫上限,只能算建议。
接下来发生的事情,正是这篇文章存在的原因。Agent 在自己的报告中诊断出了额度追踪的漏洞,写出了修复方案——通过 search 累计统计当晚的 PR,而不是查询 open 列表——然后拒绝实施这项修复,因为它当晚的自我修改预算已经用完。它标记了这个修复项,然后停了下来。
一个系统发现了自身治理机制中的缺陷,却不愿在没有预算的情况下修复它,说明它的预算确实具有约束力。
连续平稳运行三个晚上后,这个循环变成了这样:某一晚的报告写道:“值得在未来修改:跳过 archived repo,反正它们也无法接收 PR。”到了第二天晚上,唯一的自我修改名额恰好就被用于实现这项修复——通过 bash -n 验证后完成 merge。报告甚至会告诉下一次运行需要验证什么:确认那四个 archived repo 是否真的从检查结果中消失了。
在这个过程中,它修复了一个连我自己 code review 时都漏掉的代码 bug;发现自己的某项指标一直在悄悄测量错误的东西(变量名写的是“最后一次 memory 更新”,但 API query 实际查询的却不是这个);还在发现五个所谓“需要处理的 repo”其实已经 archived 或 deprecated 后,主动调低了自己的 backlog 统计——这个多算出来的数字,还是它自己前一天晚上引入的。它会主动标记自己产生的坏数据。我管理过的大多数人,都需要经过指导才会这样做。
没过几天,我本地的八个 clone 就落后于 remote 了。这当然会发生:机器人每天晚上都会向 GitHub commit,却根本不知道我的笔记本电脑存在。我过去的每一种 workflow 习惯,都建立在“我是唯一写入者”这个前提之上。现在出现了第二位永不睡觉的作者,于是在动手修改任何东西之前先执行 git pull,已经从一种良好习惯变成了生存必需。如果你也要构建这样的系统,记得告诉你的团队——哪怕你的团队和我一样,只有你自己。
上限必须按每晚计算,而不是按单次运行计算——并且要从一个能经受崩溃与重启的数据源中统计。
对自治能力进行分级。机械性变更可以获得 merge 权限;需要判断的事项只能创建 issue;自我修改的预算则限制为一次。
报告优先。一次失败的运行如果留下了部分报告,就仍然可以调试。一次什么都没留下的失败运行,只能靠招魂。
kill switch 不应包含任何会出问题的活动部件。只需要一个名为 PAUSE 的文件。人在压力之下,根本记不住更聪明的设计。
让它自己写 postmortem。本文中的每一个 bug,实际上都是在每晚报告中被发现的——包括它自己的 bug。
还有一个判断错误,没有对应的 diff。就在我构建这一整套系统期间,有三份已经完成、而且能带来报酬的 pitch,在我的草稿箱里躺了整整三十五天,随时可以发送。这个自动化系统确实值得构建——但我构建它,也有一部分原因是:打磨一个机器人让人感觉很舒服,而开口向一个真人要钱却不是。现在,这套技术资产每晚都会自我修复。没有人替你造出那个帮你按下发送按钮的 Agent,因为那个 Agent 必须由你亲自运行。
我是一名云与基础设施架构师——拥有 25 年企业级项目经验,现在经营着一家个人咨询公司。如果你的团队需要一个已经见识过你们所遇问题之企业级版本的人,可以通过 jennifer@jenatech.io 联系我。
如果需要采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。