Латентные дебаты между LoRA-экспертами: две роли из трёх читаются, а канал теряет говорящего
Совет LoRA-экспертов обменивается скрытыми состояниями, а мы пытаемся подслушать обмен. Мысль отдельного эксперта читается, само сообщение канала — теряет автора в 75 случаях из 75. С граблями, замерами, контролями и открытым кодом.
Авторы: slavb18

Мы строим совет из нескольких LoRA-экспертов поверх одной замороженной Qwen2.5-3B, который оценивает техническое собеседование. Эксперты обмениваются не текстом, а скрытыми состояниями — это и есть «латентный дебат». В этой статье — как это устроено, какие грабли мы собрали и, главное, как проверили две вещи: переносит ли такой обмен реальную информацию и можно ли расшифровать переданную «мысль» обратно в текст.
Сразу честный итог. Мысль отдельного эксперта из вектора читается — для технаря и лидера, но не для аудитора, у того не читается вообще. А главное разочарование дальше: когда мы расшифровали само сообщение, которое летит по каналу, декодер во всех 75 случаях из 75 решил, что говорит один и тот же эксперт. Что сказали — доезжает, кто сказал — теряется.
Правка от 01.09.2026. Первая редакция этой статьи называлась «…и научились читать эти споры» и утверждала, что декодирование переданного латента — свободная территория. И то и другое оказалось неверным, исправлено ниже. Заодно добавлены разбор голосования против дебата и ссылки на открытый код с DOI.
🎯 Зачем вообще спорить в латенте
Мы делаем объективную оценку senior-инженеров. Главная боль — инфляция компетенций: кандидат уверенно называет технологии, а за словами нет ни одного числа и ни одного инцидента. Один интервьюер такому верит, другой — нет. Оценка не воспроизводится.
Идея: не один судья, а совет из разных ролей, которые спорят. Обмениваться можно текстом — но это медленно (каждый эксперт генерирует полный ответ). Поэтому обмен идёт скрытыми состояниями: эксперт передаёт соседу вектор из остаточного потока модели, а не слова.
Сразу скажу, чего мы тут не изобрели. Латентом между агентами обмениваются давно: CIPHER (ICLR 2024), Interlat, ещё полтора десятка работ. Смесь LoRA-экспертов, где скрытое состояние решает, какой адаптер сильнее подмешать, — это X-LoRA, MoLoRA, AdaMix. А декодировать скрытое состояние головой самой модели умеет logit lens (2020) и его аккуратная версия tuned lens (Belrose et al., 2023). Наш декодер — LayerNorm + Linear — ровно из этого семейства, никакой магии.
Своим мы считаем не приём, а что именно читаем и зачем. Все эти инструменты смотрят внутрь одного прохода одной модели: интересно, как модель думает. Мы читаем вектор, которым эксперты обменялись друг с другом, и не ради понимания модели, а чтобы сказать человеку: вот этот эксперт, вот на этом куске стенограммы, вот настолько повлиял на решение. Разница между «интересно» и «объясните, почему кандидату отказали».
И да, «латент» — это hidden state (активация остаточного потока размерности 2048), а не KV-кэш. KV-кэш между агентами никто не передаёт.
🧩 Стенд: данные с известной правдой
Данные синтетические, и это сделано сознательно: нам нужна объективная правда, с которой сравнивать, а не «мнение ещё одной LLM». Порядок обратный интуиции — сначала правда, потом диалог:
- Для каждого из 300 кандидатов сэмплируется скрытый профиль: непрерывные
tech,lead,inflation. - LLM генерирует стенограмму под этот профиль: высокий
tech→ конкретика («таблица на 800 млн строк, 5 тыс. RPS, bloat»); высокаяinflation→ блеф («десятки тысяч RPS, под терабайт» — масштаб есть, чисел нет). - Метка найма — детерминированное правило над профилем:
вето при inflation ≥ 78, иначе 0.6·tech + 0.4·lead ≥ 62.
Правило совпадает с меткой на 300 из 300 записей. Эксперт учится восстанавливать скрытую черту из текста, а мы честно знаем правильный ответ.
🧠 Три эксперта, которые видят РАЗНОЕ
Это главный урок. Мы обучаем три LoRA-адаптера поверх одной замороженной Qwen2.5-3B в 4 битах:
model = FastLanguageModel.get_peft_model(
model, r=16, lora_alpha=32, lora_dropout=0,
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"])

Почему разные входы критичны, мы поняли на грабле. Прошлые «советы» спорить не могли по построению: бутстрап-клоны одной модели коррелировали 0.97–0.99 — по сути один эксперт в трёх копиях. Когда эксперты одинаковы, спорить не о чем, а любой обучаемый роутер над ними встаёт на месте. Разные входы дают неустранимое разногласие: блефёр звучит глубоко → технарь завышает ему глубину, а аудитор видит зазор «заявлено ↔ подтверждено».
🔀 Латентный дебат: мысль вектором, а не текстом
Берём hidden state эксперта (форма 8 × 2048), прогоняем через обучаемую проекцию и вставляем соседу во вход как soft-токены:
proj = nn.Sequential(nn.LayerNorm(H), nn.Linear(H, H)) # H = 2048
soft = proj(thought) # hidden state -> soft-токены
e = torch.cat([emb(промпт_соседа), soft, emb(хвост)], dim=1)

🪤 Грабли, на которых мы обожглись
1. Сырой hidden-state нельзя класть прямо в пространство эмбеддингов. Первая (наивная) реализация вставляла hidden-state последнего слоя прямо во входные эмбеддинги. Это разные пространства и разные масштабы — модель разваливалась и выдавала «прошёл» с выдуманным обоснованием почти для всех. Починка — та самая обучаемая проекция (LayerNorm + Linear).
2. unsloth глобально патчит transformers. При импорте он патчит Qwen2Attention/Qwen2DecoderLayer, поэтому декодер приходится учить в отдельном процессе на немодифицированном transformers — иначе обратный проход рвётся об inplace-ядра инференса.
3. Полный прогон завис. Генерация через inputs_embeds без надёжного eos крутилась до max_new_tokens — ~16 минут на пример. Прибили, но дорогое сохранили (мысли, вердикты, проекцию) и дальше считали из кэша.
4. max_new_tokens тихо испортил выборку. Классика: поставили лимит «для экономии» — и все отрицательные вердикты обрезались (они длиннее). Фильтр «оставить распарсенное» выкинул почти все «не прошёл» → выборка из одних «прошёл» → Brier = 0.0000 и F1 = 1.000 как чистый артефакт. Чуть не отчитались о достижении KPI. Предохранитель:
if share > 0.20:
raise RuntimeError(f"{arm}: {share:.0%} непарсящихся при pass={lab} — выборка портится систематически")
🔍 Декодирование: читается ли мысль
Учим декодер: обучаемая проекция переводит hidden-state эксперта в soft-токены, и по ним замороженная база восстанавливает вердикт. Декодер видит только латент — ни стенограммы, ни вопроса.
И сразу — почему одной цифры «латент читается на X%» недостаточно: декодер мог просто выучить типовой ответ. Значение имеет только разрыв между своим латентом и контролем. Поэтому три контроля: латент чужого примера, нулевой вектор и константа-приор.

По решению совпадение из своего латента — 89% и 95%, из чужого — 57% и 49% (уровень монетки). Декодер читает именно эту мысль. И честный отрицательный результат, который мы не прячем: латент аудитора не декодируется — скорее всего, «инфляция» кодируется в скрытом состоянии не так линейно, как глубина и лидерство.
💥 А теперь неприятное: канал теряет говорящего
До сих пор мы декодировали латент одного эксперта. Но в дебате по каналу идёт не он — идёт среднее скрытых состояний остальных. Логично проверить, читается ли то, что реально передаётся.
По содержанию читается: расшифровка среднего даёт MAE 8.0 против настоящего среднего мнения остальных. Как сводка работает.
А вот с личностью беда. Мы спросили декодер, чью мысль он читает, — и он ответил «лидер» в 75 случаях из 75, на обоих каналах. Среднее скрытых состояний не ведёт себя как смесь, которую можно разложить обратно на слагаемые: оно схлопывается в одну доминирующую роль.
Проще говоря: если эксперты общаются через усреднение hidden state, то сказать «на решение повлиял вот этот» по такому сообщению нельзя. Само собой из архитектуры это не выпадает — нужен либо другой канал (не среднее, а, скажем, конкатенация с меткой роли), либо декодер, которого специально учили разделять вклады.
Это главный результат работы, и он отрицательный. Мы шли доказывать, что латентный спор можно аудировать, а получили, что аудировать можно мнение отдельного эксперта, но не спор.
🗳 Почему голосование не спасает
Отдельный прогон, на более раннем стенде с пятью бутстрап-экспертами (оговорка про него — в ограничениях). Эксперты сами собой разделились надвое: трое чрезмерно строгих (зарезали десять хороших кандидатов, F1 0.875), двое точных (1.000).
| судья | F1 | tp | fp | fn | tn |
|---|---|---|---|---|---|
| строгие (×3) | 0.875 | 35 | 0 | 10 | 14 |
| точные (×2) | 1.000 | 45 | 0 | 0 | 14 |
| большинство голосов | 0.875 | 35 | 0 | 10 | 14 |
| среднее индекса | 0.875 | 35 | 0 | 10 | 14 |
| медиана индекса | 0.875 | 35 | 0 | 10 | 14 |
| порог по среднему | 0.875 | 35 | 0 | 10 | 14 |
| дебат | 1.000 | 45 | 0 | 0 | 14 |
Смотреть надо не на F1, а на матрицу ошибок: у всех четырёх агрегаций она одна и та же. Это подпись механизма — если ты мнения только считаешь, ты обязан повторить решение строгого большинства. То, что в ансамбле есть двое, знающих правильный ответ, никого не интересует: их не спрашивают, их считают.
Забрать отсюда стоит вот что, даже если латентные дебаты вам не нужны: если ансамбль ошибается систематически, а не случайно, голосование не спасёт. Проверяется за минуту — сравните матрицы ошибок у разных способов агрегации.
⚖️ Работает ли сам дебат — или это шум
Когда технарь получает мысль аудитора, он меняет оценку из-за содержания реплики или просто шумит?

Сдвиг направленный. Технарь двигает оценку в разные стороны: блефёрам вниз, честным вверх. Дифференциал −7,2 ± 3,3 балла, интервал [−13,7; −0,8] не накрывает ноль. Будь это шум или общая осторожность — сдвиг был бы одного знака у всех.
Не списывает. Корреляция оценки технаря с мнением аудитора после дебата падает (0,31 → 0,17). Если бы он копировал соседа, она бы выросла. Значит взял сигнал, но пришёл к своему числу.
Честная граница. Механизм доказан, но прирост на итоговом решении — +0,006, не значим. Почему? Мы это измерили: узкое место не в дебате, а в точности экспертов. Правило найма на истинных чертах даёт F1 = 1.000, на оценках экспертов — 0.754. Сколько мнения ни переливай, слабого эксперта дебат не спасёт.
🧾 Честные ограничения
- Это синтетика. Живого человека внутри нет. Главный риск переноса снимаем реальными расшифровками собеседований, где скрытую «правду» заменяет реальный исход — прошёл ли специалист испытательный срок.
- Стендов на самом деле два, и один так себе. Всё про латент считалось на генераторе с непрерывным профилем, описанном выше (300 кандидатов, доля прошедших 0.38). А таблица «голосование против дебата» — со стенда постарше, на 239 диалогов, у которого мы потом нашли плохую разметку: метка «прошёл» оказалась почти чистой функцией архетипа кандидата, и честный слабый кандидат проходил. Ту таблицу стоит читать как рассказ о поведении агрегаций при системном разногласии — вывод держится на совпадении матриц ошибок и от разметки не зависит, — а не как измерение сеньорности.
- Латент аудитора не читается, и канал теряет отправителя — два открытых пункта, второй серьёзнее.
- Прирост дебата на решении мал — и мы знаем, почему (точность экспертов), а не делаем вид, что всё отлично.
- Один раунд спора, один сплит, один базовый чекпоинт.
И то, ради чего всё писалось. Прочитать мнение отдельного эксперта мы умеем, и это уже полезно: в корпоративном найме «покажите, почему модель так решила» — не бантик, а условие допуска. В России это вообще требование закона: ч. 3 ст. 16 152-ФЗ обязывает оператора разъяснить человеку, как было принято автоматическое решение по нему. А вот прочитать спор мы не умеем.
♻️ Проверьте наши цифры сами
Код, данные и препринт открыты: github.com/iconicompany/latent-debate, DOI 10.5281/zenodo.22214122.
Там не демка, а проверка. Все таблицы этой статьи пересчитываются из сохранённых предсказаний — без видеокарты, без скачивания модели, без ключей:
python verify/analyze_latent.py --preds results/latent.json
За секунды печатает round-trip-таблицу, парный бутстрап на 5000 итераций с доверительными интервалами, совпадение решений против контролей и тот самый аудит канала. Если наши цифры вам не нравятся — не спорьте с постом, запустите и посмотрите.
Отдельно выложен бенчмарк data/council-N300.jsonl (CC BY 4.0) — тот самый, где правильный ответ известен по построению. Это самое переиспользуемое, что здесь есть.
Мы делаем объективную оценку инженеров в закрытом контуре заказчика. Если тема близка — заходите в наш канал @iconicompany, там разбираем такие эксперименты и то, как из них получается продукт.