Activation Steering 2.0: от CAA к RepEng и PyReFT — туториал | AiManual
AiManual Logo Ai / Manual.
06 Июл 2026 Гайд

Activation Steering 2.0: Забудьте про CAA, вот как управлять LLM через обучаемые интервенции

Полный туториал по улучшению activation steering: PCA-фильтрация, RepEng и PyReFT. Код Python, сравнение методов, подводные камни. Для исследователей и инженеро

Если вы пробовали 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. Но это всё еще статическая добавка, не адаптирующаяся к контексту.

💡
PCA-фильтрация — быстрый способ улучшить CAA без дообучения. Подходит, если у вас мало вычислительных ресурсов и вы хотите попробовать steering «на коленке».

Шаг 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 — мощный инструмент, его можно использовать как для улучшения безопасности, так и для манипуляции. Всегда тестируйте на репрезентативных выборках и документируйте, какие адаптеры и где включены.

А теперь — идите и пилите код. Пусть ваши модели ведут себя так, как вам нужно, а не так, как получилось.

Подписаться на канал