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 тиждень







