Файл, который мой агент читает первым в каждой сессии, я завёл 11 августа. Сейчас в нём 934 строки и он прошёл через 83 коммита. Большинство из них это моя поправка, превращённая в правило с датой и вставленная туда, где показалось уместно. Когда агент пропустил одно из этих правил, первым делом я заподозрил место, где оно лежит в файле. В голове у меня был график из Lost in the Middle. Точность высокая на обоих краях окна и проседает в середине. Я считал эту кривую свойством языковых моделей вообще, но ни разу не проверил её на модели, которую вызываю каждый день.
Поэтому я взял задачу из этой статьи и прогнал её сам.
Стенд
Liu и соавторы (arXiv 2307.03172, TACL 2024) придумали синтетическую задачу, которая мне нравится тем, что она не требует никаких рассуждений. JSON-объект из случайных пар ключей и значений и вопрос про один ключ. Форму я оставил, а идентификаторы укоротил до 8 шестнадцатеричных знаков, чтобы 200 пар помещались в контекст модели, которая крутится на процессоре. Внутри одного размера и одного сида объект один и тот же на всех 11 глубинах. Двигается только ключ, о котором спрашивают, от первой строки до последней. Поэтому эффект глубины не смешивается с эффектом другого объекта.
После объекта и ключа каждый вызов получает одну и ту же инструкцию:
Answer with the value only, 8 hex characters, nothing else. If the key is not in the object, answer NOT FOUND.
Главное здесь второе предложение. Отдельная серия спрашивает про ключ, которого в объекте нет вообще. Там единственный верный ответ NOT FOUND. Без явного разрешения отказаться модель, которая никогда не говорит, что не знает, просто выполняла бы мою инструкцию.
Маленькие модели это Qwen2.5 0.5B Instruct в float32 и 1.5B Instruct в bfloat16, жадное декодирование, transformers 5.16.1 и torch 2.14.0 на 4 ядрах процессора. Модель, которой я пользуюсь на самом деле, это claude-haiku-4-5. Я вызывал её через Claude Code CLI без режима рассуждений, без инструментов и с пустым конфигом MCP. Над папкой не было ни одного CLAUDE.md, потому что CLI молча подхватывает этот файл. Haiku дошла до 6000 пар, это около 96 тысяч токенов промпта и примерно половина её окна.
Это мерили до меня и много раз. Needle in a haystack Камрадта и RULER (arXiv 2404.06654) выясняют, в каком месте окна модель ошибается. Zhang с соавторами (arXiv 2605.23170) разбирают, из чего сделаны неверные ответы. NoLiMa (arXiv 2502.05167) показала, что дословное совпадение вроде моего держится на гораздо большей длине, чем задачи, где нужен хоть один шаг вывода. В отчёте Context Rot от Chroma, июль 2025, отмечено, что Claude Sonnet 4 и Opus 4 склонны воздерживаться от ответа, когда не уверены. Сам я хотел проверить 2 вещи: насколько далеко от цели ложится промах и держится ли отказ.
Кривая, которую я ждал
Модель на 0.5B ломается уже на самом маленьком размере: на 25 парах, 565 токенах, она отвечает верно только 45 раз из 88. Но кривая не U-образная. Если сложить 4 размера, последнюю строку она находит в 62.5 процента случаев и все остальные глубины ниже, без провала в середине. Модель на 1.5B отвечает верно 90 раз из 99. Её промахи разбросаны где-то между 30 и 80 процентами глубины и при 3 сидах на ячейку формой я это назвать не могу.
Haiku ответила верно 243 раза из 245 и 3 из этих вызовов были первой пробой на 100 парах. Оба промаха на 6000 парах, один в середине и один на 90 процентах. Во всех остальных ячейках на всех размерах стоит 100 процентов. А первым сюрпризом там оказалось совсем другое. Конверт ответа показывает 3 входных токена для этих промптов и какое-то время я думал, что объект до модели не доходит вовсе. На деле весь объём уходит в cache_creation_input_tokens.
Куда ложится промах
Модель на 0.5B дала 253 неверных ответа и 227 из них выглядят точно как верный: 8 шестнадцатеричных знаков и ничего больше. Только 24 раза она ответила NOT FOUND на ключ, который в объекте был. 71 раз вместо значения пришёл ключ, в 65 случаях какой-то другой ключ из объекта. 140 раз пришло другое значение из того же объекта и я посмотрел, насколько далеко от цели оно лежит. При равномерном выборе среди остальных строк 3.5 процента таких значений пришлись бы на строку, соседнюю с целью. Вышло 16.4 процента и чаще любой другой строки модель брала ту, что стоит сразу после цели. Медиана расстояния 7 строк против 36 при равномерном выборе. То есть неверные значения у неё приходят в основном из строк рядом с нужной, но не из неё самой. Промахов у haiku всего 2, по ним ничего не скажешь.
Ключ, которого не было
А вот здесь haiku ведёт себя иначе. До 2000 пар, около 32 тысяч токенов, она ответила NOT FOUND 40 раз из 40. Дальше доля падает с размером, до 8 из 20 на 6000 парах. Остальные 12 ответов на 6000 были по 8 шестнадцатеричных знаков: 8 значений из объекта и 4, которых в нём нет нигде. Вызывающий код никак не может увидеть, что это догадка.
Некоторые ответы нарушили “value only” и добавили текст. Вот ответ на 3000 парах:
Looking through the provided JSON object, I can find:
"7c98d2eb": "69e27874"
69e27874
Ключа 7c98d2eb в этом объекте нет. 69e27874 это значение с его последней строки. Модель процитировала строку, которой не существует, собранную из спрошенного мной ключа и значения из конца промпта. В сериях на 3000 и 4000 пар таких ответов с текстом вокруг было 17. 13 из них закончились словами NOT FOUND и 4 процитировали строку, которой нет. Остальные неверные ответы в этих сериях были голым шестнадцатеричным значением.
В сетке длина и число ключей растут вместе, так что причиной может быть любое из двух. Контроль это 1000 пар, перед которыми стоит проза без единого шестнадцатеричного слова, всего около 82 тысяч токенов. Это длиннее промпта на 4000 пар, где из 20 отказов уже пропали 5. NOT FOUND пришёл 10 раз из 10, а с ключом, который в объекте есть, ответ был верным 5 раз из 5. Значит, на этой задаче одна только длина отказ не сломала, по крайней мере когда длина стоит перед данными. Главный подозреваемый скорее число похожих ключей. Контроль маленький, всего 15 вызовов. Маленькие модели здесь не помогают ни в какую сторону. 0.5B ответила NOT FOUND 8 раз из 80 на объектах от 25 до 200 пар, а 1.5B 24 раза из 24 до 100 пар.
Чего я не проверял
Задача одна, причём на дословный поиск. Правило из моего файла, которое агент пропустил, это вопрос следования инструкции. NoLiMa даёт хороший повод думать, что следование инструкциям ломается раньше, чем поиск шестнадцатеричной строки, так что это не доказывает, что с файлом всё в порядке. Замер только убирает самое простое объяснение. Haiku насчитывает в файле около 25 тысяч токенов, меньше 32 тысяч, на которых она отказывалась каждый раз, когда ключа не было. Но в настоящей сессии файл лежит внутри гораздо большего контекста, рядом с системным промптом и перепиской.
Из моделей через API здесь только haiku и дальше 96 тысяч токенов я не заходил. В строках на 500 и 2000 пар только 3 сида на глубину, в строках на 4000 и 6000 их 8. 2 модели Qwen шли в разной разрядности и влияет ли это, я не проверял.
Из этого я вынес одну проверку в коде. Значение, которое вернулось из большой таблицы, надо заново найти в этой таблице, по тому ключу, про который спрашивали. Проверки, что такое значение там вообще есть, мало: выдуманная строка выше её бы прошла. Вызовы haiku, на которых стоят эти числа, обошлись примерно в 45 долларов.