Реализация AI-персонализации контента на сайте
Персонализация — это показывать разным пользователям разный контент на одной странице: другой порядок блоков, другой заголовок, другой CTA, другие товары. AI здесь управляет выбором вариантов на основе профиля и контекста пользователя.
Уровни персонализации
Поверхностный — переменные в тексте (имя пользователя, город), динамические заголовки. Без ML.
Сегментный — контент для сегментов (новичок / опытный, B2B / B2C, регион). Правила на основе атрибутов.
Поведенческий — контент на основе истории действий: что просматривал, покупал, читал.
Предиктивный — AI предсказывает следующее действие и оптимизирует контент под конверсию.
Профиль пользователя
// Накопление профиля в real-time
class UserProfileManager {
constructor(userId) {
this.userId = userId;
this.profileKey = `profile:${userId}`;
}
async trackEvent(event) {
const updates = {};
switch (event.type) {
case 'page_view':
updates[`categories.${event.category}`] = { increment: 1 };
updates['total_sessions'] = { increment: 1 };
break;
case 'purchase':
updates['purchases_count'] = { increment: 1 };
updates['total_spent'] = { increment: event.amount };
updates['last_purchase'] = event.timestamp;
break;
case 'content_read':
updates['read_count'] = { increment: 1 };
updates[`topics.${event.topic}`] = { increment: event.readTime };
break;
}
await redis.hIncrBy(this.profileKey, updates);
await redis.expire(this.profileKey, 86400 * 30); // 30 дней
}
async getProfile() {
const raw = await redis.hGetAll(this.profileKey);
return {
topCategories: getTopN(raw.categories, 5),
topTopics: getTopN(raw.topics, 5),
purchasesCount: parseInt(raw.purchases_count || 0),
totalSpent: parseFloat(raw.total_spent || 0),
segment: this.classifySegment(raw),
};
}
classifySegment(profile) {
if (profile.purchases_count > 10) return 'loyal';
if (profile.purchases_count > 0) return 'buyer';
if (profile.total_sessions > 5) return 'engaged';
return 'new';
}
}
Персонализация главной страницы
// API endpoint для персонализированной главной
async function getHomepageContent(userId, context) {
const profile = await getUserProfile(userId);
const geo = context.country || 'RU';
const device = context.device || 'desktop';
// Параллельно получаем все блоки
const [hero, featured, recommendations, cta] = await Promise.all([
getPersonalizedHero(profile, geo),
getFeaturedContent(profile.topCategories),
getPersonalizedProducts(userId, profile, 8),
getPersonalizedCTA(profile),
]);
return { hero, featured, recommendations, cta };
}
async function getPersonalizedHero(profile, geo) {
const variants = await getHeroVariants(); // A/B варианты из CMS
// Правила выбора варианта
if (profile.segment === 'loyal') {
return variants.find(v => v.segment === 'loyal') || variants[0];
}
if (geo === 'BY') {
return variants.find(v => v.geo === 'BY') || variants[0];
}
if (profile.topCategories.includes('sale')) {
return variants.find(v => v.theme === 'deals') || variants[0];
}
return variants[0]; // default
}
LLM-генерация персонализированного текста
Для высокоценных пользователей — динамические заголовки и описания:
async function generatePersonalizedHeadline(product, userProfile) {
const cacheKey = `headline:${product.id}:${userProfile.segment}`;
const cached = await redis.get(cacheKey);
if (cached) return cached;
const prompt = `
Сгенерируй заголовок карточки товара (до 10 слов) для пользователя.
Товар: ${product.name}, категория: ${product.category}
Профиль: сегмент=${userProfile.segment}, интересы=${userProfile.topTopics.join(',')}
Тон: профессиональный, без клише.
Верни только текст заголовка.
`;
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
max_tokens: 30,
temperature: 0.7,
});
const headline = response.choices[0].message.content.trim();
await redis.setex(cacheKey, 3600 * 6, headline); // кэш 6 часов
return headline;
}
Динамические CTA
const CTA_VARIANTS = {
new: {
text: 'Начать бесплатно',
subtext: 'Без кредитной карты',
color: 'blue',
},
engaged: {
text: 'Попробовать Pro',
subtext: '14 дней бесплатно',
color: 'green',
},
buyer: {
text: 'Обновить план',
subtext: 'Разблокируй все функции',
color: 'purple',
},
loyal: {
text: 'Реферальная программа',
subtext: 'Заработай за каждого друга',
color: 'orange',
},
};
function PersonalizedCTA({ userId }) {
const { profile } = useUserProfile(userId);
const variant = CTA_VARIANTS[profile.segment] || CTA_VARIANTS.new;
return (
<button
className={`cta-button cta-${variant.color}`}
onClick={() => {
trackCTAClick(userId, profile.segment);
navigate(getCtaDestination(profile.segment));
}}
>
{variant.text}
<span>{variant.subtext}</span>
</button>
);
}
Контекстная персонализация (без авторизации)
Для анонимных пользователей — сигналы из текущей сессии:
function getContextualSignals(request) {
return {
referrer: request.headers.referer, // откуда пришёл
utm_source: request.query.utm_source, // рекламный канал
utm_campaign: request.query.utm_campaign,
geo: request.headers['cf-ipcountry'], // Cloudflare geo
device: detectDevice(request.headers['user-agent']),
timeOfDay: getTimeOfDay(request.headers['x-forwarded-for']),
entryPage: request.url,
};
}
function getPersonalizationForAnonymous(signals) {
// Пришёл из рекламы "скидки" → показать sale-баннер
if (signals.utm_campaign?.includes('sale')) {
return { hero: 'sale', cta: 'discount' };
}
// Мобильный + вечер → показать app download
if (signals.device === 'mobile' && signals.timeOfDay === 'evening') {
return { hero: 'mobile-app', cta: 'download' };
}
// B2B сигнал из LinkedIn
if (signals.referrer?.includes('linkedin')) {
return { hero: 'b2b', cta: 'demo' };
}
return { hero: 'default', cta: 'default' };
}
Edge Personalization (Cloudflare Workers / Vercel Edge)
Для максимальной скорости — персонализация прямо на Edge, до Origin:
// Cloudflare Worker
export default {
async fetch(request, env) {
const url = new URL(request.url);
const userId = getCookie(request, 'user_id');
const segment = userId
? await env.KV.get(`segment:${userId}`)
: 'anonymous';
// Модифицируем запрос к Origin с сегментом
const newRequest = new Request(request.url, {
...request,
headers: {
...Object.fromEntries(request.headers),
'X-User-Segment': segment || 'new',
'X-User-Geo': request.cf.country,
},
});
return fetch(newRequest);
}
};
Измерение эффекта
-- Конверсия по сегментам и вариантам персонализации
SELECT
p.variant,
p.segment,
COUNT(DISTINCT p.user_id) AS shown,
COUNT(DISTINCT c.user_id) AS converted,
ROUND(COUNT(DISTINCT c.user_id)::numeric / COUNT(DISTINCT p.user_id) * 100, 2) AS cvr
FROM personalization_events p
LEFT JOIN conversion_events c
ON c.user_id = p.user_id
AND c.created_at BETWEEN p.created_at AND p.created_at + INTERVAL '7 days'
WHERE p.created_at >= NOW() - INTERVAL '30 days'
GROUP BY p.variant, p.segment
ORDER BY cvr DESC;
Сроки
- Сегментная персонализация (правила) — 3–4 дня
- Поведенческий профиль + персонализация рекомендаций — плюс 3–4 дня
- LLM-генерация динамических заголовков — плюс 2 дня
- Edge персонализация на Cloudflare Workers — плюс 2 дня
- Полная система с аналитикой, A/B, 5+ вариантами — 3–4 недели







