AI-агенты для SDLC: проверка кода, генерация тестов, автоматизация документации


title: "AI-агенты для SDLC: проверка кода, генерация тестов, автоматизация документации" slug: agents-sdlc-code-review-test-gen-2026-ru date: 2026-02-22 lang: ru

Ключевые факты

  • langchain 1.2.10: Фреймворк оркестрации агентов с инструментами, памятью и цепочками
  • langgraph 1.0.10: Фреймворк графов состояния для многошаговых рабочих потоков агентов с циклами и персистентностью
  • gitpython 3.1.46: Python-интерфейс Git для извлечения diff, истории коммитов и операций с ветками
  • anthropic 0.84.0: Клиент Claude API с поддержкой tool use и потоковых ответов
  • openai 2.24.0: Клиент GPT-4 API с функциональными вызовами и структурированными выходами
  • coverage.py 7.13.4: Инструмент измерения покрытия кода Python с поддержкой анализа веток и строк
  • pytest 9.0.2: Фреймворк тестирования с поддержкой фиксчур, параметризации и плагинов
  • tree-sitter 0.25.2: Инкрементальный парсер для анализа AST множества языков (Python, JavaScript, TypeScript, Rust, Go)
  • Python ast module: Встроенный парсер AST для извлечения сигнатур функций, docstring и потока управления
  • GitHub Actions API: Webhook-события на pull_request (opened, synchronize, reopened) запускают рабочие потоки проверки
  • Claude 3.7 Sonnet: Контекстное окно 200K токенов (примерно 150,000 слов или 400-600 Python-файлов по 250 строк каждый)
  • Оценка контекста: Средний Python-файл содержит 250 строк; 200K токенов вмещают примерно 500-800 файлов в зависимости от плотности комментариев
  • SWE-bench Verified: Claude 3.5 Sonnet достигает 49.0% на разрешении реальных GitHub-issues; GPT-4o — 38.8%
  • Latency tool calling: Типичный цикл агента с 3 инструментальными вызовами занимает 8-15 секунд end-to-end при 200ms на вызов API
  • Mutation testing: Инструменты mutmut или cosmic-ray внедряют небольшие изменения кода для проверки эффективности тестов
  • GitHub PR review API: POST /repos/{owner}/{repo}/pulls/{number}/reviews с встроенными комментариями на конкретные строки
  • AST node types для покрытия: ast.If, ast.ExceptHandler, ast.For, ast.While, ast.With для определения веток
  • Pre-commit hook latency: Локальная AI-проверка на staged diff занимает 5-10 секунд для малых изменений, блокирует коммит при критических проблемах
  • False positive rate: Enterprise-агенты проверки кода обычно достигают 10-30% ложноположительных результатов после настройки под соглашения проекта
  • Agent persistence: LangGraph поддерживает контрольные точки состояния в SQLite или Redis для возобновления долгоживущего анализа кода

Обзор SDLC-агентов

SDLC-агенты автоматизируют задачи контроля качества, объединяя большие языковые модели со структурированным инструментарием. В отличие от статического анализа (который применяет фиксированные правила) или простых LLM-промптов (которым не хватает контекста), агенты активно собирают информацию перед принятием решений.

Агенты проверки кода получают diff PR, читают содержимое всех файлов, проверяют связанные тесты, ищут использование символов по кодовой базе и генерируют структурированные комментарии с уровнями серьёзности (critical, warning, suggestion, praise) и опциональными предложениями по исправлениям.

Агенты генерации тестов анализируют отчёты покрытия для определения непокрытых веток, читают существующие тестовые файлы для изучения паттернов фиксчур и стилей утверждений, извлекают сигнатуры функций и docstring, затем генерируют pytest-тесты, которые органично интегрируются с конвенциями тестирования проекта.

Агенты генерации документации определяют отсутствующие docstring с помощью анализа AST, понимают поведение функции из реализации и тестов, создают docstring в стиле Google или NumPy с разделами Args, Returns и Raises, соответствующими стандартам проекта.

Агенты описания PR изучают историю коммитов после ветвления ветки, определяют затронутые модули, извлекают ссылки на трекеры задач и генерируют резюме, объясняющие что изменилось и почему, снижая когнитивную нагрузку на рецензентов.

Фреймворк принятия решений

Задача SDLC Использовать агента когда Использовать статический анализ когда
Проверка безопасности Ошибки, зависящие от логики (напр., обход авторизации, race conditions) Недостатки на основе паттернов (SQL injection с конкатенацией строк, hardcoded secrets)
Проверка кода Решения архитектуры, выявление граничных случаев, обратная связь по дизайну API Нарушения стиля, порядок импортов, длина строки, циклическая сложность
Генерация тестов Приоритизация пробелов покрытия, интеграция фиксчур, реалистичные данные тестов Базовые заготовки тестов, шаблоны параметризованных тестов
Документация Объяснение неочевидного поведения, примеры использования, рекомендации по обработке ошибок Генерация type hints, извлечение сигнатур функций
Рефакторинг Семантическое переименование (class → interface), миграция API по файлам Простой find-replace, автоматическая очистка импортов
Производительность Анализ сложности алгоритма, выявление bottleneck'ов в I/O Статические regex-паттерны (выявление N+1 запросов в коде ORM)

Статический анализ превосходит по скорости и консистентности для проверок на основе правил. Агенты превосходят в контекстзависимых решениях, требующих понимания бизнес-логики, конвенций проекта или взаимодействий между файлами.

Таблица параметров

Параметр Значение Примечания
model claude-opus-4-5, claude-sonnet-4-5, gpt-4o Opus для сложного рефакторинга; Sonnet для проверки кода; GPT-4o для совместимости с OpenAI
max_tokens 4096-16384 4K достаточно для проверки одного файла; 16K для мультифайлового рефакторинга с полным обновлённым кодом
temperature 0.0-0.2 Более низкая температура (0.0) для детерминированной проверки кода; 0.2 для генерации тестов с вариативностью
tools 4-8 инструментов на агента Проверка кода: get_file_content, get_related_tests, get_test_coverage, search_for_usages
timeout 120-300 секунд Timeout GitHub Actions workflow; локальные pre-commit hooks используют 30s timeout
context_window 200K токенов (Claude), 128K токенов (GPT-4) Зарезервируйте 20% для output; используйте контекст эффективно, загружая только изменённые файлы + связанные тесты
min_coverage_threshold 80% Quality gate блокирует слияние, если покрытие упадёт ниже порога после изменений PR
severity_weights critical: 1.0, warning: 0.8, suggestion: 0.5 Фильтруйте комментарии ниже порога для снижения шума; critical всегда постятся
max_tool_output_size 5000 символов Обрезайте результаты grep для предотвращения переполнения токенов; используйте head/tail для больших файлов
git_diff_context --unified=5 Показывайте 5 строк контекста выше/ниже изменений для лучшего понимания LLM
mutation_timeout 300 секунд Mutation testing может быть медленным; запускайте асинхронно или в nightly builds, не на каждом PR

Распространённые ошибки

Ошибка 1: Игнорирование конвенций проекта (ложноположительные результаты)

Воздействие: Инженеры игнорируют все комментарии агента после 5+ жалоб на стиль, которые противоречат стандартам команды.

Неправильно: Агент жалуется на однострочные if-утверждения, когда проект их постоянно использует:

# Агент неправильно жалуется: "Используйте явный блок для читаемости"
if not user: return None
if not is_valid(data): raise ValueError("Invalid")

Правильно: Загружайте конвенции проекта из существующих примеров кода и файлов конфигурации:

def load_project_conventions(repo_path: str) -> str:
    conventions = []
    for config_file in ['.editorconfig', 'pyproject.toml', '.pylintrc']:
        path = os.path.join(repo_path, config_file)
        if os.path.exists(path):
            with open(path) as f:
                conventions.append(f"# {config_file}\n{f.read()}")

    # Выберите 5 последних файлов для изучения паттернов
    result = subprocess.run(
        ["git", "log", "--name-only", "--format=", "-10"],
        cwd=repo_path, capture_output=True, text=True
    )
    sample_files = result.stdout.strip().split('\n')[:5]
    for f in sample_files:
        if f.endswith('.py'):
            with open(os.path.join(repo_path, f)) as fh:
                conventions.append(f"# Sample: {f}\n{fh.read()[:1000]}")
    return '\n\n'.join(conventions)

Ошибка 2: Проверка целых файлов вместо изменённых строк

Воздействие: Агент читает файл из 10,000 строк, когда PR изменяет 3 строки, впустую тратя 90% контекстного окна и увеличивая latency в 4 раза.

Неправильно: Загружайте полный файл для каждого изменённого файла:

for file in changed_files:
    content = get_file_content(file)  # Загружает весь файл
    messages.append({"role": "user", "content": content})

Правильно: Извлекайте только изменённые блоки с окружающим контекстом:

def get_changed_hunks(diff: str) -> list[dict]:
    hunks = []
    current_file = None
    for line in diff.split('\n'):
        if line.startswith('diff --git'):
            current_file = line.split()[-1].lstrip('b/')
        elif line.startswith('@@'):
            # Парсим хидер блока: @@ -old_start,old_count +new_start,new_count @@
            hunks.append({'file': current_file, 'context': line})
    return hunks

Ошибка 3: Блокировка PR без оценок уверенности

Воздействие: Инженеры используют --no-verify для обхода проверок агента после двух ложноположительных результатов, полностью поражая систему.

Неправильно: Жёсткая блокировка на любом комментарии с "critical" серьёзностью:

critical = [c for c in comments if c.severity == "critical"]
if critical:
    print("❌ BLOCKED: Critical issues found")
    sys.exit(1)

Правильно: Добавляйте оценки уверенности и блокируйте только высокоуверенные проблемы:

@dataclass
class ReviewComment:
    file: str
    line: int
    severity: str
    category: str
    message: str
    confidence: float  # 0.0-1.0
    suggestion: Optional[str] = None

def should_block_merge(comments: list[ReviewComment]) -> tuple[bool, str]:
    high_conf_critical = [c for c in comments
                          if c.severity == "critical" and c.confidence >= 0.85]
    if high_conf_critical:
        reasons = '\n'.join(f"  {c.file}:{c.line} {c.message}" for c in high_conf_critical)
        return True, f"High-confidence critical issues:\n{reasons}"
    return False, "OK"

Ошибка 4: Генерация тестов без их запуска

Воздействие: Сгенерированные тесты имеют ошибки импортов, неправильные имена фиксчур или некорректные утверждения, которые немедленно падают.

Неправильно: Пишите сгенерированные тесты прямо в файл без валидации:

test_code = generate_tests_for_file(source_file)
with open(test_file, 'w') as f:
    f.write(test_code)

Правильно: Запускайте сгенерированные тесты и итерируйте над ошибками:

def generate_and_validate_tests(source_file: str, max_attempts: int = 3) -> str:
    for attempt in range(max_attempts):
        test_code = generate_tests_for_file(source_file, previous_errors=errors if attempt > 0 else None)

        # Пишем в временный файл и запускаем
        with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
            f.write(test_code)
            temp_path = f.name

        result = subprocess.run(
            ["pytest", temp_path, "-v"],
            capture_output=True, text=True, timeout=30
        )

        if result.returncode == 0:
            return test_code  # Тесты прошли

        errors = result.stdout + result.stderr
        # Цикл повторит с ошибками как контекст

    raise Exception(f"Failed to generate valid tests after {max_attempts} attempts")

Ошибка 5: Без обрезки выходов инструментов

Воздействие: grep возвращает 50,000 строк совпадений, потребляя всё контекстное окно и вызывая timeout или отказ API.

Неправильно: Возвращайте полный вывод grep:

def search_for_usages(symbol: str) -> str:
    result = subprocess.run(
        ["grep", "-rn", symbol, "."],
        capture_output=True, text=True
    )
    return result.stdout  # Может быть мегабайты

Правильно: Обрезайте и предоставляйте резюме:

def search_for_usages(symbol: str, max_lines: int = 50) -> str:
    result = subprocess.run(
        ["grep", "-rn", symbol, ".", "--include=*.py"],
        capture_output=True, text=True
    )
    lines = result.stdout.split('\n')
    if len(lines) > max_lines:
        sample = '\n'.join(lines[:max_lines])
        return f"Found {len(lines)} usages (showing first {max_lines}):\n{sample}\n\n[... {len(lines) - max_lines} more usages truncated]"
    return result.stdout

Агент проверки кода PR

Production-агент проверки кода следует этому конвейеру: webhook-триггер → получение diff → сбор контекста через инструменты → анализ с LLM → постинг структурированных комментариев на GitHub.

Архитектура: GitHub Actions workflow прослушивает события pull_request (opened, synchronize), проверяет код с полной историей, запускает Python-скрипт, который вызывает Anthropic API с определениями инструментов, парсит структурированный JSON-ответ, постит встроенные комментарии через GitHub API.

Основная реализация

from anthropic import Anthropic
import subprocess
import json
import os
from dataclasses import dataclass, asdict
from typing import Optional

client = Anthropic()

@dataclass
class ReviewComment:
    file: str
    line: int
    severity: str  # critical, warning, suggestion, praise
    category: str  # security, logic, performance, style, test-coverage
    message: str
    confidence: float  # 0.0-1.0
    suggestion: Optional[str] = None

def get_pr_diff(repo_path: str, base_branch: str = "main") -> str:
    result = subprocess.run(
        ["git", "diff", f"origin/{base_branch}...HEAD", "--unified=5"],
        cwd=repo_path,
        capture_output=True,
        text=True
    )
    return result.stdout

def get_file_content(file_path: str, repo_path: str) -> str:
    full_path = os.path.join(repo_path, file_path)
    try:
        with open(full_path, 'r', encoding='utf-8') as f:
            return f.read()
    except FileNotFoundError:
        return f"File not found: {file_path}"
    except UnicodeDecodeError:
        return f"Binary file: {file_path}"

def get_test_coverage(file_path: str, repo_path: str) -> dict:
    result = subprocess.run(
        ["coverage", "json", "-o", "-"],
        cwd=repo_path,
        capture_output=True,
        text=True
    )
    try:
        data = json.loads(result.stdout)
        file_data = data.get('files', {}).get(file_path, {})
        return {
            'percent_covered': file_data.get('summary', {}).get('percent_covered', 0),
            'missing_lines': file_data.get('missing_lines', []),
            'excluded_lines': file_data.get('excluded_lines', [])
        }
    except:
        return {'error': 'Coverage data not available'}

def get_related_tests(file_path: str, repo_path: str) -> str:
    base_name = os.path.basename(file_path).replace('.py', '')
    result = subprocess.run(
        ["find", "tests/", "-name", f"*{base_name}*"],
        cwd=repo_path,
        capture_output=True,
        text=True
    )
    test_files = [f for f in result.stdout.strip().split('\n') if f]

    if not test_files:
        return "No related test files found."

    content = []
    for test_file in test_files[:3]:  # Ограничьте 3 тестовыми файлами
        test_content = get_file_content(test_file, repo_path)
        content.append(f"# {test_file}\n{test_content[:2000]}")  # Первые 2000 символов

    return '\n\n'.join(content)

def search_for_usages(symbol: str, repo_path: str, max_lines: int = 50) -> str:
    result = subprocess.run(
        ["grep", "-rn", symbol, ".", "--include=*.py"],
        cwd=repo_path,
        capture_output=True,
        text=True
    )
    lines = result.stdout.strip().split('\n')
    if len(lines) > max_lines:
        sample = '\n'.join(lines[:max_lines])
        return f"Found {len(lines)} usages (showing first {max_lines}):\n{sample}"
    return result.stdout

tools = [
    {
        "name": "get_file_content",
        "description": "Read the complete content of a file in the repository to understand full context.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Relative path from repo root"}
            },
            "required": ["file_path"]
        }
    },
    {
        "name": "get_related_tests",
        "description": "Find and return test files related to a source file to understand test coverage patterns.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Path to source file"}
            },
            "required": ["file_path"]
        }
    },
    {
        "name": "get_test_coverage",
        "description": "Get test coverage percentage and uncovered line numbers for a specific file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Path to source file"}
            },
            "required": ["file_path"]
        }
    },
    {
        "name": "search_for_usages",
        "description": "Search codebase for usages of a function, class, or variable to understand impact.",
        "input_schema": {
            "type": "object",
            "properties": {
                "symbol": {"type": "string", "description": "Function or class name"}
            },
            "required": ["symbol"]
        }
    }
]

def run_review_agent(diff: str, repo_path: str, conventions: str = "") -> list[ReviewComment]:
    messages = [
        {
            "role": "user",
            "content": f"""You are performing a thorough code review. Here is the diff:

```diff
{diff}

Project conventions: {conventions}

Review this code for:

  1. Security vulnerabilities (SQL injection, XSS, hardcoded secrets, SSRF, insecure deserialization)
  2. Logic errors and edge cases (off-by-one, null pointer, race conditions)
  3. Performance issues (N+1 queries, inefficient algorithms, memory leaks)
  4. Missing error handling (uncaught exceptions, silent failures)
  5. Test coverage gaps (new code without tests, missing edge case tests)

Use available tools to gather full file context, related tests, coverage data, and usage patterns before making judgments.

For each issue found, provide a confidence score (0.0-1.0) indicating certainty:

  • 0.9-1.0: Definite bug or security issue
  • 0.7-0.9: Likely issue requiring human review
  • 0.5-0.7: Suggestion based on best practices
  • Below 0.5: Optional style/readability comment

Return your review as a JSON array of ReviewComment objects: [ {{ "file": "path/to/file.py", "line": 42, "severity": "critical", "category": "security", "message": "SQL injection vulnerability: user input concatenated into query", "confidence": 0.95, "suggestion": "Use parameterized query: cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))" }} ]

Only include comments with confidence >= 0.6. Provide actionable suggestions with code examples where possible.""" } ]

while True:
    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=8192,
        tools=tools,
        messages=messages
    )

    if response.stop_reason == "tool_use":
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input, repo_path)
                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})

    elif response.stop_reason == "end_turn":
        for block in response.content:
            if hasattr(block, 'text'):
                try:
                    text = block.text
                    start = text.find('[')
                    end = text.rfind(']') + 1
                    if start >= 0 and end > start:
                        comments_data = json.loads(text[start:end])
                        return [ReviewComment(**c) for c in comments_data]
                except (json.JSONDecodeError, TypeError) as e:
                    print(f"Failed to parse review response: {e}")
                    return []
        return []
    else:
        break

return []

def execute_tool(tool_name: str, tool_input: dict, repo_path: str) -> str: if tool_name == "get_file_content": return get_file_content(tool_input["file_path"], repo_path) elif tool_name == "get_related_tests": return get_related_tests(tool_input["file_path"], repo_path) elif tool_name == "get_test_coverage": coverage = get_test_coverage(tool_input["file_path"], repo_path) return json.dumps(coverage, indent=2) elif tool_name == "search_for_usages": return search_for_usages(tool_input["symbol"], repo_path) return "Tool not found"


### Постинг комментариев на GitHub

```python
import requests

def post_review_to_github(
    repo: str,
    pr_number: int,
    comments: list[ReviewComment],
    token: str
):
    url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28"
    }

    # Группируйте комментарии по файлам
    review_comments = []
    for c in comments:
        review_comments.append({
            "path": c.file,
            "line": c.line,
            "body": f"**[{c.severity.upper()}]** {c.category}\n\n{c.message}\n\nConfidence: {c.confidence:.0%}" + (f"\n\n```suggestion\n{c.suggestion}\n```" if c.suggestion else "")
        })

    review_body = {
        "body": f"AI Code Review - {len(comments)} issues found",
        "event": "COMMENT",
        "comments": review_comments
    }

    response = requests.post(url, headers=headers, json=review_body)
    response.raise_for_status()
    return response.json()

Агент генерации тестов

Агенты генерации тестов анализируют пробелы покрытия, изучают паттерны тестирования проекта и генерируют pytest-тесты, которые интегрируются с существующими фиксчурами и стилями утверждений.

Подход, управляемый покрытием: Запускайте существующий набор тестов с coverage.py, определяйте непокрытые ветки с помощью анализа AST, читайте связанные тестовые файлы для понимания паттернов фиксчур, генерируйте тесты, нацеленные на конкретные непокрытые пути.

Реализация

import ast
import coverage
from pathlib import Path

def extract_branches(source_code: str, filename: str) -> list[dict]:
    tree = ast.parse(source_code, filename=filename)
    branches = []

    for node in ast.walk(tree):
        if isinstance(node, ast.If):
            branches.append({
                "type": "if",
                "line": node.lineno,
                "condition": ast.unparse(node.test),
                "has_else": len(node.orelse) > 0
            })
        elif isinstance(node, ast.ExceptHandler):
            exception_type = ast.unparse(node.type) if node.type else "Exception"
            branches.append({
                "type": "exception",
                "line": node.lineno,
                "exception_type": exception_type
            })
        elif isinstance(node, (ast.For, ast.While)):
            branches.append({
                "type": "loop",
                "line": node.lineno,
                "has_else": len(node.orelse) > 0
            })

    return branches

def get_uncovered_lines(file_path: str, repo_path: str) -> list[int]:
    cov = coverage.Coverage(data_file=os.path.join(repo_path, '.coverage'))
    cov.load()

    try:
        analysis = cov.analysis(file_path)
        return list(analysis[3])  # Возвращает номера пропущенных строк
    except coverage.misc.NoSource:
        return []

def generate_tests_for_file(
    source_file: str,
    repo_path: str,
    test_file: str = None
) -> str:
    source_content = get_file_content(source_file, repo_path)
    uncovered_lines = get_uncovered_lines(source_file, repo_path)
    branches = extract_branches(source_content, source_file)

    # Найдите существующие тесты для изучения паттернов
    if test_file is None:
        base_name = os.path.basename(source_file).replace('.py', '')
        test_file = f"tests/test_{base_name}.py"

    existing_tests = get_file_content(test_file, repo_path) if os.path.exists(os.path.join(repo_path, test_file)) else ""

    messages = [
        {
            "role": "user",
            "content": f"""Generate pytest tests for uncovered code paths in {source_file}.

Source file:
```python
{source_content}

Uncovered lines: {uncovered_lines}

Branches detected: {json.dumps(branches, indent=2)}

Existing tests (learn fixture patterns and assertion style):

{existing_tests[:3000]}

Requirements:

  1. Focus ONLY on untested branches and uncovered lines
  2. Match the existing test style exactly (fixture usage, assertion style, naming conventions)
  3. Include edge cases: empty inputs, None values, boundary conditions, exception paths
  4. Write realistic test data that matches domain context
  5. Add docstrings explaining what each test verifies

Output ONLY valid pytest code, no markdown fences or explanations.""" } ]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=8192,
    messages=messages
)

return response.content[0].text

### Интеграция с mutation testing

Mutation testing проверяет качество тестов путём внедрения небольших изменений кода (мутаций) и проверки того, что тесты их обнаруживают. Выжившие мутации указывают на слабые тесты.

```python
def run_mutation_analysis(source_file: str, repo_path: str) -> dict:
    result = subprocess.run(
        ["mutmut", "run", "--paths-to-mutate", source_file, "--runner", "pytest"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        timeout=300
    )

    results_result = subprocess.run(
        ["mutmut", "results"],
        cwd=repo_path,
        capture_output=True,
        text=True
    )

    survived = []
    for line in results_result.stdout.split('\n'):
        if 'survived' in line.lower():
            survived.append(line.strip())

    return {
        "survived_mutations": survived,
        "output": results_result.stdout
    }

def generate_tests_for_survived_mutations(
    source_file: str,
    survived_mutations: list[str],
    repo_path: str
) -> str:
    source_content = get_file_content(source_file, repo_path)

    prompt = f"""The following mutations survived in {source_file} (tests did not catch them):

{chr(10).join(survived_mutations)}

Source file:
```python
{source_content}

For each survived mutation, write a pytest test that would catch that specific mutation. Analyze what behavioral change the mutation represents and write a test verifying the ORIGINAL behavior.

Output ONLY pytest code."""

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=4096,
    messages=[{"role": "user", "content": prompt}]
)

return response.content[0].text

## Интеграция с CI/CD

GitHub Actions workflow запускает агента проверки кода на каждом событии PR:

```yaml
# .github/workflows/ai-code-review.yml
name: AI Code Review

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  ai-review:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for diff context

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install anthropic==0.84.0 gitpython==3.1.46 pytest==9.0.2 coverage==7.13.4
          pip install -r requirements.txt

      - name: Run existing tests with coverage
        run: |
          coverage run -m pytest tests/
          coverage json

      - name: AI Code Review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
        run: python scripts/ai_review.py
# scripts/ai_review.py
import os
import sys
from review_agent import run_review_agent, get_pr_diff, post_review_to_github, load_project_conventions

def main():
    repo = os.environ["REPO"]
    pr_number = int(os.environ["PR_NUMBER"])
    token = os.environ["GITHUB_TOKEN"]
    repo_path = os.getcwd()

    diff = get_pr_diff(repo_path, base_branch="main")

    if not diff.strip():
        print("No changes to review")
        return 0

    conventions = load_project_conventions(repo_path)
    comments = run_review_agent(diff, repo_path, conventions)

    # Фильтруйте только высокоуверенные комментарии
    filtered = [c for c in comments if c.confidence >= 0.7]

    if filtered:
        post_review_to_github(repo, pr_number, filtered, token)
        print(f"Posted {len(filtered)} review comments")

        critical = [c for c in filtered if c.severity == "critical" and c.confidence >= 0.85]
        if critical:
            print(f"❌ Found {len(critical)} high-confidence critical issues - consider addressing before merge")
            # Note: We don't block here, just warn
    else:
        print("No significant issues found")

    return 0

if __name__ == "__main__":
    sys.exit(main())

Производительность и бенчмарки

Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.

Время выполнения агента: Агенты проверки кода с 3-5 инструментальными вызовами завершаются за 8-15 секунд end-to-end. Агенты генерации тестов с анализом покрытия занимают 12-20 секунд. Агенты мультифайлового рефакторинга могут занимать 30-60 секунд в зависимости от размера кодовой базы.

Использование контекстного окна: Типичный Python-файл с 250 строками и умеренными комментариями потребляет примерно 400-500 токенов. Контекстное окно Claude в 200K токенов может вместить 400-500 полных файлов, но практические лимиты ниже (100-150 файлов) для резервирования места под выходы инструментов и ответы агентов.

False positive rate: После первоначальной настройки под конвенции проекта production-агенты проверки кода достигают 15-25% false positive rate. Это улучшается до 10-15% после 2-3 месяцев сбора обратной связи и доработки промпта. Статические анализаторы обычно достигают 5-10% false positive rate, но обнаруживают меньше типов проблем.

Улучшение покрытия: Агенты генерации тестов обычно увеличивают покрытие веток на 8-15 процентных пункта при запуске на кодовых базах с существующим 60-70% покрытием. Убывающая отдача происходит выше 85% покрытия, поскольку оставшиеся пробелы часто связаны с обработкой ошибок, сложной для триггеризации без мокирования.

SWE-bench производительность: На SWE-bench Verified (реальные GitHub-issues) Claude 3.5 Sonnet достигает 49.0% уровня разрешения, GPT-4o достигает 38.8%. Это представляет потолок возможностей для автономного исправления ошибок; production-агенты с guidance в цикле человека работают значительно лучше.

Стоимость GitHub Actions: Каждая проверка PR потребляет примерно 15,000-30,000 токенов (input + output). При цене Anthropic в $3 за миллион input-токенов и $15 за миллион output-токенов, стоимость за проверку составляет примерно $0.05-0.15. Для репозиториев со 100 PR/месяц месячная стоимость составляет $5-15.

Adoption pre-commit hook: Локальные pre-commit hooks с 30-секундным timeout достигают 40-60% adoption разработчиков в командах, которые изначально сопротивлялись CI-based review. Мгновенная обратная связь в момент коммита решает общие возражения о нарушении потока работы.