geo/backend/app/services/strategy/geo_plan_generator.py

571 lines
21 KiB
Python
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.

from __future__ import annotations
import asyncio
import json
import logging
from dataclasses import dataclass, field
from typing import Any
from app.config import settings
from app.services.scoring.scoring_service import ScoringResultV2
from app.utils.json_extractor import extract_json
logger = logging.getLogger(__name__)
@dataclass
class GeoPlanActionItem:
action_type: str
title: str
description: str
reason: str
priority: str
target_keyword: str | None = None
target_platform: str | None = None
content_style: str | None = None
estimated_impact: str | None = None
difficulty: str = "medium"
execution_params: dict[str, Any] | None = None
@dataclass
class GeoPlanData:
title: str
estimated_weeks: int
actions: list[GeoPlanActionItem] = field(default_factory=list)
weekly_plan: list[dict[str, Any]] = field(default_factory=list)
def _get_weakest_dimensions(
mention_rate_pct: float,
rank_pct: float,
sentiment_pct: float,
citation_pct: float,
competitive_pct: float,
) -> list[tuple[str, float]]:
dimensions = [
("提及率", mention_rate_pct),
("推荐排名", rank_pct),
("情感倾向", sentiment_pct),
("引用质量", citation_pct),
("竞品对比", competitive_pct),
]
return sorted(dimensions, key=lambda x: x[1])
def _generate_rule_based_plan(
brand_name: str,
overall_score: float,
target_score: int,
mention_rate_pct: float,
rank_pct: float,
sentiment_pct: float,
citation_pct: float,
competitive_pct: float,
total_queries: int,
platform_scores: dict[str, float],
competitor_data: dict[str, Any],
) -> GeoPlanData:
actions: list[GeoPlanActionItem] = []
score_gap = target_score - overall_score
estimated_weeks = min(12, max(4, int(score_gap / 5) + 4))
if mention_rate_pct < 50:
actions.append(GeoPlanActionItem(
action_type="content_creation",
title=f"提升{brand_name}在AI平台的提及率",
description=(
f"当前提及率仅{mention_rate_pct:.0f}%品牌在AI回答中被提及的频率较低。"
f"需要创建高质量内容提高品牌在AI搜索结果中的出现频率。"
),
reason=f"提及率得分率{mention_rate_pct:.0f}%低于50%阈值,是最需要优先改善的维度",
priority="high",
target_keyword=f"{brand_name}+行业关键词",
target_platform="知乎",
content_style="专业严谨",
estimated_impact="预计可将提及率提升15-25个百分点",
difficulty="medium",
execution_params={
"keyword": f"{brand_name} 行业解决方案",
"platform": "知乎",
"style": "专业严谨",
"word_count": 2000,
},
))
if citation_pct < 40:
actions.append(GeoPlanActionItem(
action_type="content_creation",
title=f"提升{brand_name}引用内容质量",
description=(
f"当前引用质量得分率仅{citation_pct:.0f}%AI对品牌的引用多为浅层提及。"
f"需要创建深度评测和对比内容,增加被深度引用的概率。"
),
reason=f"引用质量得分率{citation_pct:.0f}%低于40%阈值,引用缺乏深度正面描述",
priority="high",
target_keyword=f"{brand_name}评测/对比",
target_platform="通用",
content_style="专业严谨",
estimated_impact="预计可将引用质量得分率提升15-25个百分点",
difficulty="medium",
execution_params={
"keyword": f"{brand_name} 深度评测对比",
"platform": "通用",
"style": "专业严谨",
"word_count": 3000,
},
))
if rank_pct < 40:
actions.append(GeoPlanActionItem(
action_type="content_optimization",
title=f"提升{brand_name}在AI推荐中的排名",
description=(
f"当前推荐排名得分率仅{rank_pct:.0f}%品牌在AI推荐列表中排名靠后。"
f"需要优化现有内容,增加品牌在推荐场景中的出现概率。"
),
reason=f"推荐排名得分率{rank_pct:.0f}%低于40%阈值,排名靠后用户看到概率低",
priority="high",
target_keyword=f"最佳{brand_name}推荐",
target_platform="通用",
content_style="专业严谨",
estimated_impact="预计可将推荐排名提升2-3位",
difficulty="medium",
execution_params={
"keyword": f"最佳{brand_name}推荐",
"platform": "通用",
"style": "专业严谨",
"word_count": 2000,
},
))
if competitive_pct < 40:
ahead_competitors = []
if competitor_data:
brand_mentions = competitor_data.get("brand_mentions", 0)
for name, count in competitor_data.get("competitor_mentions", {}).items():
if count > brand_mentions:
ahead_competitors.append(name)
ahead_str = "".join(ahead_competitors[:3]) if ahead_competitors else "竞品"
actions.append(GeoPlanActionItem(
action_type="content_creation",
title=f"缩小与{ahead_str}的差距",
description=(
f"当前竞品对比得分率仅{competitive_pct:.0f}%"
f"品牌在AI引用中落后于主要竞品。需要创建对比内容突出品牌优势。"
),
reason=f"竞品对比得分率{competitive_pct:.0f}%低于40%阈值,品牌落后于主要竞品",
priority="high",
target_keyword=f"{brand_name} vs {ahead_competitors[0] if ahead_competitors else '竞品'}",
target_platform="知乎",
content_style="专业严谨",
estimated_impact="预计3-6个月内可将竞品对比得分率提升15-25个百分点",
difficulty="hard",
execution_params={
"keyword": f"{brand_name} vs {ahead_competitors[0] if ahead_competitors else '竞品'} 对比评测",
"platform": "知乎",
"style": "专业严谨",
"word_count": 2500,
},
))
if sentiment_pct < 40:
actions.append(GeoPlanActionItem(
action_type="content_optimization",
title=f"改善AI平台对{brand_name}的情感倾向",
description=(
f"当前情感倾向得分率仅{sentiment_pct:.0f}%AI在引用品牌时倾向使用负面或中性表述。"
f"需要优化内容以增加正面引用比例。"
),
reason=f"情感倾向得分率{sentiment_pct:.0f}%低于40%阈值,负面引用影响品牌形象",
priority="medium",
target_keyword=f"{brand_name}优势/正面评价",
target_platform="通用",
content_style="轻松活泼",
estimated_impact="减少负面引用比例10-20个百分点",
difficulty="medium",
execution_params={
"keyword": f"{brand_name} 优势 正面评价",
"platform": "通用",
"style": "轻松活泼",
"word_count": 1500,
},
))
if total_queries < 10:
suggested_queries = [
f"{brand_name}怎么样",
f"{brand_name}推荐",
f"最佳{brand_name}",
f"{brand_name}评测",
f"{brand_name}对比",
]
actions.append(GeoPlanActionItem(
action_type="query_expansion",
title="扩展查询词覆盖范围",
description=(
f"当前仅有{total_queries}个查询词,覆盖范围不足,"
f"无法全面反映品牌在AI搜索中的表现。"
),
reason=f"查询词数量仅{total_queries}低于10个阈值分析结果不够全面",
priority="high" if total_queries < 3 else "medium",
target_keyword=None,
target_platform=None,
content_style=None,
estimated_impact="更多查询词可提升评分准确度,发现更多优化机会",
difficulty="easy",
execution_params={
"suggested_queries": suggested_queries,
},
))
schema_score = 0
if schema_score == 0:
actions.append(GeoPlanActionItem(
action_type="schema_optimization",
title="添加FAQ结构化数据",
description=(
"当前网站缺少结构化数据(Schema)AI搜索引擎无法有效提取品牌信息。"
"添加FAQ Schema可以显著提升品牌在AI回答中的引用概率。"
),
reason="网站Schema标记缺失AI搜索引擎无法高效提取品牌关键信息",
priority="medium",
target_keyword=None,
target_platform=None,
content_style=None,
estimated_impact="添加Schema后预计可提升引用率10-15个百分点",
difficulty="easy",
execution_params={
"optimization_type": "add_faq_schema",
},
))
weak_platforms = sorted(
platform_scores.items(),
key=lambda x: x[1],
)[:3]
weak_platform_names = [p[0] for p in weak_platforms if p[1] < 40]
if weak_platform_names:
actions.append(GeoPlanActionItem(
action_type="platform_targeting",
title=f"重点优化{''.join(weak_platform_names)}平台表现",
description=(
f"在这些平台上品牌评分低于40分AI引用率极低。"
f"需要针对性优化各平台的内容策略。"
),
reason=f"平台{''.join(weak_platform_names)}评分低于40分AI引用率极低",
priority="high",
target_keyword=None,
target_platform=weak_platform_names[0],
content_style=None,
estimated_impact="预计可将弱平台评分提升20-30分",
difficulty="hard",
execution_params={
"target_platforms": weak_platform_names,
},
))
priority_order = {"high": 0, "medium": 1, "low": 2}
actions.sort(key=lambda a: priority_order.get(a.priority, 1))
actions = actions[:8]
weekly_plan = _generate_weekly_plan(actions, estimated_weeks)
title = f"{brand_name} GEO优化方案 - 从{overall_score:.0f}分提升至{target_score}"
return GeoPlanData(
title=title,
estimated_weeks=estimated_weeks,
actions=actions,
weekly_plan=weekly_plan,
)
def _generate_weekly_plan(
actions: list[GeoPlanActionItem],
estimated_weeks: int,
) -> list[dict[str, Any]]:
weekly_plan: list[dict[str, Any]] = []
high_actions = [i for i, a in enumerate(actions) if a.priority == "high"]
medium_actions = [i for i, a in enumerate(actions) if a.priority == "medium"]
low_actions = [i for i, a in enumerate(actions) if a.priority == "low"]
weeks_per_high = max(1, (estimated_weeks // 2) // max(len(high_actions), 1)) if high_actions else 0
week_idx = 0
for i, action_idx in enumerate(high_actions):
week_num = week_idx + 1
impact_str = actions[action_idx].estimated_impact or ""
try:
num_part = impact_str.split("-")[-1].replace("个百分点", "").strip()
expected_val = max(3, int(num_part)) if num_part.isdigit() else 5
except (ValueError, IndexError):
expected_val = 5
expected = f"+{expected_val}"
weekly_plan.append({
"week": week_num,
"action_indices": [action_idx],
"expected_score_change": expected,
})
week_idx += weeks_per_high
remaining_weeks = estimated_weeks - week_idx
medium_per_week = max(1, len(medium_actions) // max(remaining_weeks, 1)) if medium_actions and remaining_weeks > 0 else len(medium_actions)
batch: list[int] = []
for i, action_idx in enumerate(medium_actions):
batch.append(action_idx)
if len(batch) >= medium_per_week or i == len(medium_actions) - 1:
week_idx += 1
weekly_plan.append({
"week": week_idx,
"action_indices": batch[:],
"expected_score_change": "+3",
})
batch = []
for action_idx in low_actions:
week_idx += 1
weekly_plan.append({
"week": week_idx,
"action_indices": [action_idx],
"expected_score_change": "+2",
})
return weekly_plan
GEO_PLAN_PROMPT = """你是一个GEO生成式引擎优化策略专家。基于以下品牌诊断数据制定一个8周GEO优化方案。
品牌: {brand_name}
当前评分: {overall_score}/100
目标评分: {target_score}/100
评分维度:
- 提及率: {mention_rate_percentage}%
- 推荐排名: {rank_percentage}%
- 情感倾向: {sentiment_percentage}%
- 引用质量: {citation_percentage}%
- 竞品对比: {competitive_percentage}%
竞品数据: {competitor_data}
平台评分: {platform_scores}
请返回JSON格式:
{{
"title": "方案标题",
"estimated_weeks": 8,
"actions": [
{{
"action_type": "content_creation",
"title": "行动标题",
"description": "详细描述",
"reason": "基于诊断数据的原因",
"priority": "high",
"target_keyword": "推荐关键词",
"target_platform": "推荐平台",
"content_style": "推荐风格",
"estimated_impact": "预期效果",
"difficulty": "medium",
"execution_params": {{
"keyword": "关键词",
"platform": "平台",
"style": "风格",
"word_count": 2000
}}
}}
],
"weekly_plan": [
{{
"week": 1,
"action_indices": [0, 1],
"expected_score_change": "+5"
}}
]
}}
要求:
1. 行动项必须基于诊断数据,优先解决最弱维度
2. 每个行动项必须有 execution_params可直接传给内容生成API
3. 行动项按优先级排序
4. 生成5-8个行动项
5. 周计划要合理分配任务"""
async def _generate_llm_plan(
brand_name: str,
overall_score: float,
target_score: int,
mention_rate_pct: float,
rank_pct: float,
sentiment_pct: float,
citation_pct: float,
competitive_pct: float,
total_queries: int,
platform_scores: dict[str, float],
competitor_data: dict[str, Any],
) -> GeoPlanData:
if not settings.ENABLE_LLM or not settings.DEEPSEEK_API_KEY:
logger.info("LLM未启用或API Key未配置使用规则生成方案")
return _generate_rule_based_plan(
brand_name=brand_name,
overall_score=overall_score,
target_score=target_score,
mention_rate_pct=mention_rate_pct,
rank_pct=rank_pct,
sentiment_pct=sentiment_pct,
citation_pct=citation_pct,
competitive_pct=competitive_pct,
total_queries=total_queries,
platform_scores=platform_scores,
competitor_data=competitor_data,
)
try:
prompt = GEO_PLAN_PROMPT.format(
brand_name=brand_name,
overall_score=round(overall_score, 1),
target_score=target_score,
mention_rate_percentage=round(mention_rate_pct, 1),
rank_percentage=round(rank_pct, 1),
sentiment_percentage=round(sentiment_pct, 1),
citation_percentage=round(citation_pct, 1),
competitive_percentage=round(competitive_pct, 1),
competitor_data=json.dumps(competitor_data, ensure_ascii=False, indent=2),
platform_scores=json.dumps(platform_scores, ensure_ascii=False, indent=2),
)
from openai import OpenAI
client = OpenAI(
api_key=settings.DEEPSEEK_API_KEY,
base_url="https://api.deepseek.com",
)
response = await asyncio.to_thread(
client.chat.completions.create,
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
max_tokens=3000,
)
content = response.choices[0].message.content
if not content:
raise ValueError("LLM返回空响应")
json_str = extract_json(content)
result = json.loads(json_str)
valid_action_types = {
"content_creation", "content_optimization",
"query_expansion", "schema_optimization", "platform_targeting",
}
valid_priorities = {"high", "medium", "low"}
valid_difficulties = {"easy", "medium", "hard"}
actions: list[GeoPlanActionItem] = []
for item in result.get("actions", []):
action_type = item.get("action_type", "content_creation")
if action_type not in valid_action_types:
action_type = "content_creation"
priority = item.get("priority", "medium")
if priority not in valid_priorities:
priority = "medium"
difficulty = item.get("difficulty", "medium")
if difficulty not in valid_difficulties:
difficulty = "medium"
actions.append(GeoPlanActionItem(
action_type=action_type,
title=item.get("title", "优化行动"),
description=item.get("description", ""),
reason=item.get("reason", ""),
priority=priority,
target_keyword=item.get("target_keyword"),
target_platform=item.get("target_platform"),
content_style=item.get("content_style"),
estimated_impact=item.get("estimated_impact"),
difficulty=difficulty,
execution_params=item.get("execution_params"),
))
if not actions:
logger.warning("LLM未返回有效行动项回退到规则生成")
return _generate_rule_based_plan(
brand_name=brand_name,
overall_score=overall_score,
target_score=target_score,
mention_rate_pct=mention_rate_pct,
rank_pct=rank_pct,
sentiment_pct=sentiment_pct,
citation_pct=citation_pct,
competitive_pct=competitive_pct,
total_queries=total_queries,
platform_scores=platform_scores,
competitor_data=competitor_data,
)
weekly_plan = result.get("weekly_plan", [])
return GeoPlanData(
title=result.get("title", f"{brand_name} GEO优化方案"),
estimated_weeks=result.get("estimated_weeks", 8),
actions=actions[:8],
weekly_plan=weekly_plan,
)
except Exception as e:
logger.error(f"LLM生成方案失败: {e},回退到规则生成")
return _generate_rule_based_plan(
brand_name=brand_name,
overall_score=overall_score,
target_score=target_score,
mention_rate_pct=mention_rate_pct,
rank_pct=rank_pct,
sentiment_pct=sentiment_pct,
citation_pct=citation_pct,
competitive_pct=competitive_pct,
total_queries=total_queries,
platform_scores=platform_scores,
competitor_data=competitor_data,
)
async def generate_geo_plan(
brand_name: str,
scoring_result: ScoringResultV2,
target_score: int,
total_queries: int = 0,
platform_scores: dict[str, float] | None = None,
competitor_data: dict[str, Any] | None = None,
) -> GeoPlanData:
if settings.ENABLE_LLM and settings.DEEPSEEK_API_KEY:
return await _generate_llm_plan(
brand_name=brand_name,
overall_score=scoring_result.overall_score,
target_score=target_score,
mention_rate_pct=scoring_result.mention_rate.percentage,
rank_pct=scoring_result.recommendation_rank.percentage,
sentiment_pct=scoring_result.sentiment_score.percentage,
citation_pct=scoring_result.citation_quality.percentage,
competitive_pct=scoring_result.competitive_position.percentage,
total_queries=total_queries,
platform_scores=platform_scores or {},
competitor_data=competitor_data or {},
)
return _generate_rule_based_plan(
brand_name=brand_name,
overall_score=scoring_result.overall_score,
target_score=target_score,
mention_rate_pct=scoring_result.mention_rate.percentage,
rank_pct=scoring_result.recommendation_rank.percentage,
sentiment_pct=scoring_result.sentiment_score.percentage,
citation_pct=scoring_result.citation_quality.percentage,
competitive_pct=scoring_result.competitive_position.percentage,
total_queries=total_queries,
platform_scores=platform_scores or {},
competitor_data=competitor_data or {},
)