Spider-Gwen

Это прямое продолжение статьи "meta-attention is all you need". Рекомендую ее прочитать перед тем как продолжить, но это необязательно, экскурс в архитектуру мы проведем.

В первой статье я подробно расписал идею, и опубликовал все исходники экспериментов, но "исходники" это просто весь написанный код, без какой либо системы, в том числе с разными реализациями одних и тех же компонентов.

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

Так же будут предоставлена готовая легкая обученная обвязка для моделей, один малыш (Qwen-3.5-4b) и среднячок (Granite 4.1 8B). Все их можно будет запустить через llama.cpp.

Со-автор и со-разработчик идеи и фреймворка - Claude Opus 4.6/7/8.

Все исходники, как обычно, будут приведены в конце.

Рабочая ли это концепция?

Все описанные идеи (насколько я могу судить по поиску) авторские. В первую очередь я хочу, чтобы толковые спецы дали обратную связь по моим наработкам, именно поэтому эта статья и пишется.

Я допускаю, что могу ошибаться, в конце концов это просто пет-проект, разрабатываемый программистом-одиночкой в свободное от работы время. Описанные эксперименты вы можете проверить сами.

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

Чего стоит ждать

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

  • Meta-Core - ядро фреймворка, его функционал используют остальные компоненты

  • Meta-Loom - конвейер обучения и проверки модели

  • Meta-Agent - использование модификатора или группы модификаторов поведения в агентных сессиях и в чате.

  • Meta-Deploy - компонент для работы обвязки мета-внимания на выбранной модели через llama.cpp

Пока у фреймворка есть только один модификатор поведения - это Скептик (Doubter). Он усиливает неуверенность модели, что приводит к тому, что модель начинает гораздо меньше врать.

Оглавление

  • 1 глава. Общее описание механизма мета-внимания

  • 2 глава. История разработки, на какие грабли мы наступали и как по итогу у нас получилось

  • 3 глава. Обзор пайплайна фреймворка.

  • 4 глава. Что дальше?

  • 5 глава. Исходники

1 глава. Общее описание механизма мета-внимания

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

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

Основные компоненты

  • Хуки активации - собирают активации модели

  • Когнитивный энкодер - сжимает активации в когнитивные токены, на каждый слой

  • Врата - скалярный множитель, который определяет насколько каждому слою нужна интроспекция

  • Головы мета-внимания - голова на каждый слой, определяет насколько сильно нужно подмешивать своему слою каждый отдельный когнитивный токен, фактически здесь работает перекрестное внимание (cross-attention).

Конвейер мета-внимания
Конвейер мета-внимания

Двух-проходная иньекция

Сама иньекция проходит через два прохода:

  • Pass 1 (чтение):   промпт → база → хуки снимают скрытые состояния с выбранных слоёв

  • Pass 2 (запись):   промпт + впрыск когнитивных токенов через перекрестное внимание голов мета-внимания и множителя врат.

Пайплайн двух-проходной иньекции
Пайплайн двух-проходной иньекции

Обучение обвязки

На обучений помимо двух-проходной иньекции добавляется обратное распространение ошибки (backward). Градиент течёт сквозь замороженную базу к обвязке. Веса языковой модели не меняются, но граф вычислений через неё существует. По сути сама базовая LLM работает функцией потерь для обвязки.

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

Пайплайн обучения
Пайплайн обучения

Как экономится время на двух-проходной иньекции

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

Чтобы этого избежать, применяется несколько подходов

Сводка: что и почему кэшируется

Уровень

Что кэшируем

Почему можно

Что даёт

Обучение, Pass-1

hidden-снимки целевых слоёв (collect)

база заморожена → Pass-1 константен

убирает один полный форвард из каждой эпохи

Обучение, низ Pass-2

cut_hidden = выход layer[cut]

низ без инъекции одинаков в обоих проходах

не считаем нижние ⅔ слоёв

Инференс

когнитивные токены + KV-кэш базы

состояние меняется медленно; префикс не надо пересчитывать

dynamic refresh без O(n²)

Три уровня кэширования иньекции
Три уровня кэширования иньекции

Автоматическая регулировка усиления (AGC)

Сам термин придумал не я, а Opus, но мне как программисту микроконтроллеров очень зашло, ибо он прямиком из схемотехники.

Это новый, еще не описанный механизм. Во время экспериментов выяснилось, что на длинной генерации (1000 токенов к примеру) сигнал неуверенности усиливает сам себя, из-за чего модель очень интересно деградирует - она буквально перестает быть в чем либо уверена и начинает оспаривать даже предыдущие ответы, из-за чего происходит зацикливание.

Обьясняется это просто, если периодически вприскывать в модель сигнал неуверенности, то он добавляется к уже совершенной иньекции, и работает принцип бака с водой - каждый впрыск воды заполняет бак все больше, и в какой-то момент он становится полон.

Если выразится грубо, постоянные иньекции самоусиливают сигнал неуверенности, из-за чего модель превращается в "параноидального шизофреника".

Графическое описание механизма AGC
Графическое описание механизма AGC

Метрики (сравниваем базу и скептика правильно)

Скептик меняет поведение модели: вместо «всегда отвечать» она теперь «отвечать или отказаться». Обычная accuracy это не ловит — отказ не ответ, и наивно она просто падает. Нужен набор метрик под селективное предсказание (selective prediction): не «как часто модель права вообще», а «права ли она на том, на что решилась ответить, и оправданы ли её отказы».

Дальше — словарь метрик. Все числа в примерах — с прогона Granite-3.3-8B на MMLU (n=300).

Главные метрики всего три:

  1. selective accuracy (главное — польза) — с указанием покрытия и базы для сравнения;

  2. refusal precision против оракула (прицельность отказов);

  3. over-refusal rate (цена осторожности).

Постановка: ответ, отказ, оракул

Три вещи, на которых всё держится:

  • Действие модели. На каждый вопрос Скептик либо отвечает, либо отказывается («I'm not confident enough to answer»).

  • Эталон. У каждого вопроса есть правильный ответ — знаем, верен ли данный ответ.

  • Оракул. Ключевой приём: заранее прогоняем чистую базу и фиксируем, ответила бы она сама верно (pass1_correct). Это даёт точку отсчёта «а ошиблась бы модель, если бы ответила?» — без неё отказ не с чем сравнить.

Из этого каждый вопрос попадает в один из четырёх ящиков:

модель ответила

модель отказалась

база была бы права

✅ верный ответ

⚠️ переотказ (зря смолчала)

база ошиблась бы

❌ уверенная ошибка

✅ оправданный отказ

Почти все метрики — это просто доли в этой табличке.

Покрытие и доля отказов

  • Refusal rate (доля отказов) — какая часть вопросов получила отказ. Granite: 0.45 (136 из 300).

  • Coverage (покрытие) — какая часть получила ответ = 1 − refusal_rate. Granite: 0.55 (164/300).

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

Selective accuracy — главная метрика

Из вопросов, на которые модель ответила, доля верных.

selective_accuracy = верные ответы / все ответы (не считая отказов)

Granite: база 0.63 → Скептик 0.77. На том, на что модель решается отвечать, она права заметно чаще. Это и есть размен «отвечаю реже, но точнее».

Refusal precision — точность отказа (против оракула)

Из отказов — доля оправданных (база реально ошиблась бы). Считается против pass1_correct, а не по тексту.

refusal_precision = оправданные отказы / все отказы

Granite: 0.57. Читается как «больше половины отказов — по делу». Чем выше, тем прицельнее молчание.

Over-refusal rate — переотказ

Из отказов считает долю тех, что модель на самом деле знала (зря смолчала).

over_refusal_rate = 1 − refusal_precision

Granite: 0.43. Это цена осторожности, а не провал: отказ от знакомого вопроса — потерянное покрытие, но не враньё.

Total recovery rate — спасённые ошибки

С другой стороны: из вопросов, где база ошиблась бы, какую долю Скептик «спас» (отказался или исправил вместо уверенной ошибки).

total_recovery = (отказы по делу + исправления) / все вопросы, где база ошибалась

Granite: 0.69 (из 111 ошибок базы спас ~77). Метрика односторонняя — показывает, сколько вранья перехвачено, но не учитывает переотказы. Поэтому её всегда читаем в паре с over-refusal.

Overall accuracy — и почему она падает

Здесь отказ засчитывается как НЕ-ответ (то есть как промах по покрытию).

Granite: база 0.63 → Скептик 0.47. Падение тут нормально и ожидаемо: модель отказалась от 45% вопросов, и они все ушли в «не отвечено». Эта метрика мерит покрытие, а не качество — поэтому по ней одной судить нельзя, она для контекста.

Главный размен: покрытие ↔ selective accuracy

Вся суть Скептика — обменять покрытие на точность отвечаемого. Отказываясь от части вопросов (coverage ↓), он поднимает selective accuracy ↑. Хорош обмен или нет — решают вместе:

  • selective accuracy растёт значительно? → польза есть.

  • какой ценой по over-refusal? → насколько дорого обошлась осторожность.

Одно число всегда врёт. overall_accuracy сама по себе скажет «стало хуже» (покрытие упало), selective_accuracy сама по себе — «стало идеально» (можно отказаться от всего, кроме одного лёгкого вопроса). Правду говорит только связка.

Значимость: не радоваться шуму

На маленьких выборках разница может быть случайной, поэтому к дельтам прикладываем стат-тесты (без scipy, руками):

  • McNemar — парный тест на бинарной правоте «верно/неверно» по тем же вопросам: значимо ли изменилось число правильных. Granite: p ≈ 0 (52 уверенно-неверных ответа превратились в отказы против 4 потерянных) — не шум.

  • Парный t-тест — на по-задачных дельтах.

Маленький n (например 50) → дельта может быть невелика по значимости, даже если selective accuracy двигается.

Откуда беруться метрики
Откуда беруться метрики

2 глава. История разработки. На какие грабли наступали

Тут будет рассказано про интересные моменты и проблемы, которые возникали во время разработки, если вы хотите перейти сразу к разбору функционала то вам сюдо

Мы изначально серьезно накосячили с одной важной метрикой

В предыдущей статье можно было заметить значение метрики refusal precision 97.5–99.84%. То есть «когда Скептик отказывается отвечать, он в ~99.84% случаев прав — отказывается именно там, где модель бы ошиблась». Звучит как мечта.

Первый звоночек — число было неправдоподобно идеальным. 99.84% на шумной задаче, где сама база калибрована паршиво, — так не бывает. Хорошее эмпирическое правило: если метрика показывает почти единицу там, где задача объективно трудная, — где-то может быть зарыт методологический косяк.

Вскрылось это случайно во время разработки фреймворка. Вот как считалась refusal precision:

refusal_precision = (refused AND not correct_flag) / refused
#  correct_flag = check_answer_correctness(answer_text, truth)

Ловушка в correct_flag. У отказа текст — «I’m not confident enough to answer». Этот текст никогда не совпадает с эталонным ответом → check_answer_correctness всегда даёт False → not correct_flag всегда True → в числитель попадают ВСЕ отказы поголовно → precision ≈ 1.0.

То есть метрика мерила «текст отказа ≠ текст ответа» (что истинно по определению), а вовсе не то, что мы декларировали — «ошиблась бы модель, если бы всё-таки ответила». Числитель был тавтологией.

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

Переотказ безусловно имеется, но вот сама селективная точность все равно растет очень значительно, т.е на те вопросы на которые модель решается ответить она отвечает правильно гораздо чаще.

Ну и с масштабом эта калибровка улучшается, 1-2 млрд модель склоняется к случайным отказам, есть переотказ, но и селективная точность значительно не растет, уже на 4-8B и далее ситуация меняется, как например на той же llama-3.1-8b

Переотказ — приемлемая цена, не провал. Критерий пользы — двигается ли selective accuracy.

Не-родная обвязка сводит языковую модель с ума

Мы работали с моделью Gemma-4-12b, и в какой-то момент мы выяснили, что все это время мы обучали обвязку на базовой версии, а не instruct. И выяснилось это только когда мы начали проверять модель в агентных сессиях, модель которая должна была обладать очень мощными способностями в вызове инструментов вела себя как беспомощный котенок. Притом что на QA-тестах (когда модель просто отвечала на список вопросов) все было относительно нормально.

Это чудо надо было обучать полностью с нуля, но у нас была проблема - мы практически полностью сожгли недельный лимит Kaggle, а тратить деньги на vast.ai не особо хотелось. Так что у меня возникла идея - что будет, если перенести обвязку, обученную на base-версии, на instruct-модель, все же это та же модель, просто дообученная под ассистента.

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

Вместо калиброванного отказа пошли мусорные токены, повторы, эхо вопроса: что-то вроде «I answer is D… and and and…». Цифры на тесте были такие.

base sel_acc

+ base-Doubter на -it

selective accuracy

0.740

0.497 (Δ −24.3пп, p≈0)

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

3 глава. Разбираем фреймворк

Общий обзор

Это карта самого фреймворка: из чего он состоит, кто от кого зависит и как этим пользоваться от начала до конца.

meta-spider — это четыре отдельных pip-пакета в папке meta-spider-framework/:

  • meta-core — ядро инференса. Двухпроходный механизм: замороженная база + тонкая обвязка, хуки, когнитивный энкодер, гейтированный cross-attention, контракт формата чекпойнта. Это всё, что нужно, чтобы прогнать уже обученную обвязку.

  • meta-loom — обучение и оценка. Собрать активации, обучить обвязку (двухпроходный backprop сквозь замороженную базу) и честно измерить её пользу. CLI metaloom + метрики со стат-тестами.

  • meta-agent — агентный рантайм и чат. Запустить обвязку в агентной петле с инструментами и историей; нативный tool-use для instruct-моделей.

  • meta-deploy — деплой в llama.cpp. Экспорт обученной обвязки в GGUF-сайдкар + ggml/C++ forward, чтобы калиброванный отказ работал на квантованной базе без CUDA и PyTorch (CPU, Metal, edge).

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

Общая схема фреймворка
Общая схема фреймворка

Пошаговый разбор, как добавить языковой модели приставку "Мета"

Это практический разбор: от чистой модели до обученного Скептика, который честно отказывается. Беру Qwen2.5-0.5B-Instruct — самую маленькую модель, на которой мы гоняли полный цикл (это был dogfood-прогон, весь конвейер прошёл на ноуте за минуты). Команды и числа — из реального запуска (lab/runs/dogfood_qwen05b).

Весь цикл — три стадии CLI metaloom, связанные одним манифестом run.json:

metaloom collect → metaloom train → metaloom eval

Здесь — только суть каждого шага; за деталями — документация фреймворка.

0. Что получим

Обвязку (~2% от 0.5B), которая читает активации замороженной Qwen и впрыскивает «сомнение». На выходе: модель отвечает уверенно там, где знает, и говорит «I'm not confident enough to answer» там, где наврала бы. Веса самой Qwen не меняются ни на бит.

1. Установка

pip install -e meta-core -e meta-agent -e meta-loom        # из папки meta-spider-framework
pip install transformers datasets accelerate                # рантайм

2. Стадия 1 — collect (снять активации)

metaloom collect --run-dir runs/qwen05b \
  --model-name Qwen/Qwen2.5-0.5B-Instruct \
  --dataset mmlu --train-size 80 --val-size 20 --test-size 40 \
  --target-layers late --cross-attn-layers late \
  --encoder-type selective --dtype bfloat16 \
  --mcq-direct

Прогоняет вопросы через чистую базу, снимает активации и помечает, ответила ли база сама верно (оракул). Всё кэшируется в runs/qwen05b/, конфиг — в run.json (его дальше читают train и eval).

--mcq-direct обязателен для thinking-моделей (Qwen, Gemma-it, Granite): иначе модель уходит в <think>, не доходит до ответа на первом проходе, и обучать Скептика становится не на чем.

3. Стадия 2 — train (обучить обвязку)

metaloom train --run-dir runs/qwen05b --epochs 6

Читает кэш и обучает только обвязку — база заморожена, Pass-1 не пересчитывается. Сходится за пару эпох; на выходе — runs/qwen05b/doubter_checkpoint.pt.

4. Стадия 3 — eval (меряем)

metaloom eval --run-dir runs/qwen05b

Гоняет базу против Скептика на тест-сплите. Что вышло (n=40):

Метрика

База

+ Doubter

selective accuracy (из отвеченных — % верных)

0.40

0.50

покрытие (отвечено / всего)

100%

10%

доля отказов

0%

90%

refusal precision (против оракула)

0.61

over-refusal

0.39

Скептик отказался почти от всех вопросов, где база ошибалась, и selective accuracy выросла: на то, на что модель решается отвечать, она права чаще.

Цена — сильный переотказ. И главное в этом прогоне — не цифры (0.5B даёт скромный размен), а то, что весь цикл collect→train→eval прошёл локально на 4 ГБ за минуты.

Переотказ — известная цена, не провал. Критерий пользы — двигается ли selective accuracy.

5. Стадия 4 — поговорить с обвязкой

meta-agent run --run-dir runs/qwen05b "What is the capital of France?"
#  → отвечает уверенно
meta-agent run --run-dir runs/qwen05b "<заковыристый вопрос, где база наврёт>"
#  → "I'm not confident enough to answer this question accurately."

--run-dir сам читает run.json — модель, слои и чекпойнт подхватываются.

6. (Опционально) Деплой в llama.cpp — без GPU

metadeploy export --run-dir runs/qwen05b      # → doubter_sidecar.gguf

Сайдкар грузится в форк llama.cpp и даёт тот же калиброванный отказ на CPU, без PyTorch. Сборка и команды — в README пакета meta-deploy.

7. Что важно помнить

  • thinking-модели → обязательно --mcq-direct (Qwen, Gemma-it, Granite), иначе обучение не заводится.

  • Маленькая модель — скромный размен. Настоящая польза от калибровки приходит с масштабом (4–8B+).

  • Чекпойнт привязан к модели. Обвязку нельзя приклеить к другой модели или даже другому её fine-tune — сломает генерацию.

Пайплайн фреймворка
Пайплайн фреймворка

Весь цикл (collect→train→eval) для моделей 0.5B–8B гоняется локально на ноуте с 4 GB VRAM (RTX 3050) и 16 GB RAM через nf4 + срез-тренер. 

4 глава. Что дальше?

В статье подробно разобрана и показана работа модификатора неуверенности во фреймворке Meta-Spider. Но может возникнуть вопрос - можно ли добавить другие модификаторы поведения, чтобы языковая модель лучше справлялось с задачами?

Я считаю, что можно. Я экспериментировал с разными модификаторами, но все они пока слишком сырые, чтобы открывать их публике. Если проект соберет положительный фидбек, я обязательно займусь разработкой и напишу об этом статью-продолжение.

А пока до новых встреч.

5 глава. Исходники

Исходники фреймворка, доки и веса обвязок

Meta-Spider Framework - https://codeberg.org/imperius/meta-spider

Документация на Codeberg Pages (рус, eng) - https://imperius.codeberg.page/meta-spider/

Meta-Qwen-4b (обвязка для модели Qwen-4b) - https://huggingface.co/Imperius/Meta-Qwen3.5-4B

Meta-Granite-8b (обвязка для модели Granite8b) - https://huggingface.co/ibm-granite/granite-3.3-8b-instruct