AI-система автоматического код-ревью
Автоматизированное код-ревью не заменяет архитектурное review — оно убирает механическую часть: проверку стиля, очевидных уязвимостей, покрытия тестами, нарушений принятых паттернов. Senior-разработчик тратит 15–20% времени на ревью; большую часть этого времени уходит на комментарии типа "здесь нет обработки ошибок" или "переменная называется data, назови конкретнее". AI-система берёт этот слой на себя.
Компоненты системы
Diff Analyzer — получает GitHub/GitLab webhook с PR diff, разбирает изменения по файлам.
Code Analyzer — LLM-агент с инструментами: поиск по кодовой базе, чтение связанных файлов, запуск статического анализа.
Review Generator — формирует структурированные комментарии с указанием строк, severity, и предложением исправления.
PR Commenter — публикует комментарии через GitHub/GitLab API на конкретные строки.
Review агент с инструментами
from anthropic import Anthropic
from github import Github
import subprocess
import json
from dataclasses import dataclass
from typing import Literal
client = Anthropic()
gh = Github("GITHUB_TOKEN")
@dataclass
class ReviewComment:
file: str
line: int
severity: Literal["critical", "warning", "suggestion", "nitpick"]
category: Literal["security", "performance", "style", "logic", "test_coverage", "error_handling"]
comment: str
suggestion: str | None = None
def get_pr_diff(repo_name: str, pr_number: int) -> dict:
"""Получает diff PR по файлам"""
repo = gh.get_repo(repo_name)
pr = repo.get_pull(pr_number)
files = {}
for f in pr.get_files():
files[f.filename] = {
"patch": f.patch,
"additions": f.additions,
"deletions": f.deletions,
"status": f.status, # added/modified/removed
}
return files
def run_static_analysis(code: str, language: str) -> str:
"""Запускает статический анализатор"""
if language == "python":
result = subprocess.run(
["ruff", "check", "--select=ALL", "-"],
input=code.encode(),
capture_output=True,
)
return result.stdout.decode()[:2000]
return ""
def read_related_file(file_path: str) -> str:
"""Читает связанный файл для контекста"""
try:
with open(file_path) as f:
return f.read()[:3000]
except FileNotFoundError:
return f"File {file_path} not found"
REVIEW_TOOLS = [
{
"name": "run_static_analysis",
"description": "Run static analysis (ruff/eslint/etc) on code",
"input_schema": {
"type": "object",
"properties": {
"code": {"type": "string"},
"language": {"type": "string", "enum": ["python", "typescript", "go"]}
},
"required": ["code", "language"]
}
},
{
"name": "read_related_file",
"description": "Read a related file to understand context (models, tests, etc)",
"input_schema": {
"type": "object",
"properties": {"file_path": {"type": "string"}},
"required": ["file_path"]
}
}
]
def review_file(filename: str, diff: str, full_code: str = None) -> list[ReviewComment]:
"""Проводит ревью одного файла"""
messages = [{
"role": "user",
"content": f"""Review this code change. File: {filename}
Diff:
{diff}
{f"Full file content:{chr(10)}```{chr(10)}{full_code}{chr(10)}```" if full_code else ""}
Use tools to run static analysis and read related files if needed.
Then return a JSON array of review comments with this structure:
{{
"comments": [
{{
"line": <line_number_in_diff>,
"severity": "critical|warning|suggestion|nitpick",
"category": "security|performance|style|logic|test_coverage|error_handling",
"comment": "<explanation>",
"suggestion": "<code suggestion if applicable>"
}}
]
}}"""
}]
# Agentic loop
while True:
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=REVIEW_TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
# Извлекаем JSON из ответа
text = response.content[-1].text
try:
data = json.loads(text[text.find("{"):text.rfind("}") + 1])
return [ReviewComment(file=filename, **c) for c in data.get("comments", [])]
except Exception:
return []
# Обрабатываем tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use":
if block.name == "run_static_analysis":
result = run_static_analysis(**block.input)
elif block.name == "read_related_file":
result = read_related_file(**block.input)
else:
result = "Unknown tool"
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
Публикация комментариев в PR
def post_review(repo_name: str, pr_number: int, comments: list[ReviewComment]):
"""Публикует review комментарии в GitHub PR"""
repo = gh.get_repo(repo_name)
pr = repo.get_pull(pr_number)
# Получаем commit для привязки комментариев
commit = list(pr.get_commits())[-1]
review_comments = []
critical_count = 0
for comment in comments:
if comment.severity == "critical":
critical_count += 1
body = f"**[{comment.severity.upper()}]** `{comment.category}`\n\n{comment.comment}"
if comment.suggestion:
body += f"\n\n**Suggestion:**\n```python\n{comment.suggestion}\n```"
review_comments.append({
"path": comment.file,
"line": comment.line,
"body": body,
})
# Общий статус review
if critical_count > 0:
event = "REQUEST_CHANGES"
body = f"AI Review: найдено {critical_count} критических проблем. Требуется исправление."
elif len([c for c in comments if c.severity == "warning"]) > 3:
event = "REQUEST_CHANGES"
body = f"AI Review: найдено {len(comments)} замечаний, из них {critical_count} критических."
else:
event = "COMMENT"
body = f"AI Review: найдено {len(comments)} несущественных замечаний."
pr.create_review(
commit=commit,
body=body,
event=event,
comments=review_comments,
)
Специализированные чекеры
LLM хорошо видит логические ошибки, но для паттерн-матчинга эффективнее специализированные проверки:
import ast
import re
class SecurityChecker:
"""Проверяет код на типичные уязвимости"""
DANGEROUS_FUNCTIONS = {"eval", "exec", "compile", "pickle.loads", "yaml.load"}
SQL_INJECTION_PATTERNS = [
r'execute\s*\(\s*[f"\']', # f-string в execute()
r'\.format\s*\(', # .format() в SQL
r'%\s*\(', # % в SQL запросе
]
def check_python(self, code: str) -> list[dict]:
issues = []
try:
tree = ast.parse(code)
except SyntaxError:
return issues
# Проверяем вызовы опасных функций
for node in ast.walk(tree):
if isinstance(node, ast.Call):
func_name = ""
if isinstance(node.func, ast.Name):
func_name = node.func.id
elif isinstance(node.func, ast.Attribute):
func_name = f"{node.func.value.id if isinstance(node.func.value, ast.Name) else ''}.{node.func.attr}"
if func_name in self.DANGEROUS_FUNCTIONS:
issues.append({
"line": node.lineno,
"severity": "critical",
"category": "security",
"comment": f"Использование {func_name} потенциально опасно",
})
# SQL injection patterns
for pattern in self.SQL_INJECTION_PATTERNS:
for match in re.finditer(pattern, code):
line_no = code[:match.start()].count("\n") + 1
issues.append({
"line": line_no,
"severity": "critical",
"category": "security",
"comment": "Возможная SQL-инъекция: используй параметризованные запросы",
})
return issues
GitHub Actions интеграция
# .github/workflows/ai-review.yml
name: AI Code Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run AI Review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install anthropic pygithub ruff
python scripts/ai_review.py \
--repo "${{ github.repository }}" \
--pr "${{ github.event.pull_request.number }}"
Практический кейс: команда 8 разработчиков
Проблема: senior-разработчик тратил 6–8 часов в неделю на ревью. 40% комментариев — повторяющиеся замечания (нет error handling, хардкод конфигурации, не написаны тесты).
Внедрение AI Review:
- Критические проблемы (безопасность, явные баги): блокируют merge
- Предупреждения: показываются, но не блокируют
- Nitpicks: опциональный чеклист
Результаты:
- Механических комментариев от senior: -71%
- Среднее время до первого ревью: 4 часа → 3 минуты (AI срабатывает сразу)
- Количество багов, дошедших до production: -34%
- Время senior на ревью: 7 часов → 2 часа (только архитектурные решения)
Важный вывод: AI-система находила реальные баги в 23% PR — не просто стилевые замечания.
Сроки
- Базовый review с постингом в GitHub: 3–5 дней
- Специализированные чекеры безопасности + статический анализ: 1 неделя
- Тонкая настройка под конвенции проекта: 1–2 недели
- Интеграция в CI/CD с политиками merge: 1 неделя







