在 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. **建议**: 数据库连接应使用上下文管理器
优化方向
- 支持更多语言:添加 TypeScript、Go、Java 等语言的 AST 解析
- 自定义规则:允许用户编写自定义审查规则
- CI 集成:对接 GitHub Actions、GitLab CI
- 报告可视化:生成 HTML 报告,支持高亮标记
- 增量审查:只审查变更的文件,节省时间
有了这个工具,代码 review 不再依赖别人,本地跑起来就能用。你的代码安全吗?试试今天的方法自己检查一下。
这篇文章对你有帮助吗?