实现 GEO 监控:从多引擎探测到优化闭环

听过搞过 SEO(Search Engine Optimization,搜索引擎优化),那么来看看怎么搞 GEO(Generative Engine Optimization,生成式引擎优化)。前者是浏览器搜索引擎等的核心,后者是现在流行的 AI 搜索引擎/对话大模型的核心。并且,GEO 一般建立在 SEO 之上的,简单理解就是 SEO 是地基,地基打好,上层建筑才能做好。
在我看来,S 跟 G 差别在于从一个小零件 到一个实体 的区别:例如对于一辆车,seo 就是提取其中的关键零件(发动机、轮胎、方向盘、变速箱等等),这些做好了,车的性能就好,大家就更容易用到,去使用;而 geo 则是怎么塑造这个车,关心的是这个车有什么功能,带来什么便利,别人怎么使用,使用方法是什么,是否值得使用,也就是建立一个完整的形象个体并向你表达。
再简单点,你问小程序怎么做,seo 直接上关键字(关键零件:小程序、怎么做),给你一些文章(有这些零件的车,用不用在于你);geo 是找到这些文章(车:做的好才能优先想到你),总结一下(结合大部分车),从头到尾跟你讲(跟你说这个车是啥,怎么才能生产这个车,包括其中的细节)。
以上仅代表个人理解,有问题有错误请各位大佬评论区指正!下面进入正题,基于品牌实现 GEO 监控模块。

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

传统 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 流程总览

sequenceDiagram participant Scheduler as APScheduler participant Runner as GeoEngineRunner participant Probe as probe_engine participant LLM as 目标大模型 participant Analyzer as GeoAnalyzer participant DB as MySQL participant Diagnoser as GeoDiagnoser participant Feishu as 飞书 Webhook Scheduler->>Runner: run_due_geo_tasks() Runner->>DB: 创建 GeoRun,标记 is_running loop 品牌 × 问题 × 引擎 loop sample_count 次采样 Runner->>Probe: probe_engine(engine, prompt) Probe->>LLM: 中性 system + 用户问题 LLM-->>Probe: 原始回答 Probe-->>Runner: ProbeResult Runner->>Analyzer: analyze(brand, answer, competitors) Analyzer-->>Runner: 结构化 JSON end Runner->>Analyzer: aggregate_samples() 多数投票 Runner->>DB: 写入 GeoSnapshot end Runner->>DB: 更新 Run 状态 Runner->>Feishu: 未提及/负面推荐告警 Runner->>Diagnoser: schedule_diagnosis(run_id) 后台任务 Diagnoser->>Feishu: 诊断摘要(可选) Runner->>Feishu: 基线回归告警(可选)

3.2 调度入口

GEO 任务由 APScheduler 周期性触发:

python 复制代码
# 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 是整个模块的「心脏」。其核心逻辑可以概括为:

python 复制代码
# 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 或已禁用的引擎

默认问题模板示例:

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

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

4.1 厂商注册表

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

python 复制代码
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_urlmodel 字段含义
  2. PROVIDER_DEFAULTS 增加预设
  3. 配置 .env 中对应的 GEO_*_API_KEY
  4. 管理端「引擎管理」新增记录

4.2 探测 LLM

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

python 复制代码
# 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:

python 复制代码
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:

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 级指标,供诊断、看板、基线对比复用:

python 复制代码
# 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 在后台启动诊断:

python 复制代码
# 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),触发飞书回归告警。


8. 告警体系

GeoFeishuAlert 支持三类飞书 Webhook 通知:

类型 触发条件
探测告警 未提及 / 负面推荐,且达到 threshold 条数
诊断摘要 诊断完成后推送根因与建议 Top 3
回归告警 当前 Run 提及率较基线下降 ≥ regression_threshold

告警配置挂在任务级 alert_config JSON 字段,示例:

json 复制代码
{
  "enabled": true,
  "alert_not_mentioned": true,
  "alert_warn": true,
  "threshold": 1,
  "regression_threshold": 0.1
}

Webhook 优先使用 GEO_FEISHU_WEBHOOK_URL,可回退到舆情监控的 MONITOR_FEISHU_WEBHOOK_URL


9. 取消与并发控制

GEO 探测可能耗时较长(品牌数 × 问题数 × 引擎数 × 采样次数 × LLM 延迟),需要可靠的并发控制:

  • 任务级 is_running 标志防止重复执行
  • cancel_requested + 循环内 is_cancel_requested() 检查实现手动停止
  • 僵尸 Run 清理:超过 60 分钟仍为 running 则标记失败
  • 手动执行通过 asyncio.create_task 后台化,API 立即返回
python 复制代码
# backend/app/core/geo/cancel.py
async def is_cancel_requested(task_id: int) -> bool:
    async with AsyncSessionLocal() as db:
        result = await db.execute(
            select(GeoTask.cancel_requested).where(GeoTask.id == task_id)
        )
        return bool(result.scalar_one_or_none())

10. 环境配置

env 复制代码
# 各引擎 API Key
GEO_DASHSCOPE_API_KEY=
GEO_DEEPSEEK_API_KEY=
GEO_DOUBAO_API_KEY=
GEO_HUNYUAN_API_KEY=
GEO_MOONSHOT_API_KEY=
GEO_ZHIPU_API_KEY=

# 分析器(建议便宜模型)
GEO_ANALYZER_PROVIDER=dashscope
GEO_ANALYZER_MODEL=qwen-turbo

# 执行参数
GEO_PROBE_TIMEOUT_SECONDS=60
GEO_DEFAULT_SAMPLE_COUNT=2
GEO_DEFAULT_INTERVAL_MINUTES=1440

# 告警
GEO_FEISHU_WEBHOOK_URL=

注意

  • 豆包 model_code 需填火山方舟推理接入点 ID(ep-xxxx
  • DeepSeek 推荐 deepseek-v4-flashdeepseek-chat 已弃用
  • 混元可通过 extra_body.enable_enhancement 开启搜索增强

11. 核心代码文件(仅供参考)

文件 职责
backend/app/core/geo/engine.py 任务调度与执行主循环
backend/app/core/geo/probe.py 单引擎探测
backend/app/core/geo/analyzer.py 回答结构化分析 + 采样聚合
backend/app/core/geo/llm.py 探测/分析 LLM 工厂
backend/app/core/geo/provider_registry.py 厂商预设与可用性检查
backend/app/core/geo/metrics.py Run 级指标聚合
backend/app/core/geo/diagnoser.py 诊断报告生成
backend/app/core/geo/run_brief.py 执行分析报告(事实汇总,不含建议)
backend/app/core/geo/content_generator.py 优化内容草稿生成
backend/app/core/geo/publisher.py 发布到企业文档 + 向量库
backend/app/core/geo/alert.py 飞书告警
backend/app/core/geo/cancel.py 取消控制
backend/app/api/v1/geo.py REST API 层
backend/app/core/scheduler/__init__.py 定时任务注册
frontend/src/views/admin/geo/* 管理端页面

12. 设计经验总结

  1. 探测与分析分离:探测用大模型、分析用便宜模型,成本和效果兼顾。
  2. 中性 prompt 是测量基准:GEO 的数据可信度取决于探测时不对品牌做任何引导。
  3. OpenAI 兼容层降低接入成本 :新厂商只需配置 base_url + model_code 以及 apiKey,无需重写调用逻辑。
  4. 采样 + 投票抑制随机性:LLM 回答本身有波动,多次采样聚合让指标更稳定。
  5. 闭环比监控更有价值:诊断 → 草稿 → 发布向量库 → 基线对比,让 GEO 从「看数据」变成「改数据、验效果」。

以上实现仅代表个人,如有问题请指正;欢迎大家评论区交流。

转载请备注出处!

相关推荐
甲维斯1 小时前
GLM5.2+ZCode复刻坦克大战,自测50万帧!
前端·ai编程·游戏开发
掘金者阿豪1 小时前
微信小程序虚拟支付与广告转化回传实战记录
后端
Csvn2 小时前
useRef 的 5 个冷门但救命的高级用法
前端
小小小小宇2 小时前
Harness Engineering 与 AI 联动
前端
鱼人2 小时前
HTML5 页面性能优化大全
前端
ping某2 小时前
专栏-null 和 undefined 到底是什么?
前端·javascript·后端
用户900463370402 小时前
5MB vs 4KB vs 无限大:浏览器存储谁更强?
前端
小小小小宇2 小时前
Harness Engineering 全解析与应用
前端
神奇小汤圆3 小时前
别再只会用ArrayList了!Java集合框架的性能天花板到底在哪?
后端