ИИ-агенты и prompt engineering ИИ-инженерия: карта трека, что умеют модели и что от них ждать
0%

ИИ-инженерия: карта трека, что умеют модели и что от них ждать

ИИ-инженерия: карта трека, что умеют модели и что от них ждать

Есть два разных ремесла, которые в вакансиях называют одинаково. Первое — машинное обучение: вы собираете датасет, выбираете архитектуру, крутите градиенты, боретесь с переобучением. Второе — ИИ-инженерия: модель уже обучена кем-то другим, у вас есть HTTP-эндпоинт, и ваша работа — превратить вероятностный текстовый генератор в предсказуемый компонент продакшн-системы.

Этот трек — про второе. Мы почти не будем обучать модели (кроме статьи про LoRA, где обсудим, когда это вообще оправдано). Мы будем заниматься тем, чем на практике занят ИИ-инженер: проектировать контекст, ограничивать вывод схемой, подмешивать свои данные, давать модели инструменты, ставить лимиты, считать деньги, ловить регрессии и не пускать в прод то, что ломается на 5% запросов.

Ключевой сдвиг мышления: LLM — это не функция, это подсистема с распределением исходов. Обычный код на одном входе даёт один выход. LLM на одном входе даёт распределение выходов, и ваша задача — сузить это распределение до приемлемого инженерными средствами: форматом ответа, схемой, валидацией, ретраями, проверками. Всё остальное в треке — техники сужения этого распределения.


Карта трека

Порядок статей не случаен: каждая следующая техника решает проблему, которую создала предыдущая. Промптинг упирается в неструктурированный вывод — приходит structured output. Structured output упирается в незнание ваших данных — приходит RAG. RAG упирается в необходимость действовать, а не только отвечать — приходят инструменты и агенты. Агенты упираются в невозможность понять, стало ли лучше — приходят evals. Evals упираются в счёт за API — приходит продакшн-экономика.

# Статья Какую проблему решает
01 Основы LLM для инженера «Почему это стоит столько и работает так медленно»
02 Основы промптинга Модель отвечает не то и не в том формате
03 Продвинутые техники Модель ошибается на многошаговых рассуждениях
04 Структурированный вывод Ответ невозможно распарсить программно
05 RAG Модель не знает ваших документов
06 Продвинутый RAG Поиск возвращает мусор на сложных вопросах
07 Агенты и ReAct Нужно действовать, а не только отвечать
08 Мультиагентные системы Одна роль не тянет задачу целиком
09 MCP Интеграции с инструментами не переиспользуются
10 Оценка и бенчмарки Непонятно, стало лучше или хуже
11 Локальные модели Данные нельзя отдавать наружу
12 Дообучение Промпт не вмещает нужное поведение
13 Продакшн и стоимость Счёт растёт быстрее выручки
14 Безопасность и инъекции Пользователь перехватывает управление агентом
15 ИИ в жизненном цикле разработки Как применить всё это к собственной работе

Если вам не хватает фундамента под капотом — что такое трансформер, откуда берётся attention, как считается softmax — соседние треки закрывают этот слой: нейросети и машинное обучение. Здесь мы берём модель как чёрный ящик с известными свойствами.


Что модели умеют на самом деле

Маркетинг оперирует процентами на бенчмарках, инженерная практика — профилями надёжности. Разница принципиальная: 90% на бенчмарке означает, что каждый десятый ответ неверен, и вопрос лишь в том, дорого ли вам стоит этот десятый.

Полезнее делить задачи не по «сложности», а по стоимости ошибки и проверяемости результата.

Правый нижний и правый верхний квадранты — там, где живёт вся реальная ценность: результат можно проверить дешевле, чем произвести. SQL-запрос выполняется на тестовой базе. Патч прогоняется тестами. Извлечённые поля сверяются со схемой и суммами. Слева — зона, где LLM выглядит убедительно и ошибается незаметно; там нужен человек, а не более длинный промпт.

Про бенчмарки: как их читать

Цифры на публичных наборах устаревают за месяцы, поэтому запоминать конкретные проценты бессмысленно — важно понимать, что каждый бенчмарк меряет и как он врёт.

Бенчмарк Что меряет Главная слабость
MMLU Знания по 57 предметам, выбор из вариантов Насыщен, часть вопросов с ошибками, утечка в претрейн
HumanEval 164 маленькие функции на Python Не про инженерию: нет репозитория, зависимостей, легаси
GPQA Вопросы уровня PhD, «google-proof» Узкие домены, малый объём
SWE-bench Реальные issue из GitHub-репозиториев Чувствителен к обвязке: скор меняется в разы от харнеса
τ-bench Агент + инструменты + диалог с пользователем Синтетические домены
ARC-AGI Абстрактные визуальные закономерности Не коррелирует с бизнес-задачами
Chatbot Arena Слепые парные сравнения людьми Меряет приятность ответа, а не корректность

Историческая калибровка полезнее абсолютных чисел. В статье про SWE-bench (октябрь 2023) лучшая модель закрывала около 2% задач. Через два года ведущие модели с агентной обвязкой закрывают больше половины, и SWE-bench Verified из «невозможного» превратился в «показатель качества харнеса». Вывод для практики: разрыв между моделью и вашим результатом определяется обвязкой сильнее, чем выбором модели. Одна и та же модель с плохим и хорошим агентным циклом даёт разницу в разы.

Актуальные срезы смотрите на живых лидербордах: HELM, swebench.com, Chatbot Arena. И главное — свой датасет из 50 реальных запросов вашего продукта информативнее любого публичного бенчмарка; как его собрать, разбираем в статье про оценку.


Где модели ломаются

Это раздел, который отличает инженера от энтузиаста. Каждый пункт — предсказуемый режим отказа с известным контрмерами.

1. Уверенная выдумка. Модель обучена продолжать текст правдоподобно, а не воздерживаться. На вопрос о несуществующем API она сгенерирует красивую сигнатуру. Контрмеры: RAG с обязательными цитатами, инструмент проверки, chain-of-verification (статья 03).

2. Провал в середине контекста. Информация в середине длинного контекста используется хуже, чем в начале и конце — эффект показан в Lost in the Middle. Окно в миллион токенов не означает, что можно свалить туда всё. Контрмеры: реранжирование, сжатие контекста, помещать критичное в начало и конец.

3. Хрупкость к формулировке. Перестановка двух предложений в системном промпте меняет метрику на несколько процентов. Это не баг, а свойство: у вас нет градиента, только текстовый интерфейс. Контрмеры: версионирование промптов, регрессионный прогон на каждое изменение, никаких правок промпта «на глаз в проде».

4. Дрейф модели. Провайдер обновил модель — ваши промпты, откалиброванные под прошлую версию, поехали. Реальный пример из практики миграций: агрессивные формулировки вида CRITICAL: YOU MUST use this tool работали на моделях, которые плохо следовали инструкциям, и начинают переcрабатывать на моделях, которые следуют им буквально — инструмент вызывается там, где не нужен. Контрмеры: пин версии модели, прогон evals перед миграцией, документированный чек-лист изменений.

5. Prompt injection. Любой текст, попавший в контекст — письмо, веб-страница, содержимое файла — это потенциальная инструкция. Классическая работа: Not what you’ve signed up for. Если у агента есть инструменты, инъекция превращается в удалённое выполнение действий. Контрмеры: разделение каналов доверия, ограничение прав инструментов, подтверждение необратимых действий (статья 14).

6. Накопление ошибки в агентном цикле. Если каждый шаг верен с вероятностью $p$, то $n$ шагов подряд дают $p^n$. При $p = 0{,}95$ и $n = 20$ это уже 36%. Отсюда — вся дисциплина агентного дизайна: проверяемые шаги, откат, критик, ограничение глубины цикла.

7. Отсутствие калибровки уверенности. Модель не умеет надёжно сообщать «я не знаю». Просьба «оцени свою уверенность от 0 до 100» даёт число, слабо связанное с реальной вероятностью правоты. Контрмеры: внешние сигналы — согласованность нескольких прогонов (self-consistency), совпадение с найденными источниками, успешность выполнения сгенерированного артефакта.


Анатомия ИИ-приложения

Любое серьёзное LLM-приложение состоит из одних и тех же слоёв. Понимание этой схемы даёт словарь для всего трека.

Три вещи, которые в этой схеме обычно забывают и потом дорого доделывают:

  • Гейт на входе. Ограничение частоты, фильтрация PII, отсев заведомо неподходящих запросов. Дешевле отказать, чем сгенерировать.
  • Валидация на выходе как часть контура, а не как логирование. Провал схемы должен приводить к повторной попытке с сообщением об ошибке, а не к 500-й пользователю.
  • Трейс с самого первого дня. Логи вызовов — единственный источник будущего eval-датасета. Приложение без трейсов невозможно улучшать: вы не знаете, на чём оно ошибается.

Лестница возможностей: не поднимайтесь раньше времени

Самая частая и самая дорогая ошибка новичка — начать с мультиагентной системы там, где хватило бы одного промпта с JSON-схемой.

Лестница от одного промпта до мультиагентной системы

Правило простое: поднимайтесь на следующую ступень только после того, как текущая измеримо провалилась на вашем eval-датасете. Каждая ступень добавляет стоимость, латентность и площадь отказа. Anthropic формулирует это же в Building Effective Agents: большинство успешных продакшн-систем — это не агенты, а простые композиции вызовов.

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

Ступень Когда достаточно Симптом, что пора выше
Промпт Одношаговое преобразование текста Ответ нельзя распарсить
Схема Нужен машиночитаемый результат Модель не знает фактов о вашем домене
RAG Ответ выводится из документов Нужно посчитать, записать, вызвать API
Workflow Шаги известны заранее Порядок шагов зависит от промежуточных результатов
Агент Нужна адаптивная последовательность действий Одна роль путает цели, качество не растёт с числом итераций
Мультиагент Нужны разделение ролей и независимая проверка

Первый рабочий пример

Возьмём типовую задачу: классификация входящего тикета поддержки со структурированным выводом. Это ступень 2 — и её достаточно для огромного числа продуктов.

import json
import anthropic
from pydantic import BaseModel, Field
from typing import Literal

client = anthropic.Anthropic()  # ключ берётся из окружения

class TicketTriage(BaseModel):
    """Результат разбора тикета — контракт между моделью и вашим кодом."""
    category: Literal["billing", "bug", "feature_request", "account", "other"]
    severity: Literal["low", "medium", "high", "critical"]
    # Строгие enum'ы вместо свободного текста: невалидное значение
    # физически не пройдёт валидацию схемы на стороне API.
    summary: str = Field(description="Одно предложение, не более 20 слов")
    needs_human: bool = Field(description="true, если нужен живой оператор")

SYSTEM = """Ты — система разбора обращений в поддержку SaaS-платформы.
Классифицируй обращение строго по данным из текста.
Если данных для уверенной классификации не хватает — ставь category=other
и needs_human=true. Не додумывай факты, которых нет в обращении."""

def triage(ticket_text: str) -> TicketTriage:
    response = client.messages.parse(
        model="claude-opus-4-8",
        max_tokens=1024,
        system=[{
            "type": "text",
            "text": SYSTEM,
            # Префикс стабилен между запросами — кэшируем его.
            # Чтение из кэша примерно в 10 раз дешевле обычного ввода.
            "cache_control": {"type": "ephemeral"},
        }],
        messages=[{"role": "user", "content": ticket_text}],
        output_format=TicketTriage,   # схема принуждает модель к валидному JSON
    )
    return response.parsed_output

Обратите внимание на четыре инженерных решения, а не на промпт:

  1. Схема как контракт. Literal вместо str означает, что модель не может вернуть категорию "биллинг " с опечаткой и пробелом — она ограничена на уровне декодирования.
  2. Явный путь отказа. Инструкция «не хватает данных → other + needs_human» превращает неопределённость в наблюдаемый сигнал вместо выдумки.
  3. Кэш префикса. Системный промпт неизменен, значит его можно закэшировать. Требование одно: префикс должен совпадать побайтово. Любая динамика внутри него — дата, имя пользователя, случайный ID — обнуляет кэш молча.
  4. Никакого temperature. На новых моделях Anthropic (Opus 4.7/4.8, Sonnet 5) параметры сэмплирования удалены из API и возвращают 400. Поведение задаётся промптом и параметром усилия, а не температурой. Что это значит для воспроизводимости — разбираем в статье 01.

Тот же контракт на TypeScript:

import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";

const client = new Anthropic();

const TicketTriage = z.object({
  category: z.enum(["billing", "bug", "feature_request", "account", "other"]),
  severity: z.enum(["low", "medium", "high", "critical"]),
  summary: z.string(),
  needs_human: z.boolean(),
});

export async function triage(ticketText: string) {
  const response = await client.messages.parse({
    model: "claude-opus-4-8",
    max_tokens: 1024,
    system: [{ type: "text", text: SYSTEM, cache_control: { type: "ephemeral" } }],
    messages: [{ role: "user", content: ticketText }],
    output_config: { format: zodOutputFormat(TicketTriage) },
  });

  // parsed_output равен null, если разбор не удался — обрабатываем явно,
  // а не через `!`, иначе редкий провал схемы уронит обработчик.
  if (!response.parsed_output) {
    throw new Error(`Схема не сошлась, stop_reason=${response.stop_reason}`);
  }
  return response.parsed_output;
}

Экономика: считайте до того, как писать код

ИИ-функция — единственная часть вашего продукта, у которой себестоимость линейна по трафику и видна в отдельном счёте. Считать её нужно на этапе дизайна.

Модель ценообразования у всех провайдеров одинакова: цена за миллион входных токенов и за миллион выходных, при этом выход дороже входа в 4–5 раз. Возьмём актуальный прайс Anthropic (документация) как рабочий ориентир — у других провайдеров порядок величин сопоставим:

Модель Вход, $/1M Выход, $/1M Контекст
Claude Opus 4.8 5.00 25.00 1M
Claude Sonnet 5 3.00 15.00 1M
Claude Haiku 4.5 1.00 5.00 200K

Посчитаем реальную нагрузку: 100 000 запросов в сутки, 3000 входных токенов, 500 выходных.

Ежедневный объём: 300M входных + 50M выходных токенов.

Конфигурация Расчёт В сутки В месяц
Всё на Opus 300·5 + 50·25 2750 $ ~82 500 $
Всё на Sonnet 300·3 + 50·15 1650 $ ~49 500 $
Всё на Haiku 300·1 + 50·5 550 $ ~16 500 $
Sonnet + кэш префикса (2500 из 3000 токенов стабильны) 250·0.3 + 50·3 + 50·15 975 $ ~29 250 $
Роутинг: 70% Haiku, 30% Sonnet 0.7·550 + 0.3·1650 880 $ ~26 400 $
Роутинг + кэш ~600 $ ~18 000 $

Из одной таблицы следуют три главных рычага продакшн-экономики, которым посвящена статья 13:

  • Кэширование префикса — самый дешёвый по усилиям выигрыш. Запись в кэш стоит примерно 1.25× от обычного ввода, чтение — около 0.1×. Значит, окупаемость наступает уже со второго запроса с тем же префиксом. Условие — префикс неизменен побайтово, а рендер идёт в порядке tools → system → messages.
  • Роутинг моделей — 70% запросов в типичном продукте тривиальны. Классификация, извлечение, форматирование прекрасно работают на младшей модели; старшая нужна для остатка.
  • Сокращение выхода. Выходной токен в пять раз дороже входного и вдобавок в сотни раз медленнее (вход обрабатывается параллельно, выход — последовательно). Требование «отвечай одним абзацем без преамбулы» экономит и деньги, и время.

Отдельно: batch API даёт 50% скидки для всего, что не требует мгновенного ответа — ночная разметка, переиндексация, массовая генерация. Если задача терпит час, это бесплатная половина бюджета.


Латентность: где на самом деле уходит время

Интуиция «поиск медленный, модель быстрая» неверна ровно наоборот.

Бюджет латентности RAG-запроса

Разложение важно тем, что показывает бесполезность локальных оптимизаций. Ускорение векторного поиска с 40 до 10 мс даёт 0.3% выигрыша по общему времени. Реальные рычаги — другие:

  • Стриминг. Не сокращает полное время, но снижает воспринимаемую задержку с 9 секунд до 0.8: пользователь видит первый токен и начинает читать. Для любого интерактивного интерфейса это обязательно.
  • Сокращение объёма генерации. 500 токенов вместо 1500 — это минус 11 секунд напрямую.
  • Кэш префикса — сокращает не только цену, но и время до первого токена: предзаполнение уже сделано.
  • Параллелизация независимых вызовов. Если поиск и классификация намерения не зависят друг от друга, запускайте их одновременно.
  • Прогрев кэша. Запрос с max_tokens: 0 на старте воркера прогревает кэш префикса, чтобы первый живой пользователь не платил за холодный старт латентностью.

Практическое следствие: проектируйте под TTFT (time to first token) и токен/с, а не под полное время ответа. Именно эти две метрики видит пользователь.


Агентный цикл: что происходит внутри

Забегая в статью 07 — вот минимальный цикл, который отличает агента от вызова модели. Схема из ReAct, 2022 год, и с тех пор принципиально не менялась.

Две детали, на которых спотыкаются все:

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

Провалившийся инструмент всё равно требует результата. Возвращайте tool_result с флагом ошибки и внятным текстом. Молча выброшенный вызов ломает соответствие идентификаторов и приводит к 400 от API либо к зацикливанию.


Дисциплина оценки: без неё это не инженерия

Если у вас нет способа измерить изменение, вы не разрабатываете — вы гадаете. Минимальный eval-контур строится за час и окупается на первой же миграции модели.

Каркас на 40 строк, которого достаточно для старта:

import json, statistics, concurrent.futures as cf
from dataclasses import dataclass

@dataclass
class Case:
    input: str
    expected: dict        # эталон, размеченный человеком
    tags: list[str]       # для разреза метрик: "длинный", "на английском", "неоднозначный"

def load_cases(path: str) -> list[Case]:
    """Кейсы берутся из продовых трейсов, а не выдумываются:
    выдуманные кейсы меряют ваше воображение, а не продукт."""
    with open(path, encoding="utf-8") as f:
        return [Case(**json.loads(line)) for line in f]

def score(actual: dict, expected: dict) -> float:
    """Начинайте с точных проверок. LLM-as-judge — только там,
    где ответ принципиально неформализуем (см. статью 10)."""
    keys = expected.keys()
    return sum(actual.get(k) == expected[k] for k in keys) / len(keys)

def run_eval(fn, cases: list[Case], workers: int = 8) -> dict:
    with cf.ThreadPoolExecutor(max_workers=workers) as pool:
        results = list(pool.map(lambda c: (c, fn(c.input)), cases))

    scores = [score(out.model_dump(), c.expected) for c, out in results]
    by_tag: dict[str, list[float]] = {}
    for (c, _), s in zip(results, scores):
        for t in c.tags:
            by_tag.setdefault(t, []).append(s)

    return {
        "n": len(scores),
        "mean": round(statistics.mean(scores), 4),
        # p10 важнее среднего: он показывает худшие 10% — именно они
        # порождают тикеты в поддержку и отток пользователей.
        "p10": round(sorted(scores)[len(scores) // 10], 4),
        "by_tag": {t: round(statistics.mean(v), 4) for t, v in by_tag.items()},
        "failures": [c.input[:80] for (c, _), s in zip(results, scores) if s < 1.0][:10],
    }

Сложность: $O(n)$ вызовов модели на прогон, время — $O(n / w)$ при $w$ параллельных воркерах, память — $O(n)$ на хранение результатов. При 50 кейсах и 8 воркерах прогон занимает секунды и стоит центы. Это единственная метрика в треке, экономить на которой нельзя.

Три правила, выстраданные практикой:

  • Разрезы важнее среднего. Средние 0.92 могут скрывать 0.60 на кейсах с длинным вводом. Тегируйте кейсы с первого дня.
  • p10, а не mean. Пользователи запоминают худшие ответы, а не типичные.
  • Список провалов — главный артефакт прогона. Не число, а конкретные входы, которые нужно прочитать глазами.

Типичные ошибки первых месяцев

Начинать с агента. Симптом: три недели отладки цикла для задачи, которая решалась одним вызовом со схемой. Лечение — лестница выше.

Промпт как строковая константа в коде. Промпт — это артефакт с версией, набором тестов и историей изменений. Правка «на глаз» без прогона — гарантированный регресс.

Игнорировать stop_reason. Обрезка по max_tokens выглядит как валидный ответ и молча теряет хвост. Всегда проверяйте причину остановки до чтения содержимого.

Тихая порча кэша. datetime.now() или UUID в системном промпте обнуляют кэш, и вы платите полную цену, не получая ошибки. Диагностика одна: если cache_read_input_tokens равен нулю на повторных запросах — ищите динамику в префиксе.

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

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

Валидировать вывод логированием. Провал схемы — это ветка исполнения с ретраем и фолбэком, а не строка в логе.

Один длинный промпт вместо композиции. Промпт на 4000 слов, который делает пять вещей, отлаживать невозможно: изменение ради одной задачи ломает другую. Пять промптов по 200 слов тестируются независимо.


Как проходить трек

Трек рассчитан на инженера, который умеет писать код и разворачивать сервисы, но не занимался LLM системно. Формат каждой статьи одинаков: интуиция → строгое объяснение → рабочий код → замеры и trade-offs → типичные ошибки → продакшн-практика.

Практическая рекомендация: ведите один сквозной проект через весь трек. Возьмите реальную задачу — разбор ваших логов, поиск по вашей документации, ассистент по вашему API — и наращивайте её от статьи к статье. Каждая техника из трека будет применяться к тому же коду и тому же eval-датасету, и вы увидите настоящие дельты, а не учебные примеры.

Что понадобится: Python 3.11+ или Node 20+, API-ключ любого провайдера (бюджета в 20 $ хватит на весь трек), Docker для локальных моделей в статье 11, и git — потому что промпты и датасеты версионируются как код.


Мини-итог

  • ИИ-инженерия — это управление распределением исходов вероятностной подсистемы инженерными средствами: схемой, валидацией, поиском, лимитами и evals.
  • Бенчмарки говорят о моделях, а не о вашем продукте; разрыв между моделью и результатом определяется обвязкой сильнее, чем выбором модели.
  • Ломается предсказуемо: выдумка, провал в середине контекста, хрупкость к формулировке, дрейф версий, инъекции, накопление ошибки $p^n$ в цикле.
  • Поднимайтесь по лестнице промпт → схема → RAG → workflow → агент → мультиагент только после измеренного провала текущей ступени.
  • Экономика решается тремя рычагами: кэш префикса, роутинг моделей, сокращение выхода. Вместе они дают снижение счёта в 3–4 раза.
  • Латентность на 90% состоит из генерации: спасают стриминг, короткий ответ и параллелизация, а не быстрый векторный индекс.
  • Без eval-датасета из реальных запросов вы не разрабатываете, а гадаете.

Источники

Что дальше

Прежде чем писать промпты, нужно понимать, чем вы платите и что именно уходит в модель. Следующая статья разбирает LLM с точки зрения инженера: как текст превращается в токены, почему контекст стоит денег и деградирует к середине, что на самом деле делает температура и почему её убрали из новых API, и как считать стоимость запроса до его отправки.

Как устроена LLM с точки зрения инженера: токены, контекст, температура, стоимость

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

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

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

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