给外贸独立站做 GEO,最容易卡住的一步是没法验收。排名有 Search Console,收录有索引入口,被 AI 引用这件事过去只能靠手工问几句。这篇给一套能落库、能算引用率、能区分出现形态的最小实现。
先说结论:日志里的爬虫记录、平台侧的报告、外部抽样问询,三者看到的是不同的东西,混着用会得出错误结论。先分清口径,再写代码。
一、三个观测口径的能力边界
| 观测方式 | 能拿到什么 | 拿不到什么 |
|---|---|---|
| Search Console 生成式 AI 性能报告 | AI 功能里的曝光量 | 点击、查询词、位置;2026-08-31 全球上线,官方口径里只有曝光 |
| 服务器访问日志 | 爬虫 UA、抓取路径、抓取频次 | 是否被引用。抓取只是前置条件 |
| 外部抽样问询 | 答案里出现的域名、出现位置、原文片段 | 全量,只能抽样;引擎版本变化会断档 |
三个容易误读的地方,写在前面。
日志里出现 GPTBot,说明内容被抓去做训练了,与被引用无关。出现 OAI-SearchBot,说明它把页面取回了检索索引,但取回不等于会出现在回答里。真正要看的,是回答文本里有没有你的域名。
谷歌那条线更特殊。Gemini 与 AI 摘要读的是 Googlebot 抓回来的常规索引,没有单独的爬虫给你放行;Google-Extended 是一个训练用途的退出开关,不是爬虫,官方写明不影响页面在搜索里的收录,也不作为排名信号。所以给谷歌这条线做监测,看的是常规搜索侧的收录与曝光。
真正有分辨力的,是第三种。下面四段代码就是围绕它搭的。
二、第一步:抽样清单与表结构
查询清单不要用行业大词,要用买家的真实提问。分三类各取若干条:品类类(谁能做某类产品)、对比类(A 与 B 的差别)、资质类(谁有某项认证)。总量控制在 20 到 50 条,条数多了跑不完,少了看不出趋势。
-- schema.sql
CREATE TABLE IF NOT EXISTS queries (
qid INTEGER PRIMARY KEY,
bucket TEXT NOT NULL, -- category | compare | cert
question TEXT NOT NULL,
lang TEXT NOT NULL -- en | de | es,按目标市场语言
);
CREATE TABLE IF NOT EXISTS citations (
cid INTEGER PRIMARY KEY,
qid INTEGER NOT NULL,
engine TEXT NOT NULL, -- chatgpt | gemini | claude
run_at TEXT NOT NULL,
hit_form TEXT, -- cited | mentioned | absent
rank_in_answer INTEGER, -- 你的域名出现在答案里的第几个来源
my_domain TEXT, -- 未命中为 NULL
cited_domains TEXT -- 本次答案里的其他域名,逗号分隔
);
-- 查询清单按三类导入,示例
INSERT INTO queries(qid, bucket, question, lang) VALUES
(1, 'category', 'reliable outdoor hardware manufacturers in China', 'en'),
(2, 'compare', 'stainless steel vs galvanized hardware for coastal use', 'en'),
(3, 'cert', 'which suppliers hold EN 1670 grade 3 certification', 'en');
表结构里 hit_form 这一列是关键。被引用(答案里带你的域名或链接)、被提及(只提了品牌名没给来源)、未出现,三种形态必须分开记。混在一起算会系统性高估,因为「被提及」在多数引擎里并不产生可点击的露出。
三、第二步:批量问询与记录
调用层做成可插拔的,每个引擎一个函数,方便引擎换代时只改一个文件。记录时同时写下本次答案里的其他域名,用来算份额。
# survey.py
import datetime, sqlite3
import httpx
PROVIDER = {}
def register(name):
def deco(fn):
PROVIDER[name] = fn
return fn
return deco
@register("chatgpt")
def fetch_chatgpt(question, lang, key):
r = httpx.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": "Bearer " + key},
json={"model": "gpt-4.1",
"messages": [{"role": "user", "content": question}]},
timeout=60,
)
return r.json()["choices"][0]["message"]["content"]
def extract_domains(answer):
# 实现略:按 markdown 链接与裸域名两类正则抽取,去重后保序
...
def judge_form(domains, mine):
if not domains:
return "absent"
return "cited" if mine in domains else "mentioned"
def run_survey(conn, keys, engines=("chatgpt",)):
rows = conn.execute("SELECT qid, question, lang FROM queries").fetchall()
now = datetime.datetime.now().isoformat(timespec="seconds")
for qid, question, lang in rows:
for eng in engines:
answer = PROVIDER[eng](question, lang, keys[eng])
domains = extract_domains(answer)
conn.execute(
"INSERT INTO citations(qid, engine, run_at, hit_form,"
" rank_in_answer, my_domain, cited_domains)"
" VALUES (?,?,?,?,?,?,?)",
(qid, eng, now, judge_form(domains, MY_DOMAIN),
rank_of(domains, MY_DOMAIN), MY_DOMAIN if MY_DOMAIN in domains else None,
",".join(domains)),
)
conn.commit()
两个实现上的注意点。rank_of 记的是你的域名在答案里的序号,这个值比「有没有出现」更能反映趋势,从第 7 个来源挪到第 2 个来源是实质变化。另一点是 run_at 必须落库,引擎更新会让历史数据断裂,只有带时间戳的记录才能在断档时把两段数据分开看。
也可以不用 API,手工在浏览器里问、把答案贴进一张表。工程上的收益只是省时间,判定逻辑完全一样。
四、第三步:算引用率与份额
分母用查询数而不是记录数,避免某个引擎多跑了几次把比例算歪。
-- metrics.sql
SELECT engine,
COUNT(DISTINCT qid) AS queries,
COUNT(DISTINCT CASE WHEN hit_form = 'cited' THEN qid END) AS cited_queries,
ROUND(100.0 * COUNT(DISTINCT CASE WHEN hit_form = 'cited' THEN qid END)
/ COUNT(DISTINCT qid), 1) AS cite_rate_pct,
ROUND(AVG(CASE WHEN hit_form = 'cited' THEN rank_in_answer END), 1) AS avg_rank
FROM citations
GROUP BY engine;
读这张表看三件事。引用率看趋势不看绝对值,同一批查询每周跑一次,四周之后看曲线方向。平均位置看你在答案里的排序,这个指标对外部变化最敏感。份额要把所有出现的域名汇总后算,用「你的域名出现次数 ÷ 全部答案里出现的域名总数」,它回答的是这个题域里你有多少话语权。
另外建议把三个引擎分开出表。同一批查询在三个引擎上的重合度并不高,合并成一个数字会掩盖差异。
五、第四步:日志侧核对抓取是否正常
抽样问询回答的是「有没有被引用」,日志回答的是「有没有被抓到」。后者是前者的前置条件,两条线要一起看。
# 先看检索类爬虫这一周来没来过
zcat access.log.*.gz | grep -E "OAI-SearchBot|Claude-SearchBot" | \
awk '{print $1, $7}' | sort | uniq -c | sort -rn | head -20
如果这批记录是零,抽样问询那边再跑也没有意义,先回去查两件事:robots.txt 里检索类爬虫有没有被 Disallow;产品页的主要内容是不是靠前端脚本渲染出来的,因为多数 AI 爬虫不执行 JavaScript,只读到服务器返回的 HTML。
一条经验:这一步和独立站搭建阶段的技术选型是同一件事。把关键内容做成服务端就能输出的 HTML,后面所有监测才有意义;反过来,页面靠脚本填内容,日志里查不到抓取,抽样也查不到引用,两个读数同时为零,很容易被误判成内容质量问题。
日志这一侧有个盲区。页面标题、meta 描述、结构化数据、图片替代文字这四类字段属于取段质量的范畴,抓取日志看不出差别。10 月 1 日谷歌更新文档时,把发布前的人工核查范围明确到这四类元数据上,写法不统一或者干脆空着,表现是抓取正常、引用率上不去。建议在记录表里加一列,标注每次抽样时答案用的是你自己的表述,还是竞品改写过的表述,这一列能把这四类字段的问题暴露出来。
六、几个常被问到的问题
要不要每天跑。高价值查询每天跑,普通查询每周跑一次就够。频率只影响趋势的平滑度,不影响判定逻辑。
用中文问还是英文问。按目标市场的语言来。面向欧洲客户的独立站,用当地语言问的答案与用英文问的答案,来源名单经常不一样,两种都要采。
能不能用现成的第三方监测工具。可以,但要知道它的口径。多数工具只统计「提及」不区分「带链接引用」,拿它的数字去做内部验收会偏高,建议只用来做行业对比。
七、这套读数的边界
抽样不等于全量,20 到 50 条查询只能代表你选中的那一小片问题域。引擎更新与新功能上线会让数据断档,跨版本比较要谨慎。这套数字适合内部排优先级,不要拿去对外承诺效果。
我们做外贸独立站运营的时候,把上面的清单与表结构做成了固定模板,查询清单在 NeoGress 建的后台里对应到产品分类,换行业时只替换清单,代码不动。适不适合你的团队,看两点:有没有人能每周固定跑一次,以及能不能接受用趋势而不是单次读数做判断。
最后一个可以今天就做的小动作:把你最关心的三条查询手工问一遍,把答案里出现的域名按顺序抄下来,连续抄四周。这四周的记录会告诉你,独立站SEO 里哪些动作在这套新名单上真正起了作用。
你打算把哪三条查询放进第一批清单?评论区说一下,我看看分类有没有漏。