Реализация автоматической генерации SEO-текстов для карточек товаров (AI)
Карточка товара без уникального текста — это либо дублированный контент из прайса поставщика, либо шаблонная фраза из 30 слов. Ни то ни другое не ранжируется. При каталоге от нескольких тысяч позиций писать тексты вручную нецелесообразно — это задача для автоматизации.
Генерация SEO-текстов через языковую модель закрывает эту задачу при правильной архитектуре: подготовленные промпты, контроль качества на выходе, кэширование и ревью перед публикацией.
Что на входе, что на выходе
Вход — структурированные данные товара: название, категория, характеристики, бренд, теги. Выход — SEO-описание длиной 150–600 слов с целевыми ключевыми словами, естественно вписанными в текст.
Пример входных данных:
{
"id": "SKU-4821",
"name": "Кроссовки Nike Air Max 270",
"category": "Мужская обувь / Кроссовки",
"brand": "Nike",
"attributes": {
"material": "mesh + synthetic",
"sole": "Air Max unit",
"colors": ["black/white", "navy/grey"],
"sizes": "40–46",
"weight": "310g"
},
"tags": ["беговые", "повседневные", "амортизация"],
"targetKeywords": ["nike air max 270 купить", "кроссовки найк аир макс 270"]
}
Промпт-инжиниринг для товарных текстов
Промпт — это не «напиши описание товара». Хороший промпт задаёт структуру, тон, длину, требования к ключевым словам и запреты:
function buildProductSeoPrompt(product: Product, keywords: string[]): string {
return `
Write a product description for an e-commerce catalog in Russian.
Product: ${product.name}
Category: ${product.category}
Brand: ${product.brand}
Key attributes: ${JSON.stringify(product.attributes)}
Tags: ${product.tags.join(", ")}
Requirements:
- Length: 200–400 words
- Include these keywords naturally (not forced): ${keywords.join(", ")}
- Structure: opening benefit statement → key features (3–5 points) → use cases → closing
- Tone: informative, no hype, no superlatives like "лучший" or "уникальный"
- Do NOT use: "данный товар", "представляем вашему вниманию", bullet points
- Do NOT start with the product name
- Write for a person who is comparing options, not for someone who already decided
Output: plain text, no markdown, no headings.
`.trim();
}
Пакетная обработка с очередями
Генерировать тексты синхронно нельзя — запрос к GPT занимает 3–10 секунд, а позиций могут быть тысячи. Правильная схема — очередь задач:
// jobs/generate-seo-text.ts
import { Queue, Worker } from "bullmq";
import { openai } from "../lib/openai";
import { db } from "../lib/db";
const seoQueue = new Queue("seo-generation", {
connection: { host: "localhost", port: 6379 },
});
// Постановка задач в очередь
export async function queueProductsForGeneration(productIds: string[]) {
const jobs = productIds.map((id) => ({
name: "generate",
data: { productId: id },
opts: {
attempts: 3,
backoff: { type: "exponential", delay: 5000 },
removeOnComplete: 100,
},
}));
await seoQueue.addBulk(jobs);
}
// Воркер
const worker = new Worker(
"seo-generation",
async (job) => {
const product = await db.products.findById(job.data.productId);
if (!product) return;
const keywords = await getTargetKeywords(product);
const prompt = buildProductSeoPrompt(product, keywords);
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini", // дешевле для массовой генерации
messages: [{ role: "user", content: prompt }],
temperature: 0.7,
max_tokens: 600,
});
const text = completion.choices[0].message.content?.trim();
if (!text) throw new Error("Empty response");
// Сохраняем как черновик, не публикуем автоматически
await db.productSeoTexts.upsert({
productId: product.id,
text,
status: "draft",
model: "gpt-4o-mini",
generatedAt: new Date(),
});
},
{
connection: { host: "localhost", port: 6379 },
concurrency: 5, // 5 параллельных запросов к API
}
);
Контроль качества
Автоматически сгенерированный текст нужно валидировать до сохранения. Минимальный набор проверок:
interface ValidationResult {
passed: boolean;
issues: string[];
}
function validateSeoText(text: string, product: Product): ValidationResult {
const issues: string[] = [];
if (text.length < 500) {
issues.push(`Too short: ${text.length} chars`);
}
// Проверяем наличие ключевых слов
const missingKeywords = product.targetKeywords.filter(
(kw) => !text.toLowerCase().includes(kw.toLowerCase())
);
if (missingKeywords.length > 0) {
issues.push(`Missing keywords: ${missingKeywords.join(", ")}`);
}
// Стоп-слова
const stopPhrases = [
"данный товар",
"представляем вашему вниманию",
"уникальный",
"лучший в своём классе",
];
for (const phrase of stopPhrases) {
if (text.toLowerCase().includes(phrase)) {
issues.push(`Contains stop phrase: "${phrase}"`);
}
}
// Спам ключевых слов
const wordCount = text.split(/\s+/).length;
for (const kw of product.targetKeywords) {
const kwCount = (text.toLowerCase().match(new RegExp(kw.toLowerCase(), "g")) || []).length;
const density = kwCount / wordCount;
if (density > 0.03) {
issues.push(`Keyword density too high for "${kw}": ${(density * 100).toFixed(1)}%`);
}
}
return { passed: issues.length === 0, issues };
}
Тексты, не прошедшие валидацию, помечаются флагом needs_review и попадают в отдельную очередь для повторной генерации с уточнённым промптом.
Интерфейс ревью
Редактор видит список черновиков с возможностью принять, отклонить или редактировать:
GET /admin/seo-texts?status=draft&page=1
→ список карточек с текстом, кнопки: Опубликовать / Регенерировать / Редактировать
POST /admin/seo-texts/:id/approve
→ меняет status на published, обновляет карточку товара
POST /admin/seo-texts/:id/regenerate
→ добавляет задачу обратно в очередь с пометкой attempt=2
Регенерация с обратной связью — воркер читает причину отклонения и добавляет её в промпт:
if (job.data.rejectionReason) {
prompt += `\n\nPrevious attempt was rejected. Reason: ${job.data.rejectionReason}. Fix this in the new version.`;
}
Стоимость и масштаб
GPT-4o-mini на момент написания стоит $0.15 за миллион входящих токенов и $0.60 за миллион исходящих. Один товарный текст — примерно 300–500 токенов на вход и 400–500 на выход. Итого около $0.0004 за текст. 10 000 карточек — порядка $4. Это делает массовую генерацию экономически обоснованной даже при частом обновлении каталога.
При большом каталоге стоит настроить триггер на обновление атрибутов товара — если изменилось название или ключевые характеристики, текст автоматически помечается устаревшим и ставится на перегенерацию.







