🚀

用本地 LLM 做代码审查:不联网、不花钱、保护源代码

在 VPS 上搭一个本地代码审查工具,用 Ollama + LLaMA 3 自动检查代码质量、安全风险和代码风格,全程本地运行,零成本。

👁️ - 次阅读

你有没有遇到过这种情况:写了一段代码,觉得没问题,但上线后出 bug,回头一看全是低级错误。

请同事 review 太麻烦。用 GitHub Copilot 要联网收费。用在线 AI 工具,代码全传给别人了,数据安全没保障。

今天做一个本地代码审查工具。用 Ollama 跑 LLaMA 3,代码完全留在本地,自动检查代码质量、安全风险和风格问题。

项目背景

代码审查是保证代码质量的关键环节。但手动 review 太费时,特别是个人项目或者小团队。

AI 辅助代码审查已经很成熟,但大多数方案需要联网:

  • GitHub Copilot:要付费,代码上传
  • CodeRabbit:云端 API,数据外传
  • ChatGPT:免费但有隐私问题

本地部署一个代码审查工具,有几个明显优势:

  • 代码不外传,保护商业逻辑
  • 零 API 费用,长期运行无成本
  • 可自定义审查规则,适配团队规范
  • 离线可用,出差也能 review

技术选型

组件选择理由
LLM 引擎Ollama本地运行,模型自选
模型LLaMA 3 8B代码理解能力强,资源占用低
语言Python生态丰富,代码解析方便
代码分析AST 模块解析 Python 语法树,提取结构
输出Markdown可读性好,方便归档

Ollama 安装很简单:

curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3:8b

实现步骤

第一步:初始化项目结构

mkdir ai-code-reviewer
cd ai-code-reviewer
pip install ollama ast

第二步:编写代码审查核心模块

创建 code_reviewer.py:

#!/usr/bin/env python3
"""本地 AI 代码审查工具"""

import ast
import os
import sys
from pathlib import Path
from typing import List, Tuple

import ollama


# ============ 配置区 ============
MODEL = os.getenv("REVIEW_MODEL", "llama3:8b")
MIN_LINE_COUNT = int(os.getenv("MIN_LINE_COUNT", "5"))
MAX_TOKENS = int(os.getenv("MAX_TOKENS", "2000"))

REVIEW_PROMPT = """你是一位资深 Python 工程师,请对以下代码进行审查。

关注以下维度:
1. **安全性**:SQL 注入、路径遍历、硬编码密码等风险
2. **代码质量**:重复代码、过长函数、不良命名
3. **性能**:不必要的循环、内存泄漏、低效算法
4. **风格**:PEP 8 规范、注释缺失、类型注解

请用中文回复,格式如下:
- **问题列表**:每条问题一行,标注严重级别(严重/一般/建议)
- **优化建议**:给出具体的改进代码示例
- **总体评价**:一句话总结代码质量

代码:
```python
{code}

"""

def read_python_file(filepath: str) -> Tuple[List[str], int]: “““读取 Python 文件并返回代码行列表””” with open(filepath, “r”, encoding=“utf-8”) as f: lines = f.readlines() return lines, len(lines)

def validate_syntax(code_lines: List[str]) -> List[str]: “““检查 Python 语法错误””” errors = [] try: ast.parse("".join(code_lines)) except SyntaxError as e: errors.append(f"语法错误: 第 {e.lineno} 行 - {e.msg}") return errors

def detect_hardcoded_secrets(lines: List[str]) -> List[str]: “““检测硬编码的密钥和敏感信息””” patterns = [ (r’password\s*=\s*["'][^"']{5,}’, “疑似硬编码密码”), (r’api_key\s*=\s*["'][^"']{5,}’, “疑似硬编码 API Key”), (r’secret\s*=\s*["'][^"']{5,}’, “疑似硬编码密钥”), (r’PRIVATE_KEY\s*=\s*["']’, “疑似硬编码私钥”), ]

issues = []
for i, line in enumerate(lines, 1):
    for pattern, desc in patterns:
        if __import__('re').search(pattern, line, re.IGNORECASE):
            issues.append(f"第 {i} 行: {desc}")
return issues

def detect_sql_injection(lines: List[str]) -> List[str]: “““检测可能的 SQL 注入风险””” issues = [] for i, line in enumerate(lines, 1): # 简单的字符串拼接 SQL 检测 if import(’re’).search(r’(SELECT|INSERT|UPDATE|DELETE)\s+.%\s’, line, re.IGNORECASE): issues.append(f"第 {i} 行: 疑似使用 % 格式拼接 SQL") if import(’re’).search(r’(SELECT|INSERT|UPDATE|DELETE)\s+..format(’, line, re.IGNORECASE): issues.append(f"第 {i} 行: 疑似使用 format 拼接 SQL") if import(’re’).search(r’(SELECT|INSERT|UPDATE|DELETE)\s+.*f["']’, line, re.IGNORECASE): issues.append(f"第 {i} 行: 疑似使用 f-string 拼接 SQL") return issues

def analyze_structure(code_lines: List[str]) -> dict: “““分析代码结构(函数数、类数、圈复杂度)””” try: tree = ast.parse("".join(code_lines)) except SyntaxError: return {}

functions = [node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)]
classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)]

# 简单估算函数行数
func_lines = {}
for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef):
        end_line = getattr(node, 'end_lineno', node.lineno + 10)
        func_lines[node.name] = end_line - node.lineno

return {
    "function_count": len(functions),
    "class_count": len(classes),
    "functions": functions,
    "large_functions": {k: v for k, v in func_lines.items() if v > 50}
}

def review_with_llm(code: str) -> str: “““调用本地 LLM 进行代码审查””” prompt = REVIEW_PROMPT.format(code=code)

response = ollama.chat(
    model=MODEL,
    messages=[{"role": "user", "content": prompt}],
    options={
        "num_predict": MAX_TOKENS,
        "temperature": 0.3
    }
)
return response["message"]["content"]

def review_file(filepath: str) -> dict: “““审查单个 Python 文件””” lines, total_lines = read_python_file(filepath)

# 代码行数太少则跳过
if total_lines < MIN_LINE_COUNT:
    return {"skip": True, "reason": "代码行数过少"}

# 语法检查
syntax_errors = validate_syntax(lines)

# 安全检测
hardcoded_secrets = detect_hardcoded_secrets(lines)
sql_injections = detect_sql_injection(lines)

# 结构分析
structure = analyze_structure(lines)

# LLM 审查(代码截取前 2000 字符)
code_snippet = "".join(lines)[:2000]
llm_review = review_with_llm(code_snippet)

return {
    "filepath": filepath,
    "total_lines": total_lines,
    "syntax_errors": syntax_errors,
    "security_issues": hardcoded_secrets + sql_injections,
    "structure": structure,
    "llm_review": llm_review
}

def generate_report(results: List[dict]) -> str: “““生成审查报告””” report = ["# 代码审查报告\n", f"生成时间: {import(‘datetime’).datetime.now().strftime(’%Y-%m-%d %H:%M’)}"]

for result in results:
    if result.get("skip"):
        continue
    
    report.append(f"\n## {result['filepath']}\n")
    report.append(f"- 代码行数: {result['total_lines']}")
    
    if result["syntax_errors"]:
        report.append(f"\n**语法错误:**")
        for err in result["syntax_errors"]:
            report.append(f"- ❌ {err}")
    
    if result["security_issues"]:
        report.append(f"\n**安全隐患:**")
        for issue in result["security_issues"]:
            report.append(f"- ⚠️ {issue}")
    
    if result["structure"].get("large_functions"):
        report.append(f"\n**长函数警告:**")
        for func, lines in result["structure"]["large_functions"].items():
            report.append(f"- ⚠️ 函数 `{func}` 超过 50 行({lines} 行)")
    
    if result["llm_review"]:
        report.append(f"\n**AI 审查意见:**\n")
        report.append(result["llm_review"])

return "\n".join(report)

def main(): “““主入口””” if len(sys.argv) < 2: print(“用法: python code_reviewer.py <文件/目录> [–output report.md]”) print(“示例:”) print(" python code_reviewer.py myproject/") print(" python code_reviewer.py main.py –output report.md") sys.exit(1)

target = sys.argv[1]
output = None

if "--output" in sys.argv:
    idx = sys.argv.index("--output")
    if idx + 1 < len(sys.argv):
        output = sys.argv[idx + 1]

# 收集 Python 文件
if os.path.isfile(target):
    files = [target]
else:
    files = list(Path(target).rglob("*.py"))

if not files:
    print(f"未找到 Python 文件: {target}")
    sys.exit(1)

print(f"开始审查 {len(files)} 个文件...\n")

results = []
for filepath in files:
    filepath = str(filepath)
    print(f"审查: {filepath}")
    try:
        result = review_file(filepath)
        results.append(result)
    except Exception as e:
        results.append({"filepath": filepath, "error": str(e)})

# 生成报告
report = generate_report(results)

if output:
    with open(output, "w", encoding="utf-8") as f:
        f.write(report)
    print(f"\n报告已保存到: {output}")
else:
    print("\n" + "=" * 50)
    print(report)

if name == “main”: main()


### 第三步:运行审查

```bash
# 审查单个文件
python code_reviewer.py myproject/main.py

# 审查整个目录
python code_reviewer.py myproject/

# 保存报告
python code_reviewer.py myproject/ --output review_report.md

第四步:集成到开发流程

可以添加到 Git hook 中,提交前自动审查:

# .git/hooks/pre-commit
#!/bin/bash
python /path/to/code_reviewer.py . --output /tmp/code_review.md
cat /tmp/code_review.md

运行效果

审查一个包含 SQL 注入风险的示例文件:

# test_app.py
import sqlite3

def get_user(username):
    conn = sqlite3.connect("db.sqlite3")
    # 硬编码密码
    password = "admin123"
    # SQL 注入风险
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return conn.execute(query).fetchone()

审查结果:

# 代码审查报告
生成时间: 2026-08-08 10:30

## test_app.py
- 代码行数: 8

**语法错误:**
(无)

**安全隐患:**
- ⚠️ 第 5 行: 疑似硬编码密码
- ⚠️ 第 7 行: 疑似使用 f-string 拼接 SQL

**AI 审查意见:**
1. **严重**: 第 5 行硬编码密码,应使用环境变量
2. **严重**: 第 7 行存在 SQL 注入风险,应使用参数化查询
3. **建议**: 函数缺少类型注解和 docstring
4. **建议**: 数据库连接应使用上下文管理器

优化方向

  1. 支持更多语言:添加 TypeScript、Go、Java 等语言的 AST 解析
  2. 自定义规则:允许用户编写自定义审查规则
  3. CI 集成:对接 GitHub Actions、GitLab CI
  4. 报告可视化:生成 HTML 报告,支持高亮标记
  5. 增量审查:只审查变更的文件,节省时间

有了这个工具,代码 review 不再依赖别人,本地跑起来就能用。你的代码安全吗?试试今天的方法自己检查一下。

这篇文章对你有帮助吗?