一、背景:当你的流量入口变成了AI的“嘴”

如果你运营着一个内容站点,大概率已经发现了一个令人不安的趋势:搜索引擎的“蓝色链接”点击率在下降,而ChatGPT、Perplexity、Kimi等AI工具的“生成式答案”正在吃掉越来越多的用户查询。用户得到了答案,却可能从来没访问过你的网站。

这就是GEO(生成式引擎优化)要解决的核心问题:让你的内容被AI答案引用,并带上可点击的信源链接。

但这里有一个所有SEO从业者都会遇到的困境——传统SEO我们可以用SEMrush/Ahrefs/Search Console来监控关键词排名和点击量,而在AI搜索的世界里,你的内容有没有被引用?被引用的频率是多少?在什么语境下被引用?竞争对手有没有抢走本该属于你的引用位? 这些问题目前几乎没有现成的商业工具能完美回答。

因此,对于真正想深耕GEO的开发者或技术团队来说,自己动手搭建一套AI搜索可见性追踪系统,可能是当前阶段最务实的做法。本文将手把手带你用Python实现一个最小可用的原型。


二、系统目标与架构设计

我们要做的系统,核心功能可以概括为一句话:针对你关心的关键词,定期向主流AI搜索引擎发送查询,解析返回结果中是否引用了你的域名,记录并追踪趋势。

最小化架构包含三个模块:

  1. 查询调度器:管理关键词列表,定时向AI搜索API发起请求。

  2. 引用解析器:从API返回结果中提取引用来源,判断是否命中目标域名。

  3. 存储与可视化:将每次查询的结果存入数据库,生成可见性趋势图。

技术选型上:

  • API选择:Perplexity AI 提供公开API,返回结构中包含引用来源URL,最适合做监控;同时可扩展接入 Google Gemini(通过 Google AI Studio)来追踪 Google AI Overviews 的引用情况(需开启 grounding 功能)。

  • Python库requests 处理 API 调用,sqlite3 轻量存储,matplotlib 或 Streamlit 做可视化。

  • 部署:可按需运行脚本,或用 cron / GitHub Actions 定时触发。


三、第一步:调用Perplexity API获取结构化引用

Perplexity的API返回格式非常清晰:答案文本中内嵌了引用编号,而答案末尾的 citations 字段直接列出了所有引用URL。这为我们省去了解析自然文本去提取网址的麻烦。

首先,你需要去 Perplexity 官网申请 API Key。

以下是一个基础调用示例:

python

import requests
import json

API_KEY = "your-perplexity-api-key"
API_URL = "https://api.perplexity.ai/chat/completions"

def query_perplexity(prompt: str, model: str = "sonar-pro") -> dict:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json"
    }
    payload = {
        "model": model,
        "messages": [
            {"role": "system", "content": "You are a helpful assistant. Always include citations."},
            {"role": "user", "content": prompt}
        ],
        "max_tokens": 1024,
        "temperature": 0.2,          # 低温确保引用稳定
        "return_images": False,
        "search_recency_filter": "month"  # 可控制时效性
    }
    response = requests.post(API_URL, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()

# 测试查询
result = query_perplexity("2025年最值得买的无线降噪耳机推荐")
answer = result["choices"][0]["message"]["content"]
citations = result.get("citations", [])

print("AI答案片段:", answer[:200])
print("引用列表:", citations)

注意参数调优:

  • temperature 设为较低值(0.1-0.3)以保证引用结果的相对稳定性,避免同一个问题两次返回截然不同的来源。

  • search_recency_filter 可以根据业务需求设为 "day""week""month" 或省略,影响AI抓取内容的时效范围。

  • 模型选择 sonar-pro 比基础版有更丰富的引用和更长的上下文处理能力。


四、第二步:引用解析与域名匹配

拿到引用列表后,我们需要从中识别出“自己的域名”是否出现,以及出现的位序、次数等。

简单实现如下:

python

from urllib.parse import urlparse

TARGET_DOMAINS = ["mysite.com", "myblog.cn"]  # 你要监控的域名列表

def analyze_citations(citations: list[str]) -> dict:
    analysis = {
        "total_citations": len(citations),
        "matched_domains": [],
        "match_positions": []  # 第一个匹配的引用排在第几位
    }
    for idx, url in enumerate(citations):
        parsed = urlparse(url)
        domain = parsed.netloc.lower().lstrip("www.")
        if domain in TARGET_DOMAINS:
            analysis["matched_domains"].append(domain)
            analysis["match_positions"].append(idx + 1)  # 转换为人类可读的第几位
    analysis["is_any_match"] = len(analysis["matched_domains"]) > 0
    return analysis

# 接上一段
citations = result.get("citations", [])
analysis = analyze_citations(citations)
print(analysis)

对于更精细的分析,可以进一步提取引用片段中的锚文本(即AI在答案中用来链接的那几个词),但这需要解析答案文本中的引用标记(如 [1][2])并匹配到对应 URL。Perplexity 的答案文本内嵌格式通常是 [1] 等编号,编写正则即可提取上下文。这里留作进阶扩展。


五、第三步:存储到SQLite并构建趋势

单次查询不够,我们要让系统定时运行,把每次的快照记录下来,形成可见性趋势。

建表SQL:

sql

CREATE TABLE IF NOT EXISTS citation_checks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    keyword TEXT NOT NULL,
    check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    total_citations INTEGER,
    matched BOOLEAN,
    matched_domain TEXT,
    match_position INTEGER,
    full_citations TEXT,       -- JSON格式存储完整引用列表
    answer_snippet TEXT
);

完整的主监控函数:

python

import sqlite3
from datetime import datetime

DB_PATH = "geo_monitor.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS citation_checks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            keyword TEXT NOT NULL,
            check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            total_citations INTEGER,
            matched BOOLEAN,
            matched_domain TEXT,
            match_position INTEGER,
            full_citations TEXT,
            answer_snippet TEXT
        )
    ''')
    conn.commit()
    conn.close()

def save_check(keyword: str, analysis: dict, citations: list, snippet: str):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        INSERT INTO citation_checks (keyword, check_time, total_citations, matched,
                                     matched_domain, match_position, full_citations, answer_snippet)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    ''', (
        keyword,
        datetime.now().isoformat(),
        analysis["total_citations"],
        analysis["is_any_match"],
        analysis["matched_domains"][0] if analysis["matched_domains"] else None,
        analysis["match_positions"][0] if analysis["match_positions"] else None,
        json.dumps(citations),
        snippet[:500]
    ))
    conn.commit()
    conn.close()

def run_keyword_check(keyword: str):
    result = query_perplexity(keyword)
    answer = result["choices"][0]["message"]["content"]
    citations = result.get("citations", [])
    analysis = analyze_citations(citations)
    save_check(keyword, analysis, citations, answer)
    return analysis

之后只需将你要监控的关键词列表循环跑一遍:

python

KEYWORDS = [
    "2025最好的项目管理软件",
    "如何选择适合油皮的防晒霜",
    "Python异步编程最佳实践"
]

init_db()
for kw in KEYWORDS:
    print(f"正在检查关键词: {kw}")
    res = run_keyword_check(kw)
    status = "✅ 被引用" if res["is_any_match"] else "❌ 未被引用"
    print(f"  {status},总引用数:{res['total_citations']},匹配位置:{res.get('match_positions')}")


六、第四步:可视化与预警

有了时间序列数据,就可以画出关键域的可见性变化趋势。最简单的实现是用 matplotlib

python

import matplotlib.pyplot as plt
import sqlite3
import pandas as pd

def plot_visibility_trend(keyword: str):
    conn = sqlite3.connect(DB_PATH)
    df = pd.read_sql_query(
        "SELECT check_time, matched FROM citation_checks WHERE keyword=? ORDER BY check_time",
        conn, params=(keyword,)
    )
    conn.close()
    
    if df.empty:
        print("暂无数据")
        return
    
    df["check_time"] = pd.to_datetime(df["check_time"])
    df["matched_int"] = df["matched"].astype(int)
    
    # 按天聚合,计算当日被引用比例
    daily = df.groupby(df["check_time"].dt.date)["matched_int"].mean()
    
    plt.figure(figsize=(10,4))
    plt.plot(daily.index, daily.values, marker='o')
    plt.axhline(y=1.0, color='green', linestyle='--', alpha=0.5, label='总是被引用')
    plt.xticks(rotation=45)
    plt.title(f'AI搜索可见性趋势: "{keyword}"')
    plt.ylabel('被引用频率 (0~1)')
    plt.tight_layout()
    plt.show()

更进一步,可以设置预警阈值:当某个核心关键词连续N次检查都未命中时,自动发送邮件或Slack通知,提醒内容团队可能需要更新或重新优化对应页面。


七、扩展与进阶

上述原型只覆盖了最核心的链路,生产级系统还需考虑以下扩展:

1. 多AI引擎覆盖

  • Google Gemini + Grounding:在 Google AI Studio 中调用 Gemini 模型并开启 Google Search grounding,返回结果中会包含 groundingMetadata 含引用URL。

  • Kimi / 秘塔等国产AI搜索:目前大多没有公开API,可考虑使用无头浏览器(Playwright)模拟查询并解析DOM,复杂度较高但可行。

2. 引用位置的语义分析
仅仅“被引用”还不够,我们需要知道AI是以正面还是负面语境提及我们?这需要对AI答案片段进行情感分析或语义相似度计算。简单的实现:提取提及你品牌/域名的答案句子,用BERT模型判断其情感倾向。

3. 竞争情报
除了监控自己,还可以加入竞争对手的域名列表,每次查询时一并分析“谁抢到了本属于我们的引用位”。存储时加入 competitor_hits 字段,形成竞争态势矩阵。

4. 排期与去重
设置合理的查询频率(建议每小时或每天),并为同一关键词在不同时间的多次查询做去重逻辑(Perplexity的结果有缓存,频繁查询可能返回相同引用),可考虑通过加入略微变动的提示词或调整 temperature 来增加结果多样性。


八、写在最后:GEO不是玄学,可以被工程化

很多人觉得GEO是“玄学”,因为AI的引用行为像个黑箱。但事实上,只要我们把监控和实验体系建起来,这个黑箱完全可以被系统地探索和优化。 这套追踪系统的本质,是让你的GEO实践拥有数据闭环——你可以A/B测试不同内容写法、结构化数据标记、信息密度策略,并通过客观的引用率数据来判定谁更有效。

在AI搜索时代,可见性不再仅仅是搜索引擎的排名,而是AI“脑海里的排名”。而能够测量它的人,才有可能真正优化它。希望这套原型能成为你踏入GEO深水区的第一块垫脚石。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐