Я дал qwen3.5-9B (8-bit) и qwen3-coder-30B (iq2_xxs) одну задачу — исправить падающие тесты в Python-проекте. 9B справился за 3 шага. 30B сделал 24 шага, потерял нить, повторил одни и те же вызовы инструментов и вернул уверенный неправильный ответ.

У 30B больше параметров. Он проиграл.

Причина не в модели — в harness’е. Три месяца я строил агентный CLI для локальных LLM и разбирался, почему маленькая модель с правильным окружением стабильно бьёт большую без него. Вот что нашёл.

Типичный сценарий провала

Задача: исправить падающие тесты в репозитории.

Модель читает тест. Читает исходник. Придумывает фикс — и возвращает ответ. Уверенно. С объяснением. Тесты при этом всё ещё красные.

Модель не запустила тесты. Она не могла — у неё не было механизма проверить себя. Она просто сгенерировала правдоподобный ответ и остановилась.

Это не глупость. Это архитектура. Без цикла обратной связи модель работает вслепую — как повар, которому не дают пробовать еду. Он может быть хорошим поваром. Просто блюдо с вероятностью 50% окажется пересолённым.

Что такое harness и почему он важнее модели

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

Умная модель в плохом harness’е работает хуже, чем средняя модель в хорошем. Это контринтуитивно — мы привыкли думать что качество = размер. Но на практике разрыв между “9B не справляется” и “9B справился за 3 шага” — это не веса, это инфраструктура вокруг них.

Я написал lema — опенсорсный агентный CLI для локальных LLM. Расскажу что внутри и какие решения оказались нетривиальными.

Цикл верификации: модель не должна верить себе на слово

Главная идея простая: когда модель говорит “я сделал” — harness не верит ей. Он запускает тесты сам.

Если тесты красные — вывод об ошибках уходит обратно в контекст, и модель продолжает работу. Если зелёные — принимаем результат. Модель физически не может сказать “готово” пока тесты не пройдут.

Это меняет всё. Модель перестаёт гадать и начинает знать. Вместо “кажется правильно” — “проверил, работает”. Одна эта штука убрала большую часть ошибок в тестах.

Небольшая деталь из практики: если модель сама запускает тесты через bash — harness это видит и учитывает при своей проверке. Двойного запуска не происходит. Но и ложного “зелёного” тоже.

Память: чтобы не начинать с нуля каждый раз

Когда задача завершается циклом провал → успех (тесты были красными, потом стали зелёными), harness сохраняет урок. Не весь транскрипт — короткую выжимку: что за задача, какая команда упала, что помогло.

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

DRY-принцип, но на уровне агентских сессий. Модель не изобретает одно и то же решение дважды.

Есть и ручной режим: /remember в TUI позволяет сохранить что угодно — соглашения проекта, особенности API, что-то что модель должна помнить всегда.

Контекст: маскировка дешевле суммаризации

Длинные задачи переполняют контекстное окно. Большинство агентов в этот момент либо крашатся, либо начинают галлюцинировать.

Классический ответ — попросить модель написать резюме разговора. Проблема в двух частях: это дополнительный LLM-вызов (медленно), и маленькая модель пишет плохие резюме — дропает детали, которые понадобятся на следующем шаге.

Исследование JetBrains Research (arXiv 2508.21433) показало: маскировка на 52% дешевле суммаризации и при этом точнее. Идея: не удалять старые сообщения, а заменять тяжёлые выводы инструментов коротким плейсхолдером — “вывод скрыт, файл такой-то, 487 строк”. Рассуждения модели и сами вызовы инструментов остаются нетронутыми. Агент знает что делал и может перечитать нужный файл если нужно.

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

Инструменты: называй как модель ожидает

Самая незаметная причина провалов SLM — schema misalignment. Модель галлюцинирует правдоподобное название инструмента вместо реального, потому что видела похожее в pretraining.

Исследование PA-Tool (arXiv 2510.07248) измерило это: одно только переименование инструментов под pretraining-конвенции даёт +17% точности и минус 80% ошибок несоответствия. Без изменения модели, без тюнинга.

Поэтому в lema инструменты называются максимально скучно и предсказуемо: read_file, write_file, edit_file, grep, glob, bash. Никаких умных имён — только те, которые модель видела тысячи раз.

Второй принцип — не больше 7±2 инструментов в контексте. Точность выбора деградирует после десяти. Когда нужно больше — dynamic retrieval, показываем только релевантные задаче.

Третий — минимальный вывод. read_file отдаёт запрошенный диапазон, не весь файл. grep — строки с контекстом, не весь файл. Меньше шум = больше места для рассуждений.

Effort dial: для SLM overthinking опаснее, чем underthinking

Когда модель плохо справляется — инстинкт говорит “дай ей больше думать”. Больше токенов на рассуждение, больше шагов. Это ошибка.

Research 2026 (arXiv 2604.10739, 2507.14417) документирует inverse scaling: при росте thinking-бюджета точность SLM сначала растёт, потом падает. Модель начинает сомневаться в правильных ответах, переусложнять простые задачи, топтаться на одном месте.

Поэтому effort в lema — это не “думай больше”. Это пресет конкретных параметров: сколько шагов, сколько токенов, какой тон инструкции. Четыре уровня: low (быстро, кратко), medium (дефолт), high (планируй, проверяй инструментами), ultra (максимум шагов для цикла верификации).

Ключевой нюанс в ultra: он даёт в три раза больше шагов, но не в три раза больше токенов на мышление. Больше tool actions, не больше thinking. Для SLM запустить тесты ещё раз эффективнее, чем думать о них дольше.

auto выбирает уровень по задаче без LLM-вызова: длинный фикс или рефакторинг → high, короткий вопрос → low, иначе medium.

AGENTS.md: правила, которые не забываются

Стандартная проблема: кладёшь правила в system prompt, модель соблюдает их первые несколько шагов — потом забывает.

Это не баг модели. LLM хорошо помнит начало и конец контекста, слепнет к середине. На длинной задаче правила уходят именно туда.

lema решает это re-injection’ом: правила инжектируются дважды — в начало контекста и в конец перед каждым вызовом модели. В конце — конденсированная версия, только ключевые строки. Конденсация детерминированная, без LLM-вызова.

Поддерживаются AGENTS.md (open standard, принят OpenAI, Google, Cursor, Aider, Gemini CLI), CLAUDE.md и .lema/rules.md. Кладёшь файл в корень проекта — lema читает автоматически.

Баг, который было приятно найти

При тестировании qwen3.5-9b модель возвращала пустой ответ при явно ненулевом числе completion-токенов. Выглядело как зависание.

Прямой curl к LM Studio показал: с thinking-моделями сервер возвращает content: "" и кладёт всё мышление в отдельное поле reasoning_content. Наш тип сообщения это поле не знал — оно дропалось при разборе ответа.

Фикс несложный: добавить поле в тип, а если content пустой при непустом reasoning_content — запросить финальный ответ явно. И важная деталь: reasoning_content не должен попадать в историю контекста — иначе thinking-токены начнут накапливаться и жрать окно на каждом следующем шаге.

Классический случай когда баг объясняет поведение, которое иначе выглядит как “модель просто плохая”.

Сравнение: 9B vs 30B на одних задачах

Тестировал на pomodoro-таймере на Python (~4 файла, реальные тесты):

Модель

Квант

Шагов на задачу

Результат

qwen3-coder-30B

iq2_xxs

20-26

средний, часто false success

qwen3.5-9B

8-bit

2-4

высокий, верифицированный

30B технически мощнее. Но агрессивный квант (iq2_xxs — это очень низко) убивает качество рассуждений до такой степени, что модель теряет нить, повторяет вызовы инструментов и не замечает противоречий в своих действиях.

9B с нормальным квантом и harness’ом: читает файл → делает правку → верификация зелёная → готово.

Вывод простой: качество кванта важнее размера модели, когда harness делает свою работу.

Итог

Три месяца экспериментов свелись к одному выводу: локальные модели не дурнее, чем кажутся. Дурнее окружение.

Разница между “9B не справляется” и “9B справился за 3 шага” — это не размер весов. Это наличие цикла обратной связи, правильных имён у инструментов и памяти между сессиями.

Что удивило больше всего:

  • Переименование инструментов дало заметный прирост без изменения промптов

  • Маскировка работает лучше суммаризации — и это контринтуитивно

  • Inverse scaling реален: “думай дольше” иногда ухудшает результат

  • 9B с 8-bit стабильно бьёт 30B с iq2_xxs при правильном harness’е

Честные ограничения: большие кодовые базы (500+ файлов), задачи без явного verifier, всё что требует глобального понимания архитектуры — пока не его история.

lema — опенсорсный проект, MIT, TypeScript, zero runtime deps. Нужен LM Studio с загруженной моделью:

npm install -g @iivgll4/lema
lema ping
lema "fix the failing tests"

Исходники: github.com/iivgll/lema

Если строили что-то похожее или пробовали другие подходы к верификации — интересно сравнить в комментариях.

Комментарии (3)


  1. OlegGavrilov
    01.07.2026 06:07

    Кликбейтный заголовок сделал только хуже, когда оказалось что вы сравнивали модели с настолько разным квантованием. Сравните ваш харнесс с тем же pi на одной и той же локальной модели, вот это будет действительно интересно.


    1. vaashaforostov
      01.07.2026 06:07

      оно быть может и да, но мне, как относительному новичку, который только начинает познавать локальные ИИ и столкнулся с первыми проблемами их использования - было интересно почитать все равно, и статья навела меня на вектор поиска того, что мне нужно, чтобы уйти от дорогого уже теперь codex, хоть и неглупого


  1. al-chemist
    01.07.2026 06:07

    LLM хорошо помнит начало и конец контекста, слепнет к середине. На длинной задаче правила уходят именно туда.

    Этому есть какое-то подтверждение, или это «я так вижу»?