基于大语言模型的代码审查助手设计与实现:从Diff解析、Prompt约束到结果校验的工程实践
基于大语言模型的代码审查助手设计与实现
很多团队已经在写 Copilot、写问答机器人,但真正落到研发流程里,代码审查往往更难做。原因很直接:它不是单轮问答,而是要同时理解 Diff、上下文文件、团队规范、历史问题模式,还得把输出控制在可执行、可追踪的范围内。
我最近做了一个内部可复现版本,目标很务实:让 LLM 先做首轮 Review,把明显问题筛出来,再交给人工做最终判断。先说结论,实测在一组 420 个 PR 样本上,规则类问题召回率从 0.41 提到 0.68,误报率控制在 0.19 左右。没想到,真正拉开差距的不是模型大小,而是输入组织和结果校验。
短句先放这:工程细节决定上限。
一、我想解决的不是“替代审查”,而是“减少低效往返”
人工 Code Review 最耗时间的地方,通常不是架构级判断,而是一些高频重复问题:空指针判断漏写、SQL 拼接、日志缺上下文、接口返回码不统一、异常吞掉不抛、单测缺覆盖。
这些问题有特点。它们分布广、重复率高、修复成本低。如果能在 PR 提交后几分钟内自动给出建议,研发同学能更早改掉,Reviewer 看到的就会更干净。这样做的收益很实际。
我把目标拆成了两层:
- L1:规则与模式问题发现,偏稳定,适合先落地
- L2:设计与风险提示,波动大,需要保守输出
说实话,一开始我也想一步做到“懂业务、懂设计、懂性能”。后面跑完第一轮样本,发现这样很容易把系统做成一个“会说很多但不够准”的评论机。于是我收缩范围,先把 L1 做扎实,再给 L2 留观察位。
二、系统结构:输入预处理 + 分阶段审查 + 结果裁剪
整个代码审查助手我拆成 5 个模块:
- PR 变更采集
- 上下文补全
- Prompt 编排与模型调用
- 结果校验与去重
- 评论回写与效果埋点
避免一段列太满,我直接画成处理流:
Git PR -> Diff 解析 -> 文件类型路由 -> 上下文拼接 -> LLM Review -> 规则校验 -> 评论生成 -> 平台回写
这里最容易被忽略的是 文件类型路由。不同文件要给不同审查策略。
举个例子:
- Java Service 文件,看事务、异常、日志、空值、接口语义
- SQL 文件,看索引命中风险、全表扫描、where 条件约束
- YAML 配置,看默认值、敏感信息、超时和开关项
- 前端接口调用,看异常处理、状态判断、埋点漏报
如果所有文件都走一个总 Prompt,结果通常会散。短句。不要偷这个懒。
三、输入怎么组织:只喂 Diff 不够,要带“最小必要上下文”
很多 Demo 只把 git diff 扔给模型,然后期待它给出高质量评论。线上一跑,问题马上出现:
- 模型看不懂函数调用的上游含义
- 重构场景里误判为逻辑缺失
- 只看到新增代码,看不到被复用的旧工具方法
- 对团队规范完全无感
所以我最后采用的是 Diff + 邻近代码 + 审查规则 + 输出约束 的组合输入。
1)Diff 主体
保留新增、删除、上下文行号,尽量不要压缩成自然语言。原始结构对定位问题很有帮助。
2)邻近代码窗口
我给每个 hunk 补 40~80 行邻近上下文。这个值不是拍脑袋,是对照实验后选的。
- 20 行以下,语义经常断
- 40 行左右,准确率提升明显
- 超过 100 行,成本开始抬头,收益变小
3)团队审查规则
这部分很关键,但别写成大段原则口号。模型更吃得动的是 可执行规则。
比如:
规则R12:禁止直接拼接 SQL,必须使用参数化查询。
命中条件:出现 select/update/insert/delete 字符串拼接。
输出要求:指出风险位置,给出参数化改法。
4)输出格式约束
我强制模型输出 JSON,字段固定:
- file_path
- line_start
- line_end
- severity
- category
- summary
- suggestion
- confidence
这样做的目的很简单:后处理更稳,回写 PR 评论也方便。
四、核心 Prompt 设计:不要让模型“自由发挥”
代码审查最怕两种情况:一种是空泛,另一种是过度解读。为了解决这个问题,我把 Prompt 分成了系统层和任务层。
系统层 Prompt
系统层只做约束,不塞业务废话:
你是企业代码审查助手。
你的任务是基于给定代码变更发现明确、可定位、可修复的问题。
不要评价代码风格偏好,不要猜测未提供的业务背景。
若证据不足,输出 no_issue。
所有结论必须引用具体文件和行号区间。
输出必须符合 JSON Schema。
任务层 Prompt
任务层按文件类型拼接不同规则。
请审查以下 Java 服务层代码变更,重点检查:
- 空值处理是否遗漏
- 异常是否被吞掉
- 日志是否包含关键上下文
- 数据库写操作是否有事务边界
- 外部接口调用是否设置超时或失败处理
输入内容:
1. PR diff
2. 邻近上下文代码
3. 团队规则列表
输出要求:
- 只报告高置信度问题
- 每个问题给出修复建议
- 没有问题时返回 no_issue
这里有一个经验:宁可少报,也别胡报。
因为代码审查场景里,误报会直接消耗开发者耐心。一次两次还能忍,连续几轮全是“这条建议不适用”,系统很快就没人看了。
五、结果后处理:没有这一层,线上可用性会掉很多
模型输出完,并不代表可以直接贴到 PR 评论区。我最后加了 4 个后处理步骤。
1)JSON Schema 校验
先校验字段是否完整,类型是否正确,行号是否为正整数,severity 是否在允许范围内。
Python 示例:
from jsonschema import validate, ValidationError
REVIEW_SCHEMA = {
"type": "object",
"properties": {
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file_path": {"type": "string"},
"line_start": {"type": "integer", "minimum": 1},
"line_end": {"type": "integer", "minimum": 1},
"severity": {"type": "string", "enum": ["low", "medium", "high"]},
"category": {"type": "string"},
"summary": {"type": "string"},
"suggestion": {"type": "string"},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"required": [
"file_path", "line_start", "line_end", "severity",
"category", "summary", "suggestion", "confidence"
]
}
}
},
"required": ["issues"]
}
def validate_output(payload: dict) -> bool:
try:
validate(instance=payload, schema=REVIEW_SCHEMA)
return True
except ValidationError:
return False
2)行号映射校验
模型有时会引用不存在的行号,尤其是在多 hunk 文件里。这个要和原始 diff 做映射校验,不通过就丢弃。
def check_line_range(issue, diff_line_map):
file_path = issue["file_path"]
start = issue["line_start"]
end = issue["line_end"]
valid_lines = diff_line_map.get(file_path, set())
return all(line in valid_lines for line in range(start, end + 1))
3)相似问题去重
一个问题可能被模型从不同角度说两遍,比如“异常吞掉”和“catch 后未处理”其实是同一类。这里我用 file_path + line_range + category 做粗去重,再加一句向量相似度做合并。
4)置信度阈值过滤
我线上默认只放行 confidence >= 0.78 的建议,低于这个值只记录日志,不发评论。
这个阈值是调出来的。不是玄学。
六、代码实现示例:用 LangChain/LlamaIndex 不是重点,关键是流程可复现
下面给一个简化版实现,核心思路是可迁移的,用什么框架都行。
1)PR Diff 解析
from unidiff import PatchSet
def parse_diff(diff_text: str):
patch = PatchSet(diff_text)
files = []
for patched_file in patch:
hunks = []
for hunk in patched_file:
hunks.append({
"source_start": hunk.source_start,
"source_length": hunk.source_length,
"target_start": hunk.target_start,
"target_length": hunk.target_length,
"lines": [str(line) for line in hunk]
})
files.append({
"path": patched_file.path,
"is_added": patched_file.is_added_file,
"is_removed": patched_file.is_removed_file,
"hunks": hunks
})
return files
2)按文件类型路由规则
RULESETS = {
".java": ["null_check", "exception_handling", "transaction", "logging"],
".sql": ["sql_injection", "full_scan", "where_guard"],
".yaml": ["timeout", "secret_exposure", "default_value"],
".ts": ["api_error_handling", "null_guard", "tracking"]
}
def select_rules(file_path: str):
for ext, rules in RULESETS.items():
if file_path.endswith(ext):
return rules
return ["general_safety"]
3)构造审查请求
import json
def build_review_prompt(file_path, diff_text, context_text, rules):
return f"""
你是企业代码审查助手,请基于以下信息输出审查结果。
文件路径:
{file_path}
审查规则:
{json.dumps(rules, ensure_ascii=False)}
代码变更:
{diff_text}
邻近上下文:
{context_text}
输出要求:
1. 仅输出 JSON
2. 只报告明确问题
3. 每个问题包含 file_path, line_start, line_end, severity, category, summary, suggestion, confidence
4. 若未发现问题,返回 {{"issues": []}}
""".strip()
4)审查主流程
def review_file(llm_client, file_path, diff_text, context_text):
rules = select_rules(file_path)
prompt = build_review_prompt(file_path, diff_text, context_text, rules)
raw = llm_client.generate(prompt)
payload = json.loads(raw)
if not validate_output(payload):
return []
valid_issues = []
for issue in payload["issues"]:
if issue["confidence"] >= 0.78:
valid_issues.append(issue)
return valid_issues
这个实现不复杂,但足够落地。你可以先跑在 GitLab webhook 上,也可以挂到 GitHub Action 里。
七、评论怎么写:比“发现问题”更难的是“让人愿意改”
如果评论写得像审判书,开发者会天然排斥。所以我给评论模板做了限制:
- 先说问题点,不上价值判断
- 给出原因,尽量贴近运行风险
- 给一个最小修复建议
- 不写模糊话,比如“建议优化一下”
差评论示例
这里代码有问题,建议修改。
可用评论示例
在 OrderService.java 第84行附近,catch 块记录日志后直接返回默认值,异常没有继续抛出或转换处理。
这会让上游调用方无法感知下单失败,线上排查时只剩一条日志。
建议改为抛出业务异常,或返回带错误码的结果对象,并补充订单号、用户ID到日志上下文。
一句话,建议必须能落地到代码改动。
八、评估怎么做:别只看“模型觉得自己对不对”
我做这类系统时,评估分成离线和线上两部分。
离线评估集构造
我从历史 PR 和 Review 评论里抽样,整理成了 420 个样本,按问题类型分桶:
- 空值与异常处理
- SQL 与数据访问
- 日志与可观测性
- 配置与安全项
- API 调用稳定性
每个样本都标注:
- 是否存在有效问题
- 问题所在文件和行号
- 建议是否可执行
核心指标
我主要看 4 个指标:
Precision:报出来的问题里,有多少是真的Recall:真实问题里,抓到了多少Line Hit Rate:行号命中率Accept Rate:开发者实际采纳率
一组实测结果
下面是我在同一批样本上的对照结果:
| 方案 | Precision | Recall | Line Hit Rate | Accept Rate |
|---|---|---|---|---|
| 只喂 Diff | 0.62 | 0.41 | 0.57 | 0.28 |
| Diff + 上下文 | 0.70 | 0.56 | 0.73 | 0.39 |
| Diff + 上下文 + 规则约束 + 后处理 | 0.79 | 0.68 | 0.84 | 0.51 |
这个表很说明问题:真正带来提升的,不是单一 Prompt 技巧,而是整套输入与校验策略。
九、线上接入方式:建议先做“旁路模式”
如果你准备在团队里上线,我建议别一开始就自动评论到 PR。先走一段时间旁路模式。
做法很简单:
- 模型照常跑
- 结果只写到日志和看板
- 人工对照真实 Review 结果
- 统计误报、漏报、采纳率
等指标稳定后,再放开自动评论。
我当时就是这么做的。前两周只做旁路,累计看了 160 多个 PR。说实话,刚开始误报比预期高,主要集中在重构代码和测试桩代码上。后面加了“文件目录白名单”和“测试文件降权”,噪声一下就下来了。
GitHub Action 简化示例
name: llm-code-review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get diff
run: git diff origin/${{ github.base_ref }}...HEAD > pr.diff
- name: Run review bot
run: python review_runner.py --diff pr.diff
- name: Post comments
run: python post_comments.py
十、几个容易踩坑的点
1)大文件直接全量送模型
超过几百行的大 Diff,结果通常会漂。建议按 hunk 拆分,再在文件级做归并。
2)把“代码风格”混进“风险审查”
风格类问题很容易让输出变多,但价值并不高。像 import 顺序、空行格式这类,交给 linter 更合适。
3)没有历史样本回放
每次换 Prompt、换模型、换规则,都应该跑历史数据回归测试。不然线上效果变差,很难知道是哪一步引起的。
4)置信度完全相信模型自报
模型给的 confidence=0.95 不代表真的有 95% 把握。我的做法是把这个值当作排序特征,再结合规则命中、行号有效性、历史采纳率做二次打分。
一个简单示例:
def rerank_score(issue):
score = 0.0
score += issue["confidence"] * 0.6
score += 0.2 if issue.get("line_valid") else 0.0
score += 0.1 if issue.get("rule_hit") else 0.0
score += min(issue.get("similar_accept_rate", 0.0), 1.0) * 0.1
return round(score, 4)
十一、我最后采用的落地策略
如果你也想做一个可复现版本,我建议按下面的节奏推进:
阶段 A:先做高频稳定问题
只覆盖几类最常见问题:
- 空值处理
- 异常吞掉
- SQL 拼接
- 日志缺关键信息
- 超时和失败处理缺失
阶段 B:补上评估和回放
建立一套固定样本集,每次改 Prompt 和规则都跑一遍,对比 Precision、Recall、采纳率。
阶段 C:再尝试设计级建议
例如事务边界是否合理、方法职责是否过重、幂等语义是否完整。这类问题更难,建议单独打标签,默认不自动评论。
这个推进顺序比较稳。不会一上来就把预期抬太高。
十二、局限性
这个方案也不是没有短板。
我目前最不满意的一点,是对跨文件语义的把握还不够稳。比如一个接口 DTO 改了字段,调用链里另一个文件的判空没跟着更新,单看局部 Diff 有时抓不到。要补这块,后面得接入更完整的代码索引和调用关系分析。
但即便如此,用它做 PR 首轮筛查 已经够用了。
十三、总结
如果把大语言模型直接塞进代码审查流程,大概率会得到一个“偶尔有用,但经常打扰人”的系统。真要让它在线上站住,关键不在一句神奇 Prompt,而在这些工程动作:
- 输入要带最小必要上下文
- 文件类型要分流
- 规则要写成可执行约束
- 输出要做结构化校验
- 结果要靠历史样本持续回放
我自己的体感是,代码审查助手很适合做成“低风险、高频、先筛后判”的工具位。把那些反复出现的小问题尽早拦住,人工 Reviewer 才有精力看真正难的部分。
如果你正在做 DevOps、研发效能或者 LLM 应用落地,这个方向值得试一轮。先别追求全能,先把误报压下来,团队接受度会高很多。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)