geo/backend/app/services/analytics/insights.py

313 lines
12 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.

"""智能洞察生成 - 调用LLM分析数据"""
import json
import logging
import uuid
from datetime import datetime, timedelta
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.analytics import PublishRecord, ContentMetrics, OptimizationInsight
from app.services.llm import LLMFactory
logger = logging.getLogger(__name__)
_INSIGHT_SYSTEM_PROMPT = """你是一位专业的内容营销数据分析师擅长GEOGenerative Engine Optimization领域。
请根据提供的内容数据分析报告,生成结构化的洞察与优化建议。
输出格式必须是合法JSON数组每个洞察对象包含以下字段
- insight_type: "trend" | "anomaly" | "opportunity" | "suggestion"
- title: 简短标题不超过50字
- description: 详细说明100-200字
- recommendation: 可执行的具体建议50-150字
- severity: "info" | "warning" | "success"
输出示例:
[
{
"insight_type": "trend",
"title": "AI引用量持续增长",
"description": "过去30天内容被AI模型引用次数增长了35%...",
"recommendation": "继续保持结构化写作风格增加Q&A格式内容...",
"severity": "success"
}
]
仅输出JSON数组不要包含其他文字。"""
class InsightGenerator:
async def generate_insights(self, organization_id: str, session: AsyncSession) -> list[dict]:
"""
分析近30天数据生成智能洞察
- 对比各平台表现
- 识别内容表现模式(什么类型内容互动率高)
- 发现AI引用趋势
- 给出可执行建议
"""
# 1. 汇总近30天数据
summary = await self._build_data_summary(organization_id, session)
if not summary["records"]:
logger.info("组织 %s 没有发布记录,跳过洞察生成", organization_id)
return []
# 2. 构造分析prompt
user_prompt = self._build_analysis_prompt(summary)
# 3. 调用LLM生成洞察
llm = LLMFactory.get_default()
try:
response = await llm.chat(
messages=[
{"role": "system", "content": _INSIGHT_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
]
)
raw_content = response.content.strip()
# 尝试提取JSON数组
if "```" in raw_content:
raw_content = raw_content.split("```")[1]
if raw_content.startswith("json"):
raw_content = raw_content[4:]
insights_data: list[dict] = json.loads(raw_content)
except Exception as e:
logger.error("LLM生成洞察失败: %s", e)
insights_data = self._fallback_insights(summary)
# 4. 解析并存储到OptimizationInsight表
saved = []
for item in insights_data:
insight = OptimizationInsight(
id=str(uuid.uuid4()),
organization_id=organization_id,
insight_type=item.get("insight_type", "suggestion"),
title=item.get("title", "")[:200],
description=item.get("description", ""),
recommendation=item.get("recommendation", ""),
severity=item.get("severity", "info"),
)
session.add(insight)
saved.append({
"id": insight.id,
"insight_type": insight.insight_type,
"title": insight.title,
"description": insight.description,
"recommendation": insight.recommendation,
"severity": insight.severity,
"applied": insight.applied,
"created_at": insight.created_at.isoformat() if isinstance(insight.created_at, datetime) else None,
})
await session.commit()
logger.info("组织 %s 成功生成 %d 条洞察", organization_id, len(saved))
return saved
async def analyze_single_content(self, publish_record_id: str, session: AsyncSession) -> dict:
"""分析单篇内容表现"""
pr_stmt = select(PublishRecord).where(PublishRecord.id == publish_record_id)
pr_result = await session.execute(pr_stmt)
record = pr_result.scalar_one_or_none()
if not record:
return {"error": "发布记录不存在"}
metrics_stmt = (
select(ContentMetrics)
.where(ContentMetrics.publish_record_id == publish_record_id)
.order_by(ContentMetrics.recorded_at.desc())
.limit(1)
)
metrics_result = await session.execute(metrics_stmt)
latest = metrics_result.scalar_one_or_none()
if not latest:
return {"error": "暂无效果数据"}
prompt = f"""请分析以下单篇内容的表现数据,给出优化建议:
内容标题:{record.content_title}
发布平台:{record.platform}
发布时间:{record.published_at}
最新效果数据:
- 浏览量:{latest.views}
- 点赞:{latest.likes},评论:{latest.comments},分享:{latest.shares}
- 收藏:{latest.bookmarks}
- AI引用次数{latest.ai_citation_count}
- 搜索曝光:{latest.search_impressions},搜索点击:{latest.search_clicks}
- 平均阅读时长:{latest.avg_read_duration}
- 完读率:{latest.read_completion_rate:.1%}
请输出JSON{{"analysis": "...", "strengths": ["...", "..."], "improvements": ["...", "..."], "geo_suggestions": ["..."]}}"""
llm = LLMFactory.get_default()
try:
response = await llm.chat(
messages=[
{"role": "system", "content": "你是GEO内容优化专家。请输出合法JSON不含其他文字。"},
{"role": "user", "content": prompt},
]
)
raw = response.content.strip()
if "```" in raw:
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
return json.loads(raw)
except Exception as e:
logger.error("单内容分析失败: %s", e)
return {
"analysis": "数据分析服务暂时不可用,请稍后重试。",
"strengths": [],
"improvements": [],
"geo_suggestions": [],
}
# ------------------------------------------------------------------ #
# 内部辅助方法
# ------------------------------------------------------------------ #
async def _build_data_summary(self, organization_id: str, session: AsyncSession) -> dict:
"""汇总近30天发布与指标数据"""
since = datetime.utcnow() - timedelta(days=30)
pr_stmt = select(PublishRecord).where(
PublishRecord.organization_id == organization_id,
PublishRecord.created_at >= since,
)
pr_result = await session.execute(pr_stmt)
records = pr_result.scalars().all()
if not records:
return {"records": [], "platform_stats": {}, "totals": {}}
record_ids = [r.id for r in records]
# 每条记录最新快照
subq = (
select(
ContentMetrics.publish_record_id,
func.max(ContentMetrics.recorded_at).label("latest"),
)
.where(ContentMetrics.publish_record_id.in_(record_ids))
.group_by(ContentMetrics.publish_record_id)
.subquery()
)
metrics_stmt = select(ContentMetrics).join(
subq,
(ContentMetrics.publish_record_id == subq.c.publish_record_id)
& (ContentMetrics.recorded_at == subq.c.latest),
)
metrics_result = await session.execute(metrics_stmt)
latest_map: dict[str, ContentMetrics] = {
m.publish_record_id: m for m in metrics_result.scalars().all()
}
# 平台维度统计
platform_stats: dict[str, dict] = {}
for r in records:
m = latest_map.get(r.id)
p = r.platform
if p not in platform_stats:
platform_stats[p] = {
"count": 0, "views": 0, "interactions": 0, "ai_citations": 0
}
platform_stats[p]["count"] += 1
if m:
platform_stats[p]["views"] += m.views
platform_stats[p]["interactions"] += m.likes + m.comments + m.shares
platform_stats[p]["ai_citations"] += m.ai_citation_count
totals = {
"total_records": len(records),
"total_views": sum(m.views for m in latest_map.values()),
"total_interactions": sum(
m.likes + m.comments + m.shares for m in latest_map.values()
),
"total_ai_citations": sum(m.ai_citation_count for m in latest_map.values()),
"avg_read_completion": (
sum(m.read_completion_rate for m in latest_map.values()) / len(latest_map)
if latest_map else 0.0
),
}
record_details = []
for r in records:
m = latest_map.get(r.id)
record_details.append({
"title": r.content_title,
"platform": r.platform,
"views": m.views if m else 0,
"interactions": (m.likes + m.comments + m.shares) if m else 0,
"ai_citations": m.ai_citation_count if m else 0,
"read_completion_rate": m.read_completion_rate if m else 0.0,
})
return {
"records": record_details,
"platform_stats": platform_stats,
"totals": totals,
}
def _build_analysis_prompt(self, summary: dict) -> str:
totals = summary["totals"]
platform_stats = summary["platform_stats"]
records = summary["records"]
top_by_views = sorted(records, key=lambda x: x["views"], reverse=True)[:5]
top_by_citations = sorted(records, key=lambda x: x["ai_citations"], reverse=True)[:3]
prompt = f"""以下是近30天内容运营数据摘要请生成3-5条洞察与建议
【总览】
- 发布内容数:{totals['total_records']}
- 总浏览量:{totals['total_views']}
- 总互动量(点赞+评论+分享):{totals['total_interactions']}
- 总AI引用次数{totals['total_ai_citations']}
- 平均完读率:{totals['avg_read_completion']:.1%}
【各平台表现】
"""
for platform, stats in platform_stats.items():
prompt += (
f"- {platform}{stats['count']}篇,"
f"浏览{stats['views']},互动{stats['interactions']}"
f"AI引用{stats['ai_citations']}\n"
)
prompt += "\n【浏览量Top5内容】\n"
for item in top_by_views:
prompt += f"- 《{item['title']}》({item['platform']}) 浏览:{item['views']} 互动:{item['interactions']}\n"
prompt += "\n【AI引用量Top3内容】\n"
for item in top_by_citations:
prompt += f"- 《{item['title']}》({item['platform']}) AI引用:{item['ai_citations']}\n"
return prompt
def _fallback_insights(self, summary: dict) -> list[dict]:
"""LLM失败时的降级洞察"""
totals = summary.get("totals", {})
insights = []
if totals.get("total_ai_citations", 0) > 0:
insights.append({
"insight_type": "trend",
"title": "内容已获得AI引用",
"description": f"近30天共获得 {totals['total_ai_citations']} 次AI引用说明内容质量已获得生成式引擎认可。",
"recommendation": "继续保持结构化、权威性的写作风格增加数据支撑和专业术语密度提升AI引用率。",
"severity": "success",
})
if totals.get("total_records", 0) > 0:
avg_views = totals.get("total_views", 0) / totals["total_records"]
if avg_views < 100:
insights.append({
"insight_type": "suggestion",
"title": "内容分发有待加强",
"description": f"当前平均浏览量为 {avg_views:.0f},建议优化内容分发渠道和发布时机。",
"recommendation": "尝试在多平台同步发布,配合社群推广,并选择目标平台的流量高峰时段发布。",
"severity": "warning",
})
return insights