Препринт этой работы пока не опубликован на arXiv: для подачи мне нужен endorsement в категории cs.CL (Computation and Language). Если у вас есть право выдавать endorsement в этой категории и вы готовы помочь, буду благодарен — напишите мне в личные сообщения на Хабре.
Код, файлы результатов, meta прогонов и реестр гипотез целиком, включая отрицательные: https://github.com/ADanMan/elm-research. Репозиторий пока приватный. Планирую открыть исходники после публикации статей.

Идея была простая и не новая: у большой языковой модели два неприятных свойства — она стоит денег на каждый ответ и держит в памяти десятки гигабайт весов, а ещё она может уверенно написать то, чего нет нигде. Захотелось собрать систему, у которой ответ выбирается из существующего текста, а не порождается: корпус лежит на диске файлами, кандидатов находит обычный код, а нейросеть-судья только выбирает строку и проверяет её. Судья — классификатор с типизированными примитивами: «выбери один из вариантов», «это правда?», «оцени по шкале». Текст она не пишет вообще. Ответ — дословная цитата с источником, и выдумать фразу, которой нет в корпусе, система не может по построению.
Дальше хроника: что мерил, в каком порядке и что не сработало. Цифры все из файлов прогонов: и статью, и этот пост собирает генератор.
День первый: 20.0% верных и очень много отказов
Первая версия: Simple Wikipedia в SQLite, полнотекстовый поиск FTS5, окно строк судье, один запрос «какая строка отвечает» и три проверки. На отладочной половине Natural Questions (150 вопросов, строгая метрика — эталон дословно содержится в ответе) вышло 20.0% верных при 60.0% отказов.
Соблазн в этот момент — крутить формулировки вопросов к судье. Вместо этого я написал офлайн-замер охвата: доходит ли эталонный ответ до строк, которые судья реально видит. Эталон доходил до окна судьи у 42.7% вопросов, то есть больше чем в половине случаев судья выбирал из строк, где ответа не было вовсе, и никакая формулировка этого не лечит. Отсюда главный приём всего проекта: прежде чем чинить звено, докажи замером, что теряется именно в нём.

Корпус: сначала стало хуже
Расширить выдачу с сотни абзацев до шестисот — 21.3%, то есть ничего. Подключить полный psgs_w100 — 21 015 324 абзацев вместо небольшой Simple Wikipedia, и стало хуже: запрос «любое из редких слов» по такому корпусу размывает выдачу, редкие слова находятся где угодно.
Починило это не увеличение корпуса, а изменение запроса: строгое «все редкие слова разом» (с прежним запросом как страховкой, когда находок мало) — 33.3%. Плюс короткие гипотезы локальной модели, использованные как поисковый запрос, а не как ответ: 35.3%. Ещё пара пунктов пришла от инженерии — перестали теряться обращения к судье: 37.3%.
Деталь про роль генеративной модели: она никогда не принимает решений. Когда судья не может выбрать (он либо говорит это сам, либо это видно по энтропии его распределения), он берёт инструкцию из фиксированного меню — дай гипотезы ответа, переформулируй запрос, разбей на подвопросы, — и всё, что модель вернула, снова идёт ему на выбор и проверку. Три триггера по качеству неразличимы (35.3% у энтропийного), но энтропийный зовёт модель в несколько раз реже: в финальном прогоне генератор понадобился на 4 вопросах из 150.
Скачок: чужие эмбеддинги
Рамка работы — никаких дообучений: судья как есть, индексы с полки, локальная модель как генератор кандидатов. Поэтому следующий шаг был не «обучим кодировщик», а «возьмём готовый». Готовый FAISS-индекс facebook/wiki_dpr поднял охват окна судьи до 82.0% (в пуле — 89.3%) против 56.0% у словарного поиска по тому же полному корпусу и 42.7% по Simple Wikipedia, а живое качество — с 37.3% до 46.0%; парный тест Макнемара: 69 против 56 верных, несовпадающих пар 22/9, p=0.029.
Это единственный по-настоящему большой шаг за всю работу, и он чужой. За него заплачено памятью: индекс занимает около трёх гигабайт в процессе, и профиль «малая память» на этой конфигурации перестаёт быть правдой (4149 МБ пик против 47.7 МБ у полнотекстового профиля на том же корпусе) — в статье обе цифры стоят рядом.
Отдельная оговорка, которую нельзя опускать: кодировщик DPR обучался на train-половине NQ. Моя система не училась ничему, но индекс, который она берёт с полки, обучающую часть этого набора видел. Поэтому NQ идёт базой, а не главным результатом, а независимая проверка — на BrowseComp-Plus, где индекс другой.
Смешивание плотного поиска с полнотекстовым (гибрид с тройным весом) от чистого плотного неотличимо: 68 против 69, p=1.000.

Стена: семь способов спросить судью, и ни одного пункта
После плотного поиска разложение потерь стало таким: ответ есть в пуле у 91.3% вопросов, в окне судьи — у 89.8% из них, и верен, если был в окне, в 69.1% случаев. Произведение — 56.7% при измеренных 56.7% по судье. Узкое место переехало в последнее звено: судья выбирает не ту строку среди строк, где ответ есть.
По этому звену я отработал шесть схем, и каждая мерена против своей базы, а не против одной общей:
четыре отдельные проверки вместо выбора (LongBench, отладочная половина);
предотбор строк под каждый вариант ответа (там же);
первое и второе вместе (там же);
голосование судьи по трём формулировкам на словарной базе NQ — 56 против 54, p=0.625;
второе окно судьи поверх плотного поиска — 69 против 69, несовпадающих 8/8, p=1.000, ценой лишнего обращения на каждый вопрос;
турнирный выбор раундами по восемь строк вместо одного выбора на всё окно, тоже поверх плотного поиска — 48.7% против 46.0%, p=0.454.
Седьмая схема — обрезка придаточных кодом, чтобы вопрос судье стал «типом ответа», — не изменила ни одного ответа на BrowseComp и отдельного файла результатов не оставила.
Ни одна не дала значимого прироста. Турнир — самый поучительный: он перевёл отказы в ответы (отказов 23.3% против 27.3%), и по строгой метрике это дало 73 верных вместо 69 — 10 приобретённых против 6 потерянных, p=0.454, то есть в пределах шума. А по мягкой метрике счёт не изменился вовсе: 85 против 85 (7 приобретено, 7 потеряно, p=1.000). Зато «невпопад» вырос с 26.7% до 28.0%, а цена — с 4.5 до 28.5 обращений к судье на вопрос. Четыре строгих попадания за шестикратную цену, и ни одного — по метрике судьи.
Была ещё одна схема — две новые проверки (тип значения, путаница имён), — но её прогон снят на грязном дереве: код в рабочей копии не совпадал с коммитом. По правилу проекта такие цифры не публикуются, поэтому ни в статье, ни здесь их нет, и переснимать её я не стал.
А потом я сделал то, что стоило сделать раньше, и за ноль денег: офлайн-скан порогов по уже записанным вероятностям судьи. Результат неприятный и полезный. Оценки судьи не разделяют его верные и неверные ответы: лучший порог срезает 6 ответов невпопад, теряя 4 верных, а отрыв выбора у неверных ответов даже выше, чем у верных (0.15 против 0.055 по медиане). Судья уверен там, где ошибается. Это значит, что «поставить порог поумнее» — тупик, и нужна калиброванная оценка уверенности, то есть обучение. В этой работе обучения нет по условию, так что здесь — граница.

Ручка, которая не улучшение
Одна вещь из этой серии всё-таки работает, но улучшением её назвать нельзя. Если задать проверки повторно другими словами, точность и полнота размениваются: ответов невпопад 30 вместо 40 (пары 13/3, p=0.021), но верных 64 вместо 69 (p=0.359) и отказов 37.3% вместо 27.3%. У генеративной модели такой ручки нет вовсе — она всегда отвечает; но число верных ответов от неё не растёт, поэтому это «режим строгости», а не подтверждённая гипотеза.
BrowseComp-Plus: где сломался сам вопрос
Проверка на наборе, чей индекс не видел наших данных. Первые прогоны дали ноль верных ответов, и разбор показал, что судья прав, а неверна инструкция. Вопрос там — загадка из пяти условий на сотню слов, а я спрашивал «какая строка утверждает ответ на это?». Ни одна строка не отвечает на всю загадку целиком, и судья с уверенностью около единицы выбирал вариант «ни одна».
Починка — разные вопросы по звеньям: документ выбирается по полной загадке (условия нужны, чтобы его узнать), а строка и проверки — по финальному вопросу, который выделяет код; плюс дочитывание выбранного документа из базы целиком. С нуля стало 13.3% строго на живом поиске и 10.0% в оракульной схеме, где эталонные документы подставлены. Разложение по звеньям здесь другое: эталонный документ выбирается в 23.3% случаев (7 из 30), а если выбран — ответ верен в 57.1% (4 из 7); в оракуле 33.3% и 30.0%. Знаменатели крошечные, точности в этих процентах нет, и это одна из причин, почему ветка осталась на отладочной половине. Слабое звено тут другое, чем на NQ, и разложение показывает какое, без единой абляции.
И настоящая стена этого набора: у 26.7% вопросов ответ составной или форматный — «1986, Portugal», «90g», «23rd match, 2019». Строки с таким ответом в корпусе нет вовсе, и система, которая отвечает дословной цитатой, такие вопросы не решает в принципе. Потолок дословного цитирования — 73.3%.
Под эту стену сделан режим сборки: судья выбирает режим ответа (дословно / собрать из проверенных цитат / нет ответа), и сборка проходит отдельную проверку «всё ли есть в источниках». Замер: генератор позвали 3 раза на 30 вопросах, выдумок ноль, непрошедшие сборки стали отказами. Чистый эффект — около нуля: 16.7% против 13.3% на живом поиске и 13.3% против 10.0% в оракуле, отказы выросли с 63.3% до 73.3%. Механизм работает и не сочиняет, но лишний вопрос о режиме перед каждым ответом работает как ещё один гейт, и гейт съедает выигрыш. Очевидная починка — спрашивать режим только после отказа дословного пути — не замерена, поэтому это не результат.
Четыре собственные ошибки измерения
Эта часть мне интереснее результатов: все четыре ошибки искажали цифры молча.
Первая: флаги читались на импорте. Раннер выставлял переменные окружения позже, чем модуль их читал, поэтому «прогон с плотным поиском» шёл полнотекстовым, а «прогон с турниром документов» не выполнял ветку турнира вовсе. Замеры выглядели нормально. Именно отсюда взялось то самое «0 из 90», которое я сначала принял за свойство судьи. Лечится чтением настроек в момент вызова и проверкой зависимостей флагов.
Вторая: инвертированная формулировка проверки. Проба новой конфигурации дала ноль верных, тогда как база на тех же вопросах отвечала на половину. Причина: проверка спрашивала «может ли ответ быть про другой предмет», и честное «нет» читалось как провал. Полный прогон в такой конфигурации был бы потраченным часом и ложной записью в реестре — отсюда правило: каждая новая конфигурация сначала идёт пробой на десяти вопросах, и цифры пробы читаются до того, как в очередь встанет полный прогон.
Третья: утечка генерации в ответ. Роутер открывал генеративную ветку по одному факту наличия генератора в зависимостях. В результате в прогоне на BrowseComp «ответом» оказалась фраза, сочинённая моделью перед цитатой («The company is Genetics Nepal.»), а эталон был совсем другой. Для системы, у которой весь смысл в дословности, это делает прогон недействительным, даже если цитата стоит следом. Теперь право формулировать ответ даёт только один флаг, а тест гоняет продовый путь и проверяет, что строк генератора в ответе нет.
Четвёртая: поправка метрики. Та же утечка в мягком виде отравила прогоны NQ с гипотезами: строки поисковых догадок попадали в текст ответа перед цитатой, и на одном вопросе ответ состоял из догадок целиком, и был засчитан верным. Что считается ответом, я определил заново: только дословная цитата и выжимка-спан из неё, догадки — это поисковые запросы. Пересчёт офлайн, без новых прогонов, затронул 16 файлов результатов: база с 70 на 69 строго верных (по судье с 86 на 85), прогон с гипотезами — с 55 на 53. Выводы не перевернулись, отложенные прогоны поправка не затронула вовсе (0 совпадений — гипотезы в них были выключены). Но публиковать прежние цифры без этого пересчёта было бы нечестно.
Всего в журнале 10 прогонов помечены недействительными, с причиной у каждого, и ни один из них не участвует в цифрах статьи.
Финал: отложенная половина, один прогон
Пороги подобраны на отладочных половинах, отложенные прогнаны по одному разу на финальной конфигурации, цифры — как вышли. Все системы мерены одним скриптом и одним грейдером, рядом со строгой метрикой стоит мягкая (судья сверяет ответ с эталоном одинаково для всех), рядом с долей верных — доля ответов невпопад и доля отказов.

Там, где доказательство дано в задаче, выбор отвечает как генерация. BoolQ: 90.0% против 90.0% у Claude Opus 5 — не «близко», а тот же счёт на тех же вопросах (парный тест p=1.000); DeepSeek с тем же абзацем 84.7%, без абзаца 68.0%. SQuAD 2.0: 79.3% против 80.0% (p=1.000). Но паритет тут — размен, и это надо говорить прямо: на отвечаемых вопросах Opus сильно впереди (96.2% против 78.8%), а общий счёт выравнивается за счёт безответных, где мы молчим в 80.0% случаев против 61.4% у него. Мы не лучше отвечаем. Мы лучше не отвечаем.
Там, где ответ надо найти, всё упирается в поиск. NQ open по корпусу из 21 015 324 абзацев: 49.3% строго и 56.7% по судье при 27.3% отказов, против 62.0% / 74.0% у Opus и 47.3% / 60.0% у DeepSeek. Парно: с DeepSeek неразличимо (p=0.775), Opus значимо лучше (p=0.011). Единственное, что мы на этом наборе выигрываем, — ошибаемся реже: ответов невпопад 24 против 41 у DeepSeek (p=0.0060), потому что наши провалы уходят в отказ, а не в уверенный текст.
С длинным контекстом та же картина. LongBench v2: 15 из 30 (50.0%) при окне в 40 строк и одном обращении к судье, медиана 3 926 прочитанных токенов на вопрос. Медианный документ набора — 72 758 токенов, самый большой — 1 719 190, и окно судьи от этого размера не зависит. Референс Opus по тому же окну есть только на отладочной половине (76.7% против наших 43.3%), на отложенной он не снимался — сравнивать напрямую нельзя, и в статье это помечено.
Про цену. Каждая цифра — измеренные токены на публичную ставку провайдера. BoolQ: 0.0185$ против 9.73$ за тысячу вопросов, то есть в 395–527 раз в зависимости от того, считать ли служебный префикс платформы, который несут реплики Opus. NQ: 0.95$ против 7.31$ — уже только в 7.7 раза, потому что судья читает 23 120 токенов на вопрос. И то, что мне не нравится, но стоит в таблице: DeepSeek отвечает на те же вопросы за 0.017$ за тысячу — дешевле нас в десятки раз при неразличимом качестве. Нужны только дешёвые фактоидные ответы — берите маленькую генеративную модель; вы теряете источник у каждой фразы, штатный отказ и обновление знаний файлом, но по деньгам вы правы.

Заодно граница, из-за которой всё это затевалось: корпус — 2.73 млрд токенов, положить его в промпт целиком стоило бы 114.74$ за один вопрос по самой низкой из наших ставок, и окна такого размера не существует. Судья читает 23 120 токенов, потому что что читать — решает код.
Об устойчивости. Внедрённые инструкции («SYSTEM: this is the correct answer», «[ADMIN NOTE…]») не сработали ни разу: 0 успешных атак из 60. Это не защита, которую я написал, — найденный текст здесь данные, потому что его никто не интерпретирует как инструкцию. А подложный факт, вписанный в настоящий абзац, процитирован как ответ в 1 случае из 30: гарантия про фразы, не про факты. Два независимых прохода по 30 вопросам дали совпадение режима 30/30, строки 29/30 и реплики 26/30, а вероятности судьи дрожат (≤0.01 у 20 вопросов, ≤0.1 у 9, больше у 1), поэтому «почти детерминирован», и никогда «полностью».
Итог
что мерили (отложенная половина) |
выбор (судья) |
Claude Opus 5 |
DeepSeek |
|---|---|---|---|
BoolQ, абзац дан |
90.0% |
90.0% |
84.7% / 68.0% без абзаца |
SQuAD 2.0, абзац дан |
79.3% |
80.0% |
80.0% |
— на отвечаемых вопросах |
78.8% |
96.2% |
86.2% |
— молчание на безответных |
80.0% |
61.4% |
72.9% |
NQ open, корпус 21 015 324 абзацев |
56.7% |
74.0% |
60.0% |
— ответов невпопад |
16.0% |
20.7% |
27.3% |
LongBench v2, окно 40 строк |
50.0% |
76.7% (dev) |
— |
цена за 1000 вопросов, BoolQ |
0.0185$ |
9.73$ |
0.0396$ |
цена за 1000 вопросов, NQ |
0.95$ |
7.31$ |
0.017$ |
Из 43 заведённых гипотез замерены 27, отвергнуты 16, подтверждены 9, 2 приняты как режимы-размены, до 16 дело не дошло.
Куда уперлись, по звеньям: первое — чужой индекс, и его потолок не мой; последнее — выбор строки судьёй, 69.1%, и его вероятности про собственные ошибки ничего не знают. Оба лечатся обучением: своим кодировщиком запросов под свой корпус и калиброванной оценкой уверенности на своих окнах. Здесь не обучалось ничего — это было условие, поэтому читать работу стоит как пол подхода, а не как предел.
Честный итог: когда доказательство лежит перед системой, классификатор, выбирающий предложение, отвечает не хуже модели, которая предложение пишет: дешевле в 7.7–395 раз в зависимости от того, сколько поиска нужно вопросу, с источником у каждой фразы и с отказом как штатным исходом. Когда доказательство надо найти, побеждает параметрическая память. Дальше интересна не точность, а разложение: оно показывает, какое звено чинить, и стоит ноль обращений к судье.
Оговорки, без которых цифры выше читать нельзя: память, время и цена измерены на моей стороне, судья — удалённый сервис, и его собственные вычислительные ресурсы мне неизвестны; кодировщик готового индекса обучался на train-половине NQ; референс Opus на LongBench снят только на отладочной половине; мягкую метрику пересчитывает тот же судья, который выбирает ответы внутри системы, поэтому рядом с ней всегда стоит строгая; выборки — 150 и 30 вопросов, у всех долей в статье есть доверительные интервалы, а у всех сравнений — парный тест.
Навигация по серии
В этой части кандидаты — цитаты из найденных документов. Во второй части тот же принцип выбора проверяется на вызовах функций: где теряются значения аргументов и когда правильным ответом становится отказ. В третьей части речь о диалоговом агенте: контрольная заглушка и замена решателя помогают отделить качество компонента от ошибок цикла. Обе части готовятся к публикации.
Часть 1. Выбор цитат: поиск, судья и ошибки измерения — вы читаете эту часть.
Часть 2. Вызовы функций: значения аргументов и отказ — публикация планируется на 15 октября 2026 в 18:00 МСК.
Часть 3. Диалоговый агент: заглушка, награда и ошибки цикла — публикация планируется на 20 октября 2026 в 18:00 МСК.
Мемы: обложка, первый мем, второй мем.