Почему ваш ИИ — идиот, хотя модель на бумаге гениальна?
Каждый инженер, кто хоть раз выкатывал RAG-систему в продакшен, знает этот момент: ты даешь модели идеальный промпт, грузишь базу знаний, запускаешь — а она выдает чушь. Ты бросаешься дообучать модель, менять гиперпараметры, но проблема не в ней. Она в том, что инференс-система сломана.
Я видел десятки команд, которые тратили недели на fine-tuning, хотя достаточно было заменить один парсер или переписать чанк-стратегию. Модель не тупая — она просто скормлена мусором. Или ей не дали достаточно контекста. Или маршрутизатор повел запрос не в тот эндпоинт.
Если вы активно гуглите "апдейт весов" или "cтохастический градиент", когда ваш чат-бот не может найти номер договора в базе — остановитесь. С вероятностью 90% виноват не бэкпроп.
Давайте разберем три слоя, где реально ломается качество ответа. Без абстракций, с живыми кейсами и диагностическими скриптами.
Слой 1: Ретривер подсел на наркотики (retrieval)
Самая частая история. Вы пихаете в векторную базу данные, но чанки нарезаны через одно место. Модель получает нерелевантные куски текста и, разумеется, синтезирует из них бред. Проблема: ретривер нашел шум, а не сигнал.
Типичный случай: юридический RAG
Вы ищете пункт договора о штрафных санкциях. Chunk size — 1024 токена, overlap — 200. Ретривер возвращает 5 чанков, но первый — оглавление документа, второй — реквизиты сторон, третий — общие положения. Модель видит кучу текста, но не находит конкретную цифру. В ответе: «В договоре могут быть предусмотрены штрафы» — обобщение, которое бесполезно.
Как это диагностировать? Простой скрипт: повторите запрос 10 раз, достаньте сырые чанки до передачи в LLM. Если в чанках нет ответа — собака зарыта в ретривере.
import openai, chromadb
# Для каждого запроса логируем retrieved chunks
def diagnose_retrieval(query: str, collection):
results = collection.query(query_texts=[query], n_results=5)
for i, chunk in enumerate(results['documents'][0]):
print(f'Chunk {i}: {chunk[:200]}...')
# Проверяем, содержит ли чанк прямые ключевые слова из ответа
if 'штраф' not in chunk:
print(' ❌ Missing target keyword')Решение: пересмотреть стратегию чанкинга. Не режьте документы тупо по токенам. Разбивайте по смысловым блокам: заголовки, параграфы, пункты. Добавьте метаданные (тип раздела, номер страницы) и используйте гибридный поиск (семантический + keyword). Попробуйте small-to-big: сначала ищете короткие чанки (128 токенов), потом подтягиваете родительские куски большего размера.
Здесь отлично заходит статья про “Модель на 99% в тестах, на 0% в продакшене: как время и данные ломают ML-системы” — именно там описан тот самый разрыв между метриками на бенчмарках и реальностью.
Слой 2: Контекстное окно — не резиновое (context stuffing)
Вы решили: «А давай засунем в системный промпт всю документацию, модель же умная, разберется». И получили обратный эффект: модель «забывает» задание, теряет фокус, начинает галлюцинировать, потому что внимание размазывается по тоннам мусора. Проблема: потеря сигнала среди шума.
Исследования 2025-26 показали: модель использует эффективно только первые 10-15% своего контекстного окна. Остальное — dead zone. Если вы засовываете 128К токенов, а нужная информация лежит на 100К токене — модель ее не увидит.
Типичный сценарий: чат-бот техподдержки. В контексте — 20 страниц инструкций, 50 предыдущих диалогов. Пользователь задает простой вопрос: «Как сменить тариф?». Модель отвечает: «Смена тарифа недоступна в вашем регионе», хотя на самом деле доступна на странице 3. Почему? Потому что в ближайших к вопросу 5-7 чанках лежала информация об ограничениях, а не о смене тарифа.
Диагностика: логируйте позицию релевантного документа в контексте. Если он дальше 70% окна — виновник найден.
# Пример логирования позиции релевантного чанка
position = relevant_chunk_offset // total_tokens
if position > 0.7:
print('⚠️ Relevant info in dead zone')Решение:
- Ранжируйте чанки по релевантности — самые важные ставьте в начало (первые 15% контекста).
- Уменьшайте количество чанков. Лучше 2-3 идеальных, чем 5+ сомнительных. Поставьте threshold на similarity score.
- Используйте sliding window или map-reduce: сначала ретривите много чанков, потом с помощью LLM-фильтра выбираете релевантные.
Кстати, о том, как модель может логически «зависнуть» из-за неправильного расположения информации, читайте статью “Когда ИИ ошибается не фактом, а мыслью: почему логические сбои убивают доверие к нейросетям” — это не про галлюцинации, а про трещины в рассуждении.
Слой 3: Маршрутизатор — как слепой котенок (routing)
У вас несколько специализированных моделей или промптов под разные задачи. Вы пишете router — кусок логики, который решает, куда отправить запрос. И этот кусок глючит. Пользователь спрашивает «Как мне оплатить счет?», а роутер отправляет вопрос в агента по смене пароля. Результат — бессмысленный ответ.
Почему это происходит? Потому что роутер часто тоже LLM-прокладка (одним промптом решаем, какой навык активировать). А LLM не всегда предсказуемо классифицирует запросы, особенно если промпт роутера плохо задан или тестовых примеров мало.
Как это выглядит на практике
У вас есть три сценария: tech support, sales, contract analysis. Пользователь пишет: «Какие штрафы за просрочку платежа?». Классификатор решает, что это продажи (потому что слово «платеж» ассоциируется с оплатой нового заказа). Ответ: «Наши тарифы на подключение премиум-аккаунта...». Полное фиаско.
Диагностика: логируйте decision от роутера и сверяйте его с ground truth label.
# Пример диагностики роутера
router_decision = classify_user_input(user_query)
expected = ground_truth.get(user_query)
if router_decision != expected:
print(f'Router error: got {router_decision}, expected {expected}')
# Отправляем в лог для ретроспективного анализаРешение:
- Используйте few-shot промпты с примерами для каждого класса.
- Добавьте фоллбек-рутер: если уверенность низкая — отправляйте запрос всем агентам и используйте ансамбль или голосование.
- Собирайте датасет плохо классифицированных запросов и на нем дообучайте модель-рутер. Но помните — это уже fine-tuning, который может не понадобиться, если настроить правила.
Тема контрактного анализа раскрыта в статье “LLM-галлюцинации: Как заставить нейросеть говорить правду (или хотя бы не врать так очевидно)” — там есть конкретные шаблоны промптов для юридических сценариев.
Слой 4: Формат ответа — тихий убийца (contract analysis через призму вывода)
Вы просите модель вернуть JSON, но она возвращает Markdown с полями. Или просите дать ответ эксперта, а она пишет от лица пользователя. Это не модель плохая — это вы плохо описали ожидаемый формат в промпте. Или вообще не указали.
Симптомы: парсер падает, ответы неструктурированы, пользователь не может скопировать данные.
Лечится строгим форматированием вывода. Используйте function calling (tool use) или JSON mode. Задавайте template в системном сообщении.
# Пример форсированного JSON через OpenAI API v2
response = client.chat.completions.create(
model='gpt-5-2026-05-01',
response_format={ 'type': 'json_object' },
messages=[
{'role': 'system', 'content': 'Отвечай только JSON с ключами: result, confidence, reasoning.'},
{'role': 'user', 'content': 'Штраф за просрочку 5% от суммы'}
]
)Но есть нюанс: не все провайдеры (например, GLM 4.5 Air) стабильно поддерживают strict JSON mode. Тут потребуется либо постобработка, либо fallback на regex при разборе. Подробнее о том, как оптимизировать режим вывода у GLM, читайте в “GLM 4.5 Air в режиме тупняка: как выжать максимум скорости с enable_thinking: false”.
Слой 5: Динамическое поведение — модель сегодня не та, что вчера
Вы дали одни и те же данные, один и тот же промпт, а модель отвечает по-разному. Пользователи жалуются на непредсказуемость. Проблема: стохастичность вывода или обновление модели без вашего ведома.
OpenAI, Anthropic и другие поставщики меняют поведение моделей без анонса. GPT-4o могло чуть иначе отвечать в понедельник и пятницу. GPT-5, судя по статье про фундаментальную ошибку OpenAI, Google и Anthropic, тоже этим страдает. Решение: фиксировать версию модели, юзать прокси-слой логирования и привязывать к версии промпта. Используйте семантическую нумерацию для системных сообщений.
# config.yaml
inference:
model: gpt-5-2026-05-01
system_prompt_version: v2.3.1
temperature: 0.0План действий: как найти корень зла за 1 час
1 Изолируйте модель: замените на статичный промпт-ответ
Возьмите 10 тестовых запросов. Закрепите температуру 0, отключите retrieval, захардкодьте ответы в формате, который вы хотите. Если модель на этих фиксах выдает корректные ответы — косяк не в весах, а в окружении.
2 Включите логирование каждого звена пайплайна
Логируйте чанки до LLM, содержимое контекстного окна, решение роутера, позицию релевантных кусков. Сравнивайте с эталоном.
3 Прогоните батарею синтетических тестов с известными ответами
Сгенерируйте с помощью сильной модели (GPT-5, Claude 4) вопросы, на которые в вашей базе есть точные ответы. Прогоните пайплайн и проверьте, доходят ли эти ответы до финала. Это выявит дыры в ретривере и контекстном окне.
Неочевидный совет напоследок
Я знаю, вас сейчас раздражает фраза «надо больше тестировать». Но есть один трюк, который выносит мозг: запускайте в продакшене две параллельных inference-системы с разными настройками ретривера (разный chunk size, разный threshold similarity) и сравнивайте их ответы в A/B-режиме. Вы увидите, как чувствителен результат к параметрам, которые вы никогда не трогали. Это быстрее и дешевле, чем очередной fine-tuning.
И запомните: если после всех правок модель продолжает нести чушь — значит проблема именно в ней. И тут поможет статья “Почему open-source модели проваливаются в бою, пока лидируют в гонках” — спойлер: цифры на бенчмарках не гарантируют успеха в реальных данных.