CH.01

GEO 自动化工具体系概览

GEO(Generative Engine Optimization)的自动化程度直接决定了优化效率与效果的可复现性。手动诊断、手动优化、手动监控的模式在小规模站点上尚可维持,但当业务扩展到数十甚至上百个品牌站点、多平台多语言的场景时,自动化工具链成为唯一可持续的解决方案。本章将从全局视角审视GEO自动化工具的体系架构与决策框架。

为什么需要 GEO 自动化工具

传统SEO的自动化主要围绕关键词监控、排名追踪、链接建设展开。而GEO的自动化需求远比传统SEO复杂,原因在于:

  • 多维度诊断需求:GEO需要同时关注AI可见度、品牌提及率、结构化标记完整性、语义密度、爬虫可达性等多个维度,人工逐一检查效率极低
  • 实时性要求高:AI搜索引擎的索引更新频率远超传统搜索引擎,品牌在AI回答中的出现状态可能在数小时内发生剧变
  • 跨平台一致性:GEO需要在多个平台(官网、知乎、百科、CSDN、公众号等)保持信息一致性,手动同步极易出错
  • 数据驱动决策:GEO优化效果的量化评估需要持续的数据采集与对比分析,没有自动化工具几乎不可能实现
  • 规模化扩展瓶颈:当优化目标从单一品牌扩展到品牌矩阵时,人工操作的成本呈线性增长,而自动化工具的边际成本趋近于零

GEO 自动化的三大核心场景

诊断场景

自动化检测品牌在AI搜索引擎中的可见度、结构化标记的完整性、爬虫可达性、语义密度等基础指标。核心价值:从"不知道AI怎么看我"到"精确知道每一个诊断指标"。

优化场景

自动化生成结构化内容(JSON-LD、FAQ、llms.txt)、语义优化建议、内容改写适配等。核心价值:从"手动编写每一个标记"到"模板化+引擎化批量生成"。

监控场景

自动化监控AI爬虫访问行为、AI回答中品牌提及变化、竞品GEO表现对比、效果趋势预警等。核心价值:从"出问题了才知道"到"趋势异常提前预警"。

工具体系架构

一个完整的GEO自动化工具体系可以划分为四个层次,每层承担不同的职责:

数据采集层
API / 爬虫 / 日志
分析引擎层
NLP / 评分 / 语义
执行自动化层
生成 / 分发 / 改写
监控反馈层
Dashboard / 告警 / 迭代
GEO AUTOMATION STACK

四个层次的具体职责与核心组件:

层次 核心职责 关键组件 技术栈
数据采集层 从AI搜索引擎、爬虫日志、多平台API采集原始数据 多平台API适配器、日志解析器、爬虫模拟器 Python + httpx, Scrapy, Nginx日志
分析引擎层 品牌提及解析、语义密度计算、结构化评分、异常检测 NER引擎、评分引擎、语义分析器、异常检测器 spaCy, Transformers, FastAPI
执行自动化层 自动生成结构化标记、内容分发、改写适配 JSON-LD生成器、FAQ生成器、分发调度器、改写引擎 Jinja2, LLM API, Celery
监控反馈层 实时可视化、趋势预警、竞品对比、优化建议闭环 Dashboard、告警系统、报表引擎 React + ECharts, PostgreSQL, Grafana

自建 vs 采购的决策框架

在GEO自动化工具的获取方式上,企业面临"自建"与"采购"的选择。以下是决策参考:

维度 自建 采购 SaaS 混合模式
初始成本 高(研发人力) 低(订阅费) 中等
定制灵活性 极高 受限
数据安全 完全可控 依赖第三方 核心数据自控
迭代速度 自主可控但可能慢 依赖供应商节奏 核心自研+通用采购
适用规模 中大型企业/多品牌 中小型企业 成长型企业
技术门槛 需要NLP/全栈团队 中等
🚩 决策建议

如果你的品牌矩阵超过5个、需要深度定制GEO策略、或对数据安全有严格要求,自建是长期最优解。如果团队技术资源有限、需要快速验证GEO效果,先采购SaaS跑通MVP,再逐步迁移核心能力到自建系统。混合模式(诊断用SaaS + 生成与分发自建)是目前最务实的路径。

CH.02

AI 可见度自动化诊断系统

AI可见度诊断是GEO自动化工具链中最核心的模块。它回答一个根本问题:当用户在AI搜索引擎中查询你的品牌或核心业务关键词时,你的品牌是否出现在AI的回答中?以何种形式出现?占据多大篇幅?手动测试不仅耗时,而且无法覆盖所有关键词组合和所有AI搜索引擎。自动化诊断系统将这一过程工程化、可复现化。

系统架构设计

AI可见度诊断系统采用微服务架构,核心模块包括:

任务调度器
Scheduler
多平台API适配
Perplexity/Kimi/ChatGPT
结果采集与解析
Parser + NER
评分引擎
Scoring Engine
AI VISIBILITY DIAGNOSIS PIPELINE

系统的工作流程为:调度器按配置的关键词列表和查询频率,定期向多个AI搜索引擎发送查询请求;API适配层负责对接不同平台的接口规范与认证方式;结果采集与解析层提取AI回答中的品牌提及信息;评分引擎根据多维度指标计算综合可见度评分。

多平台 API 对接方案

不同AI搜索引擎的API差异较大,需要一个统一的适配层:

平台 API类型 认证方式 速率限制 响应格式
Perplexity REST API API Key 50 req/min JSON + Citations
OpenAI ChatGPT REST API Bearer Token 60 req/min JSON
Kimi (月之暗面) REST API API Key 30 req/min JSON
秘塔搜索 Web Scraping Cookie 10 req/min HTML
Google AI Overview SERP API API Key 100 req/min JSON + Snippets

自动化提问与结果采集

提问策略的设计直接影响诊断的覆盖率和准确度。建议采用三层提问策略

  • 品牌词查询:"[品牌名]是什么"、"[品牌名]怎么样"、"[品牌名] vs [竞品]"
  • 品类词查询:"最好的[品类]推荐"、"[品类]哪个品牌好"、"[品类]排名"
  • 场景词查询:"[场景]怎么解决"、"[场景]有哪些方案"、"[场景]工具推荐"

品牌提及解析算法

采集到AI回答后,需要从中识别出品牌提及。解析算法采用NER(命名实体识别)+ 模糊匹配的混合方案:

  • 精确匹配:品牌全称、简称、英文名的完整匹配
  • 模糊匹配:基于编辑距离的品牌变体识别(如"华为"匹配"HUAWEI")
  • 上下文判断:排除同义词或泛指的误匹配,通过上下文语义消歧
  • 情感标注:判断提及是正面、中性还是负面

评分引擎设计

AI可见度评分由多个子指标加权计算:

子指标 权重 计算方式 满分
出现率 30% 品牌出现在回答中的查询占比 100
位置分数 20% 品牌在回答中的位置(越靠前分越高) 100
篇幅占比 15% 品牌相关内容占回答总篇幅的百分比 100
引用质量 15% 是否作为权威来源被引用 100
情感分数 10% 提及的情感倾向(正面加分,负面扣分) 100
竞品对比优势 10% 与同查询中竞品提及的对比优势 100

代码示例:Python + FastAPI 诊断 API 实现

geo_diagnosis/api.py Python
# GEO AI可见度自动化诊断API - 核心实现
from fastapi import FastAPI, BackgroundTasks, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict
from datetime import datetime
import httpx
import asyncio
import json

app = FastAPI(title="GEO Visibility Diagnosis API", version="1.0.0")

# 请求模型定义
class DiagnosisRequest(BaseModel):
    brand_name: str
    keywords: List[str]
    platforms: List[str] = ["perplexity", "chatgpt", "kimi"]
    competitors: Optional[List[str]] = []

class BrandMention(BaseModel):
    text: str
    position: int
    sentiment: str  # positive / neutral / negative
    context: str
    is_cited: bool = False

class DiagnosisResult(BaseModel):
    brand: str
    platform: str
    query: str
    appeared: bool
    mentions: List[BrandMention]
    visibility_score: float
    timestamp: datetime

# 多平台API适配器
class PlatformAdapter:
    def __init__(self):
        self.adapters = {
            "perplexity": self._query_perplexity,
            "chatgpt": self._query_chatgpt,
            "kimi": self._query_kimi,
        }

    async def query(self, platform: str, prompt: str) -> dict:
        handler = self.adapters.get(platform)
        if not handler:
            raise ValueError(f"Unsupported platform: {platform}")
        return await handler(prompt)

    async def _query_perplexity(self, prompt: str) -> dict:
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                "https://api.perplexity.ai/chat/completions",
                headers={"Authorization": f"Bearer {PERPLEXITY_KEY}"},
                json={
                    "model": "sonar-pro",
                    "messages": [{"role": "user", "content": prompt}],
                },
                timeout=30.0,
            )
            data = resp.json()
            return {
                "answer": data["choices"][0]["message"]["content"],
                "citations": data.get("citations", []),
            }

    async def _query_chatgpt(self, prompt: str) -> dict:
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                "https://api.openai.com/v1/chat/completions",
                headers={"Authorization": f"Bearer {OPENAI_KEY}"},
                json={
                    "model": "gpt-4o",
                    "messages": [{"role": "user", "content": prompt}],
                },
                timeout=30.0,
            )
            data = resp.json()
            return {
                "answer": data["choices"][0]["message"]["content"],
                "citations": [],
            }

    async def _query_kimi(self, prompt: str) -> dict:
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                "https://api.moonshot.cn/v1/chat/completions",
                headers={"Authorization": f"Bearer {KIMI_KEY}"},
                json={
                    "model": "moonshot-v1-128k",
                    "messages": [{"role": "user", "content": prompt}],
                },
                timeout=30.0,
            )
            data = resp.json()
            return {
                "answer": data["choices"][0]["message"]["content"],
                "citations": [],
            }

# 品牌提及解析器
class BrandMentionParser:
    def __init__(self, brand_name: str, competitors: List[str]):
        self.brand_name = brand_name
        self.competitors = competitors
        self.all_entities = [brand_name] + competitors

    def parse(self, answer: str, citations: List[str]) -> List[BrandMention]:
        mentions = []
        answer_lower = answer.lower()
        for entity in self.all_entities:
            idx = 0
            while True:
                pos = answer_lower.find(entity.lower(), idx)
                if pos == -1:
                    break
                context_start = max(0, pos - 50)
                context_end = min(len(answer), pos + len(entity) + 50)
                context = answer[context_start:context_end]
                is_cited = any(entity.lower() in c.lower() for c in citations)
                mentions.append(BrandMention(
                    text=entity,
                    position=pos,
                    sentiment=self._analyze_sentiment(context),
                    context=context,
                    is_cited=is_cited,
                ))
                idx = pos + len(entity)
        return mentions

    def _analyze_sentiment(self, context: str) -> str:
        positive_words = ["优秀", "领先", "推荐", "best", "leading"]
        negative_words = ["问题", "不足", "较差", "worst", "problem"]
        ctx_lower = context.lower()
        pos_count = sum(1 for w in positive_words if w in ctx_lower)
        neg_count = sum(1 for w in negative_words if w in ctx_lower)
        if pos_count > neg_count: return "positive"
        elif neg_count > pos_count: return "negative"
        return "neutral"

# 可见度评分引擎
class VisibilityScorer:
    WEIGHTS = {
        "appearance": 0.30,
        "position": 0.20,
        "coverage": 0.15,
        "citation": 0.15,
        "sentiment": 0.10,
        "competitive": 0.10,
    }

    def calculate(self, mentions: List[BrandMention],
                    answer_length: int,
                    brand_name: str,
                    competitors: List[str]) -> float:
        if not mentions:
            return 0.0

        brand_mentions = [m for m in mentions if m.text == brand_name]
        comp_mentions = [m for m in mentions if m.text in competitors]

        appearance_score = 100.0 if brand_mentions else 0.0
        position_score = max(0, 100 - (brand_mentions[0].position / answer_length * 100)) if brand_mentions else 0.0
        coverage = sum(len(m.context) for m in brand_mentions) / answer_length if answer_length > 0 else 0.0
        coverage_score = min(100, coverage * 500)
        citation_score = 100.0 if any(m.is_cited for m in brand_mentions) else 0.0
        sentiment_map = {"positive": 100, "neutral": 60, "negative": 20}
        sentiment_score = sum(sentiment_map[m.sentiment] for m in brand_mentions) / len(brand_mentions) if brand_mentions else 0.0
        competitive_score = 80.0 if len(brand_mentions) >= len(comp_mentions) else 40.0

        total = (
            appearance_score * self.WEIGHTS["appearance"] +
            position_score * self.WEIGHTS["position"] +
            coverage_score * self.WEIGHTS["coverage"] +
            citation_score * self.WEIGHTS["citation"] +
            sentiment_score * self.WEIGHTS["sentiment"] +
            competitive_score * self.WEIGHTS["competitive"]
        )
        return round(total, 2)

# API端点
adapter = PlatformAdapter()

@app.post("/api/v1/diagnose", response_model=List[DiagnosisResult])
async def diagnose(req: DiagnosisRequest, bg: BackgroundTasks):
    results = []
    parser = BrandMentionParser(req.brand_name, req.competitors)
    scorer = VisibilityScorer()

    for platform in req.platforms:
        for keyword in req.keywords:
            query_text = f"{keyword}"
            try:
                response = await adapter.query(platform, query_text)
                mentions = parser.parse(response["answer"], response["citations"])
                score = scorer.calculate(
                    mentions, len(response["answer"]),
                    req.brand_name, req.competitors
                )
                results.append(DiagnosisResult(
                    brand=req.brand_name,
                    platform=platform,
                    query=keyword,
                    appeared=len([m for m in mentions if m.text == req.brand_name]) > 0,
                    mentions=mentions,
                    visibility_score=score,
                    timestamp=datetime.now(),
                ))
            except Exception as e:
                continue

    return results
💡 实战要点

API适配层建议采用策略模式设计,每个平台一个适配器类,统一继承自BaseAdapter接口。当新增AI搜索引擎时,只需添加新的适配器即可,无需修改核心逻辑。速率限制建议使用令牌桶算法实现,避免触发平台限制导致IP被封禁。

CH.03

结构化内容自动生成器

GEO优化的核心在于让内容对AI搜索引擎"友好可解析",而结构化标记(JSON-LD、FAQ、llms.txt)是实现这一目标的技术基石。然而,手动编写和维护这些结构化标记不仅繁琐,而且极易出错。结构化内容自动生成器将这一过程工程化——输入品牌/产品信息,自动输出完整的结构化标记代码。

JSON-LD 标记自动生成

JSON-LD(JavaScript Object Notation for Linked Data)是Schema.org推荐的结构化数据格式,AI搜索引擎可以直接解析JSON-LD中的实体关系。自动生成器需要支持以下Schema类型:

Schema类型 适用场景 关键属性 GEO价值
Organization 品牌/公司信息 name, url, logo, sameAs, description 品牌实体识别
Product 产品页面 name, description, brand, offers, review 产品推荐引用
FAQPage 常见问题页面 mainEntity (Question + Answer) 直接问答引用
Article 文章/博客 headline, author, datePublished, image 内容溯源引用
HowTo 教程/指南 name, step (text, image) 步骤型回答引用
BreadcrumbList 页面导航 itemListElement (name, item) 层级关系理解

FAQ 模块自动生成

FAQ内容是AI搜索引擎最倾向引用的格式之一,因为其天然具备"问题-答案"的结构。自动生成器从品牌知识库中提取关键问答对,自动生成FAQPage结构化标记:

  • 问题来源:客服FAQ文档、用户评论高频问题、搜索引擎相关搜索词
  • 答案生成:基于品牌知识库 + LLM辅助生成标准化答案
  • 格式输出:FAQPage JSON-LD + HTML渲染版本,一键部署
  • 更新机制:定期检查答案时效性,标记过时内容并触发更新

llms.txt 自动生成与更新

llms.txt是GEO领域新兴的网站级指引文件,作用类似robots.txt,但面向AI爬虫而非搜索引擎爬虫。自动生成器需要:

  • 内容摘要:自动从网站全站内容中提取核心信息摘要
  • 链接索引:生成网站重要页面的结构化链接列表
  • 更新策略:网站内容变更时自动更新llms.txt的对应条目
  • 格式规范:遵循llms.txt社区规范的Markdown格式

内容模板引擎设计

内容模板引擎是生成器的核心,负责将结构化数据填充到预定义的模板中:

品牌数据源
CRM / 知识库 / CMS
模板引擎
Jinja2 + Schema定义
校验层
Schema Validator
输出层
JSON-LD / HTML / llms.txt
CONTENT TEMPLATE ENGINE PIPELINE

代码示例:JSON-LD 生成器 Python 实现

geo_generator/jsonld_generator.py Python
# GEO 结构化内容自动生成器 - JSON-LD 生成模块
import json
from typing import List, Dict, Optional
from dataclasses import dataclass, field, asdict
from jinja2 import Environment, FileSystemLoader

@dataclass
class BrandInfo:
    name: str
    url: str
    logo: str
    description: str
    same_as: List[str] = field(default_factory=list)
    contact_email: Optional[str] = None
    contact_phone: Optional[str] = None
    founding_date: Optional[str] = None
    address: Optional[Dict] = None

@dataclass
class ProductInfo:
    name: str
    description: str
    brand: str
    url: str
    image: str
    price: Optional[str] = None
    price_currency: str = "CNY"
    availability: str = "InStock"
    rating_value: Optional[float] = None
    review_count: Optional[int] = None

@dataclass
class FAQItem:
    question: str
    answer: str

class JsonLdGenerator:
    """GEO JSON-LD 结构化标记自动生成器"""

    def generate_organization(self, brand: BrandInfo) -> dict:
        """生成 Organization 类型 JSON-LD"""
        org = {
            "@context": "https://schema.org",
            "@type": "Organization",
            "name": brand.name,
            "url": brand.url,
            "logo": {
                "@type": "ImageObject",
                "url": brand.logo,
            },
            "description": brand.description,
        }
        if brand.same_as:
            org["sameAs"] = brand.same_as
        if brand.contact_email:
            org["contactPoint"] = {
                "@type": "ContactPoint",
                "email": brand.contact_email,
                "contactType": "customer service",
            }
        if brand.founding_date:
            org["foundingDate"] = brand.founding_date
        if brand.address:
            org["address"] = {
                "@type": "PostalAddress",
                **brand.address,
            }
        return org

    def generate_product(self, product: ProductInfo) -> dict:
        """生成 Product 类型 JSON-LD"""
        prod = {
            "@context": "https://schema.org",
            "@type": "Product",
            "name": product.name,
            "description": product.description,
            "brand": {
                "@type": "Brand",
                "name": product.brand,
            },
            "url": product.url,
            "image": product.image,
            "offers": {
                "@type": "Offer",
                "priceCurrency": product.price_currency,
                "availability": f"https://schema.org/{product.availability}",
            },
        }
        if product.price:
            prod["offers"]["price"] = product.price
        if product.rating_value:
            prod["aggregateRating"] = {
                "@type": "AggregateRating",
                "ratingValue": str(product.rating_value),
                "reviewCount": str(product.review_count or 1),
            }
        return prod

    def generate_faq(self, faq_items: List[FAQItem]) -> dict:
        """生成 FAQPage 类型 JSON-LD"""
        return {
            "@context": "https://schema.org",
            "@type": "FAQPage",
            "mainEntity": [
                {
                    "@type": "Question",
                    "name": item.question,
                    "acceptedAnswer": {
                        "@type": "Answer",
                        "text": item.answer,
                    },
                }
                for item in faq_items
            ],
        }

    def generate_llms_txt(self, brand: BrandInfo,
                         pages: List[Dict],
                         summary: str) -> str:
        """生成 llms.txt 文件内容"""
        lines = [
            f"# {brand.name}",
            f"> {brand.description}",
            "",
            "## 重要链接",
            "",
        ]
        for page in pages:
            lines.append(f"- [{page['title']}]({page['url']}): {page.get('desc', '')}")
        lines.extend(["", "## 摘要", "", summary])
        return "\n".join(lines)

    def to_script_tag(self, data: dict) -> str:
        """将 JSON-LD 数据转换为 HTML script 标签"""
        return f'<script type="application/ld+json">\n{json.dumps(data, ensure_ascii=False, indent=2)}\n</script>'


# 使用示例
if __name__ == "__main__":
    gen = JsonLdGenerator()

    brand = BrandInfo(
        name="示例科技",
        url="https://example.com",
        logo="https://example.com/logo.png",
        description="领先的AI解决方案提供商,专注于企业智能化转型",
        same_as=["https://zhihu.com/org/example", "https://github.com/example"],
        contact_email="info@example.com",
        founding_date="2020",
    )

    faqs = [
        FAQItem("示例科技提供哪些服务?", "我们提供AI咨询、模型定制、数据平台建设等全链路服务。"),
        FAQItem("如何联系示例科技?", "您可以通过 info@example.com 或官网在线表单联系我们。"),
    ]

    # 生成并输出
    print(gen.to_script_tag(gen.generate_organization(brand)))
    print(gen.to_script_tag(gen.generate_faq(faqs)))
CH.04

AI 爬虫监控与分析系统

了解AI爬虫如何访问你的网站,是GEO优化的基础前提。AI爬虫的访问模式、频率、偏好路径与普通搜索引擎爬虫截然不同。AI爬虫监控与分析系统帮助你实时掌握哪些AI爬虫在访问你的站点、它们访问了哪些页面、访问频率如何、是否存在异常行为。

爬虫访问日志实时监控

实时监控的核心是对Nginx/Apache访问日志进行流式解析,识别AI爬虫的User-Agent并提取关键信息:

AI爬虫名称 User-Agent 标识 所属平台 抓取频率
PerplexityBot Mozilla/5.0 (compatible; PerplexityBot/1.0) Perplexity 中等
Google-Extended Google-Extended Google AI
GPTBot Mozilla/5.0 (compatible; GPTBot/1.0) OpenAI 中等
CCBot CCBot/2.0 Common Crawl
ClaudeBot Mozilla/5.0 (compatible; ClaudeBot/1.0) Anthropic 中等
Bytespider Bytespider 字节跳动
PetalBot Mozilla/5.0 (compatible; PetalBot/2.1) 华为搜索

AI 爬虫行为分析仪表盘

仪表盘需要展示以下核心指标:

  • 爬虫访问总量:按时间维度(小时/天/周)展示各AI爬虫的访问次数趋势
  • 页面覆盖率:AI爬虫已访问页面占全站页面的百分比
  • 热门访问路径:AI爬虫最频繁访问的URL路径Top N
  • 响应码分布:200/301/403/404/500等状态码的占比
  • 访问时段分布:AI爬虫的活跃时段热力图

异常爬取告警机制

以下异常情况需要即时告警:

1 AI爬虫访问骤降 可能原因:robots.txt变更、服务器宕机、IP被封
2 大量404响应 可能原因:页面删除、URL变更未做301重定向
3 未知爬虫大量访问 可能原因:新AI搜索引擎上线、恶意爬取
4 关键页面未被爬取 可能原因:内链结构问题、页面孤立

爬虫覆盖率统计

覆盖率统计的核心逻辑是对比"全站页面清单"与"AI爬虫已访问页面清单",计算覆盖率和未覆盖页面列表:

  • 总覆盖率 = AI爬虫已访问页面数 / 全站页面数 × 100%
  • 按爬虫覆盖率:每个AI爬虫的独立覆盖率,识别哪个爬虫覆盖最全
  • 按页面类型覆盖率:文章页、产品页、FAQ页等各类页面的覆盖情况
  • 未覆盖页面清单:导出AI爬虫从未访问过的页面列表,用于针对性优化

代码示例:日志解析与爬虫识别

geo_crawler/log_parser.py Python
# GEO AI爬虫日志解析与识别系统
import re
from dataclasses import dataclass
from datetime import datetime
from typing import List, Dict, Optional
from collections import defaultdict

@dataclass
class LogEntry:
    ip: str
    timestamp: datetime
    method: str
    path: str
    status: int
    size: int
    user_agent: str
    referer: Optional[str] = None

@dataclass
class CrawlerHit:
    crawler_name: str
    crawler_type: str  # ai / search / unknown
    platform: str
    entry: LogEntry

# AI爬虫识别规则库
AI_CRAWLER_PATTERNS = {
    "PerplexityBot": {
        "pattern": re.compile(r"PerplexityBot", re.I),
        "platform": "Perplexity",
        "type": "ai",
    },
    "GPTBot": {
        "pattern": re.compile(r"GPTBot", re.I),
        "platform": "OpenAI",
        "type": "ai",
    },
    "Google-Extended": {
        "pattern": re.compile(r"Google-Extended|GoogleOther", re.I),
        "platform": "Google AI",
        "type": "ai",
    },
    "ClaudeBot": {
        "pattern": re.compile(r"ClaudeBot|anthropic-ai", re.I),
        "platform": "Anthropic",
        "type": "ai",
    },
    "CCBot": {
        "pattern": re.compile(r"CCBot", re.I),
        "platform": "Common Crawl",
        "type": "ai",
    },
    "Bytespider": {
        "pattern": re.compile(r"Bytespider", re.I),
        "platform": "ByteDance",
        "type": "ai",
    },
}

class NginxLogParser:
    """Nginx combined日志格式解析器"""
    LOG_PATTERN = re.compile(
        r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] '
        r'"(?P<method>\S+) (?P<path>\S+) \S+" '
        r'(?P<status>\d+) (?P<size>\d+) '
        r'"(?P<referer>[^"]*)" '
        r'"(?P<ua>[^"]*)"'
    )

    def parse_line(self, line: str) -> Optional[LogEntry]:
        match = self.LOG_PATTERN.match(line.strip())
        if not match:
            return None
        d = match.groupdict()
        return LogEntry(
            ip=d["ip"],
            timestamp=datetime.strptime(d["time"], "%d/%b/%Y:%H:%M:%S %z"),
            method=d["method"],
            path=d["path"],
            status=int(d["status"]),
            size=int(d["size"]),
            user_agent=d["ua"],
            referer=d["referer"] if d["referer"] != "-" else None,
        )

class CrawlerIdentifier:
    """AI爬虫识别器"""

    def identify(self, entry: LogEntry) -> Optional[CrawlerHit]:
        ua = entry.user_agent
        for name, rule in AI_CRAWLER_PATTERNS.items():
            if rule["pattern"].search(ua):
                return CrawlerHit(
                    crawler_name=name,
                    crawler_type=rule["type"],
                    platform=rule["platform"],
                    entry=entry,
                )
        return None

class CrawlerAnalytics:
    """AI爬虫访问统计分析"""

    def __init__(self):
        self.hits: List[CrawlerHit] = []
        self._by_crawler = defaultdict(list)
        self._by_path = defaultdict(list)
        self._by_status = defaultdict(int)

    def add_hit(self, hit: CrawlerHit):
        self.hits.append(hit)
        self._by_crawler[hit.crawler_name].append(hit)
        self._by_path[hit.entry.path].append(hit)
        self._by_status[hit.entry.status] += 1

    def summary(self) -> Dict:
        total = len(self.hits)
        return {
            "total_ai_crawler_hits": total,
            "by_crawler": {
                name: len(hits) for name, hits in self._by_crawler.items()
            },
            "top_paths": sorted(
                [(path, len(hits)) for path, hits in self._by_path.items()],
                key=lambda x: x[1], reverse=True
            )[:20],
            "status_distribution": dict(self._by_status),
            "unique_ips_per_crawler": {
                name: len(set(h.entry.ip for h in hits))
                for name, hits in self._by_crawler.items()
            },
        }

    def coverage_rate(self, all_paths: set) -> Dict:
        crawled_paths = set(hit.entry.path for hit in self.hits)
        covered = crawled_paths & all_paths
        return {
            "total_pages": len(all_paths),
            "crawled_pages": len(covered),
            "coverage_rate": round(len(covered) / len(all_paths) * 100, 2) if all_paths else 0,
            "uncrawled_pages": list(all_paths - crawled_paths),
        }
⚠️ 部署提醒

日志解析器建议部署为sidecar容器与Nginx共享日志卷,实时tail日志文件进行流式解析。对于高流量站点,建议将解析结果写入Redis做实时聚合,再定时批量写入PostgreSQL做持久化存储和历史分析。

CH.05

语义优化辅助工具

GEO的核心不是"关键词密度",而是"语义密度"——你的内容是否以AI可理解的方式,充分表达了品牌的核心信息。语义优化辅助工具帮助你量化内容的语义丰富度,识别语义空白,并提供结构化优化建议。

语义密度分析器

语义密度(Semantic Density)衡量的是单位文本中包含的有效信息量。与关键词密度不同,语义密度关注的是实体-关系-属性的丰富程度:

  • 实体密度:每100字中出现的命名实体数量(品牌、产品、人名、地名等)
  • 关系密度:实体之间的语义关联数量(如"X提供Y服务"、"Y属于Z品类")
  • 属性密度:每个实体被描述的属性维度数量(价格、功能、优势、场景等)
  • 语义完整性:核心信息维度是否全部覆盖(What/Who/How/Why/When/Where)

实体识别与标注工具

该工具基于NER模型自动识别内容中的命名实体,并标注实体类型和关系。GEO场景下需要特别关注的实体类型:

实体类型 GEO意义 示例 AI引用概率
品牌名 (BRAND) 品牌识别与关联 "华为"、"Apple" 极高
产品名 (PRODUCT) 产品推荐引用 "Mate 60 Pro"
技术术语 (TECH) 领域专业度背书 "大语言模型"、"RAG"
数据指标 (METRIC) 事实引用依据 "市场份额30%" 中高
人物名 (PERSON) 权威背书 "任正非"
场景词 (SCENARIO) 意图匹配 "企业数字化转型"

内容结构化评分器

评分器从结构维度评估内容对AI的友好程度,评分维度包括:

  • 标题层级规范:H1-H6层级是否完整、逻辑清晰
  • 列表与表格:是否使用列表和表格组织结构化信息
  • 定义句式:是否包含"A是B"的清晰定义句式
  • 数据支撑:是否引用具体数据和来源
  • FAQ结构:是否包含问答结构

语义相似度测试工具

该工具用于测试你的内容与目标查询意图之间的语义相似度,帮助评估内容是否"回答了正确的问题":

  • 查询-内容相似度:目标关键词/查询与页面内容的语义距离
  • 跨页面相似度:同一主题不同页面之间的相似度(避免过度重复)
  • 竞品内容差异度:你的内容与竞品同主题内容的差异化程度

代码示例:语义密度计算器

geo_semantic/density_calculator.py Python
# GEO 语义密度计算器
import re
from typing import List, Dict, Tuple
from dataclasses import dataclass

@dataclass
class Entity:
    text: str
    entity_type: str
    start: int
    end: int
    attributes: List[str] = None

@dataclass
class SemanticDensityResult:
    entity_density: float
    relation_density: float
    attribute_density: float
    completeness_score: float
    overall_score: float
    missing_dimensions: List[str]
    entities: List[Entity]

class SemanticDensityCalculator:
    """GEO语义密度分析器"""

    # 5W1H信息完整性维度
    COMPLETENESS_DIMS = {
        "what": ["是什么", "什么是", "定义", "what is", "defined as"],
        "who": ["谁", "创始人", "团队", "who", "founded by"],
        "how": ["如何", "怎么", "方法", "how to", "approach"],
        "why": ["为什么", "原因", "优势", "why", "benefit"],
        "when": ["何时", "时间", "历史", "when", "since"],
        "where": ["哪里", "地点", "区域", "where", "located"],
    }

    def calculate(self, text: str, entities: List[Entity]) -> SemanticDensityResult:
        char_count = len(text.replace(" ", ""))
        if char_count == 0:
            return SemanticDensityResult(0, 0, 0, 0, 0, [], entities)

        # 实体密度:每100字的命名实体数
        entity_density = len(entities) / (char_count / 100)

        # 关系密度:基于句式模式匹配
        relation_patterns = [
            r".*提供.*服务", r".*属于.*类别", r".*支持.*功能",
            r".*是.*的", r".*包含.*", r".*适用于.*",
        ]
        relations = sum(1 for p in relation_patterns if re.search(p, text))
        relation_density = relations / (char_count / 100) * 10

        # 属性密度:每个实体的平均属性数
        total_attrs = sum(len(e.attributes or []) for e in entities)
        attribute_density = total_attrs / len(entities) if entities else 0

        # 信息完整性检查
        text_lower = text.lower()
        covered = []
        missing = []
        for dim, keywords in self.COMPLETENESS_DIMS.items():
            if any(kw in text_lower for kw in keywords):
                covered.append(dim)
            else:
                missing.append(dim)
        completeness_score = len(covered) / len(self.COMPLETENESS_DIMS) * 100

        # 综合评分(加权平均)
        overall = (
            min(entity_density * 5, 100) * 0.30 +
            min(relation_density * 10, 100) * 0.25 +
            min(attribute_density * 20, 100) * 0.20 +
            completeness_score * 0.25
        )

        return SemanticDensityResult(
            entity_density=round(entity_density, 2),
            relation_density=round(relation_density, 2),
            attribute_density=round(attribute_density, 2),
            completeness_score=round(completeness_score, 1),
            overall_score=round(overall, 1),
            missing_dimensions=missing,
            entities=entities,
        )
CH.06

效果监控 Dashboard

数据驱动的GEO优化需要一个集中的可视化平台。效果监控Dashboard将诊断系统的评分结果、爬虫监控的访问数据、语义分析的密度指标整合到一个统一界面,实现GEO效果的实时追踪与趋势分析。

数据采集与存储设计

Dashboard的数据来源于多个子系统,需要统一的采集与存储架构:

诊断系统
可见度评分
爬虫监控
访问日志聚合
语义分析
密度/完整性
PostgreSQL
统一数据仓库
DATA COLLECTION PIPELINE

数据模型设计:

数据表 核心字段 写入频率 保留周期
visibility_scores brand, platform, query, score, timestamp 每次诊断 12个月
crawler_hits crawler_name, path, status, timestamp 实时 6个月
semantic_metrics url, entity_density, completeness, timestamp 每日 12个月
competitor_snapshots competitor, platform, score, timestamp 每周 12个月
alert_events alert_type, severity, message, timestamp 触发时 3个月

实时监控指标展示

Dashboard首页需要一览核心KPI:

  • AI可见度总评分:所有品牌/关键词/平台的综合加权评分(大数字+趋势箭头)
  • AI爬虫访问趋势:过去7天/30天的AI爬虫访问量折线图
  • 覆盖率仪表盘:页面覆盖率百分比环形图
  • 品牌提及变化:最近一次诊断与上次的对比变化
  • 活跃告警:当前未处理的告警列表

趋势分析与预警

趋势分析需要支持多维度下钻:

  • 时间维度:按天/周/月查看评分变化趋势,识别上升/下降拐点
  • 平台维度:对比不同AI搜索引擎上的表现差异
  • 关键词维度:各关键词的可见度变化趋势
  • 竞品维度:品牌与竞品的评分差距变化趋势

预警规则示例:

  • 某关键词可见度评分连续3天下降超过10% → 黄色预警
  • 某关键词可见度评分单日下降超过30% → 红色预警
  • 某平台AI爬虫访问量连续7天为零 → 红色预警

竞品对比可视化

竞品对比模块展示品牌与Top N竞品在AI搜索引擎中的表现差异,支持:

  • 雷达图:多维度对比(可见度、覆盖率、引用质量、情感分数等)
  • 排名矩阵:各关键词在AI回答中的品牌出现排名
  • 差距热力图:品牌与竞品在各关键词上的评分差距

技术栈:React + ECharts + PostgreSQL

geo_dashboard/src/components/VisibilityChart.jsx JavaScript
// GEO效果监控Dashboard - 可见度趋势图表组件
import React, { useState, useEffect } from 'react';
import ReactECharts from 'echarts-for-react';

const VisibilityChart = ({ brandId, dateRange }) => {
  const [chartData, setChartData] = useState({});
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchVisibilityData(brandId, dateRange);
  }, [brandId, dateRange]);

  const fetchVisibilityData = async (brandId, range) => {
    setLoading(true);
    const resp = await fetch(
      `/api/v1/visibility/trend?brand=${brandId}&range=${range}`
    );
    const data = await resp.json();
    setChartData(data);
    setLoading(false);
  };

  const getOption = () => ({
    tooltip: {
      trigger: 'axis',
      backgroundColor: 'rgba(10, 17, 40, 0.95)',
      borderColor: 'rgba(0, 212, 255, 0.3)',
      textStyle: { color: '#e8ecf4', fontSize: 12 },
    },
    legend: {
      data: ['Perplexity', 'ChatGPT', 'Kimi', '综合评分'],
      textStyle: { color: '#8b9dc3' },
    },
    grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
    xAxis: {
      type: 'category',
      data: chartData.dates || [],
      axisLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.2)' } },
      axisLabel: { color: '#8b9dc3' },
    },
    yAxis: {
      type: 'value',
      min: 0, max: 100,
      axisLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.2)' } },
      axisLabel: { color: '#8b9dc3' },
      splitLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.06)' } },
    },
    series: [
      {
        name: 'Perplexity',
        type: 'line',
        data: chartData.perplexity || [],
        smooth: true,
        lineStyle: { color: '#00d4ff', width: 2 },
        itemStyle: { color: '#00d4ff' },
        areaStyle: { color: 'rgba(0, 212, 255, 0.05)' },
      },
      {
        name: 'ChatGPT',
        type: 'line',
        data: chartData.chatgpt || [],
        smooth: true,
        lineStyle: { color: '#7c3aed', width: 2 },
        itemStyle: { color: '#7c3aed' },
      },
      {
        name: 'Kimi',
        type: 'line',
        data: chartData.kimi || [],
        smooth: true,
        lineStyle: { color: '#06d6a0', width: 2 },
        itemStyle: { color: '#06d6a0' },
      },
      {
        name: '综合评分',
        type: 'line',
        data: chartData.overall || [],
        smooth: true,
        lineStyle: { color: '#f59e0b', width: 3 },
        itemStyle: { color: '#f59e0b' },
      },
    ],
  });

  if (loading) return <div>Loading...</div>;

  return (
    <div className="chart-container">
      <h3>AI可见度趋势</h3>
      <ReactECharts option={getOption()} style={{ height: '400px' }} />
    </div>
  );
};

export default VisibilityChart;
CH.07

自动化内容分发系统

GEO内容需要在多个高权威信源平台同步发布,才能最大化AI引用概率。自动化内容分发系统解决的核心问题是:如何将一份内容自动适配并分发到知乎、CSDN、微信公众号、百科平台等不同格式要求的发布渠道,同时保持核心信息的一致性。

多平台内容发布自动化

每个平台的发布方式差异极大:

平台 发布方式 内容格式 自动化难度 推荐策略
知乎 API / 浏览器自动化 Markdown / 富文本 中等 API优先,备选Playwright
CSDN Markdown API / 浏览器 Markdown 中等 Markdown API
微信公众号 官方API HTML(受限) 微信公众号素材API
百度百科 浏览器自动化 百科格式 极高 人工审核+辅助生成
简书 API Markdown Markdown API
掘金 API Markdown Markdown API

内容改写与适配引擎

同一份内容直接复制到不同平台会触发重复内容检测,AI搜索引擎也可能降低对重复内容的引用权重。改写引擎基于LLM对原文进行平台适配改写:

  • 知乎适配:增加专业论述深度、引用来源、数据支撑,风格偏学术
  • CSDN适配:增加代码示例、技术细节、架构图,风格偏工程
  • 公众号适配:缩短篇幅、增加段落标题、适配移动端阅读,风格偏大众
  • 百科适配:转为中立客观的百科体,去除主观表述,增加引用

发布排程与节奏管理

多平台分发需要注意发布节奏,避免同一天在所有平台集中发布:

  • 官网首发:Day 1,确保官网是信息的权威源头
  • 知乎/技术社区:Day 2-3,在专业社区发布深度版本
  • 公众号:Day 3-4,发布大众化版本
  • 百科/新闻:Day 5-7,提交百科词条更新或新闻稿
  • 社交媒体:Day 1-7 持续,发布摘要版本引流

分发效果追踪与反馈

每次分发后需要追踪效果:

  • 发布状态:是否成功发布、是否被平台审核拒绝
  • 阅读量:各平台文章的阅读/点赞/评论数据
  • AI引用关联:发布后品牌在AI回答中的可见度是否提升
  • 闭环反馈:效果数据回流到Dashboard,指导下一轮分发策略调整

代码示例:多平台发布调度器

geo_distributor/scheduler.py Python
# GEO 多平台内容分发调度器
from typing import List, Dict, Optional
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
import asyncio

class Platform(Enum):
    WEBSITE = "website"
    ZHIHU = "zhihu"
    CSDN = "csdn"
    WECHAT = "wechat"
    BAIKE = "baike"
    JUEJIN = "juejin"
    JIANSHU = "jianshu"

@dataclass
class ContentPiece:
    title: str
    body: str
    tags: List[str] = field(default_factory=list)
    category: Optional[str] = None
    original_url: Optional[str] = None

@dataclass
class DistributionTask:
    content: ContentPiece
    platform: Platform
    scheduled_time: datetime
    adapted_body: Optional[str] = None
    status: str = "pending"  # pending / published / failed
    result_url: Optional[str] = None
    error_message: Optional[str] = None

class ContentAdapter:
    """内容平台适配器"""

    ADAPT_PROMPTS = {
        Platform.ZHIHU: "请将以下内容改写为知乎深度回答风格,增加专业论述深度和数据引用:",
        Platform.CSDN: "请将以下内容改写为CSDN技术博客风格,增加代码示例和架构说明:",
        Platform.WECHAT: "请将以下内容改写为微信公众号文章风格,段落简短,适合移动端阅读:",
        Platform.BAIKE: "请将以下内容改写为百科词条风格,中立客观,去除主观评价:",
    }

    async def adapt(self, content: ContentPiece,
                      platform: Platform) -> str:
        """使用LLM适配内容到目标平台"""
        if platform in [Platform.WEBSITE, Platform.JUEJIN, Platform.JIANSHU]:
            return content.body  # Markdown平台无需改写

        prompt = self.ADAPT_PROMPTS.get(platform, "")
        # 调用LLM API进行改写(此处简化)
        adapted = await self._call_llm(prompt + content.body)
        return adapted

    async def _call_llm(self, prompt: str) -> str:
        # LLM API调用实现
        pass

class DistributionScheduler:
    """多平台分发调度器"""

    # 发布节奏配置(天数偏移)
    SCHEDULE_OFFSETS = {
        Platform.WEBSITE: 0,   # Day 1 首发
        Platform.ZHIHU: 1,    # Day 2
        Platform.CSDN: 2,     # Day 3
        Platform.JUEJIN: 2,   # Day 3
        Platform.WECHAT: 3,   # Day 4
        Platform.JIANSHU: 4,  # Day 5
        Platform.BAIKE: 6,    # Day 7
    }

    def __init__(self, adapter: ContentAdapter):
        self.adapter = adapter
        self.tasks: List[DistributionTask] = []

    def create_schedule(self, content: ContentPiece,
                         platforms: List[Platform],
                         start_date: datetime = None) -> List[DistributionTask]:
        """创建分发排程"""
        start = start_date or datetime.now()
        tasks = []
        for platform in platforms:
            offset = self.SCHEDULE_OFFSETS.get(platform, 0)
            scheduled = start + timedelta(days=offset)
            tasks.append(DistributionTask(
                content=content,
                platform=platform,
                scheduled_time=scheduled,
            ))
        self.tasks.extend(tasks)
        return tasks

    async def execute_schedule(self):
        """执行排程中的待发布任务"""
        now = datetime.now()
        pending = [
            t for t in self.tasks
            if t.status == "pending" and t.scheduled_time <= now
        ]
        for task in pending:
            try:
                # 1. 适配内容
                adapted = await self.adapter.adapt(
                    task.content, task.platform
                )
                task.adapted_body = adapted

                # 2. 调用平台API发布
                result = await self._publish_to_platform(task)
                task.status = "published"
                task.result_url = result.get("url")
            except Exception as e:
                task.status = "failed"
                task.error_message = str(e)

    async def _publish_to_platform(self, task: DistributionTask) -> dict:
        """调用平台API发布内容"""
        # 实际实现根据各平台API对接
        pass
💡 最佳实践

分发调度器建议集成到Celery定时任务框架中,使用Redis作为Broker。每个分发任务作为一个独立Task,失败时自动重试3次(指数退避)。发布结果写入PostgreSQL,通过Dashboard实时查看分发状态。

CH.08

GEO API 设计规范

当GEO自动化工具从内部工具演进为团队协作平台,甚至开放给外部合作伙伴使用时,一套规范的API设计成为必需。本章定义GEO工具的API设计规范,确保接口的一致性、安全性和可扩展性。

RESTful API 设计原则

GEO API遵循以下核心设计原则:

  • 资源导向:URL表示资源,HTTP方法表示操作(GET查询、POST创建、PUT更新、DELETE删除)
  • 版本化:API路径包含版本号(/api/v1/),确保向后兼容
  • 统一响应格式:所有API返回统一的JSON结构,包含code、message、data字段
  • 分页标准化:列表接口统一使用page/page_size参数和total/count响应字段
  • 错误码规范:定义清晰的错误码体系,便于客户端处理

GEO 工具 API 接口规范

核心API端点设计:

模块 方法 路径 说明
诊断 POST /api/v1/diagnose 触发AI可见度诊断
诊断 GET /api/v1/diagnose/{task_id} 查询诊断结果
诊断 GET /api/v1/diagnose/history 查询诊断历史
生成 POST /api/v1/generate/jsonld 生成JSON-LD标记
生成 POST /api/v1/generate/faq 生成FAQ结构化标记
生成 POST /api/v1/generate/llms-txt 生成llms.txt文件
监控 GET /api/v1/monitor/crawlers AI爬虫访问统计
监控 GET /api/v1/monitor/coverage 爬虫覆盖率统计
语义 POST /api/v1/semantic/density 计算语义密度
分发 POST /api/v1/distribute/schedule 创建分发排程
分发 GET /api/v1/distribute/tasks 查询分发任务状态

认证与权限管理

GEO API采用JWT(JSON Web Token)认证机制,支持三级权限:

  • Admin:全部API访问权限,可管理用户和项目
  • Editor:诊断、生成、分发操作权限,可查看监控数据
  • Viewer:只读权限,仅可查看诊断结果和监控数据

速率限制与配额设计

速率限制策略:

  • 诊断API:每个API Key每分钟最多10次请求(涉及外部AI API调用成本)
  • 生成API:每个API Key每分钟最多30次请求
  • 监控API:每个API Key每分钟最多60次请求
  • 语义API:每个API Key每分钟最多20次请求(涉及NLP计算资源)

配额管理:每个项目每月有诊断次数、生成次数的总配额,超出配额需要升级计划或联系管理员。

OpenAPI 规范文档示例

geo_api/openapi.yaml YAML
openapi: "3.0.3"
info:
  title: GEO Automation API
  description: GEO自动化工具API - AI可见度诊断、结构化内容生成、爬虫监控、语义分析、内容分发
  version: "1.0.0"
  contact:
    name: GEO API Support
    email: api@geo-tool.dev

servers:
  - url: https://api.geo-tool.dev
    description: Production
  - url: https://staging-api.geo-tool.dev
    description: Staging

security:
  - BearerAuth: []

paths:
  /api/v1/diagnose:
    post:
      summary: 触发AI可见度诊断
      operationId: createDiagnosis
      tags: [Diagnosis]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DiagnosisRequest'
      responses:
        "200":
          description: 诊断任务创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiagnosisResponse'
        "401":
          description: 未授权
        "429":
          description: 请求频率超限

  /api/v1/generate/jsonld:
    post:
      summary: 生成JSON-LD结构化标记
      operationId: generateJsonLd
      tags: [Generation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JsonLdRequest'
      responses:
        "200":
          description: JSON-LD生成成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JsonLdResponse'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    DiagnosisRequest:
      type: object
      required: [brand_name, keywords]
      properties:
        brand_name:
          type: string
          example: "示例科技"
        keywords:
          type: array
          items:
            type: string
          example: ["AI解决方案", "企业智能化"]
        platforms:
          type: array
          items:
            type: string
            enum: [perplexity, chatgpt, kimi, metaso]
          default: [perplexity, chatgpt, kimi]
        competitors:
          type: array
          items:
            type: string

    DiagnosisResponse:
      type: object
      properties:
        code:
          type: integer
          example: 200
        message:
          type: string
          example: "诊断任务已创建"
        data:
          type: object
          properties:
            task_id:
              type: string
              format: uuid
            status:
              type: string
              enum: [pending, running, completed, failed]
            estimated_time:
              type: integer
              description: 预计完成时间(秒)
CH.09

部署与运维

GEO自动化工具的部署架构需要兼顾开发效率与生产稳定性。本章提供完整的容器化部署方案、CI/CD流水线设计、监控告警配置和成本优化策略。

Docker 容器化部署方案

GEO工具链采用微服务架构,每个核心模块独立容器化:

服务 容器镜像 端口 依赖 资源限制
geo-api geo-tool/api:latest 8000 PostgreSQL, Redis 2C4G
geo-dashboard geo-tool/dashboard:latest 3000 geo-api 1C2G
geo-celery-worker geo-tool/worker:latest - Redis, PostgreSQL 2C4G
geo-log-parser geo-tool/log-parser:latest - Redis, Nginx日志卷 1C2G
PostgreSQL postgres:16-alpine 5432 - 2C8G
Redis redis:7-alpine 6379 - 1C2G

CI/CD 流水线设计

CI/CD流水线分为四个阶段:

1

代码检查与测试

lint + 单元测试 + 集成测试,代码覆盖率要求 > 80%

2

构建Docker镜像

多阶段构建,镜像大小优化,打标签推送到镜像仓库

3

部署到Staging环境

自动部署到Staging,运行E2E测试验证

4

生产环境发布

手动审批后发布到生产环境,滚动更新,健康检查

监控与告警配置

生产环境监控需要覆盖以下维度:

  • 应用监控:API响应时间、错误率、QPS(Prometheus + Grafana)
  • 基础设施监控:CPU、内存、磁盘、网络(Node Exporter)
  • 业务监控:诊断任务完成率、分发成功率、AI爬虫访问异常
  • 日志监控:ELK Stack集中收集和分析应用日志

成本优化策略

  • Spot实例:Celery Worker使用云厂商Spot实例,成本降低60-70%
  • 自动缩容:非工作时间自动缩容Worker数量(夜间诊断任务少)
  • API缓存:重复查询结果使用Redis缓存,减少AI API调用成本
  • 日志轮转:PostgreSQL数据按月分区,超过6个月的冷数据归档到对象存储

代码示例:Docker Compose 配置

docker-compose.yml YAML
version: "3.8"

services:
  geo-api:
    build:
      context: .
      dockerfile: Dockerfile.api
    container_name: geo-api
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://geo:geo_pass@postgres:5432/geo_db
      - REDIS_URL=redis://redis:6379/0
      - PERPLEXITY_API_KEY=${PERPLEXITY_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - KIMI_API_KEY=${KIMI_API_KEY}
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - postgres
      - redis
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 4G
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  geo-dashboard:
    build:
      context: ./dashboard
      dockerfile: Dockerfile
    container_name: geo-dashboard
    ports:
      - "3000:80"
    depends_on:
      - geo-api
    restart: unless-stopped

  geo-celery-worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    container_name: geo-worker
    command: celery -A geo_diagnosis.celery worker -l info -c 4
    environment:
      - DATABASE_URL=postgresql://geo:geo_pass@postgres:5432/geo_db
      - REDIS_URL=redis://redis:6379/0
      - PERPLEXITY_API_KEY=${PERPLEXITY_API_KEY}
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - KIMI_API_KEY=${KIMI_API_KEY}
    depends_on:
      - postgres
      - redis
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 4G
    restart: unless-stopped

  geo-celery-beat:
    build:
      context: .
      dockerfile: Dockerfile.worker
    container_name: geo-beat
    command: celery -A geo_diagnosis.celery beat -l info
    environment:
      - DATABASE_URL=postgresql://geo:geo_pass@postgres:5432/geo_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - redis
    restart: unless-stopped

  geo-log-parser:
    build:
      context: .
      dockerfile: Dockerfile.log-parser
    container_name: geo-log-parser
    volumes:
      - /var/log/nginx:/var/log/nginx:ro
    environment:
      - REDIS_URL=redis://redis:6379/0
      - DATABASE_URL=postgresql://geo:geo_pass@postgres:5432/geo_db
      - LOG_PATH=/var/log/nginx/access.log
    depends_on:
      - redis
      - postgres
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    container_name: geo-postgres
    environment:
      - POSTGRES_DB=geo_db
      - POSTGRES_USER=geo
      - POSTGRES_PASSWORD=geo_pass
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 8G
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: geo-redis
    command: redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru
    ports:
      - "6379:6379"
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:
CH.10

开源 GEO 工具生态

GEO作为新兴领域,开源生态正在快速发展。本章盘点现有开源GEO工具,提供贡献指南,并探讨自建工具开源的最佳实践和生态未来展望。

现有开源 GEO 工具盘点

工具名称 功能定位 语言/技术栈 Star数 活跃度
geopy GEO可见度诊断框架 Python + FastAPI 1.2K+ 活跃
llms-txt-generator llms.txt文件生成工具 Node.js 800+ 活跃
ai-crawler-detector AI爬虫识别与统计 Go 600+ 活跃
jsonld-schema-builder JSON-LD结构化标记构建器 TypeScript 500+ 一般
geo-monitor GEO效果监控Dashboard React + Python 400+ 一般
semantic-density-analyzer 语义密度分析工具 Python 300+ 一般
brand-mention-tracker AI回答品牌提及追踪 Python + Redis 250+ 新项目
💡 选型建议

开源工具适合作为MVP验证或特定功能的补充,但在企业级场景下通常需要二次开发。建议先用开源工具跑通核心流程,再逐步将关键模块替换为自建实现。重点关注工具的API设计是否规范、是否有活跃的社区支持、是否支持插件化扩展

贡献与参与指南

参与开源GEO工具贡献的推荐路径:

1

选择合适的项目

从GitHub Topics搜索"geo-optimization"、"llms-txt"、"ai-visibility"等关键词,选择活跃度高、Issue响应快的项目

2

从文档和测试入手

补充文档、编写测试用例是最佳的入门方式,既熟悉代码结构又为社区创造价值

3

提交功能PR

先在Issue中讨论方案,获得维护者认可后再开发,避免PR被拒绝

4

成为维护者

持续贡献后可申请成为项目Co-maintainer,参与方向决策

自建工具开源的最佳实践

如果你决定将自建的GEO工具开源,以下最佳实践可以确保项目健康发展:

  • 清晰的README:包含项目简介、安装指南、快速开始、架构图、API文档链接
  • 规范的LICENSE:推荐MIT或Apache 2.0,企业友好且社区接受度高
  • 完善的CONTRIBUTING.md:贡献流程、代码规范、PR模板、CI要求
  • 模块化设计:核心引擎与平台适配器分离,方便社区贡献新平台适配
  • 插件化架构:通过插件机制扩展功能,而非修改核心代码
  • 版本化发布:遵循SemVer语义化版本,每次发布附带CHANGELOG
  • 文档即代码:使用MkDocs或Docusaurus维护文档,随代码一起发布
  • 安全策略:SECURITY.md说明漏洞报告方式,敏感信息不硬编码

GEO 工具生态未来展望

GEO自动化工具生态正在从"零散脚本"向"专业平台"演进,未来趋势包括:

AI原生工具链

下一代GEO工具将深度集成LLM能力,不再只是"调用AI API",而是将AI作为工具链的核心推理引擎,实现从诊断到优化到验证的自动化闭环。

多模态GEO

随着AI搜索引擎支持图片、视频、音频的输入与输出,GEO工具需要从纯文本优化扩展到多模态内容的结构化和优化。

实时GEO

当前GEO优化以"发布-等待-验证"的批处理模式为主,未来将演进为实时感知AI搜索引擎的变化并即时调整的实时模式。

GEO标准化

类似SEO领域的Schema.org和robots.txt标准,GEO领域正在形成llms.txt、ai-robots协议等新标准,工具生态将围绕这些标准构建。

隐私与合规

随着AI数据使用的合规要求趋严,GEO工具需要内置隐私检测、合规审计能力,确保优化操作符合各地法规要求。

🚀 行动建议

GEO自动化工具领域正处于从0到1的关键阶段。如果你是开发者,现在是最佳入场时机——核心工具尚无垄断性方案,开源贡献的边际价值极高。如果你是企业决策者,建议从诊断和监控两个最成熟的场景切入,验证ROI后再扩展到生成和分发环节。

推荐后续阅读