Латентные дебаты между LoRA-экспертами: как мы заставили модели спорить в скрытом пространстве — и научились читать эти споры

Мы строим совет из нескольких LoRA-экспертов поверх одной замороженной Qwen2.5-3B, который оценивает техническое собеседование. Эксперты обмениваются не текстом, а скрытыми состояниями — это и есть «латентный дебат». В этой статье — как это устроено, какие грабли мы собрали и, главное, как проверили две вещи: переносит ли такой обмен реальную информацию и можно ли расшифровать переданную «мысль» обратно в текст.
Сразу честный итог: да и да, но с оговорками. Латент технаря и лидера декодируется, а латент аудитора — не декодируется вообще. И дебат сдвигает оценки в нужную сторону, но прирост на итоговом решении пока мал — узкое место оказалось не в дебате.
🎯 Зачем вообще спорить в латенте
Мы делаем объективную оценку senior-инженеров. Главная боль — инфляция компетенций: кандидат уверенно называет технологии, а за словами нет ни одного числа и ни одного инцидента. Один интервьюер такому верит, другой — нет. Оценка не воспроизводится.
Идея: не один судья, а совет из разных ролей, которые спорят. Обмениваться можно текстом — но это медленно (каждый эксперт генерирует полный ответ). Поэтому обмен идёт скрытыми состояниями: эксперт передаёт соседу вектор из остаточного потока модели, а не слова.
Оговорка про новизну, чтобы не было вопросов: обмен латентными состояниями между агентами уже публиковали — CIPHER (ICLR 2024), Interlat, обзор на восемнадцать методов. Мы это своим не считаем. Свободно другое: ни один из этих методов не расшифровывает переданное сообщение обратно в текст. Латентный спор у всех — чёрный ящик. Вот аудируемость канала мы и проверяли.
И да, «латент» — это 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% (уровень монетки). Декодер читает именно эту мысль. И честный отрицательный результат, который мы не прячем: латент аудитора не декодируется — скорее всего, «инфляция» кодируется в скрытом состоянии не так линейно, как глубина и лидерство.
⚖️ Работает ли сам дебат — или это шум
Когда технарь получает мысль аудитора, он меняет оценку из-за содержания реплики или просто шумит?

Сдвиг направленный. Технарь двигает оценку в разные стороны: блефёрам вниз, честным вверх. Дифференциал −7,2 ± 3,3 балла, интервал [−13,7; −0,8] не накрывает ноль. Будь это шум или общая осторожность — сдвиг был бы одного знака у всех.
Не списывает. Корреляция оценки технаря с мнением аудитора после дебата падает (0,31 → 0,17). Если бы он копировал соседа, она бы выросла. Значит взял сигнал, но пришёл к своему числу.
Честная граница. Механизм доказан, но прирост на итоговом решении — +0,006, не значим. Почему? Мы это измерили: узкое место не в дебате, а в точности экспертов. Правило найма на истинных чертах даёт F1 = 1.000, на оценках экспертов — 0.754. Сколько мнения ни переливай, слабого эксперта дебат не спасёт.
🧾 Честные ограничения
- Это синтетика. Живого человека внутри нет. Главный риск переноса снимаем реальными расшифровками собеседований, где скрытую «правду» заменяет реальный исход — прошёл ли специалист испытательный срок.
- Латент аудитора не читается — открытый пункт.
- Прирост дебата на решении мал — и мы знаем, почему (точность экспертов), а не делаем вид, что всё отлично.
Но аудируемость канала — уже готовая ценность: для корпоративного найма «покажите, почему модель так решила» — условие допуска, а не бантик.
♻️ Воспроизводимость
Весь цикл — независимые вердикты, расшифровка мысли аудитора и дебат — воспроизводится на CPU, без GPU: база Qwen2.5-3B грузится в bf16, forward несколько секунд. Цифры в статье пересчитываются из сохранённых предсказаний отдельными анализаторами, а не берутся с потолка.
Мы делаем объективную оценку инженеров в закрытом контуре заказчика. Если тема близка — заходите в наш канал @iconicompany, там разбираем такие эксперименты и то, как из них получается продукт.