从一次凌晨三点被@醒的PR说起
去年有个凌晨,手机震得我差点把水杯打翻。群里@我的人说:“大佬,PR里有个空指针,CI过了但review没发现,线上炸了。”我打开那个PR,好家伙,3000行改动,reviewer只回了句“LGTM”。那一刻我意识到,靠人肉review去堵这种低级错误,迟早要猝死。
于是有了这个项目:一个能自动拉取PR diff、分析代码逻辑、生成review评论、甚至给出修复建议的Agent。不是替代人,是把那些“一眼就能看出问题”的活交给机器,让人去关注架构和设计。
架构设计:别搞成单体怪物
这个Agent我拆成了四个模块,每个都能独立跑,也方便后续替换模型或规则引擎。
PR Fetcher → Diff Analyzer → Comment Generator → Fix Suggester
PR Fetcher 负责对接GitHub/GitLab API,拉取PR的元数据和diff。这里有个坑——大PR的diff可能几万行,直接塞给LLM会爆token。我的做法是先做文件级过滤,只分析新增和修改的行,删除的行直接跳过。
Diff Analyzer 是核心。它不直接扔给LLM,而是先跑一套静态规则引擎。比如“空指针检查”、“资源未关闭”、“魔法数字”这些,用AST解析+正则匹配先扫一遍。规则引擎能覆盖60%的常见问题,而且零延迟。剩下的复杂逻辑(比如并发问题、业务语义错误)才交给LLM。
Comment Generator 负责把分析结果转成自然语言评论。这里有个设计要点:评论要带行号、严重级别、分类标签。不然开发者在几百条评论里找关键问题,跟大海捞针一样。
Fix Suggester 是锦上添花。它根据问题类型,生成修复代码片段。注意,是“建议”不是“自动修改”。我见过有人直接让Agent改代码,结果改出了更严重的bug。建议永远只输出diff格式的patch,让人工确认。
核心实现:Diff解析与行号映射
这是最容易翻车的地方。GitHub的diff格式和GitLab的diff格式有细微差别,而且不同语言的行号计算方式不同。
# 这里踩过坑:GitHub的diff行号是"@@ -旧行号,旧行数 +新行号,新行数 @@"
# 但实际解析时,旧行号和新行号可能因为上下文行而偏移
def parse_diff_hunk(hunk_header: str) -> tuple:
# 别这样写:直接用正则匹配数字
# 正确做法:先解析hunk header,再逐行计算偏移
pattern = r'@@ -(\d+),?\d* \+(\d+),?\d* @@'
match = re.match(pattern, hunk_header)
if not match:
raise ValueError(f"无法解析hunk header: {hunk_header}")
old_start = int(match.group(1))
new_start = int(match.group(2))
return old_start, new_start
解析完hunk后,要维护一个“新旧行号映射表”。比如PR中第100行对应原文件的第80行。这个映射在生成评论时至关重要——你不能告诉开发者“第100行有问题”,而应该告诉他“原文件第80行有问题”。
规则引擎:先做能做的,别什么都扔给LLM
我写了一个规则引擎,支持自定义规则。每条规则是一个函数,输入是AST节点或代码行,输出是问题描述和严重级别。
# 规则示例:检测未关闭的资源
def check_unclosed_resource(node: ast.AST) -> Optional[Issue]:
# 别这样写:只检查open()调用
# 正确做法:检查所有实现了__enter__和__exit__的对象
if isinstance(node, ast.With):
# with语句已经自动管理资源,跳过
return None
if isinstance(node, ast.Call) and hasattr(node.func, 'id'):
if node.func.id in ['open', 'socket', 'connect']:
# 检查是否在with块中
if not _is_in_with_block(node):
return Issue(
line=node.lineno,
severity='HIGH',
category='RESOURCE_LEAK',
message=f"资源 {node.func.id}() 未在with语句中使用,可能导致资源泄漏"
)
return None
规则引擎的好处是确定性强、速度快。一个PR的diff,规则引擎跑完只需要几百毫秒。而LLM跑一次可能要几秒到几十秒。所以我的策略是:规则引擎先跑,把能确定的问题标记出来,剩下的“疑似问题”再交给LLM做二次判断。
LLM调用:别让模型猜上下文
把diff直接扔给LLM,它大概率会胡说八道。因为LLM不知道这个项目的代码风格、命名规范、业务逻辑。所以我在prompt里加了三个关键信息:
- 文件上下文:该文件的前100行和后50行(如果存在),让模型知道函数签名、类定义等。
- 项目规范:从项目根目录的.editorconfig、.eslintrc、pylintrc等配置文件中提取的规则摘要。
- 历史模式:该文件最近10次commit的改动模式,让模型知道哪些是“正常改动”,哪些是“异常改动”。
# 这里踩过坑:prompt太长会导致模型忽略关键信息
# 正确做法:把上下文压缩成摘要,而不是完整代码
def build_llm_context(diff_lines: list, file_path: str) -> str:
context = []
# 只提取函数签名和类定义
context.append(f"文件: {file_path}")
context.append(f"项目语言: {detect_language(file_path)}")
context.append(f"最近改动: {get_recent_changes(file_path, limit=5)}")
# 把diff按函数/类分组,减少token
grouped = group_diff_by_function(diff_lines)
for func_name, lines in grouped.items():
context.append(f"函数 {func_name}:")
context.extend(lines)
return "\n".join(context)
评论生成:别当复读机
很多Agent生成的评论是“这段代码可能存在空指针风险”,然后没了。开发者看到这种评论只会想骂人。好的评论应该包含:
- 问题位置:精确到行号,最好还有函数名。
- 问题原因:为什么这是问题,而不是“我觉得不好”。
- 修复建议:给出具体代码,最好是diff格式。
- 严重级别:CRITICAL/BLOCKER/HIGH/MEDIUM/LOW,让开发者知道优先级。
def generate_comment(issue: Issue, fix_suggestion: str) -> str:
# 别这样写:只输出问题描述
# 正确做法:结构化输出,方便开发者快速定位
return f"""
**{issue.severity}**: {issue.category}
**位置**: {issue.file_path}:{issue.line} (函数: {issue.function_name})
**问题**: {issue.message}
**建议修复**:
```diff
{fix_suggestion}
“”"
## 修复建议生成:只给patch,不改代码
这是最容易出事的环节。我见过有人让Agent直接调用GitHub API修改PR,结果改出了编译错误。我的做法是:Agent只生成修复建议的diff,然后以评论形式发布。开发者可以一键应用,也可以手动调整。
生成修复建议时,我用了两个策略:
1. **模板匹配**:对于常见问题(如空指针检查、资源关闭),预置修复模板,直接填充变量。
2. **LLM生成**:对于复杂问题,让LLM生成修复代码,但必须经过语法检查(ast.parse)和类型检查(mypy/pyright)才能发布。
```python
# 这里踩过坑:LLM生成的修复代码可能语法错误
# 正确做法:生成后立即做语法检查
def validate_fix(fix_code: str, language: str) -> bool:
if language == 'python':
try:
ast.parse(fix_code)
return True
except SyntaxError:
return False
# 其他语言类似
return False
部署与集成:别让开发者多一步操作
这个Agent我部署成了一个GitHub App,通过webhook监听PR事件。当PR被创建或更新时,自动触发分析。分析结果以评论形式发布,同时会在PR的Checks tab里显示状态。
关键设计点:
- 增量分析:只分析新增的commit,而不是整个PR。这样对于大PR,每次更新只需要几秒。
- 缓存机制:对于相同的diff hash,直接返回缓存结果,避免重复计算。
- 降级策略:如果LLM服务挂了,只运行规则引擎,至少能覆盖60%的问题。
踩过的坑与经验
- 不要相信LLM的行号:LLM经常把行号算错。我的做法是:LLM只输出问题描述和修复代码,行号由规则引擎或diff解析器提供。
- 评论数量要控制:一个PR如果生成几百条评论,开发者会直接忽略。我的做法是:按严重级别排序,只显示前20条,其余折叠。
- 语言差异要处理:Python和JavaScript的AST解析完全不同,规则引擎要按语言写。我用了tree-sitter做统一AST解析,但性能不如原生解析器。
- 隐私问题:不要把整个代码库发给LLM。只发送diff和必要的上下文,而且要在prompt里明确说“不要存储代码”。
个人经验性建议
如果你也想做类似的Agent,我的建议是:先做规则引擎,再做LLM集成。规则引擎能解决80%的常见问题,而且稳定、可解释。LLM是用来处理那20%的复杂问题的,别让它成为瓶颈。
另外,不要追求100%准确率。代码审查本身就是主观的,有些问题在不同团队看来标准不同。我的Agent默认只标记“确定性错误”,对于“疑似问题”会加个问号,让开发者自己判断。
最后,一定要有反馈机制。开发者可以对每条评论点“有用/无用”,这些数据可以用来优化规则引擎和LLM prompt。我每个月都会根据反馈调整一次规则库,效果比一开始好很多。
这个Agent上线后,我们团队的PR review时间从平均2小时降到了20分钟,而且线上bug率下降了40%。当然,它不能替代人的设计评审,但至少能让你在凌晨三点不被@醒。

480

被折叠的 条评论
为什么被折叠?



