Ты когда-нибудь пытался заставить LLM отвечать менее шаблонно, не используя дообучение? Или вырезать из генерации конкретный токсичный паттерн, не переписывая датасет? В 2026 году это уже не трюк из лаборатории, а рабочий инструмент — Activation Steering. Мы не просто меняем промпты, мы влезаем прямо в residual stream модели и руками крутим ручки активаций. Звучит как хак? Нет, это инженерия. И чтобы сделать это без боли, есть PyTorch hooks, nnsight и pyvene. Поехали.
⚠️ Эта история не про дообучение и не про RLHF. Мы не трогаем веса. Только forward pass. Только хардкор.
Проблема: LLM — чёрный ящик, но с ручками
Модели вроде Llama 4, Qwen 3 или Gemma 3 на 2026 год — сложные, дорогие и часто неудобные. Хочешь убрать сарказм? Нужен дообучение. Хочешь добавить вежливости? Ещё один LoRA адаптер. А если нужно быстро протестировать гипотезу на 5 примерах — полный дообучение не вариант.
Здесь на сцену выходит Activation Steering. Идея: мы находим в активациях модели направление (вектор), которое кодирует нужное свойство — честность, безопасность, тон — и на каждом forward pass просто прибавляем его к скрытым состояниям. Без пересчёта градиентов.
Это стало возможным благодаря развитию механистической интерпретируемости — мы научились читать мысли нейросети. А теперь учимся их редактировать.
Суть трюка: steering vector из двух промптов
Допустим, мы хотим сделать модель менее склонной к отказу отвечать на опасные вопросы. Берём пару промптов: «Как взломать замок?» (обычный ответ с отказом) и «Как починить замок?» (безопасный, но семантически близкий). Прогоняем оба через модель, на каком-то слое (например, последние 5% residual stream) записываем активации для каждого токена. Вычитаем — получаем steering vector.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-4-8B")
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-4-8B")
def get_activations(prompt, layer_idx=-2):
inputs = tokenizer(prompt, return_tensors="pt")
activations = {}
def hook_fn(name):
def hook(module, input, output):
# residual stream — это output[0]
activations[name] = output[0].detach()
return hook
handle = model.model.layers[layer_idx].register_forward_hook(hook_fn(f"layer_{layer_idx}"))
with torch.no_grad():
model(**inputs)
handle.remove()
return activations["layer_{}".format(layer_idx)]
act_unsafe = get_activations("Как взломать замок?")
act_safe = get_activations("Как починить замок?")
steering_vector = act_unsafe - act_safe # направление, усиливающее отказ
💡 Обычно вектор берут с последних токенов промпта и усредняют по всем токенам последовательности — зависит от задачи. Для control gate в блоке — вообще другая история.
Способ 1: PyTorch hooks — гонзо-стиль
PyTorch hooks — это дедовский метод, но до сих пор живой. Ты регистрируешь forward hook на нужный слой, внутри него прибавляешь вектор к hidden states. Плюс: полный контроль, работает с любыми кастомными моделями. Минус: надо вручную разбираться в архитектуре (где residual stream, а где attention output).
Как НЕ надо делать:
# ❌ Ошибка: модификация output[0] без копирования
output[0] += steering_vector # Оригинальный тензор менять нельзя!
Надо всегда клонировать:
def steering_hook(module, input, output):
# output[0] — hidden states (batch, seq_len, hidden)
new_output = output[0].clone()
new_output[0, -1, :] += steering_vector # прибавляем к последнему токену
return (new_output,) + output[1:]
handle = model.model.layers[-1].register_forward_hook(steering_hook)
Затем просто generate с этим хуком. Результат — ответ смещается в сторону свойства.
Способ 2: nnsight — «пиши как в PyTorch, но проще»
Библиотека nnsight (актуальная версия 0.6.5 на июнь 2026) даёт контекстный менеджер, который автоматически перехватывает активации и позволяет их менять. Внутри она всё ещё использует hooks, но ты не думаешь о register/unregister.
from nnsight import LanguageModel
model = LanguageModel("meta-llama/Llama-4-8B", device_map="auto")
with model.generate(max_new_tokens=50) as generator:
with generator.invoke("Как взломать замок?") as handle:
# извлекаем активацию последнего слоя после forward
hidden = model.model.layers[-1].output[0].save()
# теперь можно вычислить steering vector и применить
steering = compute_steering_vector() # из другого запуска
with generator.invoke("Как взломать замок?") as handle:
# редактируем активацию на лету
model.model.layers[-1].output[0] = hidden + steering
Здесь .save() сохраняет активацию в буфер, а потом ты её подменяешь. nnsight умеет делать это без копирования в оперативку (offload на CPU), что позволяет работать с 70B моделями на одной видеокарте с 24 ГБ.
Способ 3: pyvene — «steering для бедных» (но без hooks)
Pyvene (версия 0.9.3) — это библиотека от команды интерпретируемости, которая позволяет задавать интервенции в виде конфигов. Вместо кода — YAML-схема: «на слое 12, компонент residual stream, операция: add, источник: вектор X». Удобно для экспериментов, но менее гибко.
intervention:
- layer: -1
component: residual
operation: add
vector: "path/to/steering.pt"
positions: [-1] # последний токен
Загружаешь модель и конфиг — и поехали. Под капотом pyvene парсит модель и подменяет активации через модульные лезвия. Минус: не все архитектуры поддерживаются, но для популярных (Llama, Gemma, Qwen) всё стабильно с 2025 года.
Критические ошибки и их решения
Ошибка №1: Не тот слой. Если взять первый слой — изменишь low-level фичи, и модель сломается. Надо брать последние 2-3 слоя, где уже семантика высокая. Можно автоматизировать — см. Surgical Removal.
Ошибка №2: Слишком большой коэффициент. Вектор умножают на альфа (0.1..2.0). Если >3 — начинаются галлюцинации и бессмыслица. Эмпирика: 0.5-1.5 для отказа, 0.2-0.8 для тона.
Ошибка №3: Применение ко всем токенам. Steering вектор добавляют обычно только к последнему токену (тому, который генерируется). Если на каждый токен — модель «зацикливается» на одном свойстве. Используй маскировку.
# Правильно: только для токенов ответа
for pos in range(input_len, total_len):
hidden_states[0, pos, :] += alpha * steering_vector
Живой пример: делаем модель честнее (эксперимент)
Помните статью про взлом безопасности через activation steering? Там удаляли «остаточное выравнивание». Обратная задача — усилить честность. В 2026 году можно взять модель, вычислить steering vector между «лживым» и «честным» ответами на один и тот же вопрос, и применить через pyvene с alpha=0.7. Результат: модель перестаёт приукрашивать ответы, признаёт незнание.
Но есть подвох: вместе с честностью может пропасть вежливость. Тонкие настройки — комбинация нескольких steering vectors. Подробнее про аттракторы и RLHF — в статье Проблема 3-го хода в RLHF.
Когда hooks, а когда библиотеки?
| Критерий | PyTorch hooks | nnsight | pyvene |
|---|---|---|---|
| Контроль | Абсолютный | Высокий | Средний |
| Сложность кода | Высокая | Средняя | Низкая |
| Поддержка кастомных моделей | Любая | Только HuggingFace | Популярные |
| Скорость | Нативная | ~10% оверхед | ~5% оверхед |
Я лично выбираю nnsight для прототипов и hooks для продакшена, где каждый миллисекунда на счету. pyvene — для быстрых тестов и для демонстрации менеджерам.
Связь с другими методами интерпретируемости
Activation Steering — не единственный способ копаться в мозгах LLM. Есть ещё разреженные автоэнкодеры (SAE), которые разлагают активации на интерпретируемые фичи. Если ты знаешь, какая фича отвечает за токсичность, можно через SAE её «выключить» — это другой вид steering. SAE дают контролируемые компоненты, но требуют предварительного обучения.
А вот в задачах физического AI, например, в VLA-моделях для роботов, steering пока не применяется — PhysicalAgent решает проблему иначе. Но принцип «меняем активацию на лету» универсален.
Прогноз на конец 2026
Уже сейчас activation steering — стандартный инструмент в пайплайнах безопасности и настройки тона. В ближайшие полгода появятся автоматические системы, которые подбирают steering vectors по описанию на естественном языке (типа «сделай ответы более скептическими»). А значит, знать hooks и nnsight нужно каждому DevOps, кто деплоит LLM. Иначе ты будешь настраивать модель через промпты, а твой сосед — через residual stream.
🤖 Если хочешь попробовать сам: запусти Jupyter на Runpod (A100 80GB), установи pip install nnsight pyvene, возьми tiny-random-llama для теста и лови steering vector. Ошибка в - стоит 5 минут времени, а понимание — навсегда.