LLM-проект без иллюзий: как тестировать промпты и снижать галлюцинации
Покажем практичный подход к качеству: наборы примеров, метрики, автоматические проверки и итерации промптов под реальные кейсы.
Содержание
LLM-проект без иллюзий: как тестировать промпты и снижать галлюцинации
LLM встраивают в продукты чаще, чем готовы честно тестировать. Итог — «работает на демо», но в реальности ответы уходят в сторону: появляются правдоподобные ошибки, несогласованность с контекстом, неверные формулы, галлюцинации источников и уверенная чушь в момент, когда от модели ждут точности.
Хорошая новость: качество LLM-поведения можно системно поднимать. Плохая — магической кнопки не существует. Нужна инженерная дисциплина: тестовые наборы, метрики, автоматические проверки, воспроизводимость и итерации промптов под реальные пользовательские сценарии.
Ниже — практичный подход к тестированию промптов и снижению галлюцинаций, ориентированный на команды, которые строят LLM-функциональность «как софт», а не как эксперимент.
Почему «галлюцинации» — это не только про генерацию
Слово «галлюцинации» часто используют как универсальный термин. В инженерной практике полезно разделять причины:
Где именно ломается качество
- Неправильное понимание запроса
Модель может неверно интерпретировать намерение: что нужно, в какой форме, с какими ограничениями. - Пробелы в контексте
Модель не видит нужные факты (из-за короткого контекста, отсутствия документов, неправильной сборки RAG). - Смешение источников
Модель «подмешивает» знания из предобучения, хотя ожидается ответ строго по предоставленным данным. - Ошибки форматирования
Даже если содержание близко, API может требовать JSON/таблицу/валидный код — и модель ломает схему. - Нарушение инструкций
Промпт может содержать противоречивые требования; модель «выбирает» то, что кажется естественным. - Температура и стохастичность
При высоких параметрах генерация становится вариативной, что усложняет стабильное тестирование. - Недостаточная проверяемость ответа
Команда иногда не определяет, что именно считать «правильным»: есть ли шкала уверенности, допустимы ли округления, обязателен ли цитатный стиль.
Чтобы снижать галлюцинации, нужно тестировать не «вообще качество», а конкретные классы ошибок.
Стратегия тестирования: от набора примеров до регрессии промптов
Тестировать LLM можно как минимум в трёх уровнях: offline (до релиза), интеграционно (с вашим пайплайном) и регрессионно (чтобы изменения промпта не ломали то, что работало).
Уровень 1: эталонные пары «вход → ожидаемое поведение»
Начните с коллекции промпт-кейсов, максимально близких к реальным запросам пользователей.
Как собирать наборы примеров
- Базовые happy-path сценарии: запросы, которые модель должна выполнять «с первого раза».
- Неполные входные данные: отсутствие важных полей, короткие описания.
- Сложные ограничения: форматы, лимиты, требования к стилю и структуре.
- Оспаривающие запросы: пользователь просит «точно со ссылками», «без догадок», «используй только данные из таблицы».
- Нестандартные форматы: разные языки, пунктуация, шумные тексты, смешанный регистр.
- Атакующие/провокационные: prompt injection (например: «игнорируй инструкции выше»), запрос на утечку системных инструкций, попытка заставить модель выдумать источник.
Важно: набор должен быть не «сколько угодно», а под задачу. Даже 50–150 тщательно размеченных примеров часто дают больше пользы, чем 1000 случайных.
Разметка должна быть инженерной
Для каждого кейса определите:
- ожидаемый формат ответа (например, JSON-валидность, наличие полей)
- критичные факты, которые нельзя придумывать
- допуски (можно ли округлять, допустима ли краткость)
- правила отказа (когда модель должна сказать «не могу ответить»)
Уровень 2: метрики, которые коррелируют с продуктом
Если вы не измеряете, вы не управляете. Для LLM метрики лучше строить не только «на вкус оценщика», а через проверяемые свойства.
Типовые метрики качества
- Exact / F1 для извлечений
Если модель извлекает сущности (например, даты, суммы, имена), используйте точность/полноту или хотя бы сравнение структур. - Schema validity (валидность структуры)
Для JSON/HTML/XML — проверка парсинга + обязательные поля. - Factuality checks (фактологическая проверка)
Если ответ должен опираться на предоставленный контекст (документы, таблицы), проверяйте совпадение ключевых утверждений с источником. - Attribution / citation compliance (ссылки/цитаты)
Требуете ли вы, чтобы цитаты присутствовали? Тогда проверка по шаблону и покрытию: упоминаются ли документы, совпадает ли хотя бы часть фраз. - Refusal correctness (корректный отказ)
В некоторых задачах правильное поведение — не отвечать. Тогда оценивайте долю корректных отказов и долю ложных отказов. - Consistency (внутренняя согласованность)
Например, если модель называет дату в одном месте, а в другом — другая дата, это ошибка. - Robustness (устойчивость к вариациям)
Один и тот же смысл, но с разными формулировками. Модель должна давать предсказуемый тип ответа.
Как превратить текст в проверяемые метрики
Используйте автоматические проверки:
- парсинг JSON,
- регулярные выражения для формата,
- сравнение сущностей,
- вызовы ваших валидаторов,
- и — аккуратно — «вторую модель как судью» только там, где иначе нельзя.
Последнее требует дисциплины (см. ниже).
Уровень 3: автоматический регрессионный прогон промптов
Промпты меняются: добавили пример — качество «на одном кейсе» выросло, а на другом просело. Поэтому нужен регрессионный контур:
- фиксируете версию промпта
- прогоняете весь набор
- сравниваете метрики с предыдущей версией
- публикуете отчёт: какие типы кейсов сломались
Это превращает «LLM engineering» в нормальную инженерную практику.
Практический дизайн тестовых прогонов: наборы, контроль случайности, воспроизводимость
Фиксируйте параметры генерации
Чтобы регрессии были сравнимыми:
- используйте фиксированный seed (если поддерживается),
- держите одинаковые параметры (temperature, top_p),
- минимизируйте вариативность без ущерба качеству.
Если вы сознательно используете стохастичность (например, для креативного контента), тогда тестируйте распределение: прогоните несколько семплов и оценивайте долю успешных.
Храните артефакты
Для каждой версии промпта сохраняйте:
- промпт (полный текст + системные инструкции, шаблоны),
- входные данные (raw),
- модель/версию API,
- параметры генерации,
- ответ (raw),
- метрики и результаты проверок.
Без этого невозможно разбирать инциденты.
Автоматические проверки: от парсинга до «не выдумывай»
1) Валидность формата: быстро и строго
Если продукт требует структурированный ответ, проверяйте строго. Например, модель должна вернуть JSON:
import json
def validate_json_response(text: str) -> dict:
try:
data = json.loads(text)
except json.JSONDecodeError as e:
raise ValueError(f"Response is not valid JSON: {e}")
# Пример схемы
required = ["answer", "confidence", "sources"]
for k in required:
if k not in data:
raise ValueError(f"Missing key: {k}")
return data
Эта проверка часто ловит «галлюцинации формата», когда модель отвечает словами, вместо нужной структуры.
2) Проверка «только по контексту»
Если вы используете RAG и хотите исключить выдумки, нужен минимум механики:
- вы храните документы/фрагменты,
- ожидаете, что ответ опирается на них,
- проверяете наличие опорных фактов.
Простой вариант — проверять совпадение ключевых утверждений по n-граммам или по «подстрочным» совпадениям, но лучше — через более интеллектуальную проверку.
Пример: извлечь утверждения и проверить покрытие
Пусть ваш пайплайн:
- разбивает ответ на утверждения,
- для каждого утверждения проверяет, что оно встречается (с допусками) в одном из контекстных фрагментов.
Условный пример «по токенам» (упрощённо):
import re
def statements(text: str) -> list[str]:
# очень грубая сегментация: для демонстрации
parts = re.split(r'[.?!]\s+', text.strip())
return [p for p in parts if p and len(p) > 20]
def normalize(s: str) -> str:
s = s.lower()
s = re.sub(r'\s+', ' ', s)
return s
def overlap_score(stmt: str, doc: str) -> float:
stmt_tokens = set(normalize(stmt).split())
doc_tokens = set(normalize(doc).split())
if not stmt_tokens:
return 0.0
return len(stmt_tokens & doc_tokens) / len(stmt_tokens)
def factual_coverage(answer: str, docs: list[str], threshold: float = 0.35) -> float:
stmts = statements(answer)
if not stmts:
return 0.0
ok = 0
for st in stmts:
best = max(overlap_score(st, d) for d in docs)
if best >= threshold:
ok += 1
return ok / len(stmts)
Это не идеальный контроль фактов, но для базового «антигаллюцинаторного» фильтра работает. На проде вы обычно усложняете проверку (например, с использованием энкодеров, NLI, или специализированных скореров).
3) Использование второй модели как «судьи»: осторожно
Если вы привлекаете LLM для оценки (например, чтобы определить «соответствует ли утверждение контексту»), важно:
- давать судье тот же контекст,
- явно просить вердикт по правилам,
- требовать обоснование ссылками на фрагменты,
- ограничить судью шаблоном ответа (валидный JSON).
Иначе судья начнёт оценивать «кажется правдоподобным» вместо «подтверждается источником».
Пример промпта для судьи (концептуально):
- вход:
{question, answer, context_docs} - output:
{verdict: "supported"|"unsupported"|"unknown", evidence: [...]}
Проверяйте валидность JSON так же строго, как основной ответ.
Итерации промптов: как улучшать без магии
Промпт — это не «текст для вдохновения». Это спецификация поведения. Улучшения должны быть подтверждены прогоном набора.
Типичные причины, почему промпт не лечит галлюцинации
- Неполная инструкция про отказ
Если вы не описали, когда модель должна сказать «не знаю», она будет «заполнять пробел». - Слишком мягкий тон требований
«Старайся опираться» ≠ «ответ должен быть подтверждён контекстом». - Конфликт требований
«Будь кратким» и «обязательно перечисли все источники» — конфликт. - Отсутствие примеров для сложных кейсов
Модель отлично повторяет паттерны из few-shot. Не хватает примеров отказа и ошибок — не будет и корректного поведения. - Неучёт форматных ограничений
Если формат важен, добавляйте структуру и примеры валидных ответов.
Что добавлять в промпт для снижения галлюцинаций
1) Явный policy: «не подтверждено — не утверждай»
Добавьте правило уровня:
- если ответ нельзя подтвердить контекстом → вернуть отказ или указать «не найдено».
Пример (фрагмент системной политики, адаптируйте под свою задачу):
Если в предоставленном контексте нет данных, необходимых для ответа, верни
{"answer": null, "reason": "NOT_FOUND_IN_CONTEXT"}.
2) Тонкий формат: требуйте структурированный результат
Например, если вы хотите уменьшить выдумки, добавьте поле sources или evidence и требуйте, чтобы оно было непустым для утверждений.
3) Few-shot по вашим реальным кейсам
Не берите «универсальные» примеры. Берите ваши типовые ошибки:
- «документы есть, но модель не цитирует»
- «модель придумывает цифры»
- «модель не соблюдает JSON»
И покажите контрпример: как надо при отсутствии данных.
4) Ограничение на «додумывание»
Полезная формулировка:
Не используйте общие знания и не делайте предположений, если этого нет в контексте.
Да, модель всё равно может нарушать — но вероятность падает, а автоматическая проверка ловит нарушения.
Метрики галлюцинаций: как превратить «кажется неправдой» в цифры
Чтобы не скатываться в субъективность, договоритесь о целевых метриках:
Для задач с контекстом (RAG)
- Support rate: доля ответов, где ключевые утверждения подтверждаются контекстом.
- Evidence completeness: покрытие evidence/sources для каждого утверждения.
- Refusal accuracy: доля корректных отказов в сценариях, где данные отсутствуют.
Для задач без контекста (чистая генерация)
- Groundedness невозможна, но можно мерить:
- format compliance,
- self-consistency (например, двойная генерация с последующим сравнением),
- numeric reliability (проверка формул/вычислений внешними инструментами),
- tool-use correctness (если модель должна вызывать функции).
Главное: измеряйте то, что реально важно вашему продукту.
Конкретный пайплайн: как организовать тесты в проекте
Ниже — пример минимально жизнеспособной архитектуры для LLM-проверок.
Шаг 1: тест-кейсы в YAML/JSON
Структура кейса:
- id: "invoice_total_missing"
input:
user_text: "Сумма в счете?"
context_docs: [] # нет документов
expected:
format: "json"
answer_is_null: true
reason: "NOT_FOUND_IN_CONTEXT"
Шаг 2: исполнитель тестов
Он прогоняет модель и сохраняет результат.
from dataclasses import dataclass
@dataclass
class TestCase:
id: str
user_text: str
context_docs: list[str]
expected: dict
def run_llm(prompt: str, model_client) -> str:
# заменить на ваш реальный вызов
return model_client.complete(prompt)
def run_case(case: TestCase, model_client, build_prompt) -> dict:
prompt = build_prompt(case.user_text, case.context_docs)
raw = run_llm(prompt, model_client)
return {"case_id": case.id, "raw_response": raw}
Шаг 3: валидаторы и скореры
import json
def score_not_found_in_context(raw: str, expected: dict) -> float:
data = json.loads(raw)
if data.get("answer") is None and data.get("reason") == expected["reason"]:
return 1.0
return 0.0
Шаг 4: отчёт и регрессия
Сравнивайте:
- average score,
- долю валидных ответов,
- долю поддержанных утверждений,
- и список кейсов, где стало хуже.
Это превращает улучшение промпта в управляемый процесс.
Iteration loop: как правильно «крутить» промпт по данным тестов
Заведите цикл:
- Вы фиксируете текущую версию промпта и запускаете тесты (baseline).
- Анализируете «слабые места»:
- какие кейсы падают,
- какой тип ошибки (формат / факт / отказ / согласованность).
- Вносите точечное изменение:
- добавляете правило отказа,
- корректируете схему ответа,
- добавляете few-shot на конкретный класс случаев.
- Запускаете полный регресс.
- Принимаете изменения, только если:
- не стало хуже по важным метрикам,
- улучшение затрагивает целевой класс ошибок.
Подводный камень: «улучшили — но сломали другое»
Частая история: добавили жёсткое правило «только по контексту» — и модель начала чаще отказывать. Пользователю это может быть хуже, чем «слегка неточно» (если в вашей доменной области допустима аппроксимация). Поэтому метрики должны отражать
Комментарии
Пока нет комментариев