From 8b73fbcc2caf91049edc3e015d0258975e927382 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 28 Dec 2025 10:13:08 +0500 Subject: [PATCH] . --- .../detaillevel_prompts_4fa4e244.plan.md | 94 +++++++++++++++++++ .../service/MarketingAnalysisService.java | 69 ++++++++++---- 2 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 .cursor/plans/detaillevel_prompts_4fa4e244.plan.md diff --git a/.cursor/plans/detaillevel_prompts_4fa4e244.plan.md b/.cursor/plans/detaillevel_prompts_4fa4e244.plan.md new file mode 100644 index 0000000..7835e37 --- /dev/null +++ b/.cursor/plans/detaillevel_prompts_4fa4e244.plan.md @@ -0,0 +1,94 @@ +--- +name: DetailLevel prompts +overview: Адаптировать генерацию промптов и выбор модели/лимитов по уровню DetailLevel для JSON-аналитики (get*Analysis) и текстовых секций (genSection*), сохранив текущий режим СТАНДАРТНО как baseline. +todos: + - id: inspect-json-methods + content: Пройти по getAudienceAnalysis/getCompetitorAnalysis/getSeasonalityAndMarket/getStrategyAndFunnel и определить точки для вставки detailLevel-ветвления + вынесения общих частей промпта + status: pending + - id: update-text-directives + content: Обновить appendDetailLevelDirectives/buildSystemPromptForSection под новые требования КРАТКО (150-250, bullet-only, soften placeholders) и ПОДРОБНО (1000-1500, академичность, 3 примера, смежные рынки, прогноз 3-5 лет, LaTeX формулы) + status: pending + - id: model-routing + content: "В genSection* и get*Analysis внедрить выбор модели по DetailLevel: КРАТКО→mini, ПОДРОБНО→text, СТАНДАРТНО→как сейчас" + status: pending + dependencies: + - update-text-directives + - inspect-json-methods + - id: max-tokens-detailed + content: Скорректировать maxTokensOverride для ПОДРОБНО в generateWithRetryForSection и добавить override в JSON get*Analysis вызовы (8000-12000, например 12000) + status: pending + dependencies: + - model-routing + - id: smoketest-3-levels + content: "Сделать короткий ручной smoketest: один запрос, три уровня, проверить объем/сегменты/плейсхолдеры/модель" + status: pending + dependencies: + - max-tokens-detailed +--- + +# План внедрения DetailLevel (КРАТКО/СТАНДАРТНО/ПОДРОБНО) + +## Цели + +- **КРАТКО**: экономим токены — краткие инструкции, меньше сегментов в JSON, текст 150–250 слов, только bullet points, модель всегда `miniModelName`. +- **СТАНДАРТНО**: **без изменений** (текущие промпты/модели/объемы принимаем baseline). +- **ПОДРОБНО**: максимальная глубина — больше сегментов в JSON + доп. поля (гипотезы/мини-кейсы), текст 1000–1500 слов, академические требования, больше формул LaTeX, модель всегда `textModelName`, увеличенный `maxTokens`. + +## Где менять код + +- Основная логика промптов и вызовов модели: [src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java](/Users/magzhanzhumabaev/Desktop/kt%20cloud/parser/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java) +- API вызовы и параметры `max_tokens`: [src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java](/Users/magzhanzhumabaev/Desktop/kt%20cloud/parser/src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java) +- Источник уровня: `MarketingAnalysisRequest.getDetailLevelEnum()` в [src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java](/Users/magzhanzhumabaev/Desktop/kt%20cloud/parser/src/main/java/kz/konturai/parser/dto/MarketingAnalysisRequest.java) + +## Изменения для текстовых секций (genSection*) + +- **Централизовать требования по уровню** в `MarketingAnalysisService.appendDetailLevelDirectives(...)`: +- **КРАТКО**: добавить явное требование **150–250 слов**, **только bullet points**, запрет вводных/«консалтинговых оборотов», и правило по плейсхолдерам: **вставлять `[[CHART_...]]/[[TABLE_...]] только если в JSON реально есть данные/раздел этого требует**. +- **ПОДРОБНО**: добавить жесткие требования: + - объем **1000–1500 слов**; + - «глубокий академический анализ»; + - **для каждого вывода — 3 конкретных примера реализации**; + - **смежные рынки** и **прогноз 3–5 лет**; + - больше расчетов в LaTeX: **CAC, LTV, ROI, Payback Period**. +- **Выбор модели по уровню** в каждом `genSection*` (там, где сейчас жестко задан `miniModelName` или mix): +- **КРАТКО → всегда `miniModelName`** +- **ПОДРОБНО → всегда `textModelName`** +- **СТАНДАРТНО → оставить текущую модельную логику как есть** (например, `genSectionSummary/product` сейчас на `miniModelName`, `market/audience/competitors/strategy` — на `textModelName`). +- Технически: добавить небольшой helper вроде `selectModelForTextSection(detailLevel, currentPrimaryModel, currentFallbackModel)` и использовать его при вызове `generateWithRetryForSection(...)`. + +## Изменения для JSON-методов (get*Analysis) + +Методы: `getAudienceAnalysis`, `getCompetitorAnalysis`, `getSeasonalityAndMarket`, `getStrategyAndFunnel`. + +- Добавить `DetailLevel detailLevel = request.getDetailLevelEnum()` и ветвление (switch/if): +- **КРАТКО**: + - сократить инструкции; + - заменить «минимум 4 сегмента» → **«2–3 ключевых сегмента»**; + - заменить требования к полям (motivation/triggers/barriers/contentStrategy и т.п.) с 3–4 предложений → **1 емкое предложение**; + - модель **всегда `miniModelName`**. +- **СТАНДАРТНО**: оставить текущий prompt builder без изменений. +- **ПОДРОБНО**: + - требовать **5–6 сегментов** (где применимо) и более глубокую детализацию; + - расширить JSON-схему (без ломания текущих полей) добавив: + - top-level: `hypotheses[]`, `miniCases[]` внутри соответствующей секции (например `audienceAnalysis.hypotheses`, `competitorAnalysis.miniCases`); + - per-item: например `segments[].hypothesis`, `segments[].miniCase`, `competitorsDetails[].miniCase`; + - модель **всегда `textModelName`**. +- **Вынести базовые части промптов** в общие методы/константы, чтобы не дублировать текст: +- базовый «Ты — ...», правила «только JSON», блок входных данных; +- отдельные функции для добавления level-specific требований (segment count / sentence counts / extra fields). + +## Max tokens для ПОДРОБНО + +- Сейчас в `generateWithRetryForSection(...)` уже есть `maxTokensOverride` для `DetailLevel.ПОДРОБНО`, но он равен `REPORT_BASE_MAX_TOKENS * 2`. +- Привести к требованию: **установить override в диапазоне 8000–12000** (например, **12000**) для ПОДРОБНО, чтобы ответы не обрезались и не упираться в потенциальные лимиты модели. +- Для JSON-методов, которые вызывают `openAIAnalyticsService.generateWithInstructionWithModel(...)` без override, добавить `maxTokensOverride` (минимум для ПОДРОБНО), чтобы подробный JSON не резался. + +## Проверка совместимости + +- Убедиться, что существующие поля, которые используются в `genSection*` (например `audienceAnalysis.ageGroups`, `competitorAnalysis.marketShareChart`) **не меняются**, а новые поля добавляются как опциональные. + +## Быстрый smoketest (локально) + +- Прогнать генерацию отчета для 3 режимов на одном и том же входе и сравнить: +- модель (`gpt-4o-mini` vs `gpt-4o`), +- длину ответа (word count приблизительно), \ No newline at end of file diff --git a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java index 91355f3..f07993f 100644 --- a/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java +++ b/src/main/java/kz/konturai/parser/service/MarketingAnalysisService.java @@ -43,6 +43,8 @@ public class MarketingAnalysisService { + "ГРАФИКИ (ОБЯЗАТЕЛЬНО): располагай теги графиков (например, [[CHART_...]]) непосредственно внутри или сразу после описания конкретных данных/цифр, к которым относится график.\n\n"; private static final int REPORT_BASE_MAX_TOKENS = 12000; + private static final int REPORT_DETAILED_MAX_TOKENS = 12000; + private static final int JSON_DETAILED_MAX_TOKENS = 12000; private DetailLevel resolveDetailLevelFromContext(Map contextJson) { if (contextJson == null) { @@ -62,12 +64,13 @@ public class MarketingAnalysisService { if (detailLevel != DetailLevel.ПОДРОБНО) { return null; } - // Требование пользователя + уточнение (hybrid): 500+ слов строго для ключевых - // разделов. - return "Твоя задача — провести глубокий академический анализ. Используй профессиональную терминологию (LTV, CAC, PESTEL, Porter's Five Forces). " - + "Каждый подраздел должен содержать не менее 500 слов и включать конкретные тактические рекомендации.\n" - + "Примечание: правило «не менее 500 слов» применяй строго для ключевых разделов IV (ЦА), V (Конкуренты), XI (Стратегия). " - + "Для остальных разделов избегай искусственного раздувания, но сохраняй академическую глубину и практическую направленность."; + return "РЕЖИМ ПОДРОБНО (SYSTEM): Проведи глубокий академический анализ с причинно-следственной логикой.\n" + + "- Объем: 1000–1500 слов (если раздел ключевой — ближе к верхней границе).\n" + + "- Для КАЖДОГО вывода приводи по 3 конкретных примера реализации.\n" + + "- Обязательно включи анализ смежных рынков и долгосрочные прогнозы на 3–5 лет.\n" + + "- Включай расчеты в LaTeX там, где уместно: CAC, LTV, ROI, Payback Period. Если не хватает чисел — делай обоснованные допущения и помечай как \"Экстраполяция\".\n" + + "- Используй фреймворки, когда релевантно: PESTEL и Porter's Five Forces.\n" + + "- Всегда опирайся на JSON-контекст, не выдумывай факты."; } private void appendDetailLevelDirectives(StringBuilder promptBuilder, DetailLevel detailLevel, String sectionId) { @@ -81,11 +84,14 @@ public class MarketingAnalysisService { if (detailLevel == DetailLevel.КРАТКО) { promptBuilder.append("РЕЖИМ КРАТКО (ОБЯЗАТЕЛЬНО):\n"); promptBuilder.append("- Игнорируй любые требования по «минимум N слов», если они есть выше.\n"); - promptBuilder.append("- Дай только самое важное в виде bullet points.\n"); - promptBuilder.append("- Максимум 10–14 буллетов; каждый буллет 1–2 строки.\n"); - promptBuilder.append("- Без длинных абзацев, без воды, без повторов.\n"); + promptBuilder.append("- Объем: 150–250 слов.\n"); promptBuilder.append( - "- Если выше требуется вставка [[CHART_...]]/[[TABLE_...]] — вставь эти плейсхолдеры в подходящие буллеты рядом с конкретными цифрами.\n"); + "- Формат: оставь обязательный заголовок раздела (если он требуется выше), далее используй ТОЛЬКО списки (bullet points).\n"); + promptBuilder.append( + "- Пиши максимально лаконично, используй только bullet points, избегай вводных слов и консалтинговых оборотов.\n"); + promptBuilder.append("- Максимум 10–14 буллетов; каждый буллет 1–2 строки.\n"); + promptBuilder.append( + "- Плейсхолдеры [[CHART_...]]/[[TABLE_...]]: вставляй ТОЛЬКО если в JSON реально есть данные для соответствующего графика/таблицы и раздел это обсуждает. Если данных нет — не вставляй тег и не упоминай его.\n"); return; } @@ -94,6 +100,11 @@ public class MarketingAnalysisService { promptBuilder.append("- Дай глубокий академический анализ с причинно-следственной логикой.\n"); promptBuilder .append("- Используй терминологию (LTV, CAC, PESTEL, Porter's Five Forces) там, где уместно.\n"); + promptBuilder.append("- Объем: 1000–1500 слов.\n"); + promptBuilder.append("- Для КАЖДОГО вывода приводи по 3 конкретных примера реализации.\n"); + promptBuilder.append("- Обязательно включи анализ смежных рынков и долгосрочные прогнозы на 3–5 лет.\n"); + promptBuilder.append( + "- Включай расчеты в LaTeX там, где уместно: CAC, LTV, ROI, Payback Period. Если не хватает цифр — делай обоснованные допущения и помечай блок как \"Экстраполяция\".\n"); if ("market".equalsIgnoreCase(sectionId) || "strategy".equalsIgnoreCase(sectionId)) { promptBuilder.append( "- ОБЯЗАТЕЛЬНО ВКЛЮЧИ ФРЕЙМВОРКИ: сделай отдельные подблоки PESTEL и «5 сил Портера» (не просто упоминание терминов).\n"); @@ -102,14 +113,40 @@ public class MarketingAnalysisService { "- ОБЯЗАТЕЛЬНО ВКЛЮЧИ «5 сил Портера» (минимум: Rivalry/Threat of New Entrants/Substitutes/Buyer Power/Supplier Power) применительно к этой нише в Казахстане.\n"); } promptBuilder.append("- После каждого крупного вывода добавляй конкретные тактические рекомендации.\n"); - if (isKeyVerboseSectionForHybridRule(sectionId)) { - promptBuilder.append("- ОБЪЕМ (КЛЮЧЕВОЙ РАЗДЕЛ): не менее 500 слов.\n"); - } else { - promptBuilder.append("- ОБЪЕМ: подробно, но без искусственного раздувания.\n"); - } } } + private String selectPrimaryModelForTextSection(DetailLevel detailLevel, String standardPrimaryModel) { + if (detailLevel == null) { + return standardPrimaryModel; + } + if (detailLevel == DetailLevel.КРАТКО) { + return miniModelName; + } + if (detailLevel == DetailLevel.ПОДРОБНО) { + return textModelName; + } + return standardPrimaryModel; + } + + private String selectFallbackModelForTextSection(DetailLevel detailLevel, String standardFallbackModel) { + if (detailLevel == null) { + return standardFallbackModel; + } + if (detailLevel == DetailLevel.КРАТКО || detailLevel == DetailLevel.ПОДРОБНО) { + return null; + } + return standardFallbackModel; + } + + private String selectModelForJsonAnalysis(DetailLevel detailLevel) { + if (detailLevel == DetailLevel.ПОДРОБНО) { + return textModelName; + } + // КРАТКО и СТАНДАРТНО: всегда mini + return miniModelName; + } + private Map buildResearchNotesForSection(Map contextJson, String sectionId) { if (contextJson == null) { return new HashMap<>(); @@ -1679,7 +1716,7 @@ public class MarketingAnalysisService { String last = null; DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson); String systemPrompt = buildSystemPromptForSection(detailLevel, sectionId); - Integer maxTokensOverride = detailLevel == DetailLevel.ПОДРОБНО ? REPORT_BASE_MAX_TOKENS * 2 : null; + Integer maxTokensOverride = detailLevel == DetailLevel.ПОДРОБНО ? REPORT_DETAILED_MAX_TOKENS : null; for (int i = 1; i <= maxAttempts; i++) { String modelToUse = primaryModel;