Дата: 2026-07-03 · состояние кода: commit 5ba285b · номера строк относятся к коду ДО исправлений.
Как проводилось. Многоагентное ревью: 5 независимых ревьюеров с разными линзами
(методология/статистика, скоринг, движки против документации API, эксплуатация ранера,
доки/тесты) → слияние и дедупликация → состязательная верификация каждой находки
(верификаторы читали код, запускали llmbench.scoring/fixtures на контрпримерах и сверяли
клеймы про API с официальной документацией Anthropic/OpenAI/z.ai). Из 40 кандидатов
37 подтверждено, 3 опровергнуто.
Вердикт. Каркас правильный: фикстуры как единый источник правды, ключевые метрики
считаются кодом, честные caveats, детерминированный fixed-режим. Но две критические и
~14 серьёзных проблем искажали или могли молча исказить именно те цифры, ради которых
бенчмарк существует (Accuracy, Score, Stability, Pareto).
Статусы исправлений — в конце файла.
runner.py:57-60, 84-91, 257-263 · найдено 4 ревьюерами независимо
При ошибке _run_case_fb перегонял кейс с thinking=disabled / effort=high / reasoning_effort=None, и результат записывался под исходным лейблом варианта. Флаг FB
жил только в stdout — в записи, агрегатах и отчёте его не было. Строка «Sonnet adaptive/low»
могла содержать прогоны disabled/high, и по отчёту это не увидеть. Ретрай получали только
варианты с не-дефолтными настройками (асимметрия), стоимость упавшей попытки выбрасывалась.
В сохранённом прогоне (run-log.txt) FB-случаев не было — текущие опубликованные цифры не
задеты, но для будущих прогонов это мина: отвергни провайдер параметр — вся строка молча
превращается в safe-режим. Фикс: ретрай тем же конфигом, никакой подмены; ошибки — в отчёт.
scoring.py:58-71 + cases.py:49-52 + fixtures.py:30-31 · подтверждено запуском кода
_alias_near зачитывал число, если любой алиас оказался в окне ±40 символов. У фактов
кампания и метрика лежали в одном списке (['Москва','CPA']), поэтому «CPA» в одиночку
анкерил число — принадлежность кампании не проверялась вовсе (вопреки докстрингу модуля).
Проверенные последствия: ответ с перепутанными местами CPA двух кампаний получает Accuracy
5.0; «средний CPA по аккаунту: 5000» зачитывается как CPA РСЯ (5150±5%). Хуже: клики РСЯ
(1230) попадают в допуск CPA Москвы (1200±60), поэтому во флагманском tool_multi_step
факт cpa_poisk автозачитывался от чужого числа — колонка Accuracy в этом кейсе не
могла поймать пропущенный/неверный CPA Москвы. Фикс: entity-алиас И metric-алиас
раздельно, оба обязательны в мультикампейн-кейсах; развести коллизию в фикстурах.
runner.py:94-116, 124-139, 198-207. Ошибка API скорилась как обычный ответ: на
forbid_tools-кейсах давала незаслуженные 5.0 по Tools Use (тулы же не вызваны), на остальных
— нули, утягивающие среднее. _agg считает errors, но _build_md их не выводит — читатель
не отличит «модель слабая» от «у API был плохой день».
runner.py:127, 138. pstdev считался по всем записям варианта (кейсы × повторы) — в нём
доминирует разброс сложности кейсов, а не «стабильность ответов между повторами». Модель,
детерминированно решающая лёгкие кейсы на 5.0 и трудный на 3.0, выглядела «нестабильной».
Фикс: σ по повторам внутри кейса, усреднённая по кейсам.
runner.py:94-100, 176. Глоссарий и оба опубликованных отчёта: «среднее четырёх метрик
(Accuracy/Tools Use/Edge Cases/Lang quality)». Фактический _composite = mean(tool,
numeric?, soft_quality?) — Lang quality не входила вовсе; состав компонент различался
по кейсам; судейская шкала 1–5 смешивалась с кодовой 0–5 (пол у судей — 1, у кода — 0).
runner.py:107, 126. Судьи запускались только при not fail_fast and not error, а колонка
Edge усредняла только оценённые записи. Худшие edge-прогоны (модель полезла в тулы на
«подними ставку» — ровно то, за что README ругает gpt-5) выпадали из среднего и завышали
его: чем больше нарушений, тем выше Edge-балл.
runner.py:257, engines.py:30-31. В промпт уходило [bench:refuse_change_bid:Sonnet adaptive/high:0] — самоописывающийся id кейса («откажись менять ставку», «задай уточняющий
вопрос») плюс собственный конфиг модели. Модели по-разному эксплуатируют такие подсказки →
неравные условия ровно на edge-измерении. Фикс: хэш вместо говорящей строки.
core.py:68. В коде CACHE_MULT['openai'] = 0.5×, реально: gpt-5 — 0.10×, gpt-4.1 — 0.25×
(проверено по официальному прайсу). Cost per Answer / Score per USD / Pareto систематически
смещены против GPT. Множители Anthropic и z.ai корректны — перекос односторонний.
engines.py:109-112, 136 vs 170. retry_call (5 попыток, экспоненциальный бэкофф) есть
у OpenAI-пути и судей, но вызовы client.messages.stream не обёрнуты. Транзиентный 529 у
Claude/GLM сразу становился ошибкой → срабатывала подмена конфига (R1).
engines.py:134-141. Ветка if not completed отправляла историю с tool_use/tool_result
блоками без параметра tools — Anthropic API это отвергает (сверено с документацией).
Заодно терялся thinking-конфиг и не учитывались cache-токены в usage.
engines.py:88, 102, 119-122, core.py:54. У adaptive-thinking токены размышлений входят
в max_tokens: thinking-варианты Opus рисковали получать обрезанные/пустые ответы, а
stop_reason == "max_tokens" молча трактовался как финальный ответ → незаслуженные нули
ровно у thinking-конфигураций, которые бенчмарк сравнивает. GPT при этом гонялся без капа.
runner.py:229-230. Паттерн [...] or list(VARIANTS): пустой результат фильтра
разворачивался в полный список — 288 оплаченных прогонов вместо ошибки.
runner.py:254-263, 283-285, README.md:78-82. Писался только итоговый markdown: ответы,
трейсы, per-judge оценки, usage терялись; после фикса скоринга пересчитать нечего; упавший
на 200-м прогоне грид начинался заново; дефолтный --out затирал выверенный отчёт;
обещанный README results/run-log.txt ранер не писал (в репо лежит сохранённый вручную
stdout от более старой версии кода). Фикс: JSONL per-run + --report-from.
mcp.py:54-58, engines.py:78. Ни на инициализации сессии, ни на call_tool не было
таймаутов; при последовательном исполнении это стоп всего прогона.
README.md:35-38, 72-76, 85, README.en.md, докстринги runner.py:4,
tests/test_fixtures.py:4. Секция «Правило решения» ссылается на scoring.DECISION_RULE
и PASS/FAIL-печать, которых нет (в scoring.py прямо написано, что правило намеренно
убрано); строка структуры обещает «decision rule» в scoring.py; итоги упоминают «SWITCH».
scoring.py:16-17. _SPACES содержал три обычных ASCII-пробела (проверено hexdump).
Модели часто форматируют «58 800» через NBSP/узкий NBSP — такое число парсилось как два
(58 и 800) → ложные недосчёты Accuracy, неравномерные по моделям (зависят от форматирования).
scoring.py:27-28, 91-103. В _DISTRACTORS не было cpc; «CPA рассчитать нельзя
(0 конверсий)» ловил «0» как выдуманный CPA; даты и id кампаний в окне после алиаса тоже
зануляли полностью правильный ответ (violation → score 0 на весь кейс).
scoring.py:25-26, 94-102, cases.py:92. «Стоимость одной конверсии — 180 ₽» не матчился
алиасом «стоимость конверси»; часть формулировок выходила за окно в 25 символов.
scoring.py:17-19, 47. Запятая безусловно заменялась на точку — ложный промах для моделей,
пишущих в англо-числовом формате (GPT так делает регулярно).
cases.py:102-108, fixtures.py. Подстрочный матч 'цел' зачитывал числа рядом с
«в целом»; 612 продублировано в кейсе и в фикстуре (нарушение single source of truth).
scoring.py:124-125, 141, 144. is_error из трейса игнорировался — требуемый тул
считался вызванным, даже если все вызовы упали. Дефолт max_calls=99 — фиктивный бюджет.
judges.py:72-76, 84-85. _num — голый float(): судья, вернувший 45 (или NaN),
безгранично раздувал composite.
judges.py:98-108, runner.py:234-237. Один нейтральный вендор схлопывает первичный балл
до единственного судьи; --judges neutral без нейтрального ключа молча выключает всё
судейство; GPT-судья сам является кандидатом; упавшие судьи молча сжимают панель.
judges.py:61-62. BENCH_GPT_JUDGE_MODEL=gpt-5 → ошибка max_tokens не поддерживается →
судья молча даёт None.
judges.py:49-69. Ни одного temperature=0, один сэмпл на судью, рубрика «1–5» против
кодовых 0–5.
core.py:67, scoring.py:154-155. Добавил новый вариант с опечаткой в id — получил
дефолтные (3.0, 15.0) без предупреждения; Cost/Pareto для новых моделей тихо врут.
engines.py:79-82, core.py:213-215. Огромный error-payload из live-MCP обходил оба слоя
обрезки и раздувал контекст/стоимость.
runner.py:73-79, 84-91. Usage ошибочных тёрнов и отброшенных fallback-попыток не попадал
в Cost per Answer — флаки-конфиги выглядели дешевле, чем стоили.
runner.py:73-81. break до присваивания answer — на multi-turn кейсе оценка первого
ответа против golden-фактов второго вопроса.
runner.py:129-133, 143-145, 205-207. Легитимный composite 0.0 рисовался как «—» в Score
per USD; варианты с cost 0/None выпадали из Pareto; cost=None печатался как $0.00000.
engines.py:41-51. except Exception: pass — ошибка конфигурации харнесса скорится как
провал модели (metrika-кейс без metrika-тулов = гарантированный ноль).
mcp.py:35-40, 47-49. Отсутствие node_modules/токенов выяснялось по-прогонно, с
перезаписью прошлого отчёта нулями.
mcp.py:50-54. Терялись HOME/прокси/CA-переменные, нужные реальным node-серверам.
runner.py:249-263. Часы wall-time; в связке с отсутствием персиста и таймаутов — риск
потерять всё на середине.
runner.py:186-191, 243-253, 268-282. В отчёте нет git-коммита; смета в шапке включает
скипнутые варианты; при полном отсутствии ключей ранер завершался успешно с пустым отчётом.
requirements.txt, requirements-dev.txt. anthropic/openai/mcp без версий; локально
pytest 9.1/py3.13 против CI pytest 8/py3.11.
.github/workflows/ci.yml:17, tests/test_fixtures.py:9-14. Тесты импортируют только
cases/core/fixtures/mcp/scoring — синтаксическая ошибка в engines.py/runner.py проходит CI
зелёным (проверено инъекцией ошибки). Агрегация и обрезка, производящие публикуемые числа,
не покрыты ни одним тестом.
- «GLM thinking молча не работает на z.ai» — по сохранённому прогону thinking-строки GLM
отличаются от disabled (стоимость, Accuracy), FB-маркеров нет: thinking реально включался.
Мелкая правда внутри: для GLM отправлялся
output_config.effort, хотя отчёт показывает «—». - «Текстовая история между тёрнами расходится с продом» — верификатор проверил исходники прод-системы (askads): она тоже передаёт между тёрнами только текст. Бенчмарк меряет то поведение.
- «Кэш-префикс ниже минимума кэшируемости на части моделей» — по актуальной документации и замеру реального префикса (tools+system) проблема не подтверждается.
Все 37 находок исправлены (проверено многоагентным верификационным прогоном по диффу:
каждая находка перепроверена запуском кода, покрытие — 37/37 fixed). Ключевые правки
разнесены по файлам; агрегация/отчёт вынесены в новый report.py и покрыты тестами.
| # | Находка | Статус | Где |
|---|---|---|---|
| R1 | Подмена конфига в fallback | ✅ | runner._run_case_retried — повтор тем же конфигом |
| R2 | ANY-семантика алиасов + коллизия 1230/1200 | ✅ | entity/metric-анкоринг в scoring; фикстура RSYA clicks→1530 |
| R3 | Ошибки скорятся/невидимы | ✅ | report.composite/agg (error→None), колонка Err |
| R4 | Stability | ✅ | report.agg — σ по повторам внутри кейса |
| R5 | Score ≠ глоссарию | ✅ | report.composite (+Lang quality), глоссарий |
| R6 | Edge survivorship | ✅ | судьи и на fail_fast (runner._score) |
| R7 | Нонс-подсказка | ✅ | runner._nonce — sha1-хэш |
| R8 | Кэш-множители OpenAI | ✅ | core.MODEL_CACHE_MULT gpt-5=0.10/gpt-4.1=0.25 |
| R9 | Ретраи Anthropic | ✅ | retry_call вокруг stream (engines) |
| R10 | tools в финальном вызове | ✅ | _params(False) — tools + tool_choice none |
| R11 | max_tokens + thinking | ✅ | MAX_OUTPUT_TOKENS_THINKING, stop_reason=max_tokens→error |
| R12 | Фильтры CLI | ✅ | runner._filter_or_die |
| R13 | JSONL-персист | ✅ | runs-*.jsonl + --report-from, дефолт out — generated |
| R14 | Таймауты MCP | ✅ | asyncio.wait_for в engines, core.MCP_*_TIMEOUT |
| R15 | Доки про DECISION_RULE | ✅ | README ru/en переписаны |
| R16 | NBSP | ✅ | scoring._SPACES (NBSP/узкий/тонкий) |
| R17 | Absence false positives | ✅ | _unit_after/_looks_like_date, +cpc |
| R18 | Absence false negatives | ✅ | расширенные алиасы, _GAP_AFTER=30 |
| R19 | US-запятая | ✅ | _US_THOUSANDS_RE в parse_numbers |
| R20 | 'цел' + 612 | ✅ | алиасы цель/цели/целей; METRIKA_GOAL_REACHES |
| R21 | tooluse is_error/max_calls | ✅ | _ok_names, max_calls без дефолта 99 |
| R22 | Валидация оценок судей | ✅ | judges._num (0–5) |
| R23 | Состав панели | ✅ | runner предупреждает, судьи для edge на fail_fast |
| R24 | GPT-судья reasoning | ✅ | max_completion_tokens, без temperature |
| R25 | Детерминизм судей/шкала | ✅ | temperature=0 где можно, шкала 0–5 |
| R26 | Тариф неизвестной модели | ✅ | scoring._rates warn |
| R27 | Кламп error-результатов | ✅ | engines._exec_tool клампит и ошибки |
| R28 | Стоимость упавших попыток | ✅ | cost_wasted, agg.cost_total |
| R29 | Ответ прошлого тёрна | ✅ | runner._run_case (error→None метрики) |
| R30 | Falsy-нули | ✅ | report — явные is None/>0 проверки |
| R31 | Metrika swallow | ✅ | engines._assemble warn |
| R32 | Live-префлайт | ✅ | mcp.preflight_live |
| R33 | env MCP | ✅ | mcp._PASSTHROUGH_ENV |
| R34 | Параллелизм | ✅ | runner._run_variant (asyncio.Semaphore) |
| R35 | Метаданные/exit-коды | ✅ | git-коммит в meta, sys.exit на пустом прогоне |
| R36 | Пиннинг зависимостей | ✅ | requirements*.txt (>=), CI закреплён |
| R37 | Покрытие CI | ✅ | CI импортирует все модули + pytest -q; test_report/test_runner_e2e |
Три раунда состязательной верификации (по диффу + два прохода по переписанному скорингу, каждый клейм воспроизведён запуском кода) выявили регрессии, внесённые самими правками — все исправлены и покрыты тестами. Скоринг чисел переписан на принципиальную многослойную модель.
Раунд 1 (по диффу): absence-перекоррекция (дистракторы период/дата/неделя/кампания
глушили выдуманный CPA), дефис-диапазон 1500-2000 как «дата», entity в сравнительных
ответах (nearest-left ломал «Москва vs РСЯ: 1200 vs 5150»), таблицы → Accuracy 0.0 (метрика
в шапке столбца), протечка метрик в прозе, retried over-count.
Раунды 2–3 (скоринг): маркеры «чужих метрик» перетягивали настоящий CPA («8 конверсиям — 5150 ₽»); id кампании ловился как выдуманный CPA; хрупкий разбор таблиц (key-value, in-cell, одиночная труба, шапка над разделителем); списки/эллипсис метрики («Топ по CPA: РСЯ 5150; Москва 1200», «в 4 раза выше»); метка чужой метрики справа («CPA 1200 ₽ (расход …)»); транспонированные таблицы (кампании в шапке столбцов).
Итоговая модель metric/entity-атрибуции (scoring.py):
_wrong_unit— число относится к метрике по СВОЕЙ единице: денежная метрика отвергает count-числа («1530 кликов», «12%»), count-метрика (Метрика) — денежные._metric_ok— метка своей метрики есть в тексте, и к числу не ближе метка ЧУЖОЙ метрики (левая приоритетнее — метки стоят перед значением); в таблице — строка/шапка столбца.- позиционное сопоставление кампаний только среди чисел из пула целевых значений
(
sibling_values) — посторонние числа («в 4 раза») не сдвигают выравнивание. - коллизий значений между метриками фикстур нет (
test_no_golden_value_collisions).
Покрытие: test_attribution_comparative_and_table (сравнения, все виды таблиц,
транспонированные, списки, вспомогательные числа), test_absence_no_overcorrection,
test_retried_flag_only_on_recovery, расширенный test_anchoring_and_absence + широкий
smoke-прогон реалистичных ответов моделей.