"""智能洞察生成 - 调用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 = """你是一位专业的内容营销数据分析师,擅长GEO(Generative 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