Эта глава собирает типовые грабли. Команда видит, что модель отвечает, решает, что система работает, а разницу обнаруживает уже в проде. Ниже разберём такие провалы и укажем главы, где собрана защита от каждого из них.
Ошибки сгруппированы по стадии, на которой они закладываются: оценка качества → архитектура → эксплуатация → экономика.
Ситуация из реальной команды
Стартап делает «AI-ассистента по документам компании». Прототип собран за выходные, демо основателю прошло блестяще: три заготовленных вопроса — три отличных ответа. Фичу анонсируют клиентам. Дальше — четыре недели, за которые команда собирает почти полный комплект из этой главы:
- реальные пользователи задают вопросы не так, как автор демо: короче, с опечатками, на смеси языков — качество никто не мерил, оно «казалось хорошим»;
- один клиент вставил в вопрос содержимое письма на 30 страниц — упало по лимиту контекста, обработки ошибки не было;
- ответы бывали уверенно неверными, и пользователи это заметили раньше команды, потому что у команды не было ни логов ответов, ни канала жалоб;
- при инциденте провайдера сервис лежал вместе с ним;
- счёт за первый месяц превысил план в семь раз;
- на вопрос «почему ассистент вчера посоветовал клиенту несуществующий тариф» ответить не смог никто: ни промпта той версии, ни контекста того вызова не сохранилось.
Ни одна из этих проблем не экзотична. Все они — следствия одних и тех же выводов-пропастей. Разберём их по одной.
Ошибки оценки качества
1. «Проверил на своих примерах — работает»
Автор фичи проверяет на примерах, которые сам придумал. Они грамматичны, однозначны и похожи друг на друга — потому что придуманы одной головой за один вечер. Реальный трафик выглядит иначе: обрывки, опечатки, два вопроса в одном, вопросы не по теме, копипаста логов.
Вывод «работает» на такой выборке — это selection bias в чистом виде. Диагностика простая: если все ваши тестовые примеры дают правильный ответ, у вас не хорошая система, а слабый набор примеров.
Как правильно: golden dataset из реальных или реалистично разнообразных примеров, включая граничные и adversarial — глава 38. До прода — хотя бы 30 штук и час честной разметки.
2. «Один раз ответило правильно — значит, отвечает правильно»
LLM недетерминирована (глава 3). Один прогон — это одна точка из распределения. Особенно коварны пограничные входы: сегодня модель отвечает billing, завтра на том же тексте — refund, и оба раза «уверенно».
Как правильно: важные случаи прогоняются много раз; тесты проверяют инварианты (валидность схемы, попадание в enum, наличие обязательных полей), а не точное совпадение строк. Тест, который сравнивает ответ модели посимвольно с эталоном, будет мигать вечно, и команда научится его игнорировать, что хуже отсутствия теста.
3. Вера в самооценку модели
В контракте есть поле confidence: 0.92, и команда строит на нём пороги, никогда не проверив, коррелирует ли оно с фактической точностью. Это число модель сгенерировала так же, как весь остальной текст, — правдоподобно. Не измеренный confidence — украшение, а не сигнал.
Как правильно: калибровка на размеченных данных (глава 45 делает это в задаче 45.1). Не калибровали — не используйте в логике.
Ошибки архитектуры
4. Свободный текст как формат ответа
Backend парсит ответ модели регулярками и split-ами. Пока формулировки стабильны — живёт; при обновлении модели или редкой формулировке — падает, причём не на вашем тесте, а на проде ночью.
Как правильно: контракт + схема + валидация + repair loop — главы 7–9. Это самый дешёвый в исправлении пункт списка, поэтому он же — самый обидный, когда его ловят в проде.
5. «У модели окно 1M — зальём всё»
В контекст отправляется вся база знаний, вся история диалога, весь документ — «пусть модель сама разберётся». Результат: latency растёт, стоимость растёт, качество падает, потому что релевантное тонет в шуме (глава 3: контекст — это внимание, а не память).
Как правильно: контекст проектируется как SQL-запрос — только нужное (главы 18–19). Лимит на каждый источник контекста — архитектурное решение, а не свойство модели.
6. Бизнес-логика и факты внутри промпта
«Если клиент премиум — скидка 15%», «наши тарифы: ...» — вписано в шаблон промпта. Теперь правила бизнеса исполняются вероятностно, а факты устаревают при каждом изменении прайса.
Как правильно: логика — в коде, факты — из источников истины, модель — интерпретирует (главы 2, 5). Проверка на месте: если предложение промпта можно заменить if-ом или SELECT-ом — его нужно заменить.
7. Синхронный вызов там, где место очереди
LLM-вызов на 40 секунд живёт внутри HTTP request-response. Пользователь ждёт, nginx режет соединение, пользователь жмёт ещё раз, задача выполняется дважды, денег уходит вдвое.
Как правильно: всё, что дольше UX-бюджета, — в очередь с идемпотентностью и статусами (главы 15, 24, 30 — там же разобран этот инцидент с кнопкой в админке).
Ошибки эксплуатации
8. Нет диагностического следа
Самая дорогая ошибка списка — потому что она блокирует исправление всех остальных. Нет лога вызовов: не восстановить, какой prompt, какая версия, какой контекст и какой ответ были в инциденте. Отладка превращается в пересказы («пользователь говорит, что ассистент нагрубил»), улучшение — в гадание.
Как правильно: llm_call_log с первого дня — версия промпта, модель, токены, стоимость, latency, статус (глава 24). Это не «наблюдаемость потом» — это условие возможности отвечать на вопрос «почему».
9. Ошибки провайдера не обработаны
Провайдер для новичка — это «облако, которое отвечает». Провайдер в реальности — внешняя система с таймаутами, rate limit-ами, деградациями и обновлениями моделей без вашего согласия. Код без обработки этого наследует все инциденты провайдера и добавляет свои: ретраи без backoff устраивают самострел (глава 27 начинается с хроники такой ночи).
Как правильно: таймауты, ограниченные ретраи транзиентного, circuit breaker, спроектированная деградация — глава 27.
10. PII уходит без контроля
В логах — полные тексты обращений с именами и телефонами; в промпт уходит всё письмо клиента, хотя нужна одна строка; debug-режим пишет всё подряд и никогда не выключается. Юридические последствия наступают позже технических, но дороже.
Как правильно: redaction до записи, hash вместо сырья, retention-политика, минимизация контекста (главы 24, 29).
Ошибки экономики
11. Стоимость узнают из счёта
Никто не посчитал стоимость события до запуска, не заложил ретраи и проверочные вызовы, не поставил лимиты. Счёт в конце месяца становится дизайн-ревью, проведённым задним числом и за деньги.
Как правильно: калькулятор до кода, три сценария, бюджет с enforcement — главы 43–44.
12. Флагманская модель на каждый чих
Классификация из пяти категорий выполняется самой дорогой моделью — «чтобы точно». Разница в цене между ярусами моделей — 10–50 раз; на масштабе это разница между фичей с положительной экономикой и фичей, которую закроют.
Как правильно: самая дешёвая достаточная модель, измеренная на golden dataset; дорогая — как эскалация (глава 45).
Карта: симптом → диагноз
Таблица для быстрой самодиагностики — по симптому найти ошибку и главу:
| Симптом | Ошибка | Глава |
|---|---|---|
| «На демо работало, пользователи жалуются» | 1: selection bias | 38 |
| Тесты с ответами модели мигают | 2: точное сравнение недетерминированного | 3, 38 |
| Пороги по confidence ведут себя странно | 3: некалиброванная самооценка | 45 |
| Ночью упал парсер ответа | 4: свободный текст | 7–9 |
| Медленно, дорого, качество «плавает» | 5: раздутый контекст | 18–19 |
| Модель «нарушает» правила бизнеса | 6: логика в промпте | 2, 5 |
| Дубли задач, оборванные запросы | 7: sync вместо очереди | 15, 24 |
| «Не можем понять, что произошло» | 8: нет следа | 24, 28 |
| Инцидент провайдера = ваш инцидент ×4 | 9: голый вызов | 27 |
| Вопросы от юристов | 10: PII без контроля | 29 |
| Счёт ×7 от плана | 11: экономика постфактум | 43–44 |
| Стоимость события не сходится с ценностью | 12: флагман везде | 45, 47 |
Правило главы
«Оно отвечает» — наблюдение об одном прогоне. «Оно работает» — утверждение о распределении: о качестве на реальном трафике, о поведении при сбоях, о стоимости на масштабе и о вашей способности ответить, почему система сделала то, что сделала. Путь от первого ко второму не проходит через улучшение ответов — он проходит через измерение, контракты, обвязку и экономику. Собственно, об этом остальная книга.
Чеклист главы
Быстрый аудит «не строю ли я сейчас историю из начала главы»:
- Качество измерено на dataset-е, который писал не автор промпта в одиночку.
- Тесты проверяют инварианты, а не точные строки.
- Ни одно поле самооценки модели не используется в логике без калибровки.
- Ответ модели — контракт со схемой, а не текст с парсером.
- У каждого источника контекста есть лимит.
- В промпте нет правил бизнеса и фактов из БД.
- Операции дольше нескольких секунд — в очереди.
- Любой вызов можно восстановить из лога: версия, вход, выход, стоимость.
- Таймаут, rate limit и падение провайдера обработаны явно.
- Стоимость события посчитана до запуска, лимиты стоят.
Практикум
Задача 6.1 — код-ревью минного поля. ⭑⭑
Ниже — сервис «AI-ответов на отзывы», написанный собирательным новичком. В нём не меньше десяти ошибок из этой главы. Найдите их без подглядывания в текст выше, для каждой укажите: строка → ошибка → чем кончится в проде → как исправить.
import openai, json, re
client = openai.OpenAI(api_key="sk-proj-XXXX") # ключ Васи, у него безлимит
PROMPT = """Ты — вежливый ассистент магазина TechnoShop.
Наши тарифы доставки: обычная 300р, экспресс 700р, бесплатно от 5000р.
Если клиент VIP (потратил больше 100000р), извинись дважды и предложи промокод SORRY15.
Ответь на отзыв клиента: {review}
В конце верни JSON с полями sentiment и reply. Будь предельно внимателен!!!"""
def answer_review(review_text: str, user_history: list[str]) -> dict:
full_context = "\n".join(user_history) # вся история покупок для контекста
resp = client.chat.completions.create(
model="gpt-5.5", # берём лучшую, чтобы точно работало
messages=[{"role": "user",
"content": PROMPT.format(review=review_text) + full_context}],
)
text = resp.choices[0].message.content
json_part = re.search(r"\{.*\}", text, re.DOTALL).group(0)
result = json.loads(json_part)
print(f"Ответили на отзыв: {review_text}") # для отладки
return result
# endpoint
@app.post("/reviews/{review_id}/ai-reply")
def ai_reply(review_id: int):
review = db.get_review(review_id)
result = answer_review(review.text, db.get_user_history(review.user_id))
db.save_reply(review_id, result["reply"])
send_email_to_customer(review.user_email, result["reply"]) # сразу клиенту
return resultКритерии приёмки:
- Найдено минимум 10 проблем; каждая привязана к конкретной строке и последствию.
- Проблемы ранжированы: что уронит прод на этой неделе, что сожжёт деньги за месяц, что взорвётся при аудите.
- Для трёх самых опасных написан исправленный вариант кода.
- Список сверен с главой: что вы не нашли — то ваша персональная слепая зона, запишите её.
Подсказки:
- Считайте не только «технические» ошибки: автоотправка письма клиенту без человека — тоже ошибка, и не последняя по цене.
- Обратите внимание на то, чего в коде нет: обработки ошибок, лимитов, лога, версии промпта, валидации.
Задача 6.2 — премортем своей фичи. ⭑⭑
Возьмите LLM-фичу, которую вы планируете (или недавно запустили). Проведите премортем: «прошло три месяца, фича провалилась — почему?». Напишите постмортем этого воображаемого провала по всем двенадцати ошибкам главы: какие из них ваша фича совершает прямо сейчас.
Критерии приёмки:
- По каждой из 12 ошибок — вердикт: защищены / уязвимы / неприменимо, с одним предложением обоснования.
- Для каждой уязвимости — оценка цены: во что обойдётся, если выстрелит (деньги, доверие, время).
- Топ-3 уязвимости превращены в задачи в вашем трекере со ссылками на главы книги.
- Документ показан коллеге, и коллега нашёл хотя бы одну уязвимость, которую вы пропустили.
Подсказки:
- Премортем работает лучше ретроспективы, потому что признаваться в будущих ошибках не стыдно. Пользуйтесь этим окном честности.