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

Откуда у большой языковой модели берётся уверенная выдумка

Языковая модель не ищет ответ в справочнике, она предсказывает продолжение текста. На входе последовательность токенов, на выходе распределение вероятностей следующего токена. Веса хранят сжатое знание о языке и мире, но в них нет картотеки с пометкой "проверено". Когда факта в весах нет, самое вероятное продолжение всё равно находится, и оно гладкое: грамматически безупречная фраза с выдуманной датой и номером приказа побеждает честную паузу. Дообучение на человеческих оценках закрепляет уверенный тон, ведь рецензенты чаще одобряли ответы, звучащие убедительно.

Проверка на живых продуктах дала дорогой ценник. Летом 2023 года суд в Нью-Йорке оштрафовал юристов на 5000 долларов: иск содержал шесть судебных решений, которых не существует, каждое с номером дела и цитатами, каждое сгенерировано моделью по запросу найти прецеденты. Зимой 2024 года канадский трибунал обязал авиакомпанию выплатить пассажиру около 650 долларов за неверную информацию, которую выдал чат-бот перевозчика на сайте. А в феврале 2023 года одна неверная фраза про космический телескоп на публичной демонстрации нового ассистента стоила поисковому холдингу около 100 миллиардов долларов рыночной стоимости за день.

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

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

Температура генерации и повторяемость ответов на практике

Первые ручки управления это параметры сэмплинга. Температура в диапазоне от 0 до 2 задаёт разброс: у нуля модель почти всегда берёт самый вероятный токен, у единицы появляется творческая свобода, выше начинается каша. Для извлечения фактов из документов держат ноль, для генерации идей поднимают к 0.8. Параметр seed закрепляет выбор конкретных токенов внутри одного запуска.

Жадное декодирование, выбор самого вероятного токена, убирает шум, но обедняет формулировки там, где нужна вариативность. Практика обратного края: генератор вариантов названий на температуре 0.9 выдаёт живые идеи, а извлекатель реквизитов из договора на той же температуре начинает видеть в тексте то, чего нет. Параметры сэмплинга это не регулятор честности, а регулятор разнообразия.

Запрос на Python с библиотекой openai версии 3.33.0 выглядит так:

from openai import OpenAI

client = OpenAI()

reply = client.chat.completions.create(
    model="gpt-4o-mini",
    temperature=0,
    messages=[
        {"role": "system", "content": "Отвечай только по тексту политики. "
         "Если ответа в тексте нет, напиши: информации нет."},
        {"role": "user", "content": question},
    ],
)
answer = reply.choices[0].message.content

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

Заземление ответов на документы компании через RAG

Выдумки делятся на два класса. Закрытый мир: модель отвечает из весов, и проверить утверждение негде. Заземлённый режим: модель отвечает по подложенным документам, и каждую фразу можно сверить с текстом. Второй подход и есть RAG, retrieval augmented generation: документы режутся на фрагменты, превращаются в векторы, по вопросу достаются ближайшие фрагменты и вкладываются в промпт как рабочий материал.

Типовой проект выглядит так: 8000 страниц регламентов режутся на 9000 фрагментов по 600 токенов с перекрытием 80 токенов, векторы считаются моделью text-embedding-3-small с 1536 измерениями и складываются в Qdrant или pgvector. По вопросу достаются пять ближайших фрагментов, в промпт добавляется жёсткая инструкция отвечать только по тексту и цитировать абзац.

Тонкости заземления решают половину успеха. Фрагменты режутся по структуре документа, по заголовкам, а не по фиксированному окну, иначе цитата рвётся посреди пункта. К вектору прикладываются метаданные: дата действия регламента, раздел, права доступа, чтобы устаревший документ не всплывал в ответах. Гибридный поиск, сложение векторной близости с совпадением по ключевым словам, спасает на запросах с аббревиатурами и редкими терминами, где чистая семантика буксует.

Поиск ближайших фрагментов умещается в несколько строк:

import numpy as np

def search(query_vec, chunk_vecs, texts, top_k=5):
    norms = np.linalg.norm(chunk_vecs, axis=1) * np.linalg.norm(query_vec)
    scores = chunk_vecs @ query_vec / norms
    best = np.argsort(-scores)[:top_k]
    return [texts[i] for i in best]

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

Структурированный вывод со схемой и проверкой полей

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

from pydantic import BaseModel, ValidationError

class TariffAnswer(BaseModel):
    tariff_name: str
    price_rub: int
    source_quote: str

def parse_answer(raw: str) -> TariffAnswer | None:
    try:
        return TariffAnswer.model_validate_json(raw)
    except ValidationError:
        return None

Схема отсекает три беды разом. Ответ либо валиден, либо уходит на повтор со сниженной температурой, либо попадает в лог для разбора. Допустим, без схемы один ответ из пятидесяти ломает парсер: на 40 000 диалогов в день это 800 сломанных сценариев. Со схемой и повтором доля падений стремится к нулю, а обязательное поле source_quote заставляет модель цитировать источник, без цитаты ответ не принимается. Мелочь из документации провайдера: в режиме JSON в тексте промпта обязательно должно встречаться само слово json, иначе запрос отклоняется.

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

Оценка качества промптов на золотом наборе вопросов

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

import json

cases = json.load(open("eval_set.json", encoding="utf-8"))
failed = []
for case in cases:
    answer = ask_model(case["question"])
    if case["must_mention"] not in answer:
        failed.append(case["id"])
print("провалено:", len(failed), "из", len(cases))

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

Промпты хранятся в git рядом с кодом, изменения проходят ревью, а в задаче фиксируется ожидаемый эффект на наборе. Без этой дисциплины через полгода никто не вспомнит, почему в инструкции появилось странное уточнение, и каждая правка превращается в лотерею.

Экономика токенов и кэширование повторяющихся промптов

Цена генеративной фичи считается до релиза, а не после первого счёта. Пример расчёта. Сервис поддержки принимает 30 000 диалогов в день, промпт с системной инструкцией и найденными фрагментами занимает 1500 токенов, ответ в среднем 250. При цене 3 доллара за миллион входных токенов и 12 долларов за миллион выходных вход съедает 45 миллионов токенов и 135 долларов в день, выход 7.5 миллиона токенов и 90 долларов. Месяц такой фичи стоит 6750 долларов, и эту цифру полезно показать продакту до старта, а не после.

Векторная часть счёта копеечная: пересчёт корпуса на 8 миллионов токенов моделью text-embedding-3-small при цене 0.02 доллара за миллион стоит 16 центов, поэтому базу можно пересчитывать хоть ежедневно.

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

Правила вывода генеративной функции в продакшен без сюрпризов

Чек перед релизом собирается из четырёх пунктов, и все четыре закрываются до выката, а не после первого инцидента:

  1. Золотой набор из пары сотен реальных вопросов проходит с долей провалов не выше порога, зафиксированного в задаче.;
  2. Каждый фактический ответ сопровождается цитатой, цитата указывает на существующий фрагмент базы знаний.;
  3. Формат ответа проверяется схемой, невалидные ответы повторяются со сниженной температурой и попадают в лог.;
  4. Расход токенов и задержки посчитаны на день вперёд, алерт срабатывает при выходе за бюджет.

После релиза наблюдение не прекращается: доля ответов "информации нет", доля ответов с цитатой, жалобы пользователей и расход токенов сводятся в один дашборд. Аномалия в любой метрике это повод вернуть фичу на золотой набор и проверить, что сломалось: промпт, база или сама модель после обновления провайдера.

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