你的编程 Agent 开启的那个导致真实 PR 被关闭的拉取请求
你的编程智能体在周二下午 3:14 提交了一个 PR。PR 描述很整洁,代码差异(diff)很小,CI 测试也是绿色的。二十分钟后,它被压缩合并(squash-merged)了。第二天下午 1:20 吃完午饭回来的同事看到了一条通知:“PR #1247 已关闭。”不是已合并,而是已关闭。分支不见了。她上周留下的 72 条评审评论也消失了——全部折叠在一个“已过期”标签下,属于一个不再出现在任何活跃列表中的 PR。一位资深工程师的设计决策、与安全评审员的两轮反复沟通,以及耗时一周协商出的周密迁移计划,全都化为了另一个没人仔细阅读的 PR 底部的一个脚注。那个压缩提交(squash commit)留下的唯一痕迹是底部的一行标签:Closed by #1893。
这就是信任编程智能体自行编写 PR 元数据的失败模式。出问题的不是代码,而是元数据。代码差异没有问题,智能体工作得很出色。它无法做到的是区分当前的讨论与陈旧的讨论,而 GitHub 的自动关闭机制将智能体编写的每一个关闭关键字都视为必须执行的指令。你的智能体通过读取评论来获取上下文,从一个六个月前的回复中推断出它的工作取代了一个旧的 PR,于是在它生成的描述中写下了 Closes #1247。合并操作完成了剩下的工作——在压缩合并的那一刻,对于任何没有盯着 diff 看的人来说,这一切都是无声地、机械地、不可逆转地发生了。
智能体在不理解的情况下挥舞的关键词
GitHub 的自动关闭机制是大多数工程师从未深思过的功能,因为它已经成为了肌肉记忆:在 PR 描述中输入 Closes #42,合并,然后看着 issue 关闭。文档列出了九个规范的关键词——close、closes、closed、fix、fixes、fixed、resolve、resolves、resolved——规则听起来很简单:PR 必须以默认分支为目标;关键词必须位于 issue 或 PR 引用之前;合并必须是真实的合并,而不是未合并的关闭。当这三个条件都满足时,链接的项目会在合并完成的瞬间自动关闭。
让这种机制在编程智能体手中变得危险的原因在于,关键词仅仅是文本。没有单独的确认,没有预览,也没有“你确定要关闭 PR #1247 吗?”的对话框。关键词仅凭被写下来就能执行。生成 PR 描述的智能体无法验证 #1247 是否真的被取代了;它只能处理接收到的输入——通常是一连串评论、issue 主体以及用户在提示词中输入的内容。如果在这些输入中的任何地方,之前的某个人写过“让我尝试一种不同的方法,我会用一个新的 PR 来取代这个”,智能体就会在生成的描述中忠实地编码这一意图,即使这种取代从未真正发生,而且原始的 PR 才是获得批准的那一个。
GitHub 原生的关键词名义上是关闭 issue,而不是 PR。但同样的惯例已通过社区工作流移植到了 PR 的替代机制中——其中安装最广泛的工作流会扫描已合并 PR 的描述,寻找 supersedes #N 字符串并关闭引用的 PR。一旦你的仓库中有了这个工作流,你实际上就赋予了每个 PR 作者(包括你的智能体)只需写下一句话就能关闭任何其他 PR 的权力。该操作不会检查被关闭 PR 的作者是否同意,不会检查被关闭的 PR 是否属于另一个团队的队列,也不会检查代码差异是否真的相关。它只检查关键词,然后执行。
被误读为当前意图的陈旧上下文
智能体的输入就像一堆乱草。它是 issue 描述、评论历史、链接的 PR、之前的对话记录,以及某人在六个月前粘贴的路线图文档的内容。编程智能体擅长阅读大规模的上下文,但却不擅长辨别它们的时间。十月份的一条评论说“现在这已经没意义了,我会把它纳入下一次架构调整”,在十一月份读起来就像是一个新鲜的想法;在二月份读起来则是陈旧的历史;但智能体并不知道二月就是二月。它读取的是 Token。每个评论的时间戳是智能体提示词模板通常会压平或忽略的元数据。对于模型来说,这条评论就在那里,存在于上下文中,与十分钟前发布的评论没有区别。
这与智能体在其他地方遇到的 失败模式相同——记忆不知道世界已经发生了变化,操作手册(runbooks)编码了现在已经修复的变通方法,评估案例(eval cases)认证了已经不再交付的产品范围。陈旧的上下文是一个大的类别,而“触发了陈旧自动关闭关键词的陈旧评论”是其中最尖锐的表现之一,因为其后果不是一个用户可以通过重新提示(re-prompt)来纠正的错误答案。其后果是对已经处于进行中的共享状态的破坏性行为。长达一周的人工评审正是那种只存在于评论中的工作——它从未进入代码,因为它是在指导关于代码的决策,而决策不会出现在代码差异中。
解决办法不是要求智能体更加小心。智能体只会像输入允许的那样谨慎。解决办法是将自动关闭关键词视为一种不可逆的操作——尽管技术上它是可逆的——并在它面前设置一道不依赖于模型判断的关卡。
“已关闭未合并”标签是洪流中的微弱信号
在 2024 年,一位在周一早上审查 PR 队列的资深工程师会注意到一个带有两个审查者和 70 条评论的“已关闭未合并” PR。他们会问:这个 PR 怎么了?他们之所以会问,是因为当时的队列足够小,异常情况非常显眼。到 2026 年,平台的情况大不相同。GitHub 本身已经注意到,来自编程 Agent 的自动 PR 创建量激增,以至于不得不对内部管道进行 30 倍规模的重新设计。你团队的队列不再是人工编写的 PR 队列;它是一个由 Agent 开启的草案、Agent 开启的后续任务、Agent 开启的修复 PR 以及夹杂在其中的少量人工编写工作所主导的队列。关于“未合并即关闭的 PR”的信噪比已经崩溃。
在 2026 年,“已关闭未合并”标签意味着一百种不同的含义。它可能意味着 Agent 的 CI 从未通过,于是 Agent 关闭了自己的草案。它可能意味着 Agent 针对陈旧分支开启了一个重复的 PR,随后又将其撤回。它可能是一个从未打算合并的实验性 PR。而真正与人相关的案例——“这是一个包含真实评审工作的真实 PR,却违背作者意愿被关闭了”——现在成了大海捞针,而且它们在视觉上的处理方式完全一样。
恢复的故事在技术上是无害的,但在操作层面上是残酷的。GitHub 可以从已关闭 PR 的“恢复分支”按钮无限期地恢复已删除的分支,一旦分支找回,PR 就可以重新开启。但重新开启 PR 并不等同于重新开启讨论。锚定在已被替换的提交上的评审评论会被标记为过时。原本每次耗时两小时的 CI 运行不会自动重新触发。已经批准了旧版本的评审者不会自动批准重新开启的版本——必须再次请求他们评审,而这个请求是在他们已经切换到其他工作的上下文之后发出的。分支回来了,但那一周的时间却回不来了。
Agent 感知的 PR 管道究竟是什么样的
直觉上的修复方案是禁止 Agent 编写自动关闭关键词。这是错误的修复方式。自动关闭关键词很有用,Agent 不需要被禁止使用它们——它需要被限制在语法的安全子集中。需要管控的操作类别不是“Agent 编写 PR 描述”,而是“Agent 编写了对其他开启的工作采取破坏性动作的 PR 描述”。
一些具体的模式正在浮现:
- 在边界处剥离关闭关键词。 在 Agent 的 PR 描述发布之前,通过一个转换程序运行它,检测
closes #N、fixes #N、resolves #N以及supersedes #N。如果N不是 Agent 明确接收到的确认目标,则将其剥离。Agent 可以写“这项工作解决了 #1247 中提出的担忧”——这是一种引用,而不是指令——如果合适,评审员可以手动将其转换为关闭关键词。指令形式应该由人类发出;引用形式则是 Agent 可以安全发出的。 - 针对破坏性元数据的强制预览。 在 Agent 编写的 PR 上点击合并按钮之前,将合并时会关闭的问题和 PR 列表显示为一个复选框网格,人工作者必须逐一确认。这与 Stripe 针对不可逆操作的确认弹窗是同样的 UX 模式:技术上的动作保持不变,但同意界面强制人类阅读即将发生的事情。
- 陈旧上下文检测。 当 Agent 的 PR 描述通过编号引用另一个 PR 时,检查该 PR 最近活动的 Timestamp。如果该 PR 在过去七天内被积极评审过,则拒绝应用关闭关键词并显示警告。这并非完美的启发式方法——积极评审不代表仍然需要——但它将默认设置从“自动关闭一切可以关闭的”反转为“仅自动关闭看起来已废弃的内容”。
- 每日审计“已关闭未合并”列表。 将评论数超过阈值的“已关闭未合并” PR 视为一种告警信号。一个拥有 70 条评审评论且处于“已关闭未合并”状态的 PR 几乎绝不是常规的清理工作;它几乎总是意味着某人的工作成果丢失了。阈值可以根据每个仓库进行调整,但原则是:投入了人工精力的 PR 值得拥有与 Agent 开启的草案不同的警报。
- 对 Agent 编写的 PR 禁用 supersedes 操作。 如果你的仓库安装了
supersedes #N社区工作流,最简单的即时缓解方案是将其范围限定为带有human-authored(人工编写)标签的 PR。Agent 可以写这个词,但除非存在人工作者标签,否则工作流会忽略它。
这些模式都不需要新平台。它们只需要将 PR 描述视为 Agent 可以编写破坏性指令的地方,并将合并按钮视为这些指令在未经进一步审查的情况下执行的地方。
这种模式不仅限于 Pull Request
这种失败的形式是通用的,并不局限于 GitHub。任何 Agent 生成机器可读的元数据,而其他系统在没有人工干预的情况下据此采取行动的地方,都会遇到同样的问题。Agent 编写的触发过滤规则的邮件主题;Agent 编写的带有 closes JIRA-1234 标记的 Jira 任务描述;Agent 编写的带有 @here 提及并呼叫团队的 Slack 消息;Agent 编写的下游管道据此路由的数据库标签。其中的每一个场景,都是一个“编程 Agent 开启了一个 PR 并关闭了一个真实 PR”的潜在变体。
防御措施在每种情况下都是一样的。Agent 可以自由编写散文。它可以自由编写引用。除非人类确认了特定目标,否则它不能向其他系统发出指令。界限在于 Agent 生成的文本本身是否会在其他地方产生副作用。如果有,该文本就需要一个关卡。如果没有——如果它只是一个评论、一个引用、一个段落——Agent 就可 以无限制地生成它。
划定这条线会让你失去一些便利性。Agent 不再能一次性完成一个并关闭它所处理问题的 PR;人类必须点击链接或输入关键词。而你得到的,是“一周的仔细评审得以保留并合并”与“一周的仔细评审消失在没人阅读的 PR 脚注中”之间的区别。这个权衡的结果显而易见。
- https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
- https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
- https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-auto-closing-issues
- https://github.com/marketplace/actions/close-superseded-prs
- https://github.blog/ai-and-ml/generative-ai/agent-pull-requests-are-everywhere-heres-how-to-review-them/
- https://github.blog/changelog/2026-03-24-ask-copilot-to-make-changes-to-any-pull-request/
- https://docs.github.com/copilot/concepts/agents/coding-agent/about-coding-agent
- https://garrytrinder.github.io/2020/05/recover-pr-after-accidentally-deleting-source-branch
- https://github.com/orgs/community/discussions/37189
- https://github.com/orgs/community/discussions/17308
- https://www.qasource.com/blog/ai-generated-code-security-risks
- https://stackoverflow.blog/2026/01/28/are-bugs-and-incidents-inevitable-with-ai-coding-agents/
