面向 AI 编码智能体的 GitHub PR 风格代码评审
在 Potato 中搭建 GitHub PR 风格的代码评审标注:diff 内联评论、文件级质量评分,以及对编码智能体产出的通过或打回结论。
为什么要做代码评审标注
大多数编码智能体基准把评估压缩成一个二元问题:测试过了没有?SWE-bench 报告 issue 的解决率,HumanEval 报告 pass@k。这些指标拿来排榜有用,但对理解代码质量没什么帮助。
一个智能体可以通过全部测试,同时写出没人愿意维护的代码:带安全漏洞的、跑得慢的,或者风格和整个代码库对着干的。就算测试全绿,人类评审也会在那个 PR 上要求修改。如果你想要的是能被人真正合并的代码,就得去评审代码本身,而不只是跑测试。
Potato 的 code_review 标注方案把 GitHub PR 评审的体验搬进了标注工具。标注者看到带语法高亮的 unified diff,点击 diff 行添加内联评论,在若干质量维度上给文件打分,最后给出 approve、request changes 或 comment 的结论——和评审一个真实的 pull request 一样。完整的方案参考见编码智能体标注文档和智能体评估指南。
这是 Potato 里的代码评审界面,展示了内联 diff 评论和文件级评分:
Potato 的代码评审界面,包含内联 diff 评论和文件级质量评分
代码评审方案概览
code_review 方案分三层:
- 内联 diff 评论:标注者点击 diff 中的任意一行,附上一条带分类的评论(bug、风格、性能、安全、逻辑、建议、疑问)
- 文件级评分:每个被修改的文件都单独获得正确性(1-5)和代码质量(1-5)两项评分
- 总体结论:标注者给出最终判断——通过、要求修改,或只留评论
这套流程和真实的代码评审对应,开发者用起来不会别扭,而它产出的结构化数据可以直接对接代码评审模型的训练。
CodingTraceDisplay:diff 是怎么渲染的
CodingTraceDisplay 组件把编码智能体的 trace 渲染成一串工具调用及其输出,并对文件编辑做了专门处理。当智能体编辑文件时,界面显示 unified diff,其中包含:
- 红色行:被删除的行(前缀
-) - 绿色行:新增的行(前缀
+) - 灰色行:上下文行(未改动)
- 行号:行号槽中同时给出旧行号和新行号
- 语法高亮:根据文件扩展名做语言感知的高亮
- 点击评论:点击任意一行会打开一个锚定到该行的评论表单
diff 由智能体的编辑操作自动算出。如果智能体用的是搜索替换类工具,Potato 会重建改动前后的状态并生成 unified diff。
对于一条 trace 中修改了多个文件的智能体(现实中的 bug 修复很常见),每个文件有自己的可折叠 diff 区块,类似 GitHub PR 的 “Files changed” 标签页。
CodingTraceDisplay 渲染代码改动时带有正确的语法高亮:
CodingTraceDisplay 渲染带语法高亮的 unified diff 和文件树侧边栏
评论分类
标注者点击 diff 行添加评论时,需要选一个分类:
| 分类 | 颜色 | 说明 | 示例 |
|---|---|---|---|
bug | 红色 | 代码存在功能性错误 | “如果 user 是 None,这里会抛 NullPointerException” |
style | 蓝色 | 代码风格或约定问题 | “本项目函数用 snake_case,不是 camelCase” |
performance | 橙色 | 代码效率低 | “这是在循环里查数据库,应该用批量查询” |
security | 紫色 | 安全漏洞 | “用户输入没有做净化就直接拼进了 SQL 查询” |
logic | 黄色 | 逻辑问题,未必立刻导致失败 | “这个条件应该是 >= 而不是 >,边界上差一” |
suggestion | 绿色 | 改进建议,不算错误 | “这里可以考虑用 context manager,资源处理更干净” |
question | 灰色 | 需要澄清 | “为什么加了这个 import?看起来并没有被用到” |
每条评论还有一个自由文本正文,标注者在这里详细说明问题,就像写真实的 PR 评论一样。
文件级评分
看完某个文件的 diff 之后,标注者从两个维度给它打分:
正确性(1-5):
- 1:不能工作,还引入了新 bug
- 2:部分可用,存在明显问题
- 3:主路径可用,但漏掉了边界情况
- 4:基本正确,只有小问题
- 5:完全正确,边界情况也处理得当
代码质量(1-5):
- 1:无法维护,毫无结构
- 2:质量差,风格或设计问题严重
- 3:可以接受,遵循了基本约定
- 4:质量好,干净可读
- 5:优秀,写法地道,文档齐全
结论选项
看完所有文件后,标注者从三种结论中选一个:
- Approve:代码可以原样合并,或者只需要无关紧要的小改动
- Request Changes:合并之前需要做实质性修改
- Comment Only:只给反馈,不做合并决定
这三项与 GitHub 的三种 PR 评审状态一一对应。
分步搭建
第 1 步:转换编码智能体 trace
编码智能体的 trace 格式五花八门。下面是三种常见智能体的例子。
从 Claude Code(JSON 导出):
python -m potato.trace_converter \
--input claude_code_sessions/ \
--output data/code_traces.jsonl \
--input-format claude_code从 Aider(聊天记录):
python -m potato.trace_converter \
--input aider_logs/ \
--output data/code_traces.jsonl \
--input-format aider从 SWE-Agent(trajectories 目录):
python -m potato.trace_converter \
--input swe_agent_trajectories/ \
--output data/code_traces.jsonl \
--input-format swe_agent_trajectory转换器输出一种标准化的 JSONL 格式。每一行是一条 trace,包含任务、智能体的各个步骤,以及文件 diff:
{
"trace_id": "swe_bench_123",
"task": "Fix the TypeError in django/db/models/query.py when calling .values() on an empty QuerySet",
"repo": "django/django",
"base_commit": "a1b2c3d",
"steps": [
{
"type": "thought",
"content": "Let me look at the QuerySet.values() method to understand the TypeError."
},
{
"type": "action",
"tool": "search",
"input": "def values",
"file": "django/db/models/query.py"
},
{
"type": "observation",
"content": "Found at line 234: def values(self, *fields):\n ..."
},
{
"type": "action",
"tool": "edit",
"file": "django/db/models/query.py",
"old_content": " def values(self, *fields):\n clone = self._clone()\n clone.query.set_values(fields)",
"new_content": " def values(self, *fields):\n clone = self._clone()\n if not fields and not self.query.values_select:\n return clone\n clone.query.set_values(fields)"
},
{
"type": "action",
"tool": "edit",
"file": "tests/queries/test_values.py",
"old_content": "class ValuesTests(TestCase):",
"new_content": "class ValuesTests(TestCase):\n def test_empty_queryset_values(self):\n \"\"\"Calling .values() on empty QuerySet should not raise TypeError.\"\"\"\n qs = Author.objects.none()\n result = list(qs.values())\n self.assertEqual(result, [])"
}
],
"files_changed": [
{
"path": "django/db/models/query.py",
"diff": "--- a/django/db/models/query.py\n+++ b/django/db/models/query.py\n@@ -234,6 +234,8 @@\n def values(self, *fields):\n clone = self._clone()\n+ if not fields and not self.query.values_select:\n+ return clone\n clone.query.set_values(fields)"
},
{
"path": "tests/queries/test_values.py",
"diff": "--- a/tests/queries/test_values.py\n+++ b/tests/queries/test_values.py\n@@ -1,4 +1,10 @@\n class ValuesTests(TestCase):\n+ def test_empty_queryset_values(self):\n+ \"\"\"Calling .values() on empty QuerySet should not raise TypeError.\"\"\"\n+ qs = Author.objects.none()\n+ result = list(qs.values())\n+ self.assertEqual(result, [])"
}
]
}第 2 步:配置代码评审方案
创建你的 config.yaml:
annotation_task_name: "Coding Agent Code Review"
data_files:
- "data/code_traces.jsonl"
item_properties:
id_key: "trace_id"
text_key: "task"
# Display coding agent traces with diff rendering
display:
type: "coding_trace"
trace_key: "steps"
diff_key: "files_changed"
syntax_highlighting: true
show_line_numbers: true
collapse_large_diffs: true
max_uncollapsed_lines: 200
annotation_schemes:
- annotation_type: "code_review"
# Inline comment categories
# File-level ratings
# Overall verdict
# Annotator settings
annotator_config:
allow_back_navigation: true
# Output settings
output:
path: "output/"
format: "jsonl"第 3 步:启动标注服务
potato start config.yaml -p 8000打开 http://localhost:8000。你会看到第一条编码智能体 trace,包括任务描述、智能体的推理步骤,以及带语法高亮的文件 diff。
第 4 步:标注者的工作流
典型的评审流程是这样的:
- 读任务:搞清楚智能体被要求做什么(比如“修复 django/db/models/query.py 中的 TypeError”)
- 看 trace:翻一遍智能体的推理步骤,理解它的思路
- 逐个文件看 diff:
- 读带语法高亮的 diff
- 点击任意一行添加内联评论
- 选择评论分类(bug、风格、性能等)
- 写下评论正文说明问题
- 给这个文件的正确性(1-5)和代码质量(1-5)打分
- 给出结论:选择通过、要求修改,或只留评论
- 提交:点击 “Submit” 或按 Ctrl+Enter
键盘快捷键可以加快流程:
| 快捷键 | 操作 |
|---|---|
j / k | 在文件之间切换 |
c | 在选中行上打开评论 |
1-5 | 为当前维度打分 |
a | 结论设为通过 |
r | 结论设为要求修改 |
Ctrl+Enter | 提交评审 |
导出格式
每份提交的评审会产出一个结构化的 JSON 对象:
{
"trace_id": "swe_bench_123",
"annotator": "reviewer_01",
"timestamp": "2026-03-22T14:32:11Z",
"review": {
"inline_comments": [
{
"file": "django/db/models/query.py",
"line": 236,
"side": "right",
"category": "logic",
"body": "This early return skips set_values entirely, but if fields are provided later via .values('name'), the previous empty .values() call will have returned a clone that never went through set_values. Consider checking if this clone is still valid downstream."
},
{
"file": "tests/queries/test_values.py",
"line": 5,
"side": "right",
"category": "suggestion",
"body": "Consider adding a test case for .values() followed by .values('name') to verify the chaining behavior after your fix."
}
],
"file_ratings": [
{
"file": "django/db/models/query.py",
"correctness": 3,
"code_quality": 4
},
{
"file": "tests/queries/test_values.py",
"correctness": 4,
"code_quality": 4
}
],
"verdict": "request_changes"
}
}这种结构化格式可以直接用于训练代码评审模型,也便于做汇总分析。
分析:处理评审数据
加载评审结果
import json
import pandas as pd
from pathlib import Path
reviews = []
for f in Path("output/").glob("*.jsonl"):
with open(f) as fh:
for line in fh:
reviews.append(json.loads(line))
print(f"Loaded {len(reviews)} code reviews")评论分类分布
from collections import Counter
all_comments = []
for rev in reviews:
for comment in rev["review"]["inline_comments"]:
all_comments.append(comment)
category_counts = Counter(c["category"] for c in all_comments)
print("Comment categories:")
for cat, count in category_counts.most_common():
print(f" {cat}: {count}")文件平均评分
ratings = []
for rev in reviews:
for fr in rev["review"]["file_ratings"]:
ratings.append(fr)
ratings_df = pd.DataFrame(ratings)
print("Average ratings by file:")
print(
ratings_df.groupby("file")[["correctness", "code_quality"]]
.mean()
.round(2)
.to_string()
)结论分布
verdict_counts = Counter(rev["review"]["verdict"] for rev in reviews)
total = sum(verdict_counts.values())
print("Verdict distribution:")
for verdict, count in verdict_counts.most_common():
print(f" {verdict}: {count} ({count/total*100:.1f}%)")按智能体统计 bug 率
如果你的 trace 里带 agent 字段,就可以比较不同智能体的 bug 率:
agent_bugs = {}
for rev in reviews:
agent = rev.get("agent", "unknown")
bug_count = sum(
1 for c in rev["review"]["inline_comments"]
if c["category"] == "bug"
)
if agent not in agent_bugs:
agent_bugs[agent] = []
agent_bugs[agent].append(bug_count)
print("Average bugs per review by agent:")
for agent, bugs in sorted(agent_bugs.items()):
print(f" {agent}: {sum(bugs)/len(bugs):.2f} (n={len(bugs)})")应用场景
训练代码评审模型
Potato 代码评审标注产出的结构化内联评论、文件评分和结论,很适合作为自动代码评审模型的训练数据。每份评审提供:
- 定位到具体 diff 行的局部反馈
- 已分类的问题(bug 还是风格还是性能)
- 多个粒度上的质量信号(行、文件、整体)
CodeRabbit 和 Graphite 的 AI 评审器用的就是这种数据格式,区别在于这里的数据来自人类专家,而不是从 LLM 蒸馏出来的。
在 SWE-bench 上评估编码智能体
SWE-bench 告诉你智能体有没有解决 issue(测试是否通过),但不告诉你代码能不能合并。对 SWE-bench 的解法跑一遍代码评审标注,你就能区分出哪些智能体是用干净的代码解决问题,哪些是靠 hack 蒙混过关。这样得到的排行榜更细致,也更贴近开发者的真实体验。
构建代码质量数据集
把大量 trace 的代码评审数据汇总起来,可以建立 AI 生成代码中常见质量问题的数据集。这类数据集可以用于:
- 微调代码生成模型,避开常见错误
- 针对 AI 生成代码的模式构建专门的 linter
- 训练分类器,在人工评审之前先标出智能体输出中可能有问题的地方
小结
Potato 的 code_review 方案把 GitHub PR 评审流程放进了智能体评估。你收集到的内联评论、文件评分和结论构成结构化的代码质量数据,比一个通过/失败的测试结果说明的东西多得多。无论你是要训练代码评审模型、把干净的 SWE-bench 解法和糙活区分开,还是只想给自己的智能体定一条质量基线,需要的都是这类数据。