Files
zeropost-engine/src/services/promptBuilder.js
T
Alexey Pavlov 5599de59ce feat: расширенная анкета канала + промпт-инжиниринг для человечности
БД (новые таблицы):
- channel_style: тон/юмор/длина/структура/эмодзи/хэштеги/примеры постов/стоп-слова
- channel_schedule: расписание, рубрики, источники, auto_publish
- generation_jobs: добавлены user_id, tokens, cost, prompt_debug
- posts: связка с job, image_url, scheduling

Новый модуль services/promptBuilder.js:
- HUMANITY_RULES: правила живого текста (антипаттерны, личный голос, конкретика)
- buildPostSystemPrompt: собирает промпт из канала + few-shot примеров
- buildCritiquePrompt: self-critique для очистки от AI-следов

services/ai.js:
- generatePost теперь использует 2-step chain: генерация + critique
- temperature настроен (0.9 для разнообразия)
- возвращает usage/токены

services/channels.js: новый сервис, работа с тремя таблицами транзакционно
routes/channels.js: CRUD под расширенную модель
routes/generate.js: связка с channelId, передача в worker

Результат на тестах: пост следует стилю few-shot примеров, без AI-маркеров
2026-05-30 22:01:38 +03:00

222 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Prompt Builder — сборка системного промпта из настроек канала.
* Логика "человечности" живёт здесь, отдельно от вызовов AI.
*/
// Базовые правила, чтобы текст не пах AI
const HUMANITY_RULES = `
ПРАВИЛА ЖИВОГО ТЕКСТА (соблюдай строго):
1. Варьируй длину предложений. Короткие. И длинные, развёрнутые, с уточнениями и придаточными. Иногда — одно слово.
2. Никогда не используй эти AI-маркеры:
- "В современном мире...", "В наше время..."
- "Важно отметить, что...", "Стоит подчеркнуть..."
- "Революционный", "Уникальный", "Поистине", "По-настоящему"
- "В заключение хотелось бы сказать..."
- "Не секрет, что..."
- Двоеточие после каждого второго утверждения
- Идеально симметричные списки на любой вопрос
3. Конкретика вместо абстракций:
- Не "многие компании" — а "Сбер, Яндекс, Тинькофф"
- Не "недавно" — а "на прошлой неделе" или "в марте"
- Не "большие цифры" — а "$2.3 млрд" или "37 тысяч пользователей"
- Имена, цифры, примеры — всегда лучше обобщений
4. Личный голос. Можно (и нужно):
- Начинать предложение с "И", "Но", "А"
- Использовать "я думаю", "мне кажется", "честно говоря"
- Высказать мнение, не быть нейтральным
- Разговорные обороты, если это подходит стилю
5. Шероховатости — это хорошо:
- Не идеальный текст, а живой
- Можно оборвать мысль и пойти в сторону
- Можно вернуться к началу неожиданно
6. Не объясняй очевидное. Доверяй читателю.
7. Не заканчивай каждый пост призывом "Подписывайтесь!" или вопросом "А что думаете вы?" — только если это органично.
`.trim();
const TONE_HINTS = {
friendly: 'Дружелюбный, тёплый. Как пишет хороший знакомый.',
serious: 'Серьёзный, без шуток. Деловой, но не сухой.',
ironic: 'С иронией, лёгким сарказмом. Подмечает абсурд.',
provocative: 'Провокационный, спорный. Не боится острых углов.',
academic: 'Академичный, с терминологией. Глубокий разбор.',
};
const LENGTH_HINTS = {
short: '150-300 знаков. Только суть.',
medium: '300-800 знаков. Развёрнутая мысль, но без воды.',
long: '800-2000 знаков. Лонгрид с подразделами или развитой мыслью.',
};
const EMOJI_HINTS = {
none: 'Эмодзи не используй вообще.',
moderate: 'Эмодзи используй умеренно — 1-3 на пост, для акцента.',
active: 'Эмодзи используй активно, но к месту. 5-10 на пост ок.',
};
const HASHTAG_HINTS = {
none: 'Хэштеги не добавляй.',
end: 'В конце поста — 2-4 релевантных хэштега.',
inline: 'Хэштеги вплети в текст естественно (1-3 штуки).',
};
const STRUCTURE_HINTS = {
plain: 'Сплошной текст без списков и заголовков.',
lists: 'Используй списки (•, цифры) где это уместно.',
headers: 'Можешь использовать подзаголовки для разделов.',
mixed: 'Структура по ситуации — где-то списки, где-то сплошной текст.',
};
const HUMOR_HINTS = {
none: 'Без юмора.',
dry: 'Сухой юмор, ирония, сарказм — изредка.',
moderate: 'Юмор умеренный, по ситуации.',
playful: 'Можно шутить, использовать мемные обороты.',
};
const GOAL_HINTS = {
educational: 'Цель — научить, объяснить. Подача от простого к сложному.',
news: 'Цель — рассказать о новостях. Факты + краткий комментарий.',
entertainment: 'Цель — развлечь. Можно подавать материал легко и игриво.',
expert: 'Цель — показать экспертизу. Глубина, нюансы, инсайты.',
sales: 'Цель — продать или подвести к действию. Польза → ценность → CTA.',
};
/**
* Собирает системный промпт для генерации поста.
* @param {object} channel - данные канала (channels + channel_style)
* @param {string} rubricContext - опционально: контекст рубрики
*/
function buildPostSystemPrompt(channel, rubricContext = '') {
const lang = channel.language === 'en' ? 'английском' : 'русском';
const style = channel.style || {};
const tone = style.tone === 'custom' && style.tone_custom
? style.tone_custom
: TONE_HINTS[style.tone] || TONE_HINTS.friendly;
const parts = [
`Ты автор Telegram-канала "${channel.name}". Пишешь на ${lang} языке.`,
'',
'КАНАЛ:',
`• Ниша: ${channel.niche || 'не указана'}`,
`• Аудитория: ${channel.audience || 'широкая'}`,
`• Цель канала: ${GOAL_HINTS[channel.goal] || GOAL_HINTS.educational}`,
channel.region ? `• Регион/контекст: ${channel.region}` : null,
'',
'СТИЛЬ:',
`• Тон: ${tone}`,
`• Обращение: ${style.formality === 'formal' ? 'на "вы", уважительно' : 'на "ты", по-простому'}`,
`• Юмор: ${HUMOR_HINTS[style.humor] || HUMOR_HINTS.moderate}`,
`• Длина: ${LENGTH_HINTS[style.post_length] || LENGTH_HINTS.medium}`,
`• Структура: ${STRUCTURE_HINTS[style.structure] || STRUCTURE_HINTS.mixed}`,
`• Эмодзи: ${EMOJI_HINTS[style.emoji_level] || EMOJI_HINTS.moderate}`,
`• Хэштеги: ${HASHTAG_HINTS[style.hashtags_mode] || HASHTAG_HINTS.end}`,
].filter(Boolean);
// Запрещённые слова и темы
if (style.banned_words?.length) {
parts.push('', `ЗАПРЕЩЕНО упоминать слова: ${style.banned_words.join(', ')}`);
}
if (style.banned_topics?.length) {
parts.push(`ЗАПРЕЩЕНО затрагивать темы: ${style.banned_topics.join(', ')}`);
}
// Рубрика
if (rubricContext) {
parts.push('', `КОНТЕКСТ РУБРИКИ: ${rubricContext}`);
}
// Главное — правила человечности
parts.push('', HUMANITY_RULES);
// Few-shot: примеры постов "как надо" — самый сильный сигнал
if (style.example_posts?.length) {
parts.push('', 'ПРИМЕРЫ ПОСТОВ В НУЖНОМ СТИЛЕ (копируй ритм, лексику, длину, манеру — но не содержание):');
style.example_posts.slice(0, 3).forEach((ex, i) => {
parts.push('', `--- Пример ${i + 1} ---`, ex.trim());
});
parts.push('--- конец примеров ---');
}
return parts.join('\n');
}
/**
* Промпт для self-critique — модель критикует свой текст и переписывает.
*/
function buildCritiquePrompt(originalText, channel) {
return `Ты получил пост для Telegram-канала "${channel.name}". Твоя задача:
1. Найди в нём признаки AI-генерации: канцелярит, штампы, "В современном мире...", избыточную симметрию, отсутствие конкретики, безличный тон.
2. Найди места, где текст звучит "слишком гладко" или "слишком правильно".
3. Перепиши пост так, чтобы он звучал как пост живого человека — но сохрани всю фактуру и смысл.
Сделай его:
- Более конкретным (имена, цифры, примеры вместо обобщений)
- С разной длиной предложений
- С личным голосом, если это уместно для канала
- Без штампов из AI-арсенала
Не пиши никаких комментариев — верни только переписанный пост.
ИСХОДНЫЙ ПОСТ:
${originalText}`;
}
/**
* Промпт для генерации идей постов (этап 1 цепочки).
*/
function buildTopicsPrompt(channel, count = 5) {
const style = channel.style || {};
return `Ты автор канала "${channel.name}" (ниша: ${channel.niche || 'общая'}, аудитория: ${channel.audience || 'широкая'}).
Цель: ${GOAL_HINTS[channel.goal] || GOAL_HINTS.educational}
Придумай ${count} конкретных, небанальных тем для постов. Не общие категории, а готовые угловые заходы.
Плохо: "Про нейросети"
Хорошо: "Как я заменил половину работы маркетолога одним промптом в Claude"
Плохо: "Тренды AI"
Хорошо: "В Сбере уволили 1500 разработчиков и наняли 200 — что происходит"
${style.banned_topics?.length ? `НЕ трогай темы: ${style.banned_topics.join(', ')}` : ''}
Верни JSON-массив строк, без пояснений: ["тема1", "тема2", ...]`;
}
/**
* Промпт для генерации статьи (для сайта zeropost.ru).
*/
function buildArticleSystemPrompt(channel, keywords = []) {
const lang = channel?.language === 'en' ? 'английском' : 'русском';
return `Ты — эксперт, пишешь SEO-статьи для блога на ${lang} языке.
Формат:
- Заголовок H1
- Лид-абзац (что в статье и почему важно)
- 3-5 разделов с H2
- Заключение
- 800-1500 слов
${keywords.length ? `Ключевые слова (вплети органично): ${keywords.join(', ')}` : ''}
${HUMANITY_RULES}
Не используй markdown-разметку для жирного/курсива — только заголовки # и ##.`;
}
module.exports = {
buildPostSystemPrompt,
buildCritiquePrompt,
buildTopicsPrompt,
buildArticleSystemPrompt,
HUMANITY_RULES,
};