8.4 · Ручной разбор ошибок
Неделя 8 · Evals · ⏱ ~4 ч · Практика
Зачем это AI-инженеру
Заголовок раздела «Зачем это AI-инженеру»После экспериментов 8.2 и 8.3 у выбранной конфигурации есть цифра, например «correctness 0,78» на dev из 56 вопросов. Метрика говорит, сколько ответов плохих, но не говорит, что чинить. Ручной разбор ошибок (error analysis) отвечает на вопрос «почему» и превращает 12 проваленных вопросов в список причин с частотами, а список — в приоритет работы. Hamel Husain называет этот шаг самым недооценённым и самым ценным в работе над AI-продуктами. На собеседовании история «прочитали 30 провалов, 40% оказались одной причиной, исправили её и получили +N п.п.» показывает инженера, а не оператора фреймворка.
Почему смотреть глазами, а не придумывать метрики
Заголовок раздела «Почему смотреть глазами, а не придумывать метрики»Соблазн: придумать ещё одну автоматическую метрику. Но неизвестно, что мерить, пока не видно, как система ошибается. В 6.1 был цикл «смотреть на данные → найти типы ошибок → написать eval → изменить систему». Сейчас делаем первый и второй шаги всерьёз.
Аналогия из жизни магазина: разбор жалоб покупателей. Общая цифра «8% обращений закончились недовольством» говорит, что плохо, но чинить нечего, пока не открыть каждую жалобу и не увидеть: здесь оператор назвал старый срок возврата, здесь перепутал гарантию двух ламп, здесь посчитал доставку не по той строке тарифа. Распределение причин подсказывает, что править первым.
Единица разбора: полный трейс
Заголовок раздела «Единица разбора: полный трейс»Чтобы найти, где цепочка сломалась, мало видеть вопрос и неверный ответ: нужен каждый шаг пайплайна. Для каждого провала читаем целиком:
- вопрос, его тип и теги ловушек (
tags); - эталон,
must_includeи цитатыevidence; - найденные куски (топ-k) с позициями,
url, флагомarchived, и где среди них релевантные; - ответ модели, флаг отказа и передачу оператору;
- вердикты и критики судей.
Всё это уже есть в results.jsonl из 6.4 или в трейсе Langfuse (6.5). Стоит сделать удобный просмотрщик: читать JSON в терминале 30 раз утомительно, и детали начнут ускользать.
Открытое и осевое кодирование
Заголовок раздела «Открытое и осевое кодирование»Трейсы собраны; теперь нужен способ превратить 30 прочитанных историй в таблицу причин и при этом не придумать причины заранее. Hamel заимствует из качественных исследований два шага:
Открытое кодирование (open coding). Читаем трейс и пишем свободную заметку о первой ошибке в цепочке: «вопрос про „Свет мини“, в топ-3 только lamp-light.md», «кусок с тарифом на 2-м месте, но модель взяла цену из соседней строки таблицы», «эталон устарел, в kb/delivery.md теперь другой тариф». На этом шаге ничего не подгоняем под готовые категории.
Почему первая ошибка: ошибки каскадируют. Если поиск не нашёл кусок, а модель потом выдумала ответ, корень — поиск; галлюцинация — следствие.
Осевое кодирование (axial coding). Когда заметок 20–30, группируем их в категории. Стартовый список:
| Категория | Признак в трейсе | Где чинить |
|---|---|---|
| R: не нашёл кусок | Релевантного куска нет в топ-k | Поиск: чанкинг, гибрид, переформулировка, k, реранкер, фильтры |
| G: нашёл, но не использовал | Релевантный кусок в топ-k, ответ неверен или ложный отказ | Промпт ответа, порядок/формат контекста, модель |
| H: галлюцинация | Ответ содержит факт, которого нет в кусках (faithfulness FAIL) | Промпт («только по кускам»), модель, отказ |
| D: плохой вопрос в наборе | Неоднозначный вопрос, неверный/устаревший эталон, неверная цитата | Набор (6.3) |
Почти наверняка появятся подкатегории и новые категории. Типичные для ассистента поддержки (почти все совпадают с ловушками из tags):
- R-wrong-product: найден кусок похожего товара — «Свет» вместо «Свет мини»,
MUG-SOUTHвместоMUG-NORTH; - R-partial: вопросу нужны две страницы, найдена одна — например, порог бесплатной доставки только из
delivery.mdбезpromo.md; - R-archived: в контекст попал
returns-2025.md, и ответ цитирует старые правила возврата (archived_in_context> 0); - G-refusal: кусок найден, но модель ответила «В базе знаний нет ответа на этот вопрос.» (ложный отказ);
- G-table: нужная таблица в топ-k, но модель взяла не ту ячейку тарифов (зону, вес);
- G-conflict: обе страницы с порогом найдены, а модель назвала одну цифру вместо двух;
- G-no-handoff: вопрос без ответа в базе («где мой заказ 1042?»), а модель ответила общими словами про статусы вместо отказа и передачи оператору;
- H-promise: обещание действия, которого ассистент не делает: «оформим возврат», «вернём деньги», «вот промокод».
Отдельно от подкатегорий появляется пятая категория верхнего уровня — J: ошибка судьи: ответ на самом деле верный (или неверный), а судья ошибся. Она важна: часть «провалов» — провалы судьи. Если их много, сначала чинится судья, иначе все дальнейшие цифры ложные.
Дерево решений для разметки
Заголовок раздела «Дерево решений для разметки»Чтобы один и тот же трейс не попадал то в R, то в H в зависимости от настроения, категории проставляем по фиксированному порядку вопросов:
провал │ ├─ вопрос/эталон корректен? ── нет ──► D │ ├─ ответ на самом деле верен? ── да ──► J (судья ошибся) │ ├─ релевантный кусок в топ-k? ── нет ──► R (R-wrong-product / R-partial / R-archived / ...) │ ├─ ответ содержит факты не из кусков? ── да ──► H (в т.ч. H-promise) │ └─ иначе ──────────────────────────────────► G (refusal / table / conflict / no-handoff / ...)Порядок важен: сначала проверяем набор и судью, потом поиск, потом генерацию. Для unanswerable шаг «релевантный кусок» пропускаем: находить нечего.
Сколько читать
Заголовок раздела «Сколько читать»Цель — 30 провалов. На dev из 56 вопросов при correctness 0,78 провалов около 12, поэтому обычно их меньше 30: тогда разбираем все, добавляем провалы из прогонов других конфигураций 8.3 и «мигающие» вопросы из 8.1 (в одном прогоне PASS, в другом FAIL). Если больше, сэмплируем со стратификацией по типу, чтобы не разобрать 30 comparative и ни одного factual.
Признак, что прочитано достаточно, — насыщение: последние 5–10 трейсов не добавляют новых категорий.
Отдельно читаем 5–10 успешных ответов. Это проверка судьи с другой стороны: нет ли среди PASS неверных ответов. Если есть, судья пропускает ошибки, и correctness завышена. Типичный пример — PASS за «12 месяцев гарантии» в вопросе про «Свет мини», у которой 6.
Распределение причин
Заголовок раздела «Распределение причин»Когда у каждого трейса есть категория, считаем, сколько раз встретилась каждая. После разметки — простая таблица:
| Причина | Кол-во | Доля |
|---|---|---|
| R-wrong-product | 9 | 30% |
| G-refusal | 6 | 20% |
| R-partial (порог доставки) | 5 | 17% |
| D (эталон/вопрос) | 4 | 13% |
| H-promise | 3 | 10% |
| J | 3 | 10% |
| Итого | 30 |
(Иллюстрация.) Такая таблица и есть результат разбора: она говорит, что сначала стоит чинить. Важно помнить: на 30 случаях доли грубые, ±десяток п.п. Отличать 30% от 20% не стоит, отличать 30% от 5% — да.
Выбор исправления
Заголовок раздела «Выбор исправления»Чиним самую частую причину, если исправление разумно по усилиям. Для примера выше:
- R-wrong-product (30%): товар почти всегда назван в вопросе. Фильтр по
categoryиз 5.1 оставляет все карточки товаров, но не отличает одну лампу от другой. Стоит проверить, распознаётся ли товар на этих вопросах; возможно, не распознаются «маленькая лампа», «мини-светильник», SKU в нижнем регистреlamp-light-mini. Исправление: распознавание товара по названию, SKU и разговорным вариантам + фильтр или буст поdocего карточки (kb/products/lamp-light-mini.md). - D (13%): исправляется сразу и дёшево, но это не улучшение системы, а улучшение набора. Исправляем, поднимаем версию набора, перепрогоняем baseline.
- J: исправляем промпт судьи, перепроверяем согласие на разметке
eval/judge_labels.jsonl.
Порядок на практике: сначала D и J (иначе меряем кривой линейкой), затем самая частая категория системы.
Измерить эффект
Заголовок раздела «Измерить эффект»Исправление без замера — снова «попробовали, вроде лучше». Исправление — это эксперимент по форме 8.2: гипотеза («распознавание товара с фильтром по карточке уберёт большую часть R-wrong-product, hit@5 +X п.п.»), прогон, парное сравнение, проверка регрессий. Два важных вопроса:
- Ушли ли ошибки той категории, которую чинили? Пересчитываем: из 9 R-wrong-product сколько теперь проходят.
- Не сломалось ли что-то ещё? Вопросы, которые проходили и стали проваливаться. Фильтр по товару может испортить comparative-вопросы: «чем отличается гарантия „Свет“ и „Свет мини“?» теперь ищет только по одной карточке. А наивный поиск подстроки находит «Свет» внутри «Свет мини» и путает товары в обратную сторону.
Новые evals из разбора
Заголовок раздела «Новые evals из разбора»Ручной разбор дорог, и повторять его после каждого изменения не получится. Поэтому то, что нашли глазами, закрепляем кодом: каждая найденная категория — кандидат в автоматическую проверку:
- R-wrong-product → метрика «доля кусков в топ-k с карточки нужного товара» (дешёвая, по
docиsource_urlsиз набора); - R-archived → счётчик
archived_in_context(уже есть в 6.4, должен быть 0); - G-refusal → счётчик ложных отказов
false_refusals(тоже уже есть); - G-conflict → assertion для вопросов с тегом
conflict: в ответе ссылки и наkb/delivery.md, и наkb/promo.md; - H-promise → assertion «нет обещаний действий» («оформим возврат», «вернём деньги») для тега
action_request; - формат ответа → assertion «есть хотя бы одна ссылка
kb/<file>.md#<anchor>».
Так набор метрик растёт из реальных ошибок, а не из списка в библиотеке.
DeepSeek и Ollama: как это сделать у нас
Заголовок раздела «DeepSeek и Ollama: как это сделать у нас»Метод разбора от провайдера не зависит. Зависят от провайдера типичные провалы, которые легко принять за ошибки системы, и то, кому можно показывать трейсы. Сверено в ноябре 2026 по api-docs.deepseek.com и docs.ollama.com.
Добавляем категорию I: инфраструктура
Заголовок раздела «Добавляем категорию I: инфраструктура»Помимо R / G / H / D / J на нашем стеке появляются провалы (часть из них уже встречалась в 6.4 и 8.3), где виновата не логика RAG, а то, как вызвана модель. Их стоит выделить в отдельную категорию I, потому что чинятся они конфигом, а не промптом или поиском:
| Признак в трейсе | Провайдер | Что на самом деле | Как проверить и чинить |
|---|---|---|---|
Пустой content, вердикт ERROR, посчитанный как FAIL |
DeepSeek JSON mode | документированное поведение: при невалидном JSON ответ может быть пустым | ретрай; считать ERROR отдельно |
| Вердикты на одном ответе разные между прогонами | DeepSeek | судья в режиме thinking, temperature не действует | thinking: disabled в конфиге судьи |
| Ответ игнорирует кусок, который точно был в топ-k | Ollama | контекст сервера меньше промпта, хвост обрезан молча (по умолчанию 4k, если видеопамяти меньше 24 ГБ) | prompt_eval_count в ответе против ожидаемого числа токенов, ollama ps |
| Таймауты, 429, внезапно долгие ответы | DeepSeek / Ollama | лимит одновременных запросов DeepSeek; у Ollama — очередь, загрузка модели после простоя, своп при 8 ГБ памяти | лог ретраев и времени, прогрев, последовательный прогон |
| Ответ на английском на русский вопрос, сломанный формат ссылок | чаще локальная 4B | слабая модель хуже держит инструкцию | это G (модель), не I; отмечаем срез по языку (ru/en) |
| Результаты «поплыли» между неделями при том же конфиге | DeepSeek | под именем модели теперь другая версия | сравнить resp.model в прогонах |
Порядок в дереве решений расширяется: сначала I (инфраструктура), потом D (набор), J (судья), R, H, G. Провал из-за обрезанного контекста, размеченный как G-refusal, отправит переписывать промпт, который ни при чём.
Помощь модели в разборе — с оговорками
Заголовок раздела «Помощь модели в разборе — с оговорками»Открытое кодирование делаем сами: в этом весь смысл. Но на шаге осевого кодирования модель может помочь сгруппировать 30 заметок в черновые категории. Для проекта 3 это можно делать в deepseek-flash: трейсы содержат только выдуманные вопросы и публичную базу знаний kb/. Правило на будущее: когда разбор пойдёт по агенту-разборщику заказов (проект 4), трейсы со снимком базы магазина, где есть личные данные клиентов, в DeepSeek и другие облака не отправляются. Локальная модель 3–4B (машина с 8 ГБ) для группировки слабовата, так что там группируем сами или работаем на демо-базе без личных данных.
Второе мнение для категории J
Заголовок раздела «Второе мнение для категории J»Есть подозрение на ошибку судьи deepseek-v4-pro? Прогоняем спорные трейсы (5–10 штук) через судью другого семейства — Claude или OpenAI (данные публичные) — и сравниваем с собственным решением. Если другой судья согласен с нами, а DeepSeek нет, это сильный довод чинить промпт судьи. Локальная модель 3–4B для этой роли не годится: она слабее оцениваемой системы.
| Что нужно разбору | Claude | OpenAI | DeepSeek | Ollama (4B, 8 ГБ) |
|---|---|---|---|---|
| Типичные провалы-«не ошибки системы» | ограничения параметров у части моделей | отказ reasoning-моделей от sampling-параметров | пустой JSON, thinking и temperature, смена версии под именем | обрезка контекста, прогрев, очередь, своп |
| Помощь в группировке заметок | можно (публичные данные) | можно (публичные данные) | можно (публичные данные) | приватно, но слабо |
| Второе мнение по J | да | да | это и есть главный судья | нет, слишком слабая |
| Трейсы с личными данными клиентов | нельзя | нельзя | нельзя, серверы в Китае | можно, но качество низкое |
Маршрут изучения
Заголовок раздела «Маршрут изучения»- Hamel: A Field Guide to Rapidly Improving AI Products → разделы про error analysis и про просмотрщик данных (≈30 мин). Обращаем внимание на идею, что лучший инструмент — простой, сделанный под свои данные.
- Hamel: AI Evals FAQ → вопросы про error analysis, open coding / axial coding и сколько трейсов читать (поиск по странице «error analysis», «axial») (≈30 мин).
- В Hamel: Your AI Product Needs Evals перечитываем раздел про просмотр данных и инструменты для этого (≈10 мин).
- (По желанию) Who Validates the Validators? (Shankar и др.) → введение и выводы (≈20 мин): про дрейф критериев, который мы увидим на себе во время разметки.
- Открываем свои результаты: последний прогон выбранной конфигурации из 8.3 (
eval/runs/<...>/results.jsonl) или трейсы в Langfuse. - В главе 6.4 · Скрипт оценки: retrieval + faithfulness перечитываем раздел «DeepSeek и Ollama» (≈10 мин): пустой JSON, thinking и temperature, обрезка контекста Ollama — это кандидаты в категорию I.
- Ollama API: /api/chat (≈10 мин): находим поля
prompt_eval_countиeval_countв ответе — ими проверяется, дошёл ли весь контекст до локальной модели.
На что обратить внимание
Заголовок раздела «На что обратить внимание»- Подгонка под готовые категории. Начинаем с открытых заметок; категории формируются после.
- Разметка следствия, а не корня. Галлюцинация после промаха поиска — это R, не H.
- Игнорирование ошибок судьи. Если 20% «провалов» — ошибки судьи, все метрики врут на эту величину.
- Исправление эталона под ответ системы. Эталон правим, только когда он объективно неверен (сверка с
kb/), и пишем причину вnotes. - Частный фикс под один вопрос («если спрашивают про гарантию мини-лампы, бери раздел X») — подгонка под набор. Исправление должно быть общим механизмом.
- Доли на 30 случаях грубые. Не делаем выводов из разницы 20% и 17%.
- Чтение без записи. Через час не вспомнить, что было в 7-м трейсе. Размечаем сразу в файл.
- Остановка на распределении. Цель главы — одно исправление с измеренным эффектом.
- Инфраструктурный провал размечен как ошибка модели. Обрезанный контекст Ollama, пустой JSON DeepSeek, судья в режиме thinking — сначала категория I.
- Ложный отказ и пропущенная передача — разные ошибки. G-refusal портит ответы на вопросы, где ответ в базе есть; G-no-handoff оставляет покупателя без оператора. Правка промпта против одной легко усиливает другую: смотрим оба счётчика.
- Личные данные в облачный «помощник по разметке». В проекте 3 трейсы публичные; в проекте 4 — нет.
Практика
Заголовок раздела «Практика»- Пишем
eval/review_failures.py: берётresults.jsonlпрогона, выбирает провалы (correctness FAIL, неверный отказ, пропущенная передача оператору), сэмплирует до 30 со стратификацией поtype, показывает по одному трейсу в читаемом виде: вопрос иtags, эталон, цитаты, топ-k с позициями,urlи пометкой релевантных и архивных, ответ, критики судей. Сразу добавляем выводresp.model, режима thinking,usage(для Ollama —prompt_eval_count) и числа ретраев: без них категорию I не увидеть. - Для каждого трейса записываем в
eval/error_labels.jsonl:id,note(свободная заметка о первой ошибке), позжеcategory,subcategory. - Читаем все 30, только заметки (открытое кодирование).
- Группируем заметки в категории (осевое кодирование): стартуем с R / G / H / D, добавляем J, I и подкатегории по необходимости. I проверяем первой в дереве решений. Проставляем категории в файле.
- (Опционально) Просим
deepseek-flashсгруппировать наши 30 заметок в черновые категории и сравниваем с собственной группировкой. Только для публичных трейсов проекта 3. - Читаем 5–10 успешных ответов; найденные неверные PASS отмечаем как J.
- Для 5–10 спорных случаев категории J получаем второе мнение судьи другого семейства (Claude или OpenAI) и записываем, с каким решением согласны мы.
- Скрипт печатает распределение причин (кол-во и доля), также по
typeи поtags. Таблицу сохраняем вexperiments/EXP-03-error-analysis.md. - Исправляем D и J (набор → новая версия; промпт судьи → повторная проверка согласия). Перепрогоняем baseline выбранной конфигурации на новой версии набора.
- Выбираем самую частую причину системы, записываем гипотезу исправления и ожидаемый эффект.
- Реализуем исправление (общим механизмом, не под конкретные вопросы), прогоняем evals, сравниваем через
compare.py: сколько вопросов исправленной категории теперь проходят, какие регрессии появились, парный тест. - Добавляем хотя бы одну новую автоматическую метрику или assertion по найденной категории в
eval/run.py. - Добавляем строку в таблицу экспериментов.
Готово, когда
Заголовок раздела «Готово, когда»- Размечено 30 провалов, есть распределение причин (таблица с количеством и долей).
- Исправлены ошибки набора и судьи (если были), версия набора поднята.
- Одно исправление самой частой причины с измеренным эффектом: сколько ошибок категории ушло, Δ метрик, регрессии, парный тест.
Тест для самопроверки
Заголовок раздела «Тест для самопроверки»Вопрос 1
Чем ручной разбор ошибок отличается от подсчёта метрик и почему нужен и тот, и другой?
Метрики говорят «сколько» ошибок, разбор — «почему»; без причин непонятно, что чинить, без метрик не измерить эффект исправления.
Вопрос 2
Поиск не нашёл кусок, модель выдумала ответ. Какая категория и почему?
b. Ошибки каскадируют; корень в поиске, галлюцинация — следствие отсутствия нужного куска.
Вопрос 3
Что такое открытое и осевое кодирование?
Открытое — свободные заметки о первой ошибке в каждом трейсе без готовых категорий; осевое — группировка заметок в категории и подсчёт их частот.
Вопрос 4
Почему сначала исправляют ошибки набора и судьи, а потом системы?
Иначе меряем кривой линейкой: неверные эталоны и ошибки судьи искажают и распределение причин, и эффект любого исправления.
Вопрос 5
Как понять, что прочитано достаточно трейсов?
По насыщению: последние 5–10 трейсов не добавляют новых категорий.
Вопрос 6
Зачем читать несколько успешных ответов при разборе ошибок?
Чтобы найти ложные PASS судьи (например, «12 месяцев» в ответе про «Свет мини»): если среди успешных есть неверные ответы, correctness завышена и судью надо чинить.
Вопрос 7
Что не так с таким исправлением?
if "мини" in question.lower() and "гарант" in question.lower(): chunks = [c for c in chunks if c["section"] == "Товары > Лампа «Свет мини» > Гарантия"]Частный хардкод под конкретный вопрос из набора: подгонка, которая не обобщается, хрупкая (опечатки, «маленькая лампа», SKU LAMP-LIGHT-MINI, другие формулировки) и может сломать соседние вопросы. Нужен общий механизм: распознавание товара в вопросе и фильтр по карточке этого товара.
Вопрос 8
После исправления категории R-wrong-product hit@5 вырос, но comparative-вопросы про две лампы стали проваливаться. Что произошло вероятнее всего?
Фильтр оставляет одну карточку, а comparative-вопросу нужны две: фильтр должен учитывать все упомянутые товары (и не находить «Свет» внутри «Свет мини» как отдельный товар).
Вопрос 9
По результатам разбора: R 30%, G 20%, D 13%. Можно ли утверждать, что R встречается в полтора раза чаще G?
Нет: на 30 случаях это 9 против 6, разница в пределах шума. Можно сказать только, что обе категории частые.
Вопрос 10
Как результаты разбора превращаются в новые evals? Пример.
Каждая частая категория получает автоматическую проверку, например R-wrong-product → доля кусков в топ-k с карточки нужного товара по метаданным doc; G-conflict → assertion на ссылки и на kb/delivery.md, и на kb/promo.md; H-promise → assertion на отсутствие обещаний «оформим возврат».
Вопрос 11
В разборе 6 из 30 провалов — локальная модель «проигнорировала» кусок, который был в топ-k. К какой категории их отнести в первую очередь и как проверить?
Сначала проверить категорию I: не обрезан ли контекст Ollama (по умолчанию 4k, если видеопамяти меньше 24 ГБ). Сравнить prompt_eval_count с ожидаемым числом токенов промпта и посмотреть ollama ps. Только если контекст дошёл целиком, это G.
Вопрос 12
Почему трейсы проекта 3 можно отдать deepseek-flash для черновой группировки заметок, а трейсы агента проекта 4 — нет?
В проекте 3 трейсы содержат только выдуманные вопросы и публичную базу знаний магазина. Агент проекта 4 читает снимок базы магазина с личными данными клиентов, а данные DeepSeek обрабатываются на серверах в Китае; личные данные клиентов в облако не отправляются, такие трейсы разбираются локально.
Что дальше
Заголовок раздела «Что дальше»Все эксперименты сделаны. В 8.5 · Проект 3: таблица экспериментов в README собираем их в историю улучшений в цифрах для портфолио.