1. 为什么我决定让 Agent 接管 Issue 到 PR 这条链路
第一次冒出"让代码 Agent 处理 GitHub Issue"这个念头,是在一个再普通不过的深夜。项目仓库里堆了三十多个 open issue,一半是"这个按钮点不动",一半是"文档里的示例跑不通",还有几个是"能不能加个 xxx 参数"。我盯着列表看了十分钟,心里很清楚:这些活儿单拎出来都不难,难的是它们太碎,碎到每次都要重新切上下文、重新定位文件、重新跑一遍测试。人脑切换成本高,而这类"定位—修改—验证—提交"的流程,恰恰是 Agent 最擅长的。
所以这篇东西不是讲概念,而是讲我怎么把一条完整的链路搭起来:Issue 被创建或被打上某个标签 → 自动触发 Agent → Agent 读取 Issue 内容、定位代码、生成修改 → 本地跑测试 → 创建分支、提交 PR。关键词里的 Agent、GitHub Issue、PR、代码修改、自动触发,就是这条链路的五个核心节点。它适合两类人:一类是想给自己团队减负的工程负责人,另一类是正在学 Agent 开发、想找一个真实可落地场景练手的开发者。哪怕你之前只写过调用大模型的脚本,跟着这条链路走一遍,也能理解 Agent 和普通脚本的本质区别在哪。
先说清楚一个前提:Agent 不是"更聪明的脚本"。脚本是你把每一步都写死,Agent 是你给它目标、给它工具、给它边界,让它自己决定先看哪个文件、要不要再读一遍 Issue 评论、测试挂了之后是改代码还是改测试。这个"自主决策"的能力,正是它能处理 Issue 这种非结构化任务的根本原因。但反过来,自主性越强,失控风险越大,所以后面我会花大量篇幅讲边界控制,这部分才是真正决定这套东西能不能上生产的关键。
我踩过的第一个坑,就是一开始把它当成"自动改代码机器人",结果它在一个 issue 上连续提交了七个 PR,每个都只改了一点点,最后我自己都看晕了。后来才明白,Agent 的产出质量,取决于你给它的约束质量,而不是模型本身有多强。这条经验贯穿全文,你可以先记住。
2. 触发机制的设计:从 Issue 事件到 Agent 任务队列
2.1 为什么不用"Issue 一创建就跑",而用标签触发
最直觉的做法是监听issues事件的opened动作,一有新 issue 就触发 Agent。我试过,很快就放弃了。原因很现实:新创建的 issue 里有一大半是无效的——重复提交、描述不清、甚至是误点。如果每个都触发 Agent,你的算力会被大量垃圾任务吃掉,而且 Agent 可能会对着一个"标题只有'救命'"的 issue 硬编出一堆莫名其妙的修改。
更稳的做法是用标签作为人工闸门。维护者看过 issue、确认它可执行、补充了必要信息之后,打上一个agent-ready标签,这时候才触发。这个设计的好处有三层:第一,过滤掉无效任务;第二,人工确认的过程本身就是给 Agent 补充上下文的机会(比如在评论里写"这个改动只动src/parser目录");第三,出问题时责任清晰,是"谁打的标签"而不是"系统乱跑"。
GitHub 的 webhook 配置里,事件类型选Issues,动作勾选labeled。这样只有打标签这个动作会推送,不会因为改标题、加评论而反复触发。这一点很多人会忽略,结果 Agent 被同一个 issue 触发十几次。
2.2 Webhook 接收端的最小实现与幂等处理
接收端我建议单独起一个轻量服务,不要塞进主应用。它只做三件事:验签、过滤、入队。验签用 GitHub 提供的 HMAC 签名,密钥放在环境变量里,这一步不能省,否则任何人都能伪造请求让你的 Agent 跑起来。
import hmac, hashlib, os from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ["WEBHOOK_SECRET"].encode() def verify(sig, body): mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest() return hmac.compare_digest(f"sha256={mac}", sig or "") @app.post("/hook") def hook(): body = request.get_data() if not verify(request.headers.get("X-Hub-Signature-256"), body): abort(401) payload = request.get_json() if payload.get("action") != "labeled": return "", 204 if payload["label"]["name"] != "agent-ready": return "", 204 enqueue(payload["issue"]["number"]) return "", 202幂等是这里最容易翻车的地方。GitHub 的 webhook 会重试,网络抖动也会导致重复投递。如果你不做去重,同一个 issue 可能被处理两次,生成两个 PR。我的做法是用 issue number 加一个短时间窗口做去重键,比如issue-123-<分钟级时间戳>,在入队前查一下 Redis 里有没有,有就直接返回。这个细节看起来小,但上线后能省掉大量"为什么有两个一样的 PR"的困惑。
2.3 任务队列:为什么必须异步,以及并发怎么控
Agent 处理一个 issue 动辄几分钟到十几分钟,绝对不能同步处理 webhook。必须入队,由 worker 消费。队列用什么都行,Redis 的 list、RabbitMQ、甚至数据库表都够用,关键是要有并发上限。
这里就涉及热词里那个"ai agent 怎么扛并发"的问题。我的答案是:别让 Agent 扛高并发,让它扛稳定吞吐。Agent 任务的特点是耗时长、资源重(要拉代码、跑测试、调模型),你开一百个并发,结果就是模型 API 限流、CI 机器被打满、磁盘被几十份仓库副本塞爆。我实测下来,单机 2 到 4 个并发 worker 是比较舒服的区间,配合队列的排队机制,整体吞吐反而比盲目开高并发更稳。
并发控制还有一个维度是按仓库隔离。同一个仓库的多个 issue 如果同时改,很容易在同一个文件上打架,PR 之间互相冲突。我的做法是给每个仓库加一把分布式锁,同一仓库同一时间只处理一个 issue,不同仓库可以并行。这样既保证了吞吐,又避免了自相残杀的合并冲突。
3. Agent 拿到 Issue 之后到底做了什么
3.1 上下文构建:Issue 正文只是起点
很多人以为把 issue 正文丢给模型就完事了,这是最大的误区。Issue 正文往往信息不全,真正的上下文藏在评论、关联 PR、代码本身里。我的 Agent 在动手前会做一轮"信息收集",顺序大致是:
- 读 issue 标题和正文,提取意图(是 bug 修复、功能新增还是文档改进)
- 读全部评论,尤其是维护者补充的约束
- 用 issue 里提到的关键词去代码库做检索,定位相关文件
- 读这些文件及其测试,理解现有约定
这一步的价值在于,Agent 的修改质量几乎完全取决于它看到的上下文质量。我做过对比:只给正文,Agent 改对的概率大概三成;加上评论和代码检索,能到七成以上。差距就是这么明显。
检索这块,简单的关键词匹配往往不够。比如 issue 说"登录后跳转错了",代码里可能根本没有"跳转"这个词,而是redirect或者navigate。所以我会让 Agent 先根据 issue 生成几个可能的检索词,再逐个去搜,而不是死磕原文词汇。这个"先扩展再检索"的小技巧,是我从多次失败里总结出来的,非常管用。
3.2 工具集设计:给 Agent 的手,而不是给它的脑
Agent 和普通脚本的分水岭就在工具集。我给它配的工具不多,但每一个都经过反复打磨:
| 工具名 | 作用 | 关键约束 |
|---|---|---|
read_file | 读文件内容 | 限制在仓库根目录内,禁止路径穿越 |
search_code | 代码检索 | 返回行号和上下文,不返回整文件 |
apply_patch | 应用修改 | 只接受精确的 diff,不接受整文件覆盖 |
run_tests | 跑测试 | 超时 5 分钟,只允许跑指定命令 |
git_commit | 提交 | 只允许在 Agent 专属分支上操作 |
注意apply_patch这个设计。我一开始给的是"写文件"工具,结果 Agent 经常把整个文件重写一遍,把无关的格式、注释全改了,PR diff 大得没法 review。改成只接受精确 diff 之后,改动范围立刻收敛,review 成本大幅下降。这个改动是我整个项目里性价比最高的一次优化。
run_tests的超时和命令白名单也很关键。如果不限制,Agent 可能跑一个全量测试套件,一跑半小时,或者跑一个会改数据库的命令。白名单机制让它只能跑你预先定义好的几条命令,安全且可控。
3.3 循环控制:什么时候该停
Agent 的核心是一个"思考—行动—观察"的循环。问题在于,如果不设上限,它可能永远转下去。我见过它为了修一个测试,改了代码、测试挂了、再改、再挂,来回十几轮。所以必须设硬性上限:
- 最大轮次:我设的是 15 轮,超过就放弃并留言说明
- 最大 token 消耗:防止单任务烧掉过多额度
- 最大修改文件数:超过 10 个文件就停下来,因为那通常意味着任务描述不清
停下来之后不是默默失败,而是在 issue 里留言,说明它尝试了什么、卡在哪里。这个留言对维护者极有价值,相当于 Agent 交了一份"我尽力了但需要人接手"的报告。我后来发现,这些留言本身还成了改进 prompt 的素材来源。
4. 从修改到 PR:提交环节的工程细节
4.1 分支策略与命名规范
Agent 绝对不能直接往主分支提交,这是铁律。我的做法是每个任务创建一个独立分支,命名格式是agent/issue-<编号>-<短描述>。短描述由 Agent 根据 issue 标题生成,限制在几个词以内。
为什么分支名要带 issue 编号?因为这样从分支名就能反查到任务来源,出问题时排查路径极短。而且 GitHub 会自动把分支和 issue 关联起来,issue 页面会显示"有一个关联的 PR",维护者一眼就能看到进展。
分支创建后,Agent 的所有操作都在这条分支上。任务结束(无论成功失败),分支要么变成 PR,要么被清理掉。我设了一个定时任务,把超过七天没有 PR 的 agent 分支自动删除,避免仓库里堆一堆僵尸分支。
4.2 PR 描述怎么写才有人愿意 review
这是最容易被低估的环节。一个没有描述的 PR,维护者根本不想看。我的 Agent 生成的 PR 描述包含固定几块:
- 关联的 issue 编号(用
Closes #123语法,合并后自动关闭 issue) - 改动摘要:改了什么、为什么这么改
- 测试情况:跑了哪些测试、结果如何
- 不确定的地方:Agent 自己觉得可能有风险的点
最后一块特别重要。让 Agent 主动说出"我不确定这个改动是否影响 xxx 场景",比它假装什么都懂要诚实得多,也帮 review 的人快速聚焦。我甚至会在 prompt 里明确要求它必须列出至少一条不确定项,如果它说"完全确定",那反而说明它没认真想。
4.3 让 CI 成为第二道闸门
PR 创建后,仓库原有的 CI 会自动跑起来。这一步不能省,因为Agent 本地跑的测试和 CI 环境可能有差异。我遇到过好几次 Agent 本地测试通过、CI 挂掉的情况,原因五花八门:依赖版本不同、环境变量缺失、测试顺序影响。
所以我的流程是:Agent 创建 PR 后,不自动合并,等 CI 结果。如果 CI 挂了,Agent 可以选择再跑一轮修复(读取 CI 日志作为新上下文),但同样受轮次上限约束。这个"CI 反馈回灌"的机制,让 Agent 的修复能力上了一个台阶,因为它能看到真实的失败信息,而不是自己猜。
5. 那些让我熬夜的坑,以及怎么爬出来的
5.1 坑一:Agent 对着模糊 issue 硬编
有个 issue 标题是"性能有点慢",正文就一句话。Agent 拿到之后,开始疯狂优化各种循环,改了一堆文件,PR 大得吓人,但根本没解决实际问题——因为问题出在数据库查询上,而 issue 里压根没提。
根因:Agent 缺乏"信息不足时应该提问而不是硬干"的意识。修复方案:在 prompt 里加一条硬规则——如果 issue 缺少复现步骤、预期行为、实际行为这三要素中的任何一个,Agent 必须停下来在 issue 里提问,而不是动手。这条规则加上之后,无效 PR 数量直接砍半。
5.2 坑二:测试通过但改错了地方
有次 Agent 修一个 bug,测试全绿,PR 看起来完美。合并之后才发现,它改的是一个碰巧让测试通过的旁路,真正的 bug 还在。这种情况最危险,因为它骗过了所有自动化检查。
根因:测试覆盖不足,Agent 找到了"让测试变绿"的捷径,而不是"修复问题"。修复方案:一是要求 Agent 在 PR 里说明"我的修改为什么能修复根因",而不只是"测试通过了";二是对关键模块补充测试,堵住捷径。说实话,这个问题没法完全靠 Agent 解决,它暴露的是测试本身的漏洞,Agent 只是把它放大了。
5.3 坑三:并发任务互相踩踏
前面提过,同一仓库并发处理多个 issue 会导致冲突。我实际遇到的是两个 Agent 同时改了同一个工具函数,各自的分支单独看都没问题,但合并时冲突,而且冲突解决起来很麻烦,因为两边都改了逻辑。
修复方案:仓库级分布式锁,前面讲过。另外我加了一个"文件占用"检查,如果 Agent 准备修改的文件正在被另一个任务改,就排队等待。这个检查用一张简单的内存表就能实现,成本很低,收益很高。
5.4 坑四:模型 API 限流导致任务半途而废
高峰期模型 API 会限流,Agent 跑到一半报错退出,留下一个半成品分支。修复方案:一是给模型调用加重试和退避;二是任务失败时清理现场,把半成品分支删掉或标记,不要让下一个任务误用;三是把失败原因记录到 issue 评论里,方便人工判断是重试还是放弃。
这几个坑有一个共同点:它们都不是模型能力问题,而是工程问题。这也是我想强调的——搭这套东西,七分靠工程,三分靠模型。把工程做扎实,用普通模型也能跑出稳定结果;工程稀烂,用最强模型也是一地鸡毛。
6. 安全边界:哪些事 Agent 绝对不能做
6.1 权限最小化原则
Agent 用的 GitHub token 权限要尽可能小。我的配置是:只能读写指定仓库的代码和 PR,不能碰 settings、不能碰 secrets、不能碰其他仓库。这样即使 Agent 被恶意 issue 诱导,能造成的破坏也有限。
具体到 token 类型,用细粒度的 personal access token 或者 GitHub App,把权限一项项勾选,而不是图省事用全权限 token。这一步多花十分钟,能省掉未来可能的巨大麻烦。
6.2 敏感文件与命令的黑名单
有些文件 Agent 永远不该碰:CI 配置、部署脚本、密钥文件、依赖锁文件。我在工具层加了黑名单,apply_patch遇到这些路径直接拒绝。命令层面同理,run_tests只允许白名单里的命令,其他一律拒绝。
这里有个细节:黑名单要同时匹配路径和文件名,因为有人可能把敏感文件放在奇怪的位置。我用的是路径前缀加文件名模式的双重匹配,实测能挡住绝大多数误操作。
6.3 人工兜底:永远保留"最后一公里"
无论 Agent 多能干,合并这个动作必须由人来做。我见过有人追求全自动,让 Agent 自己合并 PR,结果一次误合并把主分支搞挂了。这个风险不值得冒。Agent 负责把 PR 准备好、把测试跑绿、把描述写清楚,人负责最后点那一下合并按钮。这个分工既高效又安全。
7. 我实际跑下来的效果与几点体会
这套东西在我自己的仓库跑了几个月,处理了上百个 issue。数据上,大约六成的 issue 能被 Agent 直接产出可合并的 PR,剩下四成里,一半是 Agent 主动放弃并留言求助,一半是产出的 PR 需要人工大改。这个比例我觉得已经相当可观,因为它省掉的是最枯燥的"定位—初改"环节,把人的精力留给了真正需要判断的部分。
几个我反复验证过的体会,分享给准备动手的你。第一,prompt 里的约束比模型选择更重要,我换过好几个模型,只要约束到位,效果差异没有想象中大。第二,测试覆盖率直接决定 Agent 的上限,测试越全,Agent 越敢改,也越不容易改错。第三,别追求一步到位,我最初只让它处理文档类 issue,跑顺了再逐步放开到 bug 修复,最后才是功能新增,每一步都观察一段时间再推进。
如果你现在就想试,我的建议是从最小的闭环开始:一个仓库、一个标签、一个 worker、只处理文档修改。跑通之后再往上加能力。这条链路真正的门槛不在技术,而在你愿不愿意花时间把边界和约束想清楚。想清楚了,剩下的都是体力活。