Главная / Статьи / Когда классификатор тихо подменяет модель: зачем нужны заголовки с фактической моделью

Когда классификатор тихо подменяет модель: зачем нужны заголовки с фактической моделью

Тихий откат на более дешёвую модель не оставляет ни ошибки, ни строки в логе. Как найти данные о фактической модели в транскриптах Claude Code и почему провайдеры должны отдавать их в ответах.

1861 слов

Когда 7 августа Anthropic написала о послаблении биологического классификатора безопасности для своей флагманской модели, большинство комментаторов отнесли это к старому спору о чрезмерных отказах, теме, которая казалась исчерпанной. Но самое интересное слово в анонсе — не отказ, а fallback, откат.

Подмена без сигнала

Согласно анонсу, сработавший классификатор не останавливает запрос. Он перенаправляет его на Opus 5 — сильную модель, у которой нет биологических возможностей Fable 5. Ничего не блокируется и не фильтруется, никакого предупреждения не появляется. Пользователь просит топовую модель, классификатор решает, что промпт похож на биологию, и отвечает другая модель.

Модели различаются по цене. По публичному прайсу Fable 5 стоит $10 за миллион входных токенов и $50 за миллион выходных, а Opus 5 — $5 и $25, вдвое дешевле в обе стороны. Как тарифицируется перенаправленный запрос, в посте не сказано, и это не проверялось, поэтому о биллинге здесь утверждений нет. Установлено другое: подмена с задокументированной целью и опубликованной разницей в цене.

Характеристики важнее цены. В документации сравнение выглядит так:

  • Позиционирование в документации: Fable 5 рассчитана на агентов, которые работают долго, а Opus 5 — на сложное агентное программирование и бизнес-нагрузки.
  • Цена за миллион токенов, вход и выход: $10 и $50 против $5 и $25.
  • Контекстное окно: 1M у обеих.
  • Максимальный вывод: 128K у обеих.
  • Надёжная граница знаний: январь 2026 у Fable 5, май 2026 у Opus 5.

Размер контекста совпадает, потолок вывода совпадает, а знания модели-замены на четыре месяца свежее. Её собственная документация даже рекомендует её в первую очередь для требовательного агентного программирования. Анонс прямо называет цену подмены — биологические возможности, — и это потеря, которую не покажет ни один дифф кода.

Так что подмена не исчерпывает бюджет контекста, не упирается в лимит токенов и на задаче по программированию может вообще не выглядеть хуже. Это не смягчающее обстоятельство. Именно поэтому подмена и невидима.

Внимательно читаем цифры

Главная цифра — снижение примерно на 85% откатов, связанных с биологией, по всем продуктам Anthropic: одно число, только про биологию, агрегированное по всему.

Цифры по отдельным продуктам измеряют другое и приводятся в сноске про все откаты, биологические и нет. Общее число откатов, по прогнозу, упадёт примерно на две трети в Claude.ai (67%), чуть больше чем наполовину в Cowork (55%), на 17% в Claude Code и всего на 7% на Claude Platform. Это не доли от 85, и если смешать два набора цифр, получится аккуратная, цитируемая и неверная фраза.

К тому же это сокращения, а не частоты. Снижение на 7% на Platform ничего не говорит об абсолютной частоте там — возможно, откаты всегда были редкими. Зато разброс показывает, где перенастройка сработала: потребительский чат сдвинулся примерно в десять раз сильнее, чем платформа для разработчиков, а программирование оказалось посередине с 17%.

Классификатор, послабление которого сдвигает окно чата в десять раз сильнее, чем API, явно не калибровали на трафике API. Любой, кто гоняет флот субагентов, сидел ниже по течению от ограничителя, настроенного под человека, который печатает вопрос в текстовое поле.

Справедливости ради нужна одна оговорка, и для доверия она важна: ничего не утекло. Anthropic сама опубликовала детали вместе с исправлением и приложила цифру. Претензия не в сокрытии. Она в устройстве механизма.

Почему понижение хуже отказа

Отказ — хороший вид сбоя. У него есть тип, код статуса, он виден на дашборде, вызывает алерт, идёт по пути ретрая и очевидно является чужой виной.

У понижения нет ни одной из этих зацепок. Не приходит ни ошибка, ни код статуса, ни запись в логе. Возвращается валидный, правильно оформленный ответ, просто чуть хуже: патч, в котором не учтён один крайний случай, тест, списанный как нестабильный, сводка без той самой важной оговорки.

Слабый результат субагента обычно не заставляет никого винить вендора. Винят промпт, потому что это часть под прямым контролем и чаще всего именно она виновата. Его переформулируют, результаты улучшаются, и теория вроде бы подтверждается. Но новая формулировка — это и другой вход для классификатора. Обычное лекарство от плохого ответа задевает то, что могло вызвать подмену, поэтому починка выглядит рабочей, а ошибочный диагноз закрепляется. Если одна правка чинит и расплывчатый промпт, и промпт, задевший фильтр, никто не скажет, какая проблема была на самом деле.

Месяцы посредственной работы субагентов могли списывать на промпты, хотя часть была подменой, и узнать, какая именно, нельзя, потому что никто не записывал, какая модель ответила.

Данные были всегда

Ничего здесь не отсутствовало. В объекте ответа Messages API есть поле model рядом с id, content, stop_reason, stop_details и usage. Каждый когда-либо возвращённый ответ называл модель, которая его создала. Почти никто это поле не читает, потому что оно выглядит как эхо запроса.

SDK до сих пор подаёт его как эхо запроса: doc-комментарий к model в типе ответа описывает его как модель, которая выполнит промпт. Будущее время — у поля внутри уже завершённого ответа. Годами эта формулировка была верной, а теперь нет. Какая модель обслужила вызов, определяется во время выполнения, и единственная авторитетная запись об этом — ответ, который большая часть кода выбрасывает.

У пользователей Claude Code это уже лежит на диске. Каждый ход ассистента дописывается в JSONL-транскрипт в ~/.claude/projects/ с полным объектом сообщения API. Очевидный однострочник считает строки с моделями:

grep -ho '"model":"[^"]*"' *.jsonl | sort | uniq -c | sort -rn

Подсчёт по двенадцати последним транскриптам в одной папке проекта дал такие итоги: claude-opus-5 ответила на 4,691 ход, claude-opus-4-8 — на 1,231, шесть ходов помечены как синтетические, а последняя группа из 133 строк содержала только sonnet, haiku или opus.

Эти 133 — не фактические модели. Они лежат в message.content.input.model, где хранятся параметры запуска субагента: модель, запрошенная для субагента и записанная внутри вызова инструмента. Фактическая модель живёт в message.model. Имя ключа одно и то же, путь разный, а смысл противоположный. Выборка по пути убирает этот хвост:

jq -r 'select(.type=="assistant") | .message.model' *.jsonl | sort | uniq -c | sort -rn

Первая попытка ошиблась, и сам характер ошибки — это и есть урок. Один файл хранит и запрошенную, и фактическую модель под одинаковым ключом, и различает их только путь. Простой grep даёт число, но не то различие, которое важно.

Оба полных id выбирались намеренно, в сессиях, пережавших смену версии, так что эти итоги не показывают никакой подмены классификатором — в этих логах её не нашлось. Есть инструмент измерения, а не находка. В записях также есть isSidechain, помечающий трафик субагентов, и requestId, так что сравнить запрошенную и фактическую модель для каждого субагента — работа на полдня по файлам, которые уже на диске. Такую простую обвязку не должны писать сами пользователи.

Обвязка уже считает смену модели событием

Claude Code уже месяцами, релиз за релизом, считает откат полноценной концепцией. Версия 2.1.166 добавила настройку fallbackModel со списком до трёх моделей, которые пробуются по очереди, когда основная перегружена или недоступна. В 2.1.76 эти оповещения стали видны всегда, а не прятались в подробном режиме, и получили понятные названия. Релиз 2.1.183 выводит предупреждение в stderr в режиме print, если запрошенная модель устарела или её тихо обновили. В 2.1.233 неизвестный id модели даёт строку о нераспознанной модели в stderr. Последняя сборка добавляет события хуков PreModelSwitch и PostModelSwitch, чтобы код мог запретить, подтвердить или пометить переключение.

Пять решений ведут к одному выводу: о смене модели оператор должен узнать.

Всё это касается переключений, которые делает сама обвязка. Подмена, сделанная выше по течению классификатором, приходит без уведомления, без события хука, без вывода в stderr и без типизированной ошибки. Команда, решившая, что понижение из-за доступности заслуживает постоянного баннера, выпускает понижение из-за безопасности вообще без ничего. Злой умысел — маловероятное объяснение. Лучше подходит шов: обвязка сообщает о своих действиях, модель — о своих, а подмена происходит в зазоре между ними.

Тот же паттерн в других местах

19 августа OpenAI показала превью Private Safety Processing — слоя мониторинга, который смотрит на входы и выходы нескольких разговоров вместе, чтобы замечать злоупотребления, проявляющиеся только между сессиями. Компания говорит, что не хранит данные клиента и выдаёт лишь узко ограниченный сигнал о типе активности. Это ответ на 30-дневное хранение, которое Anthropic объявила в июле для соответствующих моделей, — политику, встревожившую компании, работающие с чувствительными данными.

Называть это ложью было бы нечестно. Это просто невозможно проверить. Сопоставлять поведение между сессиями и при этом ничего не хранить — утверждения в явном противоречии, и чтобы его разрешить, нужно знать, что содержит сигнал и сколько длится окно сопоставления. Ни то, ни другое не опубликовано.

Форма та же, что и с откатом: свойство сервиса, которое существенно влияет на клиентов, наблюдаемое только вендором и раскрываемое с той детализацией, которую выберет вендор. Поведение безопасности стало полем конкуренции, и конкурируют там по свойствам, для которых у клиентов нет измерительного прибора.

Что это исправило бы

Никто здесь не просит откатывать изменения. Биологический классификатор может остаться, как и откаты, которые пост перечисляет для вирусологии, токсикологии и молекулярного дизайна. Просьба маленькая: она стоит одной строки.

Подмене место в конверте ответа. Подойдёт любой вариант: заголовок, дополнительное поле в usage или атрибут телеметрии, разделяющий запрошенную модель и модель, которая ответила. Ничего необычного в этом нет. Запросы от субагентов уже уходят с заголовками agent-id и parent-agent-id, так что прикреплять к запросу сведения о модели в одну сторону — устоявшаяся практика. При этом экспорт OpenTelemetry помечает атрибутом model всего одну метрику (строки кода), и нигде нет сигнала, что ответившая модель отличалась от запрошенной.

Причину раскрывать не обязательно. Достаточно категории или даже просто факта, что это произошло, — переданного в момент, когда это происходит, внутри ответа.

Правило, которое повторяется снова и снова, переживёт и этого вендора, и этот месяц: всё, что провайдер отказывается класть в ответ, становится багом клиента. Оно попадает в его бюджет ошибок, его код-ревью, его постмортем и его оценку собственной компетентности. Те 85% измерили по ту сторону черты, где вендор. Заголовок перенёс бы следующую цифру на сторону клиента.