给 agent 更好的工具,它应该能做出更好的工作——至少直觉上是这么想的。
当你打开一个 Pull Request 时,Copilot 代码审查会读取 diff 并探索周围代码,在代码上线前找出真正的问题。为了做到这一点,它使用了自有的代码探索工具。所以当我们换成维护更好、且与 Copilot CLI 共享的工具——grep、glob 和 view 时,我们预期这是一次干净的升级。
然而,在我们的基准测试中,审查成本反而更高了,发现的问题也更少了。
但问题不在工具本身,而在于指令。一旦我们根据审查者实际阅读 PR 的方式重写了这些指令,倒退就变成了胜利:平均审查成本降低了约 20%,同时保持了相同的审查质量。
这就是一个关于如何调整工具周围的工作流最终找到解决方案的故事。
相同的工具,错误的直觉
如果你在某个 agent 框架上做过开发,很可能也继承了它的工具。它们能用,所以你会一直留着,直到某天你的用例偏离了它们最初的设计目标,它们开始悄悄拖后腿。我们当时就面临这种局面。在尝试使用共享 CLI 工具之前,Copilot 代码审查用的是自有的代码探索工具。那套工具层受早期 agent 系统启发,包括 SWE-agent 风格的仓库导航和 GitHub Copilot Autofix 的思路:列出目录、搜索文件、搜索目录、读取代码。这些工具确实管用,但它们是专门为 Copilot 代码审查设计的,而且针对当时模型的行为方式。早期的 agent 编码模型发起工具调用的次数更少,在自动拉取必要上下文方面也更弱,这意味着在模型发起的少数工具调用中,把所有相关信息都包含进去就变得更重要。
与此同时,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 成本,但也符合早期模型通常受益于自动包含附近上下文的特点。
最初,我们希望这是一次简单的迁移:把一套工具换成另一套。但当我们用共享工具在离线基准测试中测试时,审查 agent 变得效率更低、效果更差。平均成本上升,有用的评论数量下降。
日志揭示了一个浏览循环
我们内部的 Copilot 代码审查基准测试之所以有用,是因为它们展示的不仅仅是最终分数。它们展示了 agent 的执行路径,包括它调用了哪些工具、返回了多少输出、错误发生在哪里、以及它是在缩小证据范围还是扩大搜索范围。
当我们第一次在离线基准测试中使用共享的 Copilot CLI 工具时,agent 经常表现得像是在浏览仓库,而不是在审查一个 Pull Request。它会广泛搜索、猜测可能的路径、广泛阅读、发现更多要搜索的东西,然后把多余的上下文一直带下去。
这种模式可以理解。当任务是“理解这个仓库”时,广泛的探索是有用的。但这并不是审查者通常审查 PR 的方式。
一个经验丰富的审查者会从 diff 出发,提出针对性的问题:
- 这个函数在哪里被调用?
- 这个配置键在其他地方被使用了吗?
- 有没有测试或辅助函数具有相同的模式?
- 能解释这个行为的最小邻近代码范围是什么?
审查者不会在知道自己要找什么之前就打开仓库的大部分内容。他们需要的是回答问题所需的最小上下文,避免用无关代码给审查增加负担。
这一点很重要,因为每次工具调用的结果都会成为 agent 工作上下文的一部分。额外的文件内容会被带入后续的推理中,增加成本,有时还会让审查失去重点。工具结果不是一次性的打印输出;对于 agent 来说,它是留在上下文窗口中的额外 token。
日志让这种差异变得可见。问题不在共享工具,而是指令给了 agent 错误的直觉,让它无法高效、有效地进行审查。
工具本身是能用的,但它们的指令是针对在 Copilot CLI 中的使用场景调优的,暗示了错误的工作流:agent 像使用一个广泛的编码助手那样使用 grep、glob 和 view,而不是像审查者那样。编码助手可能会在做出修改前先映射整个区域,以确保不会破坏代码的其他角落。而审查者通常从 diff 出发,询问变更是否引入了问题,然后寻找最窄的邻近证据来确认或反驳。
通用的编码助手工具指令,比如 Copilot CLI 或 Copilot cloud agent 使用的那些,对于交互式助手来说是合理的。开发者可能会要求它理解仓库、规划变更、编辑文件,并在多轮对话中继续。而 Copilot 代码审查的工作范围更窄:从一个 PR 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 避免让一次小的工具失败演变成更大的探索循环。
这个改动在措辞上很小,但效果巨大。它把 agent 的节奏从“浏览、阅读、再搜索”变成了“提问、缩小、阅读、判断”。
基准测试让我们调试行为,而不仅仅是分数
共享的 harness 给了我们工具,内部的 Copilot 代码审查基准测试给了我们反馈循环。我们可以运行相同的审查示例,比较工具调用日志,更新指令,然后再次运行。这让我们能提出具体的问题:
- agent 是先缩小范围,还是先广泛阅读?
- 它是否批量处理了独立的搜索?
- 它是否只在有理由时才调用
view? - 工具指令的变更是否减少了工具错误,还是只是把错误移到了别处?
- 日志是否始终聚焦在 diff 中间出现的证据上?
- 审查是否仍然保持了我们关心的质量指标?
最有用的信号不是“指令变好了”,而是更具体的东西:agent 发起的工具调用次数大致相同,但更多调用花在了相关证据上,而不是反复扩大搜索范围。
这直接把产品级的结果和可理解的工程行为联系了起来。我们不再需要猜测分数为什么变了,而是可以检查产生该分数的工作流。
结果:平均审查成本降低约 20%
在生产环境中,调优后的行为相比对照组显示出平均审查成本降低约 20%。重要的是,它没有出现任何会阻碍发布的质量信号。
成本降低并非来自工具本身,而是来自工具周围的工作流。共享的代码探索工具、Copilot 代码审查的自定义工具指令以及内部基准测试,让 agent 的行为变得足够可见,从而可以进行调优。
这个框架在构建 agent 时很重要。人们很容易把工具当作实现细节,换一套工具,然后比较最终答案。但对 agent 来说,工具表面是产品体验的一部分。它改变了 agent 注意什么、如何搜索、携带多少上下文、以及何时决定自己收集了足够证据。
工具描述和系统指令更接近 API 文档。不清晰的 API 文档会让开发者困惑,导致低效或错误的决策。不清晰的工具提示词也会对 LLM 造成同样影响;一个微小的措辞变化就能影响成本、质量以及调查的形态,因为它改变了 agent 如何分配注意力。
相同的工具,不同的任务
我们也尝试过在 CLI 中应用同类聚焦的工具指令,但没有产生同样的收益。这是一个有价值的反例,也是对教训的重要警示。
Copilot 代码审查锚定在一个 diff 和一个审查问题上。而 Copilot CLI 处理的是更广泛的交互式编码任务,其中探索可以是工作的一部分。可能没有单一的 diff 锚点,用户可能会在多轮对话中改变方向,正确的上下文一开始可能并不明显。同样的 grep、glob 和 view 工具可以支持这两个产品,但工具周围的工作流必须与产品匹配。
结论是:共享工具能够扩展,前提是指令和基准测试与任务匹配。
