软件工程 Agent:从 Issue 到可审查 Pull Request
- Published on
软件工程 Agent:从 Issue 到可审查 Pull Request
“让 Agent 自动修 Issue”听上去很诱人。把 Issue 丢给模型,让它改代码、跑测试,再自动开一个 Pull Request,开发者只需要点合并就行。
但真正的工程工作并不是从第一行代码开始的。Issue 里可能有不完整的复现步骤,仓库可能正处于一次未完成的迁移,测试可能在主分支上本来就是红的,用户说的“修复”还可能和产品的真实预期不一致。如果 Agent 只追求把 PR 开出来,它完全可以生成一个形式完整、内容错误的变更。
我更愿意把软件工程 Agent 看成一个会主动收集证据的实习工程师:先读任务和规则,再确认现状,写出计划,做一小步改动,验证结果,最后把不确定的地方诚实交给人。它的终点不是“产生代码”,而是“产生一个人能够快速审查的改变”。
一、Issue 不是需求,先把它变成验收条件
一个 Issue 通常包含标题、描述、截图、评论和关联任务。Agent 不能只读正文第一段,应该先提取:
type IssueSpec = {
problem: string
reproduction?: string[]
expectedBehavior: string[]
constraints: string[]
relatedIssues: string[]
acceptanceCriteria: string[]
}
例如“搜索结果偶尔为空”至少要继续问:什么输入会触发?是所有用户还是特定租户?空结果是没有数据,还是请求超时后被当成空数组?修复后要保留什么行为?
如果 Issue 没有足够信息,Agent 可以先搜索日志、测试和相关代码,但不能自己编造产品规则。它可以提出一个明确的问题:“当前需求是显示空状态,还是需要自动重试?”这比猜一个方案然后让人审查整套错误实现更省时间。
二、先建立仓库基线
开始修改前,Agent 要知道当前工作区是否干净、当前分支是什么、项目如何安装和测试,以及主分支是否本来就存在失败。
仓库基线
├─ 当前分支和未提交修改
├─ 运行时与包管理器版本
├─ 相关模块的现有测试
├─ 基线测试结果
└─ 项目规则与贡献指南
这一点非常关键。假如测试在修改前就失败,修改后继续失败,Agent 不能把它描述为“本次改动导致测试失败”;反过来,如果基线是绿色,改动后出现红色,就必须认真定位,而不是删掉失败断言。
基线也应该作为 PR 的一部分记录:
修改前:pnpm test --filter search,通过 42 项
修改后:pnpm test --filter search,通过 45 项
数字不是全部,但它给审查者一个可以核对的起点。
三、工作区隔离比自动提交更重要
Agent 应该在独立分支或临时工作树中工作,避免覆盖开发者正在进行的修改。它可以读取当前工作区来理解代码,但写入范围必须明确。
type WorkspacePolicy = {
root: string
allowedPaths: string[]
forbiddenPaths: string[]
allowDependencyInstall: boolean
allowPush: boolean
allowMerge: boolean
}
默认允许修改目标模块和测试文件,禁止触碰密钥、生产配置、锁文件和无关目录。安装依赖、修改 CI、推送远程和合并 PR 都应该是独立权限,不要因为 Agent 能编辑本地文件,就自动给它更大的操作范围。
这里的原则很简单:Agent 的失败应该是一个容易丢弃的补丁,而不是一次影响整个仓库的操作。
四、计划要短,但必须能被验证
在开始写代码前,Agent 应该生成一份变更计划:
1. 在 searchService 中区分“没有结果”和“检索异常”。
2. 为异常路径增加可重试错误类型。
3. 在 SearchPanel 中展示重试按钮,不改变空结果状态。
4. 增加服务层测试和组件交互测试。
5. 运行类型检查、目标测试和构建。
计划里最好写出“不做什么”:不修改搜索算法,不调整后端接口,不进行无关重构。范围外声明不是形式主义,它会阻止 Agent 在实现过程中不断扩大目标。
如果实际探索后发现计划不成立,Agent 应该更新计划并说明原因。例如原来以为错误来自前端,结果发现请求封装已经把所有错误转成空数组;这时继续修改组件只是掩盖问题,正确做法是回到服务层重新定位。
五、实现阶段坚持最小补丁
每次改动都应该能对应一个验收条件。Agent 不需要为了展示能力而重写整个文件,尤其不要顺手升级框架、整理格式或改命名风格。
export function normalizeSearchResult(result: ApiResult) {
if (result.status === 'empty') {
return { kind: 'empty', items: [] }
}
+ if (result.status === 'timeout') {
+ return { kind: 'retryable-error', items: [] }
+ }
return { kind: 'success', items: result.items }
}
这个小补丁仍然需要检查调用方是否理解新状态。新增一个联合类型成员,可能会让某个 switch 没有处理分支;如果没有搜索引用,Agent 很容易只修到一半。
六、测试要围绕用户行为,而不是只追求覆盖率
Issue 的验收标准应该转换成测试场景:
- 有结果时,列表照常显示。
- 没有结果时,显示空状态。
- 请求超时时,显示错误和重试动作。
- 点击重试时,不会重复提交多个请求。
- 重试仍然失败时,错误状态不会被误判为空结果。
这些测试比“某个函数被调用一次”更接近用户真正看到的行为。单元测试很重要,但跨服务和组件的集成测试也不能省。Agent 应该根据变更范围选择测试,而不是只跑一个最容易通过的命令。
七、处理失败:停止比继续改更聪明
Agent 常见的危险行为,是测试失败后不断尝试修补,最终引入越来越多无关变化。可以给每次任务设置清晰的停止条件:
允许自动迭代:3 次
连续出现相同失败:暂停
变更文件超出计划范围:暂停
涉及权限、支付或数据迁移:请求人工确认
需要修改生产配置:请求人工确认
失败后先分类:环境问题、依赖问题、基线问题、实现问题还是需求歧义。若同一错误连续出现,继续重试通常没有意义,应该把错误输出、已尝试方案和当前 diff 交给人判断。
“我不知道下一步该做什么”是一个合格的工程状态;“我先把更多地方改了看看”通常不是。
八、生成 PR 时交付证据
一个可审查的 PR 描述至少包含:
## 解决的问题
说明用户遇到的现象和根因。
## 实现方式
列出修改模块和关键设计。
## 验证结果
列出执行过的命令和结果。
## 未覆盖范围
说明尚未运行的环境、已知限制和潜在风险。
## 回滚方式
说明如何撤销该变更。
Agent 不应该自动写“所有测试通过”,除非确实拿到测试命令的成功输出;也不应该把没有验证的浏览器、移动端或生产环境说成已验证。PR 的价值在于降低审查成本,夸大结果只会把风险往后推。
变更摘要还应该链接到具体文件和行号。审查者先看行为,再看实现;如果 diff 很大,Agent 应该解释为什么不能更小,而不是用长篇文字掩盖范围扩大。
九、合并前保留人的判断
软件工程里有些决定不能只由 Agent 做:数据库迁移是否可逆,权限变化是否符合业务规则,公开接口是否需要兼容旧客户端,安全修复是否应该隐藏细节,第三方许可证是否允许引入。Agent 可以收集资料、列出选项、准备补丁,但最终责任需要明确的人承担。
好的人工协作不是每一行都重写,而是在关键节点做判断:目标对不对,边界有没有越过,测试是否足够,风险是否可接受。Agent 做得越好,人越应该把时间放到这些高价值决策上。
总结:自动化的终点是可验证,不是无人负责
从 Issue 到 Pull Request,中间真正重要的是证据链:需求被拆成验收条件,仓库有清楚的基线,工作在隔离环境里进行,补丁范围可控,测试覆盖用户行为,失败有停止条件,最终 PR 诚实描述结果。
我不担心 Agent 会不会抢走“写代码”这件事。代码只是软件工程的一部分,真正难的是理解问题、尊重边界、做出取舍并对结果负责。一个好的软件工程 Agent,会让重复工作变快,却不会把不确定性伪装成确定性;它会主动收集证据,也会在证据不足时停下来。
当一个 PR 让审查者能够迅速回答“它为什么改、改了什么、怎么证明没坏、出了问题怎么退”时,自动化才真正完成了它的工作。剩下的合并按钮,应该由知道自己在承担什么的人来按下。