为什么要单独做一套模块?

传统 SEO 优化的是搜索引擎排名;GEO 优化的是 AI 大模型在回答用户问题时是否提及、如何评价你的品牌

正常用户与智能体的对话 VS GEO 监控:

维度 对话 GEO 监控
目标 准确回答用户问题 探测第三方模型的品牌曝光
模型 单一可控模型 通义、DeepSeek、豆包、混元、Kimi、智谱等多引擎
提示词 业务导向、可注入知识库 中性助手,避免诱导偏向
输出 自然语言回复 结构化指标(提及率、排名、态度、竞品共现)
后续动作 会话结束 诊断 → 内容草稿 → 发布向量库 → 基线对比

2. 整体架构

┌─────────────────────────────────────────────────────────────────┐
│                        管理端                          │
│  数据看板 │ 探测任务 │ 引擎管理 │ 品牌档案 │ 诊断报告 │ 内容草稿   │
└────────────────────────────┬────────────────────────────────────┘
                             │ REST /api/v1/geo/*
┌────────────────────────────▼────────────────────────────────────┐
│                     GeoEngineRunner(调度核心)                 │
│  加载任务 → 遍历品牌×问题×引擎 → 采样探测 → 分析聚合 → 落库       │
└──┬──────────┬──────────┬──────────┬──────────┬─────────────────┘
   │          │          │          │          │
   ▼          ▼          ▼          ▼          ▼
 probe     analyzer   metrics   diagnoser   alert
 (探测)    (分析)     (指标)    (诊断)      (飞书)
   │                              │
   ▼                              ▼
 provider_registry            content_generator
 + create_geo_llm             + publisher → knowledge_document (Chroma)

2.1 数据模型关系

一次完整的 GEO 业务链路围绕以下核心表展开:

  • geo_engines:探测引擎配置(厂商、model_code、API Key 环境变量名)
  • geo_tasks:探测任务(品牌词、竞品、问题模板、引擎列表、采样次数、告警配置、基线 Run)
  • geo_runs:每次执行记录(手动 / 定时触发)
  • geo_snapshots:单次探测快照(问题、回答、分析结果、提及指标)
  • geo_diagnoses:LLM 生成的诊断报告
  • geo_brand_profiles:品牌档案(定位、差异化、事实边界)
  • geo_content_drafts:优化内容草稿
  • geo_question_templates:场景问题库

3. 核心执行流程

3.1 流程总览

飞书 WebhookGeoDiagnoserMySQLGeoAnalyzer目标大模型probe_engineGeoEngineRunnerAPScheduler飞书 WebhookGeoDiagnoserMySQLGeoAnalyzer目标大模型probe_engineGeoEngineRunnerAPSchedulerloop[sample_count 次采样]loop[品牌 × 问题 × 引擎]run_due_geo_tasks()创建 GeoRun,标记 is_runningprobe_engine(engine, prompt)中性 system + 用户问题原始回答ProbeResultanalyze(brand, answer, competitors)结构化 JSONaggregate_samples() 多数投票写入 GeoSnapshot更新 Run 状态未提及/负面推荐告警schedule_diagnosis(run_id) 后台任务诊断摘要(可选)基线回归告警(可选)

3.2 调度入口

GEO 任务由 APScheduler 周期性触发:

# backend/app/core/scheduler/__init__.py
scheduler.add_job(_geo_tasks_job, 'interval', minutes=5, id='geo_tasks_runner')

async def _geo_tasks_job():
    from app.core.geo.engine import run_due_geo_tasks
    await run_due_geo_tasks()

run_due_geo_tasks() 会:

  1. 清理超过 60 分钟仍处于 running 的僵尸 Run
  2. 查询 status=active、未在运行、且 next_run_at <= now 的任务
  3. 逐个调用 GeoEngineRunner.run_task(db, task_id, trigger="schedule")

手动执行走 API POST /api/v1/geo/tasks/{task_id}/run,通过 asyncio.create_task 在后台跑同一套 run_task 逻辑,避免阻塞 HTTP 响应。

3.3 三层嵌套循环:品牌 × 问题 × 引擎

GeoEngineRunner.run_task 是整个模块的「心脏」。其核心逻辑可以概括为:

# backend/app/core/geo/engine.py(简化)
for brand in brands:
    for item in prompt_items:
        prompt = item["template"].replace("{keyword}", brand)
        for engine in engines:
            sample_analyses = []
            for sample_idx in range(sample_count):
                probe_result = await probe_engine(engine, prompt)
                analysis = await self.analyzer.analyze(brand, probe_result.answer, competitors)
                sample_analyses.append(analysis)

            aggregated = self.analyzer.aggregate_samples(sample_analyses)
            snapshot = GeoSnapshot(..., analysis=aggregated, ...)
            db.add(snapshot)
            await db.commit()  # 逐条提交,支持中途取消

几个设计要点:

  • 问题来源:任务可配置 question_source=inline(内联模板)或 library(场景问题库),后者支持按 recommend/compare/risk 等分类统计
  • 采样次数:默认 2 次,通过多数投票降低 LLM 随机性
  • 逐条 commit:每写入一条 snapshot 就提交,配合 cancel_requested 标志实现优雅停止
  • 引擎可用性检查:执行前过滤未配置 API Key 或已禁用的引擎

默认问题模板示例:

DEFAULT_PROMPT_TEMPLATES = [
    "{keyword}怎么样?",
    "{keyword}靠谱吗?",
    "推荐几个{keyword}相关的平台",
    "网上对{keyword}的评价如何",
]

4. 多引擎探测:统一 OpenAI 兼容层

4.1 厂商注册表

所有支持的厂商集中在 provider_registry.py,统一管理 base_url、环境变量名与官方文档链接:

PROVIDER_DEFAULTS = {
    "dashscope": {
        "name": "通义千问",
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "api_key_env": "geo_dashscope_api_key",
    },
    "deepseek": {
        "name": "DeepSeek",
        "base_url": "https://api.deepseek.com",
        "default_model": "deepseek-v4-flash",
    },
    "doubao": {
        "name": "豆包(火山方舟)",
        "base_url": "https://ark.cn-beijing.volces.com/api/v3",
    },
    # hunyuan, moonshot, zhipu ...
}

新增厂商的 SOP:

  1. 查厂商 OpenAI 兼容文档,确认 base_url 与 model 字段含义
  2. 在 PROVIDER_DEFAULTS 增加预设
  3. 配置 .env 中对应的 GEO_*_API_KEY
  4. 管理端「引擎管理」新增记录

4.2 探测 LLM

create_geo_llm 专门为 GEO 探测创建 LangChain 实例:

# backend/app/core/geo/llm.py
def create_geo_llm(engine: GeoEngine) -> ChatOpenAI:
    defaults = get_provider_defaults(engine.provider) or {}
    base_url = engine.base_url or defaults.get("base_url", "")
    api_key = resolve_api_key(settings, engine.api_key_env)

    kwargs = {
        "api_key": api_key,
        "base_url": base_url,
        "model": engine.model_code,
        "temperature": engine.temperature,
        "max_tokens": 2048,
        "timeout": settings.geo_probe_timeout_seconds,
    }
    if engine.extra_body:
        kwargs["extra_body"] = engine.extra_body  # 如混元搜索增强
    return ChatOpenAI(**kwargs)

分析器使用更便宜的模型(默认 qwen-turbo),通过 GEO_ANALYZER_PROVIDER / GEO_ANALYZER_MODEL 单独配置。

4.3 中性探测提示词

探测时必须避免「诱导模型推荐本品牌」,因此 probe.py 使用固定中性 system prompt:

NEUTRAL_SYSTEM_PROMPT = (
    "你是一个乐于助人的助手。请根据你的知识如实回答用户问题,"
    "可以列举多个选项并说明各自特点,不要刻意回避或偏向任何品牌。"
)

async def probe_engine(engine: GeoEngine, prompt: str) -> ProbeResult:
    llm = create_geo_llm(engine)
    start = time.perf_counter()
    response = await llm.ainvoke([
        SystemMessage(content=NEUTRAL_SYSTEM_PROMPT),
        HumanMessage(content=prompt),
    ])
    elapsed = int((time.perf_counter() - start) * 1000)
    return ProbeResult(prompt=prompt, answer=content, latency_ms=elapsed)

这是 GEO 测量可信度的关键:我们测的是模型在自然状态下的品牌认知,而不是 prompt 工程后的结果


5. LLM 结构化分析器

原始回答是非结构化文本,需要二次 LLM 调用提取指标。GeoAnalyzer 要求分析模型返回严格 JSON:

{
  "brand_mentioned": true,
  "mention_rank": 2,
  "recommendation": "recommend",
  "competitors_mentioned": ["竞品A"],
  "citation_urls": ["https://..."],
  "sentiment": {"category": "positive", "score": 0.6},
  "summary": "一句话摘要",
  "misunderstanding": false,
  "entity_clarity": "clear"
}

5.1 多次采样聚合

当 sample_count > 1 时,使用多数投票策略:

字段 聚合规则
brand_mentioned 超过半数采样为 true 则 true
mention_rank 取有排名采样的算术平均并四舍五入
recommendation 票数最多的态度
competitors_mentioned 各采样并集
misunderstanding 任一为 true 则 true

这种设计在成本与稳定性之间取得平衡:比单次探测更可靠,又比大量采样更经济。

5.2 容错

分析器对 JSON 解析失败、LLM 调用异常均有降级处理,返回默认的 not_mentioned 结构,避免单条失败拖垮整次 Run。


6. 指标聚合与数据看板

compute_run_metrics 将一批 snapshot 聚合为 Run 级指标,供诊断、看板、基线对比复用:

# backend/app/core/geo/metrics.py 输出结构
{
    "total": 24,
    "mentioned": 14,
    "mention_rate": 0.5833,
    "avg_mention_rank": 2.1,
    "by_engine": {"1": {"total": 8, "mentioned": 5, "mention_rate": 0.625}},
    "by_category": {"recommend": {"total": 6, "mentioned": 2, "mention_rate": 0.333}},
    "by_recommendation": {"recommend": 8, "neutral": 4, "warn": 2, "not_mentioned": 10},
    "competitor_sov": {"竞品A": 12, "竞品B": 7},
    "citation_domains": {"zhihu.com": 5},
    "misunderstanding_count": 1,
}

看板 API GET /api/v1/geo/dashboard 读取最近 Run 的 snapshot,计算趋势与引擎对比;若任务设置了 baseline_run_id,还会输出优化前后 delta。


7. 优化闭环:诊断 → 内容 → 发布 → 基线对比

GEO 模块不只是需要「监控」,也需要完整的优化闭环。

7.1 自动诊断

探测成功后,schedule_diagnosis(run_id) 通过 asyncio.create_task 在后台启动诊断:

# backend/app/core/geo/diagnoser.py
def schedule_diagnosis(run_id: int):
    asyncio.create_task(run_diagnosis_background(run_id))

GeoDiagnoser 将聚合指标、品牌档案、样本摘要拼成 prompt,让 LLM 输出:

  • summary:整体可见度总结
  • root_causes:根因列表(实体不清、信源缺失、竞品主导等)
  • recommendations:优化建议(FAQ、对比页、证据文档等)
  • weaknesses:薄弱引擎 / 场景 / 竞品共现
  • misunderstandings:品牌被误解的具体案例

诊断与探测分析使用同一套便宜模型,控制成本。

7.2 内容草稿生成

GeoContentGenerator 根据诊断建议 + 品牌档案(缺的话再根据优化补充一些),生成多篇 Markdown/Html 草稿(FAQ、对比页、教程等),存入 geo_content_drafts 表等待人工审阅。

7.3 发布到企业文档向量库/官网/各大第三方平台(可以对接官网,如果有 CMS 管理后台那更好;生成各种文章)

审阅通过后,GeoPublisher.publish_draft 将内容写入企业文档树:

GEO/
 └── {任务名}/
      ├── FAQ:xxx 怎么样?
      └── 对比:xxx vs 竞品

发布到企业文档知识库:发布时同步调用 sync_document_to_vector,写入 Chroma knowledge_document 集合。这意味着 GEO 优化内容最终进入助手可检索的知识库,形成「探测 → 补缺 → 再探测」的闭环。

7.4 基线对比

可将优化前的 Run 设为基线(PATCH /api/v1/geo/tasks/{id}/baseline),发布内容后重新探测,通过 compute_run_metrics 对比提及率变化。若下降超过 regression_threshold(默认 0.1),触发飞书回归告

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐