ИИ-агенты и prompt engineering ИИ в жизненном цикле разработки: кодинг-агенты, ревью, тесты, документация
0%

ИИ в жизненном цикле разработки: кодинг-агенты, ревью, тесты, документация

ИИ в жизненном цикле разработки: кодинг-агенты, ревью, тесты, документация

Это последняя статья трека, и она про то, ради чего большинство читателей вообще пришли: как применить всё предыдущее к собственной работе программиста. Промптинг, структурированный вывод, 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 года формулировка мягче — ИИ выступает усилителем: сильные команды становятся сильнее, слабые — быстрее генерируют мусор.

Как это примирить? Три наблюдения объясняют почти весь разброс:

  1. Знакомство с кодовой базой. GitHub-эксперимент — greenfield-задача, где у человека нет преимущества контекста. METR — зрелые репозитории, где контрибьютор держит архитектуру в голове, а агент вынужден её реконструировать. Чем больше неявного контекста в задаче, тем меньше выигрыш.
  2. Верифицируемость. Если результат проверяется за секунды (компилятор, тесты, типы), генерация окупается. Если проверка требует получаса чтения — вы обменяли написание кода на ревью чужого кода, и это редко выгодная сделка.
  3. Радиус поражения. Ошибка в скрипте миграции CSV стоит ничего. Ошибка в расчёте прав доступа стоит инцидента.

Стадии SDLC, стоимость дефекта и точки вмешательства ИИ

Отсюда практическое правило приоритизации задач для агента:

Квадрант 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 дешевле полноценных агентов. Свободный цикл выигрывает там, где нужна разведка, и проигрывает там, где структура задачи известна заранее.

Два узла здесь принципиальны и чаще всего отсутствуют в самодельных пайплайнах.

Узел «Стоп после 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 в публичном репозитории, пишет напрямую в контекст вашего агента.

Минимальный набор границ, который стоит завести до первого автономного запуска:

Граница Реализация Что предотвращает
Сеть 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.

Как этого добиться технически:

  1. Резко ограничьте набор категорий. Не «найди проблемы», а конкретный список: гонки, утечка ресурсов, необработанный null, ошибка в границах цикла, нарушение инварианта, изменение публичного контракта без версии. Стилистика — работа линтера, не модели.
  2. Требуйте сценарий отказа. Комментарий, к которому модель не может приложить конкретный вход и конкретное неверное поведение, отбрасывается автоматически. Это отсеивает бо́льшую часть «а вдруг тут будет проблема с производительностью».
  3. Дайте достаточный контекст. Ревью по одному дифу без окружающих файлов даёт много ложных срабатываний вида «переменная не проверена на null» — а она проверена тремя строками выше границы ханка.
  4. Отбрасывайте всё, что уже покрыто другим инструментом. Если у вас в CI есть mypy --strict, комментарии про типы — чистый шум.

Шаг перепроверки (двойной проход) — недорогой и заметно поднимает точность: модель, которой предъявлен собственный вывод с вопросом «это точно баг, покажи вход», отзывает значительную часть находок. Это применение 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 xreturn 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 сожрёт бюджет», а сжирает бюджет обычный ежедневный чат с моделью.

Рычаги оптимизации по убыванию эффекта:

  1. Кэширование префикса. Инструкции ревьюера + AGENTS.md + конвенции — стабильный префикс на 10–30k токенов. Без кэша он оплачивается на каждом запросе, с кэшем — 0.1×. Экономия на ревью — до 80% входной стоимости. Детали — в статье про продакшен.
  2. Роутинг по модели. Классификация (stale/ok, тип коммита, срочность инцидента) — Haiku. Ревью и агентная работа — Opus. Смешивать бинарную классификацию и написание кода в одной модели — переплата в 5 раз на половине трафика.
  3. Уровень усилий. Параметр effort для рассуждающих моделей: low на классификации, high/xhigh на агентной работе. Разница по токенам рассуждения — кратная.
  4. Лимит контекста агента. Агент, которому дали grep по монорепозиторию без ограничений, вытянет 300k токенов мусора. Инструменты должны обрезать вывод: head -c 4000 на результат grep, лимит на число файлов.
  5. Жёсткий бюджет на задачу. Лимит по токенам на одну агентную задачу с остановкой и эскалацией. Задача, съевшая бюджет, почти никогда не решается на 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 п.п. у людей, которые сами участвовали в эксперименте. Ощущение продуктивности — не свидетельство. Если вы внедряете инструмент на команду, заложите двухнедельный замер до и после хотя бы по одной метрике цикла.

Как это выглядит в зрелой команде

Сводка того, что реально стоит внедрять, в порядке возрастания риска и убывания отдачи на усилие:

  1. AGENTS.md в корне. День работы, эффект на всё остальное.
  2. ИИ-ревью с высоким порогом уверенности и метрикой fix rate. Неделя на настройку, окупается на первом же пойманном дефекте в проде.
  3. Детектор расхождения документации в CI. Пара часов, копейки по стоимости.
  4. Диагностика флейков и триаж инцидентов — модель на логах. Быстро и безопасно, потому что она ничего не пишет.
  5. Кодинг-агент в интерактивном режиме для задач из зон «сильная верификация». С человеком, который читает диф, а не пролистывает.
  6. Мутационный гейт на новых файлах — чтобы сгенерированные тесты не были декорацией.
  7. Автономный агент в CI по метке — последним, в песочнице, с гейтом на диф, только на классе задач, который вы уже прогнали руками десятки раз.

Три вопроса, которые стоит задавать любому предложению «давайте автоматизируем это ИИ»:

  • Как проверяется результат и сколько это стоит? Если проверка дороже выполнения — не автоматизируем.
  • Что произойдёт, если результат окажется неверным и это не заметят? Если ответ содержит слова «прод», «деньги» или «персональные данные» — только с человеком в цикле.
  • Как мы узнаем через месяц, что стало лучше? Если ответа нет — это не внедрение, а эксперимент; так его и называйте, и заложите точку выхода.

Источники

Что дальше

На этом трек «ИИ-агенты и prompt engineering» закончен: от токенов и промптов через RAG, агентов и MCP к оценке, безопасности и встраиванию всего этого в реальную разработку. Куда двигаться дальше — зависит от того, где у вас сейчас тоньше всего.

Если не хватает фундамента под самими моделями — идите в Машинное обучение и Нейронные сети. Если ИИ-система упирается в данные — Инженерия данных и Базы данных. Если она упирается в эксплуатацию — DevOps. Если в структуру самого приложения — Архитектурные паттерны и DDD. А если задача теперь не техническая, а «что и зачем мы вообще строим» — Продуктовый менеджмент.

Общая карта всех треков портала и рекомендуемые маршруты — в дорожной карте.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов