更好工具反让Copilot代码审查变差,我们实际改进的方法
Better tools made Copilot code review worse. Here’s how we actually improved it.
GitHub Copilot代码审查团队在将自有的代码探索工具替换为共享的Copilot CLI工具(grep、glob、view)时,发现benchmark中审查成本上升且有效评论减少。问题源于工具指令与审查工作流不匹配,导致agent像编码助手一样广泛浏览而非聚焦diff。团队重写了审查导向的工具指令,引导agent先缩小搜索范围再精确读取。调整后,生产环境中平均审查成本降低约20%,且审查质量保持不变。
给 agent 更好的工具,它就应该做得更好——这至少是直觉告诉我们的。当你发起一个 pull request 时,Copilot 代码审查会读取 diff 并探索周围代码,在问题上线前找出真正重要的隐患。为此,它使用了自有的代码探索工具。因此,当我们换成维护更好、共享的 Copilot CLI 工具(grep、glob 和 view)时,我们预期会是一次干净的升级。然而,在我们的 benchmark 中,我们发现审查成本更高了,发现的问题却更少了。但问题并不出在工具上,而是出在指令上。一旦我们按照审查者实际阅读 pull request 的方式重写了指令,这种退化就变成了胜利:平均审查成本降低了约 20%,同时保持了相同的审查质量。这就是我们如何通过调整工具周围的工作流来找到解决方案的故事。
同样的工具,错误的直觉
如果你在某个 agent 框架之上构建过应用,你可能也继承了它的工具。它们能用,所以你就留着它们,直到有一天你的用例偏离了它们的设计初衷,它们开始悄悄地拖你后腿。这正是我们当时的情况。在尝试使用共享的 CLI 工具之前,Copilot 代码审查使用的是自有的代码探索工具。那个工具层受到了早期 agentic 系统的启发,包括 SWE-agent 风格的仓库导航和 GitHub Copilot Autofix 中的一些想法:列出目录、搜索文件、搜索目录和读取代码。这些工具能用,但它们是 Copilot 代码审查专用的,并且是为当时模型的行为方式设计的。早期的 agentic 编码模型进行的工具调用更少,自动拉取必要上下文的能力也更差。这意味着在模型进行的少数几次工具调用中包含所有相关信息更为重要。
与此同时,Copilot CLI harness 拥有一套共享的、受 Unix 启发的代码探索工具:grep、glob 和 view。这个 harness 也被越来越多的 Copilot agent 产品使用,包括 GitHub Copilot cloud agent,因此 harness 的改进可以使多个产品受益。我们希望尽可能清理和共享基础设施,因此我们尝试在 Copilot 代码审查中使用来自 Copilot CLI harness 的工具。目标是减少重复的工具实现,创建一个共享的地方来改进代码探索工具,并更容易地将这些改进带到各个 Copilot 产品中。
从纸面上看,这次迁移看起来很简单:
| 旧的 Copilot 代码审查 | GitHub Copilot CLI | 用途 |
|---|---|---|
list_dir |
glob |
在打开代码前发现候选文件和目录。 |
search_file 和 search_dir |
grep |
搜索代码中匹配的文本、符号或调用点。 |
read_code |
view |
在知道路径或范围后读取相关文件内容。 |
现有的审查工具并非简单的封装。当搜索目录或读取代码范围时,它们可以返回匹配或请求的行,外加额外的周围代码上下文。这增加了 token 成本,但也符合早期模型通常受益于自动包含附近上下文的方式。
最初,我们希望这是一次简单的迁移:用一套工具替换另一套。但当我们在离线 benchmark 中测试共享工具时,审查 agent 变得效率更低、效果更差。平均成本上升了,而有用的评论数量下降了。
trace 揭示了一个浏览循环
我们内部的 Copilot 代码审查 benchmark 很有用,因为它们显示的不仅仅是最终分数。它们显示了 agent 所走的路径,包括它调用了哪些工具、返回了多少输出、错误发生在哪里,以及它是在缩小证据范围还是在扩大搜索范围。
当我们首次在离线 benchmark 中尝试共享的 Copilot CLI 工具时,agent 的行为常常像是在浏览仓库,而不是在调查一个 pull request。它会广泛搜索,猜测可能的路径,广泛阅读,发现更多要搜索的东西,并将这些额外的上下文带向前。
图 1:之前 —— 我们观察到的通用行为的简化说明:扩大搜索范围、猜测路径和积累上下文。
这种模式是可以理解的。当任务是“理解这个仓库”时,广泛的探索可能很有用。但这并不是审查者通常审查 pull request 的方式。当我审查一个 pull request 时,我从 diff 开始,并提出有针对性的问题:这个函数在哪里被调用?这个配置键还在其他地方使用吗?是否存在具有相同模式的测试或辅助函数?能解释这种行为的最小附近代码范围是什么?在我知道我要找什么之前,我不想打开仓库的大部分内容。我只需要回答问题所需的最小上下文,而不用不相关的代码使审查过载。
这一点很重要,因为每个工具结果都会成为 agent 工作上下文的一部分。额外的文件内容可能会被带入后续的推理中,增加成本,有时还会使审查变得不那么聚焦。工具结果不是一次性的打印输出;对于 agent 来说,它是留在上下文窗口中的额外 token。Trace 使这种差异变得可见。
共享工具本身不是问题。问题在于指令给了 agent 错误的直觉,使其无法进行高效和有效的审查。工具本身是有效的,但它们的指令是针对在 Copilot CLI 中的使用而调整的,并暗示了错误的工作流:agent 像使用一个广泛的编码助手一样使用 grep、glob 和 view,而不是像一个审查者。
一个编码助手可能会在进行更改之前映射整个区域,以确保它不会破坏代码的其他角落。另一方面,审查者通常从 diff 开始,询问更改是否引入了问题,然后寻找最窄的附近证据来确认或排除它。通用的编码助手工具指令,比如 Copilot CLI 或 Copilot cloud agent 使用的那些,对于交互式助手来说是有意义的。开发者可能会要求它理解一个仓库、计划一个更改、编辑文件,并在多个回合中继续。Copilot 代码审查的工作范围更窄:从一个 pull request diff 开始,收集足够的周围证据来决定更改是否引入了真正的问题,并避免加载该审查问题不需要的上下文。
因此,很明显,我们不能简单地用 Copilot CLI 的工具替换之前的 Copilot 代码审查工具,而不进行额外的提示工作。问题变成了:我们如何设计工具指令,使其在代码审查环境中有效地使用这些共享工具?
为审查者的工作流重写工具指令
接下来的迭代使指导变得特定于代码审查。我们希望 Copilot 代码审查遵循的工作流是:
- 从 diff 开始,形成具体的审查问题。
- 当路径不确定时使用
glob,并使用grep查找候选文件、符号和调用点。 - 在读取文件之前批量进行低成本的发现。
- 仅在 agent 知道它需要哪个文件或行范围时才使用
view。 - 批量进行聚焦的读取,而不是在搜索和读取之间交替进行。
用过于简化的形式来说,这就是我们编码的行为:
- 通用姿态: 使用可用工具检查可能相关的仓库上下文。
- 审查导向的指导: 从 diff 开始。先用
grep和glob缩小范围;再用view读取精确的证据。如果grep未能找到相关上下文,用更简单的转义搜索重试。如果路径错误,转向glob而不是猜测附近的路径。
例如,想象一下 diff 更改了一个决定操作是否被允许的授权辅助函数。一个相关的审查问题不是“显示调用此辅助函数的每个文件的全部内容”。它可能更窄:“是否有任何请求处理的调用者依赖于旧行为?”预期的路径很短:从 diff 中更改的辅助函数开始 → grep 该辅助函数的调用者 → glob 可能的路由、处理器或控制器文件 → view 最相关的调用者范围 → 决定是否有任何调用者改变了风险。
指导还改变了 agent 从失败搜索中恢复的方式。如果一个输入导致 grep 失败,更好的下一步是一次更简单的、修正后的搜索。如果路径错误,更好的下一步是 glob,而不是猜测相邻路径并读取恰好存在的任何内容。这促使 agent 避免让一个小工具失败演变成一个更大的探索循环。
图 2:之后 —— 提示引导的审查导向行为的简化说明:锚定在 diff 上,用 grep 和 glob 缩小范围,然后用 view 读取聚焦的范围。
这个改变措辞很小,但效果很大。它将 agent 的节奏从“浏览、阅读、再搜索”改变为“提问、缩小、阅读、决定”。
Benchmark 让我们调试行为,而不仅仅是分数
共享的 harness 给了我们工具。内部的 Copilot 代码审查 benchmark 给了我们反馈循环。我们可以运行相同的审查示例,比较工具 trace,更新指令,然后再次运行。这让我们能够提出具体的问题:agent 是先缩小范围,还是先广泛阅读?它是否批量处理了独立的搜索?它是否只在有理由时才调用 view?工具指令的更改是否减少了工具错误,还是仅仅将它们转移到了其他地方?Trace 是否始终聚焦于来自 diff 的证据?审查是否仍然保持了我们关心的质量指标?
最有用的信号不是“指令更好了”。它更具体。agent 进行了相似数量的工具调用,但将更多的调用花在了相关证据上,而不是反复扩大搜索范围。这将对产品层面的结果与可理解的工程行为联系了起来。我们不再需要猜测分数变动的原因,而是可以检查产生该分数的工作流。
结果:平均审查成本降低约 20%
在生产环境中,调整后的行为与对照组相比,平均审查成本降低了约 20%。重要的是,它没有显示出会阻碍发布的任何质量信号。这种降低并非来自工具本身,而是来自工具周围的工作流。共享的代码探索工具、Copilot 代码审查自定义工具指令以及内部 benchmark,使得 agent 的行为变得可见并可调优。
在使用 agent 进行构建时,这个框架很重要。人们很容易将工具视为实现细节,只需将一个工具换成另一个,然后比较最终答案。但对于 agent 来说,工具表面是产品体验的一部分。它改变了 agent 注意到什么、如何搜索、携带多少上下文向前,以及何时决定它拥有足够的证据。工具描述和系统指令更接近于 API 文档。不清晰的 API 文档会让开发者感到困惑,并导致低效或错误的决策。不清晰的工具提示对 LLM 也会产生同样的影响;一个小的措辞更改可能会影响成本、质量和调查的形态,因为它改变了 agent 分配注意力的方式。
同样的工具,不同的任务
我们也尝试在 CLI 中应用同样类型的聚焦工具指令,但并没有产生同样的效果。这是一个有用的反例,也是这个教训的一个重要护栏。Copilot 代码审查锚定在一个 diff 和一个审查问题上。Copilot CLI 处理更广泛的、交互式的编码任务,其中探索可能是工作的一部分。可能没有单一的 diff 锚点,用户可能会在多个回合中改变方向,并且正确的上下文在开始时可能并不明显。相同的 grep、glob 和 view 工具可以支持这两个产品,但围绕这些工具的工作流必须与产品相匹配。结论是:当指令和 benchmark 与任务相匹配时,共享工具才能扩展。
你可以使用 GitHub Copilot 代码审查亲自尝试一下。
更好的工具让 Copilot 代码审查变得更糟。以下是我们实际改进它的方法。 这篇文章最初发表在 GitHub 博客上。