ИИ в жизненном цикле разработки: кодинг-агенты, ревью, тесты, документация
Это последняя статья трека, и она про то, ради чего большинство читателей вообще пришли: как применить всё предыдущее к собственной работе программиста. Промптинг, структурированный вывод, RAG, агенты, оценка, безопасность — всё это сходится в одной точке, где ИИ встречается с git-репозиторием, CI и живым тимлидом, который должен нажать «Merge».
Сразу зафиксируем позицию, которую статья будет защищать: ИИ в SDLC — это не про генерацию кода, это про сокращение цикла обратной связи. Модель, которая пишет функцию, экономит вам 20 минут. Модель, которая находит гонку в конкурентном коде за пять минут до мержа, экономит вам двое суток отладки в проде. Это разные порядки величины, и оптимизировать надо второе.
Что на самом деле показывают измерения
Начнём с неудобного, потому что маркетинг вокруг темы плотный, а данные — противоречивые.
Позитивная сторона. Контролируемый эксперимент GitHub на 95 разработчиках (Peng et al., arXiv:2302.06590) показал ускорение на 55.8% на задаче «написать HTTP-сервер на JavaScript». Внутреннее исследование Google по нейросетевому автодополнению (arXiv:2205.06537) зафиксировало сокращение времени итерации кодирования примерно на 6% при доле принятых подсказок около 25–34%.
Негативная сторона. RCT от METR в 2025 году (arXiv:2507.09089) взял 16 опытных контрибьюторов крупных open-source проектов и 246 реальных задач в их собственных репозиториях. Результат: с ИИ-инструментами разработчики выполняли задачи на 19% медленнее, при этом субъективно оценивали своё ускорение в +20%. Разрыв между ощущением и фактом — 39 процентных пунктов.
Системная сторона. Отчёт DORA о состоянии DevOps 2024 года зафиксировал, что рост внедрения ИИ на 25% ассоциирован со снижением стабильности релизов на 7.2% и лёгким снижением пропускной способности доставки. В отчётах 2025 года формулировка мягче — ИИ выступает усилителем: сильные команды становятся сильнее, слабые — быстрее генерируют мусор.
Как это примирить? Три наблюдения объясняют почти весь разброс:
- Знакомство с кодовой базой. GitHub-эксперимент — greenfield-задача, где у человека нет преимущества контекста. METR — зрелые репозитории, где контрибьютор держит архитектуру в голове, а агент вынужден её реконструировать. Чем больше неявного контекста в задаче, тем меньше выигрыш.
- Верифицируемость. Если результат проверяется за секунды (компилятор, тесты, типы), генерация окупается. Если проверка требует получаса чтения — вы обменяли написание кода на ревью чужого кода, и это редко выгодная сделка.
- Радиус поражения. Ошибка в скрипте миграции CSV стоит ничего. Ошибка в расчёте прав доступа стоит инцидента.
Отсюда практическое правило приоритизации задач для агента:
Квадрант 4 (сильная верификация, большой радиус) — это не «нельзя», это «обязателен воспроизводящий тест до правки». Квадрант 2 — единственный, где ответ «не автоматизируем».
Анатомия кодинг-агента
Кодинг-агент — это ReAct-цикл из статьи об агентах, у которого набор инструментов заточен под файловую систему и shell. Технически там четыре компонента, и почти вся разница между инструментами — в них:
| Компонент | Что делает | Где ломается |
|---|---|---|
| Харнесс | цикл «модель → инструмент → результат → модель», управление контекстом, компактизация | переполнение контекста на 40-й итерации, потеря цели |
| Инструменты | read, write, edit, bash, glob, grep, иногда браузер и LSP |
edit без проверки актуальности файла затирает чужие правки |
| Контекст репозитория | AGENTS.md/CLAUDE.md, README, схемы БД, конвенции |
устаревшие инструкции хуже, чем их отсутствие |
| Песочница | контейнер/VM, права на сеть и запись | агент с прод-креденшелами в переменных окружения |
Полезно понимать, что выбор «агентности» — это trade-off, а не улучшение. Работа Agentless (arXiv:2407.01489) показала, что простой трёхшаговый пайплайн «локализация → правка → валидация» без свободного цикла решает значимую долю SWE-bench дешевле полноценных агентов. Свободный цикл выигрывает там, где нужна разведка, и проигрывает там, где структура задачи известна заранее.
написать падающий тест"] C --> D{"Тест падает
по нужной причине?"} D -- "нет" --> C D -- "да" --> E B -- "да" --> E["Локализация: grep/glob/LSP
сузить до 1-5 файлов"] E --> F["План правки
(отдельный вызов, дешёвая модель)"] F --> G["Правка: edit с проверкой mtime"] G --> H["Верификация:
типы → линтер → юнит → интеграция"] H --> I{"Зелёный?"} I -- "нет, попытка < 4" --> J["Диагноз по stderr,
вернуть в контекст"] J --> G I -- "нет, попытка = 4" --> K["Стоп. Отдать человеку
diff + лог попыток"] I -- "да" --> L["Мутационная проверка:
тест реально ловит баг?"] L --> M["PR с описанием
и ссылкой на трассу"] style K fill:#7a3a34,stroke:#5a2a26,color:#f0f0f0 style M fill:#4f8f6a,stroke:#2f6a48,color:#f0f0f0
Два узла здесь принципиальны и чаще всего отсутствуют в самодельных пайплайнах.
Узел «Стоп после N попыток». Агент без лимита итераций на красных тестах входит в деградационную спираль: правит симптом, ломает соседнее, правит соседнее, ломает исходное. К седьмой итерации диф вырастает втрое и содержит # TODO: временный обход. Жёсткий лимит в 3–5 попыток с обязательной эскалацией — самое дешёвое улучшение качества, которое можно внести.
Узел «Мутационная проверка». Об этом ниже, в разделе про тесты.
Контекст репозитория: файл инструкций
Единственный артефакт, который даёт наибольший прирост качества на единицу усилий, — это файл инструкций в корне репозитория. Сложился общий формат: AGENTS.md (agents.md) поддерживается большинством инструментов, CLAUDE.md — специфичен для Claude Code (документация).
Принцип написания — тот же, что в основах промптинга: описывайте то, что нельзя вывести из кода. Инструкция «используй TypeScript» бесполезна — агент увидит tsconfig.json. Инструкция «в этом репозитории any запрещён линтером, но в legacy/ он разрешён исторически, не трогай» — бесценна.
# AGENTS.md
## Команды
- Установка: `pnpm install --frozen-lockfile`
- Тесты одного файла: `pnpm vitest run path/to/file.test.ts` (НЕ `pnpm test` — он
поднимает docker-compose и идёт 6 минут)
- Типы: `pnpm tsc --noEmit`
- Линтер с автофиксом: `pnpm biome check --write`
## Порядок проверки перед коммитом
1. `pnpm tsc --noEmit`
2. `pnpm biome check`
3. Тесты только затронутых пакетов
## Архитектурные ограничения
- `packages/domain` не импортирует ничего из `packages/infra`. Это проверяется
правилом `import/no-restricted-paths`. Если нужен доступ к БД из домена — вы
решаете задачу неправильно, спросите.
- Все запросы к БД идут через репозитории в `packages/infra/repositories`.
Прямой вызов `db.query` вне этой папки — ошибка ревью.
- Миграции пишутся вручную, никогда не генерируются. Файл идемпотентен.
## Чего не делать
- Не добавлять зависимости без явной просьбы. Если кажется, что нужна
библиотека — предложи в описании PR, не ставь.
- Не переформатировать файлы, которые не относятся к задаче: это раздувает диф
и убивает `git blame`.
- Не писать комментарии, пересказывающие код (`// увеличиваем счётчик`).
- В `legacy/` не рефакторить ничего попутно. Только точечные правки.
## Тесты
- Фреймворк — vitest, стиль — AAA, без моков там, где можно поднять реальный
объект. Моки только для сети и времени.
- Каждый багфикс начинается с теста, который падает на текущем коде.
Три эмпирических правила по этому файлу:
- Держите его под 150 строк. Он попадает в каждый запрос. Файл на 800 строк — это и деньги (см. ниже), и разбавление важных инструкций неважными. То, что нужно редко, выносите в отдельные файлы и упоминайте одной строкой: «конвенции API описаны в
docs/api-conventions.md, прочитай перед правкой роутов». - Формулируйте запреты через последствия. «Не используй
console.log» модель нарушит. «console.logвpackages/serverроняет прод-логгер, используйlogger.debug» — нет. - Обновляйте его после каждого раза, когда агент ошибся системно. Заметили, что агент трижды за неделю запускал полный тестовый прогон вместо точечного — это строчка в файле, а не повод жаловаться на модель.
Агент в CI: изоляция и права
Как только агент начинает запускаться без человека за клавиатурой (по вебхуку на issue, по расписанию, по метке на PR), включаются все риски из статьи о безопасности. Ключевой сценарий: текст issue — это недоверенный ввод. Любой внешний пользователь, открывающий issue в публичном репозитории, пишет напрямую в контекст вашего агента.
(нет метки, автор вне allowlist) Триаж --> Песочница: задача принята Песочница --> Работа: контейнер поднят,
сеть по allowlist,
секретов нет Работа --> Работа: цикл правка → тесты
(лимит: 5 итераций, 30 мин, $4) Работа --> Провал: лимит исчерпан Работа --> ПроверкаДифа: тесты зелёные ПроверкаДифа --> Провал: тронуты запрещённые пути
(.github/, Dockerfile, *.lock) ПроверкаДифа --> ПроверкаДифа2: пути ок ПроверkaДифа2 --> Провал: новые зависимости
не в реестре ПроверkaДифа2 --> PR: чисто PR --> ЧеловекРевью: PR открыт от бот-аккаунта,
CI-секреты недоступны ЧеловекРевью --> Мерж: одобрено ЧеловекРевью --> Работа: запрошены правки ЧеловекРевью --> Закрыто: отклонено Провал --> [*]: комментарий с логом Отклонено --> [*] Мерж --> [*] Закрыто --> [*]
Минимальный набор границ, который стоит завести до первого автономного запуска:
| Граница | Реализация | Что предотвращает |
|---|---|---|
| Сеть | egress-allowlist: реестр пакетов, API модели, ничего больше | эксфильтрацию кода и секретов |
| Секреты | в контейнере агента их нет вообще; деплой-ключи только у отдельного джоба | утечку через curl в промпт-инъекции |
| Файловая система | том только с рабочей копией, --read-only на остальное |
правку CI-конфигов |
| Git | пуш только в ветку agent/*, запрет force-push, PR всегда от бота |
обход ревью |
| Пути | явный deny-list в проверке дифа | подмену .github/workflows/* |
| Бюджет | лимит по токенам, времени и итерациям | бесконечный цикл на 300 $ |
Вот проверка дифа, которая ставится в CI как отдельный шаг перед созданием PR:
"""Гейт для дифа, произведённого агентом. Запускается ДО открытия PR.
Сложность: O(D + P·G), где D — размер дифа в строках, P — число изменённых
файлов, G — число glob-паттернов. На практике доли секунды.
"""
from __future__ import annotations
import fnmatch
import json
import re
import subprocess
import sys
from dataclasses import dataclass
# Пути, которые агент не имеет права трогать ни при каких обстоятельствах:
# правка любого из них превращает разовую компрометацию в постоянную.
FORBIDDEN_PATHS = [
".github/**", # workflow может выкачать секреты
"**/Dockerfile", # смена базового образа
"**/*.lock", # подмена версий транзитивных зависимостей
"infra/**", # терраформ
"**/.env*",
"scripts/deploy*",
]
# Максимальный размер дифа: большой диф невозможно отревьюить, а значит
# он будет одобрен формально. Лучше заставить агента разбить задачу.
MAX_CHANGED_LINES = 400
MAX_CHANGED_FILES = 20
# Регулярки на подозрительные конструкции, появившиеся В ДОБАВЛЕННЫХ строках.
SUSPICIOUS = {
"отключение проверки типов": re.compile(r"@ts-(ignore|nocheck)|# type: ignore"),
"пропуск теста": re.compile(r"\.(skip|only)\s*\(|@pytest\.mark\.skip|t\.Skip\("),
"подавление линтера": re.compile(r"eslint-disable(?!-next-line-no-console)|noqa\s*$"),
"сетевой вызов в тестах": re.compile(r"https?://(?!localhost|127\.0\.0\.1)"),
"захардкоженный секрет": re.compile(r"(?i)(api[_-]?key|secret|token)\s*[=:]\s*['\"][A-Za-z0-9/+_-]{16,}"),
}
@dataclass
class Finding:
severity: str # "block" | "warn"
rule: str
detail: str
def changed_files(base: str, head: str) -> list[str]:
out = subprocess.run(
["git", "diff", "--name-only", f"{base}...{head}"],
capture_output=True, text=True, check=True,
)
return [line for line in out.stdout.splitlines() if line]
def added_lines(base: str, head: str) -> list[tuple[str, str]]:
"""Возвращает пары (файл, добавленная строка). Удалённые строки нас не
интересуют: подавление линтера опасно только при добавлении."""
out = subprocess.run(
["git", "diff", "--unified=0", f"{base}...{head}"],
capture_output=True, text=True, check=True,
)
result, current = [], "?"
for line in out.stdout.splitlines():
if line.startswith("+++ b/"):
current = line[6:]
elif line.startswith("+") and not line.startswith("+++"):
result.append((current, line[1:]))
return result
def check(base: str, head: str) -> list[Finding]:
findings: list[Finding] = []
files = changed_files(base, head)
for path in files:
for pattern in FORBIDDEN_PATHS:
if fnmatch.fnmatch(path, pattern):
findings.append(Finding("block", "forbidden-path", f"{path} ~ {pattern}"))
if len(files) > MAX_CHANGED_FILES:
findings.append(Finding("block", "diff-too-wide", f"{len(files)} файлов"))
adds = added_lines(base, head)
if len(adds) > MAX_CHANGED_LINES:
findings.append(Finding("block", "diff-too-large", f"{len(adds)} добавленных строк"))
for path, line in adds:
for rule, rx in SUSPICIOUS.items():
if rx.search(line):
sev = "block" if rule == "захардкоженный секрет" else "warn"
findings.append(Finding(sev, rule, f"{path}: {line.strip()[:90]}"))
return findings
if __name__ == "__main__":
base, head = sys.argv[1], sys.argv[2]
findings = check(base, head)
print(json.dumps([f.__dict__ for f in findings], ensure_ascii=False, indent=2))
sys.exit(1 if any(f.severity == "block" for f in findings) else 0)
Обратите внимание на правило пропуск теста. Это самый частый способ, которым агент «чинит» падающий тест: не разобравшись в причине, он помечает тест как skip и отчитывается об успехе. Гейт ловит это за 50 миллисекунд, а человек на ревью — примерно никогда.
ИИ-ревью: точность важнее полноты
Автоматический ревьюер — самое выгодное применение ИИ в SDLC по соотношению «польза / риск», потому что он не пишет в репозиторий. Худшее, что он может сделать, — потратить чужое внимание. Что, впрочем, тоже способ убить инструмент.
Динамика простая и наблюдается везде: ревьюер с точностью 20% через две недели перестают читать, и его эффективный recall падает до нуля. Ревьюер с точностью 60–70%, дающий 5 комментариев вместо 40, приносит реальную пользу. Оптимизируйте precision, а не recall.
Как этого добиться технически:
- Резко ограничьте набор категорий. Не «найди проблемы», а конкретный список: гонки, утечка ресурсов, необработанный
null, ошибка в границах цикла, нарушение инварианта, изменение публичного контракта без версии. Стилистика — работа линтера, не модели. - Требуйте сценарий отказа. Комментарий, к которому модель не может приложить конкретный вход и конкретное неверное поведение, отбрасывается автоматически. Это отсеивает бо́льшую часть «а вдруг тут будет проблема с производительностью».
- Дайте достаточный контекст. Ревью по одному дифу без окружающих файлов даёт много ложных срабатываний вида «переменная не проверена на null» — а она проверена тремя строками выше границы ханка.
- Отбрасывайте всё, что уже покрыто другим инструментом. Если у вас в CI есть
mypy --strict, комментарии про типы — чистый шум.
изменённых функций Ctx->>Ctx: + определения вызываемых
символов (через LSP) Ctx->>Ctx: + AGENTS.md, свежие
инциденты по этим файлам Ctx-->>LLM: промпт (кэшируемый префикс:
инструкции + конвенции) LLM-->>V: JSON findings[]
с confidence и failure_scenario V->>V: отбросить confidence < 0.7 V->>V: отбросить дубликаты линтера V->>V: отбросить строки вне диффа V->>LLM: перепроверка выживших
«подтверди или отзови» LLM-->>V: CONFIRMED / RETRACTED V->>PR: 0-6 инлайн-комментариев Dev->>PR: 👍 исправил / 👎 ложное PR->>CI: реакция пишется в метрики Note over CI: fix rate — единственная
метрика, которой стоит верить
Шаг перепроверки (двойной проход) — недорогой и заметно поднимает точность: модель, которой предъявлен собственный вывод с вопросом «это точно баг, покажи вход», отзывает значительную часть находок. Это применение chain-of-verification из статьи о продвинутых техниках.
Промпт ревьюера, который работает на практике:
Ты ревьюишь diff в репозитории <name>. Твоя задача — найти дефекты, которые
приведут к неправильному поведению в рантайме. Ты НЕ комментируешь стиль,
именование, форматирование, отсутствие комментариев и предпочтения по
архитектуре — это делают другие инструменты и люди.
Категории, которые ищешь (только эти):
1. Некорректность: неверная логика, ошибка на границе, неверное условие,
перепутанные аргументы, неучтённый случай.
2. Конкурентность: гонка, взаимоблокировка, разделяемое состояние без
синхронизации, неатомарная проверка-и-действие.
3. Ресурсы: незакрытый файл/соединение/курсор, утечка на пути ошибки.
4. Контракты: изменение публичного API без версии, нарушение инварианта,
заявленного в докстринге или типе.
5. Данные: запрос без индекса в горячем пути, N+1, отсутствие лимита на
размер выборки.
Для КАЖДОЙ находки обязательно приведи:
- file, line
- summary: одно предложение, что именно сломано
- failure_scenario: конкретные входные данные или последовательность событий
→ конкретный неверный результат или падение
- confidence: 0.0-1.0
Если ты не можешь написать конкретный failure_scenario — эта находка не
существует, не включай её.
Ограничение: не более 6 находок. Если кандидатов больше — оставь самые
серьёзные. Пустой список — нормальный и частый ответ.
Ключевые слова здесь — «Пустой список — нормальный и частый ответ». Без явного разрешения ничего не найти модель будет находить всегда, потому что структура задачи подталкивает к выдаче непустого результата.
Схема вывода — обычный structured output из соответствующей статьи:
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
const Finding = z.object({
file: z.string(),
line: z.number().int(),
category: z.enum(["correctness", "concurrency", "resource", "contract", "data"]),
summary: z.string(),
failure_scenario: z.string(),
confidence: z.number(),
});
const ReviewResult = z.object({ findings: z.array(Finding) });
const client = new Anthropic();
export async function review(diffContext: string) {
const response = await client.messages.parse({
model: "claude-opus-4-8",
max_tokens: 8000,
thinking: { type: "adaptive" },
// Стабильный префикс кэшируется: инструкции ревьюера + конвенции репозитория
// не меняются между PR, а платить за них каждый раз незачем.
system: [
{ type: "text", text: REVIEWER_PROMPT, cache_control: { type: "ephemeral" } },
],
messages: [{ role: "user", content: diffContext }],
output_config: { format: zodOutputFormat(ReviewResult) },
});
const findings = response.parsed_output?.findings ?? [];
// Порог откалиброван по разметке: см. раздел про метрики ниже.
// Не выдумывайте 0.7 из головы — измерьте на своих 100 PR.
return findings.filter((f) => f.confidence >= 0.7).slice(0, 6);
}
Как понять, что ревьюер работает
Единственная честная метрика — fix rate: доля комментариев, после которых в тот же PR приехала правка соответствующих строк. Её можно считать полностью автоматически, без разметки:
"""Fix rate ИИ-ревьюера: доля комментариев, приведших к правке кода.
Сложность: O(C · L), где C — число комментариев, L — средний размер
последующего дифа. Считается раз в сутки батчем по закрытым PR.
"""
from dataclasses import dataclass
@dataclass
class Comment:
pr: int
file: str
line: int
category: str
confidence: float
resolved_by_change: bool # строки в радиусе ±3 изменились после комментария
thumbs_down: bool # разработчик явно отметил как ложное
def fix_rate(comments: list[Comment], bucket) -> dict[str, tuple[float, int]]:
"""Возвращает {ключ: (fix_rate, объём выборки)}."""
agg: dict[str, list[Comment]] = {}
for c in comments:
agg.setdefault(bucket(c), []).append(c)
result = {}
for key, group in agg.items():
fixed = sum(1 for c in group if c.resolved_by_change and not c.thumbs_down)
result[key] = (fixed / len(group), len(group))
return result
# Разрез по категориям показывает, что именно отключить.
by_category = fix_rate(all_comments, lambda c: c.category)
# Разрез по децилям confidence даёт порог отсечения.
by_confidence = fix_rate(all_comments, lambda c: f"{int(c.confidence * 10) / 10:.1f}")
Типичная картина после первого замера на реальном проекте: correctness даёт fix rate 45–60%, concurrency — 30–40% при малом объёме, data (N+1, индексы) — 50%+, а какая-нибудь категория «читаемость», если её оставили, — 5%. Вывод очевиден: категорию выключают. По confidence обычно видно чёткий обрыв — ниже него комментарии не публикуются.
Отдельно: не пытайтесь мерить recall на реальных PR. Для этого надо знать все дефекты, а если бы вы их знали, ревьюер был бы не нужен. Приближение — «доля багов, дошедших до прода, которые ревьюер видел в дифе и промолчал», считается постфактум при разборе инцидентов и обычно удручает. Это нормально: ревьюер — один слой из пяти, см. раздел про уровни оценки.
Тесты: главная ловушка
Генерация тестов — то место, где ИИ выглядит убедительнее всего и обманывает чаще всего. Проблема формулируется одной фразой: модель, которая видит реализацию, пишет тесты, повторяющие реализацию, а не спецификацию.
Классический пример. Дана функция:
def apply_discount(price: int, percent: int) -> int:
return price - price * percent // 100
Здесь баг: при percent > 100 цена уходит в минус, а целочисленное деление округляет вниз, что при последовательном применении скидок даёт расхождение с бухгалтерией. Модель, которой показали реализацию, честно напишет:
def test_apply_discount():
assert apply_discount(1000, 10) == 900
assert apply_discount(1000, 0) == 1000
assert apply_discount(1000, 100) == 0
Тест зелёный. Покрытие 100%. Баг на месте. Тест закрепил ошибку как ожидаемое поведение — теперь при попытке исправить функцию упадёт тест, и следующий разработчик «починит» тест.
Три приёма, которые ломают этот паттерн:
1. Спецификация вместо реализации. Давайте модели докстринг, тип и описание требований — но не тело функции. Это буквально свойство эксперимента: попросите написать тесты по сигнатуре и требованиям, потом покажите реализацию отдельным шагом и спросите «какие из этих тестов падают».
2. Property-based тестирование. Модели хорошо даётся формулировка инвариантов, а поиск контрпримеров вы отдаёте машине. Для Python это Hypothesis:
from hypothesis import given, strategies as st
@given(
price=st.integers(min_value=0, max_value=10**9),
percent=st.integers(min_value=0, max_value=100),
)
def test_discount_invariants(price: int, percent: int) -> None:
result = apply_discount(price, percent)
# Инвариант 1: скидка не делает цену отрицательной.
assert result >= 0
# Инвариант 2: скидка не увеличивает цену.
assert result <= price
# Инвариант 3: монотонность по проценту.
if percent < 100:
assert apply_discount(price, percent + 1) <= result
Hypothesis найдёт price=1, percent=100 → 0 и границы за секунды. Модель никогда бы не сгенерировала именно эти входы; ей и не надо — она сгенерировала свойство.
3. Мутационное тестирование как приёмка сгенерированных тестов. Это ответ на вопрос «а тест вообще что-нибудь ловит». Инструмент вносит в код мелкие мутации (+ → -, < → <=, return x → return None) и проверяет, что тесты падают. Выживший мутант = дыра в тестах. Инструменты: mutmut и cosmic-ray для Python, Stryker для JS/TS/C#, PIT для Java.
Мутационное тестирование дорогое: O(M · T), где M — число мутантов (сотни на модуль), T — время прогона тестов. Полный прогон на большом репозитории — часы. Поэтому его запускают инкрементально, только на изменённых файлах, и именно в этом качестве оно идеально дополняет генерацию тестов ИИ:
# .github/workflows/mutation-gate.yml
name: mutation gate
on:
pull_request:
paths: ['src/**']
jobs:
mutate:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Изменённые модули
id: changed
run: |
FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD \
-- 'src/**/*.py' | tr '\n' ' ')
echo "files=$FILES" >> "$GITHUB_OUTPUT"
- name: Мутационный прогон только по ним
if: steps.changed.outputs.files != ''
run: |
mutmut run --paths-to-mutate "${{ steps.changed.outputs.files }}" \
--runner "pytest -x -q" || true
mutmut results --format json > mutants.json
- name: Гейт по доле выживших
if: steps.changed.outputs.files != ''
run: |
python - <<'PY'
import json, sys
data = json.load(open("mutants.json"))
killed = sum(1 for m in data if m["status"] == "killed")
total = sum(1 for m in data if m["status"] in ("killed", "survived"))
if total == 0:
sys.exit(0)
score = killed / total
print(f"mutation score: {score:.1%} ({killed}/{total})")
# Порог 0.6 — стартовое значение. Поднимайте по мере вычищения
# эквивалентных мутантов, иначе гейт будет ложно краснеть.
sys.exit(0 if score >= 0.6 else 1)
PY
Практическая рекомендация по порогу: начинайте с 0.5–0.6 и только для новых файлов. Мутационный балл на легаси-коде без предварительной чистки эквивалентных мутантов (мутаций, не меняющих семантику) создаёт постоянный ложный красный, и гейт отключат через неделю.
Что ещё хорошо получается у моделей в тестировании и почти не имеет подвохов:
- Миграция тестов между фреймворками (jest → vitest, unittest → pytest). Сильная верификация: старый и новый набор должны дать одинаковый результат.
- Диагностика флейков. Дать модели 30 логов упавшего теста и попросить найти общий признак — она находит зависимость от порядка выполнения или от часового пояса быстрее человека.
- Генерация фикстур и тестовых данных, особенно граничных: пустая строка, юникод-суррогаты, отрицательный ноль,
2024-02-29, строка длиной ровно в лимит поля.
Документация: боремся с расхождением
Документация — задача с самой слабой верификацией во всём SDLC. У неё нет компилятора. Именно поэтому ИИ здесь одновременно очень полезен и очень опасен: он охотно производит правдоподобный текст, который никто не проверит.
Разделим по типам — удобно взять классификацию Diátaxis: туториалы, how-to, справочник, объяснения.
| Тип | Отдавать ИИ? | Почему |
|---|---|---|
| Справочник (API, параметры, схемы) | Да, с генерацией из кода | Извлекается из типов и сигнатур, проверяемо |
| How-to («как настроить SSO») | Черновик да, публикация после прогона | Проверяется исполнением: шаги должны работать |
| Туториал | Черновик, обязательная переработка | Требует понимания, что новичку неочевидно |
| Объяснения («почему выбрали Kafka») | Нет | Это знание в головах, модель его выдумает |
Три рабочих применения:
Докстринги и комментарии «почему». Просите не пересказ кода, а контракт: предусловия, постусловия, что бросает, чего не гарантирует. Формулировка промпта решает всё:
Напиши докстринг для этой функции. Требования:
- Первая строка: что функция делает, в повелительном наклонении, одна строка.
- Секция Raises: все исключения, которые могут выйти наружу, включая
пробрасываемые из вызываемых функций.
- Секция Notes: только неочевидное — почему выбран именно этот подход, какие
предположения о входных данных не проверяются, чем чревато их нарушение.
- НЕ описывай параметры, чьи имена и типы говорят сами за себя.
- НЕ пиши "Returns: результат вычисления". Если возвращаемое значение
очевидно из типа, секцию Returns опусти.
Если в коде есть места, назначение которых тебе непонятно, перечисли их
отдельным списком ВОПРОСЫ, а не выдумывай объяснение.
Последний пункт — самый важный. Он превращает галлюцинацию в вопрос. Разработчик отвечает на три вопроса за минуту, и это ровно то знание, которого нет в коде.
Черновики ADR. Architecture Decision Records — формат, который отлично ложится на структурированный вывод: контекст, рассмотренные варианты, решение, последствия. Модель хорошо генерирует раздел «рассмотренные альтернативы» (она знает про существование альтернатив, о которых вы забыли) и плохо — «решение» (не знает ваших ограничений). Практика: разработчик пишет две строки решения, модель разворачивает контекст и альтернативы, разработчик вычёркивает неверное.
Детекция расхождения. Самое недооценённое применение. Вместо генерации документации — проверка её актуальности в CI:
"""Детектор расхождения кода и документации.
Идея: если файл кода изменился, а упоминающая его документация — нет,
это кандидат на устаревание. Модель проверяет, действительно ли изменение
затронуло то, что описано в документе.
Сложность: O(F · D) на построение индекса упоминаний, где F — файлы кода,
D — документы. Строится инкрементально и кэшируется.
"""
import anthropic
client = anthropic.Anthropic()
PROMPT = """Ниже изменение в коде и фрагмент документации, который на этот
код ссылается.
Ответь строго одним словом:
- STALE — документация теперь описывает поведение, которого нет в коде.
- OK — изменение не затрагивает то, что описано в документе.
Не предлагай улучшений. Не комментируй стиль. Опечатки, неточности стиля и
устаревшие ссылки — не твоя задача. Только фактическое противоречие между
описанным и реализованным поведением.
<изменение>
{diff}
</изменение>
<документация файл="{doc_path}">
{doc_excerpt}
</документация>"""
def is_stale(diff: str, doc_path: str, doc_excerpt: str) -> bool:
r = client.messages.create(
model="claude-haiku-4-5", # бинарная классификация — дешёвая модель
max_tokens=8,
messages=[{
"role": "user",
"content": PROMPT.format(diff=diff, doc_path=doc_path, doc_excerpt=doc_excerpt),
}],
)
text = next((b.text for b in r.content if b.type == "text"), "")
return text.strip().upper().startswith("STALE")
Это дёшево (одна классификация на пару «изменённый файл × документ»), запускается на каждом PR и ловит ровно тот класс проблем, который не ловит ничто другое: документация, которая была верна вчера.
Экономика
Разговор про ИИ в SDLC без цифр расходов — разговор ни о чём. Соберём модель для команды из 20 разработчиков, ~150 PR в месяц.
Опорные цены (Claude API, актуальный прайс): Opus 4.8 — 5 $ / 25 $ за миллион входных / выходных токенов, Sonnet 5 — 3 $ / $15, Haiku 4.5 — 1 $ / $5. Чтение из кэша — примерно 0.1× от входной цены, запись в кэш — 1.25×.
| Сценарий | Токены на единицу | Цена за единицу | В месяц |
|---|---|---|---|
| ИИ-ревью PR (Opus, кэш системного префикса) | ~40k вход / 3k выход | ~0.10 $ | ~15 $ |
| Детектор расхождения docs (Haiku) | ~8k вход / 0.02k выход | ~0.01 $ | ~5 $ |
| Кодинг-агент, средняя задача (Opus, 15 итераций) | ~600k вход / 40k выход | ~2 $–4 | ~300 $ при 100 задачах |
| Триаж инцидента (Sonnet, логи) | ~150k вход / 5k выход | ~0.5 $ | ~20 $ |
| Интерактивная работа разработчика | 3–8M вход / 100k выход в день | 5 $–20 в день | ~2000 $–8000 |
Читается сразу: интерактивная работа доминирует в счёте на порядок. Автоматические пайплайны в CI стоят копейки. Это обратно тому, чего боятся команды при внедрении, — обычно опасаются «агент в CI сожрёт бюджет», а сжирает бюджет обычный ежедневный чат с моделью.
Рычаги оптимизации по убыванию эффекта:
- Кэширование префикса. Инструкции ревьюера +
AGENTS.md+ конвенции — стабильный префикс на 10–30k токенов. Без кэша он оплачивается на каждом запросе, с кэшем — 0.1×. Экономия на ревью — до 80% входной стоимости. Детали — в статье про продакшен. - Роутинг по модели. Классификация (stale/ok, тип коммита, срочность инцидента) — Haiku. Ревью и агентная работа — Opus. Смешивать бинарную классификацию и написание кода в одной модели — переплата в 5 раз на половине трафика.
- Уровень усилий. Параметр
effortдля рассуждающих моделей:lowна классификации,high/xhighна агентной работе. Разница по токенам рассуждения — кратная. - Лимит контекста агента. Агент, которому дали
grepпо монорепозиторию без ограничений, вытянет 300k токенов мусора. Инструменты должны обрезать вывод:head -c 4000на результат grep, лимит на число файлов. - Жёсткий бюджет на задачу. Лимит по токенам на одну агентную задачу с остановкой и эскалацией. Задача, съевшая бюджет, почти никогда не решается на 30-й итерации — она решается человеком.
Типичные ошибки
Мерить принятие подсказок вместо результата. «У нас 35% подсказок принимаются» — не метрика. Принятая подсказка, которую через час переписали, — отрицательный результат. Мерьте время от открытия PR до мержа, долю PR, вернувшихся на доработку, частоту откатов, время до восстановления. То есть DORA-метрики, которые у вас, вероятно, уже есть.
Отдавать агенту плохо сформулированную задачу. Тикет «поправить экспорт, он глючит» превращает агента в генератор случайных дифов. Правило: если задачу нельзя объяснить джуну за пять минут так, чтобы он начал работу, агенту её отдавать рано. Хорошая новость — формулировку задачи тоже можно делать с моделью, в диалоге, и это часто более выгодное её применение, чем написание кода.
Позволять агенту менять тесты в рамках багфикса. Правка кода и правка тестов должны быть разными шагами с разными разрешениями. Если агент может трогать и то и другое, вероятность, что «зелёный CI» получен подгонкой теста, очень высока. Технически: тест пишется первым, коммитится отдельно, дальше файл теста в deny-list на шаге правки.
Считать сгенерированный код бесплатным. Отчёты GitClear о качестве кода фиксируют рост дублирования и падение доли «перемещённых строк» (прокси для рефакторинга) по мере роста доли ИИ-кода. Механика понятна: скопировать блок дешевле, чем выделить абстракцию, а модель не платит за копирование. Противоядие — явное правило в файле инструкций и внимание ревьюера к дубликатам, вплоть до jscpd в CI.
Игнорировать галлюцинированные зависимости. Модели выдумывают несуществующие пакеты — исследование «We Have a Package for You!» (arXiv:2406.10279) намерило заметную долю галлюцинированных имён пакетов, причём повторяющихся между запусками. Это делает атаку практичной: злоумышленник регистрирует часто галлюцинируемое имя (slopsquatting). Защита элементарна и обязательна: любое новое имя в package.json/requirements.txt из ИИ-дифа проверяется на существование, возраст и число загрузок до установки.
Автономность без эскалации. Агент, который не умеет сказать «я не понимаю задачу», будет производить правдоподобный мусор до исчерпания бюджета. В системном промпте должно быть явно: «Если после двух попыток воспроизвести проблему не удалось — остановись и опиши, что ты пробовал и какой информации не хватает. Это правильный и ожидаемый исход, а не неудача».
Промпт-инъекция через недоверенный ввод. Issue от внешнего пользователя, содержимое зависимости, комментарий в чужом PR, ответ внешнего API — всё это попадает в контекст агента. Полный разбор — в статье о безопасности; минимум для SDLC — отсутствие секретов в песочнице, egress-allowlist и запрет на правку CI-конфигов.
Верить, что стало лучше, без замера. Возвращаемся к результату METR: субъективное ощущение ускорения расходилось с фактом на 39 п.п. у людей, которые сами участвовали в эксперименте. Ощущение продуктивности — не свидетельство. Если вы внедряете инструмент на команду, заложите двухнедельный замер до и после хотя бы по одной метрике цикла.
Как это выглядит в зрелой команде
Сводка того, что реально стоит внедрять, в порядке возрастания риска и убывания отдачи на усилие:
AGENTS.mdв корне. День работы, эффект на всё остальное.- ИИ-ревью с высоким порогом уверенности и метрикой fix rate. Неделя на настройку, окупается на первом же пойманном дефекте в проде.
- Детектор расхождения документации в CI. Пара часов, копейки по стоимости.
- Диагностика флейков и триаж инцидентов — модель на логах. Быстро и безопасно, потому что она ничего не пишет.
- Кодинг-агент в интерактивном режиме для задач из зон «сильная верификация». С человеком, который читает диф, а не пролистывает.
- Мутационный гейт на новых файлах — чтобы сгенерированные тесты не были декорацией.
- Автономный агент в CI по метке — последним, в песочнице, с гейтом на диф, только на классе задач, который вы уже прогнали руками десятки раз.
Три вопроса, которые стоит задавать любому предложению «давайте автоматизируем это ИИ»:
- Как проверяется результат и сколько это стоит? Если проверка дороже выполнения — не автоматизируем.
- Что произойдёт, если результат окажется неверным и это не заметят? Если ответ содержит слова «прод», «деньги» или «персональные данные» — только с человеком в цикле.
- Как мы узнаем через месяц, что стало лучше? Если ответа нет — это не внедрение, а эксперимент; так его и называйте, и заложите точку выхода.
Источники
- Peng et al., «The Impact of AI on Developer Productivity: Evidence from GitHub Copilot», arXiv:2302.06590 — RCT с ускорением 55.8% на greenfield-задаче.
- Becker et al. (METR), «Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity», arXiv:2507.09089 — замедление на 19% при субъективном ощущении ускорения.
- Tabachnyk & Nikolov, «Productivity assessment of neural code completion», arXiv:2205.06537 — измерения Google на реальном трафике IDE.
- DORA: State of DevOps Reports — влияние внедрения ИИ на пропускную способность и стабильность.
- Jimenez et al., «SWE-bench», arXiv:2310.06770 и Xia et al., «Agentless», arXiv:2407.01489 — измерение агентов на реальных багах и почему простой пайплайн часто не хуже.
- Wang et al., «OpenHands», arXiv:2407.16741 и Yang et al., «SWE-agent», arXiv:2405.15793 — устройство харнесса и роль интерфейса «агент — компьютер».
- Spracklen et al., «We Have a Package for You!», arXiv:2406.10279 — галлюцинации имён пакетов и slopsquatting.
- Perry et al., «Do Users Write More Insecure Code with AI Assistants?», arXiv:2211.03622 — эксперимент про уверенность и безопасность.
- GitClear, AI Assistant Code Quality Research — дублирование и падение рефакторинга.
- AGENTS.md и Claude Code: память и инструкции — форматы файла инструкций.
- Anthropic, Claude Code best practices — практики агентной разработки от разработчиков инструмента.
- Hypothesis, mutmut, Stryker Mutator, PIT — property-based и мутационное тестирование.
- Diátaxis и ADR — структура документации и записи архитектурных решений.
- OWASP Top 10 for LLM Applications — модель угроз для агентов с инструментами.
Что дальше
На этом трек «ИИ-агенты и prompt engineering» закончен: от токенов и промптов через RAG, агентов и MCP к оценке, безопасности и встраиванию всего этого в реальную разработку. Куда двигаться дальше — зависит от того, где у вас сейчас тоньше всего.
Если не хватает фундамента под самими моделями — идите в Машинное обучение и Нейронные сети. Если ИИ-система упирается в данные — Инженерия данных и Базы данных. Если она упирается в эксплуатацию — DevOps. Если в структуру самого приложения — Архитектурные паттерны и DDD. А если задача теперь не техническая, а «что и зачем мы вообще строим» — Продуктовый менеджмент.
Общая карта всех треков портала и рекомендуемые маршруты — в дорожной карте.