基于 LLM 的代码审查系统:用 RAG + AST 实现智能 Code Review 的工程探索
一、代码审查的"双输"困境:高级工程师的评审产出 vs 低级工程师的等待时间
团队中有 4 名高级工程师(Staff/Senior),每人每天要审查 8~12 个 MR(Merge Request)。更关键的是,MR 的等待队列均值是 3.2 小时,而其中约 60% 的评审意见是机械性的——"变量名不符合命名规范""缺少错误处理""SQL 语句可能存在注入风险"这类问题不需要资深工程师的直觉和经验,纯粹是规则的执行。
如果能用一个 LLM 系统自动处理这 60% 的机械性审查,将人工审查聚焦于"架构合理性""安全边界""性能隐含风险"等高阶问题,MR 周转时间至少可以减半。
二、AST 解析 + LLM 的双引擎架构
单纯的 LLM 拿到一段代码 diff 就去审查存在两个问题:上下文不足以理解代码意图,以及产生幻觉式建议。引入 AST 解析层做"结构化抽取",将代码变更转化为结构化的函数签名、类型定义和数据流:
# AST 解析层 —— 将代码 diff 转化为 LLM 友好的结构化表示 import ast from dataclasses import dataclass from typing import List, Optional @dataclass class FunctionChange: name: str # 函数名 signature: str # 函数签名(参数列表 + 返回类型) complexity: int # 圈复杂度(> 10 提出警告) added_lines: int # 新增行数 has_error_handling: bool # 是否包含异常处理 dependencies: List[str] # 调用了哪些外部函数 class CodeStructureExtractor: def analyze_diff(self, diff_content: str) -> dict: """将 Git diff 解析为结构化实体列表""" changed_files = self._parse_diff(diff_content) result = {"functions": [], "imports": [], "classes": []} for file_path, additions in changed_files.items(): try: tree = ast.parse("\n".join(additions)) for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): result["functions"].append(FunctionChange( name=node.name, signature=self._get_signature(node), complexity=self._cyclomatic_complexity(node), added_lines=len(additions), has_error_handling=self._has_try_except(node), dependencies=self._extract_calls(node) )) elif isinstance(node, (ast.Import, ast.ImportFrom)): result["imports"].append(ast.unparse(node)) except SyntaxError: continue # diff 片段可能不完整,跳过 return resultLLM 评审层接收结构化的分析结果和代码 diff,分维度给出评审意见:
REVIEW_PROMPT = """你是一位资深后端工程师,请对以下代码变更进行代码审查。 ## 代码变更上下文 {code_structure} ## Diff 内容 ```{language} {diff_content}审查维度(严格按此顺序)
- 正确性:逻辑是否正确?边界条件是否覆盖?
- 安全性:是否存在注入、越权、敏感信息泄露风险?
- 性能:是否存在不必要的内存分配、锁竞争、慢查询?
- 可维护性:命名是否自解释?函数是否过长?注释是否必要?
输出格式
每个问题用 JSON 输出:
{{
"issues": [
{{
"severity": "blocker|major|minor|nit",
"category": "正确性|安全性|性能|可维护性",
"file": "文件路径",
"line": 行号,
"title": "问题简述",
"description": "详细描述",
"suggestion": "修复建议(含代码示例)"
}}
],
"summary": "一句话总结"
}}
审查原则
- Blocker 级问题:会导致服务崩溃、数据丢失或安全漏洞
- 不要输出代码风格类建议(命名、空格等,由 linter 处理)
- 每条建议必须包含可执行的代码修复示例"""
def review_diff(structure: dict, diff: str, language: str) -> dict:
response = llm_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": REVIEW_PROMPT.format(
code_structure=json.dumps(structure, indent=2),
language=language,
diff_content=diff
)}],
temperature=0.1,
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
## 三、RAG 增强——让 LLM 参考团队的修复历史 相似问题在团队历史中往往已有修复方案。RAG 检索层查找与当前 MR 最相似的 3 个历史 MR: ```python class MRKnowledgeBase: def __init__(self): self.encoder = SentenceTransformer('microsoft/codebert-base') self.vector_db = chromadb.PersistentClient(path="./mr_kb") def index_mr(self, mr_id: str, diff_content: str, review_comments: list): """将已合并的 MR 索引到向量库""" embedding = self.encoder.encode(diff_content[:4096]) # 截断长 diff self.collection.add( ids=[mr_id], embeddings=[embedding.tolist()], metadatas=[{"comments": json.dumps(review_comments)}] ) def search_similar(self, diff: str, top_k: int = 3) -> list: embedding = self.encoder.encode(diff[:4096]) results = self.collection.query( query_embeddings=[embedding.tolist()], n_results=top_k ) # 提取历史 MR 的评审意见作为 few-shot 示例 return [json.loads(m["comments"]) for m in results["metadatas"][0]]四、效果评估与人工验收
30 天试点期间的数据:
| 指标 | 人工审查 | LLM + 人工 |
|---|---|---|
| MR 平均等待时间 | 3.2h | 0.8h |
| 机械性问题拦截率 | — | 94% |
| 遗漏率(已知问题未被发现) | 15% | 12% |
| 首次审查通过率 | 42% | 58% |
| 高级工程师日审核量 | 10 MR | 5 MR |
LLM 的遗漏率(12%)甚至低于纯人工(15%),原因在于 AST 解析 + RAG 覆盖了一些人工容易疲劳漏掉的问题(如深层嵌套中的 SQL 拼接)。
五、总结
LLM 代码审查系统的关键设计点:
- AST 解析是 LLM 的前置过滤器:先结构化抽取(函数签名、复杂度、依赖),再交给 LLM 分析。纯 diff 输入会让 LLM "迷失"在语法噪音中;
- "60% 机械性审查自动化"是合理的目标线:风格、规范、常规安全检查交给 LLM,架构和性能的深度审查留给人工。过度自动化会引入危险的安全盲区;
- RAG 的 few-shot 参考价值远超预期:历史相似 MR 的修复方案被检索到后,LLM 给出的建议与团队风格一致性从 45% 提升到 78%;
- 遗漏率比通过率更重要:优化的首要指标是"有没有安全问题被漏掉",而不是"审查通过了多少个 MR"。LLM 的 12% 遗漏率仍然不可接受,每条 Blocker 必须有双重检查。
落地建议:先做"只推荐不拦截"模式(LLM 建议 + 人工裁决),运行 2 周后切换为"自动拦截 style 和 lint 问题"。