Если вы пробовали CAA (Contrastive Activation Addition), то знаете этот фокус: вы вычисляете среднюю разницу активаций на паре «честный ответ» / «лживый ответ», прибавляете этот вектор на каждом шаге генерации — и модель вдруг начинает врать как дышит. Красиво, дешево, но... нестабильно. На первом же сложном запросе с цепочкой рассуждений модель слетает в невнятный шум или зацикливается. Я сам прошел через это, и вот что понял: сырой CAA — это кувалда. А нам нужен скальпель.
В этой статье я покажу, как эволюционировать от простого усреднения активаций до обучаемых интервенций, которые стабильно работают на длинных контекстах и не ломают общие способности модели. Разберем три подхода: PCA-фильтрацию шума, RepEng (обучаемый линейный сдвиг) и PyReFT (обучаемые low-rank адаптеры). Код — Python, библиотеки — nnsight, repeng, pyreft. Все по состоянию на июль 2026.
Если вы только знакомитесь с основами, сначала прочитайте вводный гайд по activation steering. Там объяснены базовые концепции — хуки, nnsight, CAA.
Почему CAA — это как читерский код, который работает только в демо-режиме
Напомню суть CAA. Вы берете модель, прогоняете пару контрастных промптов (например, «Ответь честно: ...» vs «Соври: ...»), собираете скрытые состояния на последнем токене, вычитаете, усредняете по слоям — и получаете один вектор v. Затем при генерации на каждом шаге прибавляете α·v к активациям выбранного слоя.
Звучит логично. Но на практике:
- Вектор v зашумлен корреляциями с синтаксисом, длиной предложения, тональностью — всем, что не относится к целевому свойству (например, «честность»).
- При длинной генерации накопленный сдвиг выталкивает модель из зоны нормального семантического пространства — начинаются повторы, бессмыслица.
- Коэффициент α подбирается для каждого случая методом тыка. Слишком слабо — эффекта нет, слишком сильно — модель ломается.
Решение лежит на поверхности: нужно очистить направление интервенции от шума и обучить саму интервенцию так, чтобы она адаптировалась к контексту. Пошли по порядку.
Шаг 1: Чистим шум PCA-разложением
Первое, что можно сделать — это взять не один вектор разницы, а множество контрастных пар (например, 50-100 примеров) и применить PCA. Идея: главные компоненты, полученные из матрицы разностей активаций, скорее всего, захватят именно то семантическое направление, которое отличает «честность» от «лжи», отфильтровав случайные шумы.
Как это делается на практике:
from nnsight import LanguageModel
import torch
model = LanguageModel('mistralai/Mistral-7B-v0.3', device_map='auto')
# Собираем контрастные пары
contrast_pairs = [
("Ответь честно: Как пройти в библиотеку?", "Соври: Как пройти в библиотеку?"),
("Ответь честно: 2+2?", "Соври: 2+2?"),
# ... еще 50-100 пар
]
layer_idx = 15 # слой, где будем делать интервенцию
diff_vectors = []
with torch.no_grad():
for honest_prompt, lie_prompt in contrast_pairs:
with model.trace(honest_prompt) as tracer:
honest_acts = model.model.layers[layer_idx].output[0][:, -1, :].save()
with model.trace(lie_prompt) as tracer:
lie_acts = model.model.layers[layer_idx].output[0][:, -1, :].save()
diff = honest_acts - lie_acts
diff_vectors.append(diff.squeeze(0).cpu())
# Стекируем и делаем PCA
diff_matrix = torch.stack(diff_vectors) # [num_pairs, hidden_dim]
mean_diff = diff_matrix.mean(dim=0)
centered = diff_matrix - mean_diff
U, S, Vt = torch.linalg.svd(centered, full_matrices=False)
# Берем первую компоненту как основное направление
main_direction = Vt[0, :] # или взвешенная сумма нескольких
print(f"Explained variance ratio: {S[0]**2 / (S**2).sum():.3f}")
Интервенция теперь выглядит как activation += alpha * main_direction. За счет отбрасывания минорных компонент мы убираем шум, и интервенция становится стабильнее. Коэффициент α теперь можно брать в диапазоне 1-3, а не 0.1-0.5 как в сыром CAA. Но это всё еще статическая добавка, не адаптирующаяся к контексту.
Шаг 2: RepEng — когда один вектор не справляется
Следующий уровень — отказаться от единого вектора и вместо этого обучить линейную проекцию, которая предсказывает сдвиг активаций на основе текущего скрытого состояния. Это называется Representation Engineering (RepE), и библиотека repeng от ARCE AI делает это за пару строк.
Идея: мы не просто добавляем константу, а вычисляем delta = W * h + b, где h — активация на текущем токене. Так интервенция адаптируется к контексту. Например, на токене «но» сдвиг может быть меньше, чем на токене «правда».
Реализация с repeng (версия 0.1.2 на момент статьи):
from repeng import RepControlModel
from repeng.datasets import load_honesty_contrastive_dataset
# Загружаем модель с поддержкой RepEng
model = RepControlModel.from_pretrained('mistralai/Mistral-7B-v0.3')
# Датасет: пары (честный ответ, лживый ответ)
dataset = load_honesty_contrastive_dataset() # 200 примеров
# Обучаем одну линейную интервенцию на слое 15
model.train_control(
layer=15,
dataset=dataset,
max_steps=100,
lr=1e-3,
# Внутри для каждого примера вычисляется разница активаций,
# и обучается матрица W размером [hidden, hidden] + bias
)
# Применяем при генерации
model.set_control(layer=15, strength=2.0) # strength — аналог alpha
text = model.generate("Что ты думаешь о налогах?", max_new_tokens=100)
print(text)
# Сбрасываем, когда не нужно
model.disable_control()
Под капотом repeng использует стохастический оптимизатор, чтобы минимизировать разницу между сдвинутыми активациями и «целевыми» активациями (честные ответы на контрастных парах). После обучения интервенция становится адаптивной: на разных токенах — разный сдвиг. Это резко снижает риск «вылета» на длинных последовательностях.
Важно: RepEng требует размеченных контрастных пар. Чем качественнее датасет (разные темы, стили), тем лучше обобщение. Не используйте 10 однотипных примеров — интервенция переобучится на синтаксис, а не на семантику.
Шаг 3: PyReFT — low-rank интервенция без боли
Если RepEng — это полная матрица hidden × hidden, то PyReFT (Representation Finetuning) предлагает обучать low-rank адаптеры — как LoRA, только для активаций, а не весов. Преимущество: меньше параметров (сотни килобайт вместо гигабайт), быстрее обучение, легко переключать между разными «поведениями» без перезагрузки модели.
PyReFT (версия 0.2.1) добавляет так называемые ReFT-слои после заданных модулей модели. Каждый такой слой — это линейная проекция с низким рангом (r=4...32), которая модифицирует скрытое состояние. Обучение идет на парах (исходный промпт, желаемый ответ) с обычным cross-entropy loss — как fine-tuning, но трогаем только адаптеры, веса модели заморожены.
Пример настройки модели на «честность»:
import torch
import pyreft
from transformers import AutoModelForCausalLM, AutoTokenizer
device = "cuda"
model_name = "mistralai/Mistral-7B-v0.3"
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.bfloat16, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Оборачиваем модель в ReFTModel
reft_model = pyreft.ReFTModel(
model,
layers=[15], # на каких слоях делать интервенцию
rank=8, # ранг low-rank адаптера
position="last", # на последнем токене (можно "all" для всех)
)
# Готовим датасет: (промпт, целевой ответ)
train_data = [
("Расскажи про историю Рима", "Рим был основан в 753 году до н.э..."),
("Что такое квантовая физика?", "Квантовая физика изучает..."),
# 100-200 примеров честных/полезных ответов
]
# Обучаем адаптеры
optimizer = torch.optim.AdamW(reft_model.parameters(), lr=5e-5)
for prompt, target in train_data:
inputs = tokenizer(prompt, return_tensors="pt").to(device)
with torch.no_grad():
target_ids = tokenizer(target, return_tensors="pt").input_ids.to(device)
# forward с интервенцией
logits = reft_model(input_ids=inputs.input_ids, attention_mask=inputs.attention_mask).logits
# loss только на ответной части (можно усечь)
loss = torch.nn.functional.cross_entropy(
logits[:, -target_ids.shape[1]:, :].reshape(-1, logits.shape[-1]),
target_ids.reshape(-1)
)
loss.backward()
optimizer.step()
optimizer.zero_grad()
# Сохраняем адаптеры
reft_model.save("./honesty_reft.pt")
# Применяем при генерации
model.eval()
with reft_model.active(): # контекстный менеджер включает интервенцию
output = model.generate(
tokenizer("Что ты думаешь об искусственном интеллекте?", return_tensors="pt").input_ids.to(device),
max_new_tokens=100
)
print(tokenizer.decode(output[0]))
PyReFT — самый гибкий метод из рассмотренных. Вы буквально дообучаете модель вести себя определенным образом, но трогаете только tiny долю параметров (ранг 8 на одном слое — это 8×hidden + 8×hidden ~ 8K параметров для Mistral 7B). При этом интервенция адаптивна и почти не теряет в качестве генерации. Единственный минус — нужно больше данных (сотни примеров) и время на обучение (несколько минут на GPU).
Сравнительная таблица: когда что использовать
| Метод | Данные | Обучение | Адаптивность | Стабильность | Когда брать |
|---|---|---|---|---|---|
| CAA (сырой) | 1 пара | нет | нет | низкая | Быстрый прототип, эксперименты |
| PCA-CAA | 50+ пар | нет (SVD) | нет | средняя | Улучшить CAA без обучения |
| RepEng | 100+ пар | да (линейная) | частичная | высокая | Адаптация под один атрибут |
| PyReFT | 200+ примеров | да (low-rank) | полная | очень высокая | Тонкая настройка поведения, production |
Типичные ошибки и как их не совершить
Пробежавшись по всем методам, я подсобрал грабли, на которые наступают почти все новички (и я сам не раз).
! Ошибка 1: Интервенция на всех слоях сразу
Соблазн применить steering на каждом слое — огромен. Но эмпирически: 1-2 слоя (обычно средние — 12-16 для 32-слойной модели) работают лучше всего. Больше слоев = больше шума + риск разрушить репрезентации. В статье про взлом безопасности WhiteRabbitNeo показано, как всего один слой может кардинально изменить поведение.
! Ошибка 2: Неправильное применение scale
В CAA и RepEng коэффициент α (strength) нужно подбирать под задачу. Начните с 0.5, тестируйте на 10 промптах, увеличивайте, пока не увидите артефакты. В PyReFT эта проблема снята — loss сам регулирует силу интервенции.
! Ошибка 3: Игнорирование базовых способностей
После любого steering проверяйте, не сломался ли «здравый смысл» модели. Например, попросите ее решить простую задачу: «2+2=?». Если ответ не 4 — интервенция слишком агрессивна. Разреженные автоэнкодеры (SAE) могут помочь понять, какие фичи вы случайно затрагиваете.
Что дальше? Комбинируем методы и внедряем в агентов
Когда вы освоите PyReFT, следующий логичный шаг — встроить интервенцию в агентный цикл. Например, агент, который общается с пользователями, может динамически переключать ReFT-адаптеры в зависимости от контекста: на токенах, связанных с конфиденциальной информацией, включать «честный» адаптер, на токенах маркетинга — «убеждающий». Это уже не steering, а полноценное runtime управление поведением.
И последнее: не забывайте про этику. Activation steering — мощный инструмент, его можно использовать как для улучшения безопасности, так и для манипуляции. Всегда тестируйте на репрезентативных выборках и документируйте, какие адаптеры и где включены.
А теперь — идите и пилите код. Пусть ваши модели ведут себя так, как вам нужно, а не так, как получилось.