Персонализация без криповости: Как использовать профиль юзера

23.06.2026 09:00

Я помню день, когда мы выкатили первую версию умного ассистента для крупного e-commerce проекта. Мы гордились архитектурой: бот знал историю заказов, размер одежды, любимые бренды, частоту возвратов и даже средний чек. Первому же живому пользователю алгоритм радостно выдал: «Привет, Алексей! Вижу, ты снова ищешь беговые кроссовки 43 размера, как те Nike, что ты вернул две недели назад из-за отклеившейся подошвы. Предложить аналоги?». Алексей закрыл вкладку через три секунды. Мы создали идеального цифрового сталкера.

Проблема персонализации кроется в тонкой грани между заботой и слежкой. Когда вы приходите в любимую кофейню, бариста просто начинает готовить ваш обычный флэт-уайт на овсяном. Он не зачитывает вслух вашу выписку по карте лояльности за последний год. LLM-боты, напротив, часто страдают словесным недержанием. Получив в системный промпт полный дамп профиля, модель стремится продемонстрировать свою осведомленность. Пользователь видит машину, которая слишком старательно притворяется близким другом. Возникает отторжение.

Нам требовалось научить систему держать знания при себе и использовать их исключительно для невидимой корректировки ответов. Мы перешли от статического внедрения профиля к динамическому RAG (Retrieval-Augmented Generation) пользовательского контекста.

Откуда берутся эти атомарные факты? Мы построили асинхронный пайплайн обогащения профиля. Когда диалог с пользователем завершается, фоновый воркер анализирует транскрипт сессии. Мы используем дешевую и быструю модель через RouterAPI для извлечения сущностей (Entity Extraction). Воркер получает инструкцию: «Найди в тексте явные предпочтения, жалобы или ограничения пользователя. Верни JSON». Если пользователь вскользь упомянул: «У меня аллергия на шерсть, поэтому ищу синтетику», экстрактор формирует факт {"category": "health_restriction", "value": "аллергия на шерсть", "confidence": 0.95}. Этот факт ложится в векторную базу с привязкой к user_id. Параллельно мы подтягиваем хард-факты из CRM: статус лояльности, средний чек, предпочитаемый метод оплаты.

Вместо того чтобы вываливать в system prompt всю JSON-структуру профиля, мы разбили историю взаимодействий на атомарные факты. Каждый факт получил векторное представление и метаданные: временную метку, категорию и вес уверенности. Когда Алексей пишет «посоветуй куртку на осень», система векторизует этот запрос и ищет релевантные факты в его личном индексе. Алгоритм извлекает размер одежды и предпочтение темных цветов, но полностью игнорирует историю покупок собачьего корма или проблемы с доставкой прошлого месяца.

Здесь возникает технический нюанс: устаревание данных (time decay). Вкусы меняются. Факт «купил красные кеды» трехлетней давности имеет меньший вес, чем факт «искал строгие туфли» на прошлой неделе. При поиске по профилю мы умножаем cosine similarity на функцию затухания, зависящую от возраста факта. В контекст попадают только те крупицы информации, которые преодолевают заданный порог релевантности.

Подмешивание контекста напрямую влияет на экономику проекта. RouterAPI тарифицирует запросы по количеству токенов, и бездумная отправка всего профиля на каждый чих пользователя быстро сжигает бюджет. Динамический RAG решает и эту проблему. Ограничивая выборку фактов по порогу cosine similarity (например, > 0.75) и устанавливая жесткий лимит на количество извлекаемых записей (Top-K = 5), мы гарантируем, что системный промпт не раздувается. Мы платим только за те токены, которые реально влияют на качество текущего ответа. Более того, мы кэшируем векторы частых запросов. Если человек в пятый раз за месяц спрашивает статус заказа, мы не дергаем векторную базу, а берем готовый набор фактов из Redis.

Следующий этап — правильная упаковка найденных фактов перед отправкой в RouterAPI. Мы используем Laravel, и процесс формирования промпта происходит на лету в отдельном сервисе-декораторе. Мы жестко ограничиваем модель в том, как она может использовать предоставленные данные.

Системный промпт конструируется из трех блоков: базовой персоны, динамических фактов и строгих ограничений. Блок ограничений играет ключевую роль в борьбе с криповостью. Мы прямо запрещаем модели упоминать сам факт владения информацией.

Вместо мягкого «учитывай предпочтения», мы пишем директивно: «Тебе доступны скрытые факты о пользователе: [Размер: 43, Стиль: минимализм, Бренд: Asics]. ИСПОЛЬЗУЙ эти факты для фильтрации рекомендаций. КАТЕГОРИЧЕСКИ ЗАПРЕЩАЕТСЯ писать "Я знаю, что вы носите 43 размер" или "Учитывая вашу любовь к Asics". Просто предлагай подходящие товары так, будто это случайное совпадение».

Интеграция с RouterAPI в нашем шлюзе выглядит как сборка финального массива сообщений. Мы берем оригинальный запрос пользователя, поднимаем историю диалога из кэша, генерируем системный промпт через ProfileContextBuilder и отправляем полезную нагрузку в апстрим.

public function sendPersonalizedRequest(User $user, string $userMessage): array
{
 $relevantFacts = $this->ragService->retrieveUserFacts($user->id, $userMessage);
 
 $systemPrompt = $this->promptBuilder
 ->setBasePersona('Ты — эксперт-консультант магазина.')
 ->injectFacts($relevantFacts)
 ->addConstraint('Никогда не упоминай напрямую, откуда ты знаешь предпочтения пользователя. Не используй фразы вроде "я помню" или "в вашем профиле указано".')
 ->build;

 $messages = [
 ['role' => 'system', 'content' => $systemPrompt],
 // .. история диалога
 ['role' => 'user', 'content' => $userMessage]
 ];

 return $this->routerApiClient->chat->create([
 'model' => config('резервный канал RouterAPI.default_model'),
 'messages' => $messages,
 'temperature' => 0.3,
 ]);
}

Низкая температура (0.3) здесь не случайна. Чем выше креативность модели, тем больше шансов, что она проигнорирует запрет на упоминание фактов и сорвется в панибратство. Мы зажимаем температуру, заставляя LLM строго следовать инструкциям и выдавать сухой, но идеально подходящий результат.

Даже с жесткими системными промптами случаются пробои. Однажды мы тестировали новую, более «умную» модель. В системном промпте лежал факт: «Пользователь недавно развелся, ищет квартиру для одного». Ограничение на упоминание фактов присутствовало. Модель выдала: «Отличный выбор студии! Она идеально подойдет для начала новой жизни свободного мужчины». Формально правило не нарушено — модель не сказала «я знаю, что вы развелись». Но уровень криповости пробил потолок.

Этот инцидент заставил нас пересмотреть подход к чувствительным данным. Мы ввели классификацию фактов по уровню приватности. Данные уровня P3 (здоровье, личная жизнь, финансовые трудности) вообще не передаются в LLM для генерации текста. Они используются исключительно на уровне бэкенда для жесткой фильтрации выдачи (например, исключить дорогие товары из поиска), но сама языковая модель о них не подозревает. Мы лишили нейросеть возможности импровизировать с деликатным контекстом.

Деликатная персонализация требует архитектурной дисциплины. Мы перестали воспринимать профиль пользователя как монолитный документ, который нужно скормить нейросети. Профиль стал динамической базой знаний, из которой мы извлекаем только то, что нужно прямо сейчас.

Мы прячем контекст глубоко под капотом. Пользователь получает магический опыт: бот почему-то всегда предлагает вещи нужного размера, советует рестораны с вегетарианским меню и помнит, что доставку лучше оформлять на вечер. При этом машина сохраняет профессиональную дистанцию. Доверие строится не на демонстрации всезнания, а на стабильной, ненавязчивой пользе. Мы перестали пугать людей своими базами данных и начали просто решать их задачи.

Теги

Ещё по теме