Подключить большую языковую модель к продукту реально за вечер: ключ 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 закрывает массовые сценарии, оставляя провайдеру сложные случаи. Каждая схема несёт задержку: первые токены приходят за секунды, длинный ответ тянется десятки секунд, и интерфейс обязан это переживать.
Правила вывода генеративной функции в продакшен без сюрпризов
Чек перед релизом собирается из четырёх пунктов, и все четыре закрываются до выката, а не после первого инцидента:
- Золотой набор из пары сотен реальных вопросов проходит с долей провалов не выше порога, зафиксированного в задаче.;
- Каждый фактический ответ сопровождается цитатой, цитата указывает на существующий фрагмент базы знаний.;
- Формат ответа проверяется схемой, невалидные ответы повторяются со сниженной температурой и попадают в лог.;
- Расход токенов и задержки посчитаны на день вперёд, алерт срабатывает при выходе за бюджет.
После релиза наблюдение не прекращается: доля ответов "информации нет", доля ответов с цитатой, жалобы пользователей и расход токенов сводятся в один дашборд. Аномалия в любой метрике это повод вернуть фичу на золотой набор и проверить, что сломалось: промпт, база или сама модель после обновления провайдера.
Генеративная функция в продукте это обычная инженерная фича: с тестами, бюджетом, мониторингом и откатом. Модель не сотрудник и не база данных, а вероятностный компонент, и чем раньше команда начинает относиться к ней именно так, тем дешевле обходится каждый релиз. Чужие уроки уже оплачены: пять тысяч долларов штрафа юристам, компенсация пассажиру авиакомпании, минус сто миллиардов рыночной стоимости за одну неверную фразу на демонстрации. Свой урок всегда дороже, и платить его вовсе не обязательно.