从 SEO 到 GEO
生成式引擎优化的技术拆解,以及一套可复现的评测方法
这两年做内容和站点的同学大概都遇到过同一个问题:越来越多的用户不再点开搜索结果,而是直接问模型。于是「我们的产品在 AI 的回答里是什么样子」变成了一个需要被回答、却很难量化的问题。
这篇文章不谈概念包装,只做三件事:拆一下生成式引擎产出答案的链路、说清楚工程侧真正能动的几个地方、给一套能跑起来的评测骨架,外加一组实测数据和它的口径说明。文末会写清楚这套方法的局限。
利益相关:本文第三章的指标口径与评测骨架、第四章的实测数据,都来自启航创服 InnoLyceum AI(https://innolyceum.com/platform)的实际项目,作者参与其中。文中不含产品功能介绍;原始样本因客户保密不公开,技术结论请结合第六章的局限自行复核。
一、GEO 不是「换了个名字的 SEO」
GEO(Generative Engine Optimization,生成式引擎优化)常被当成 SEO 的新马甲。但只要把两者的产出链路画出来,就会发现优化对象根本不是一个东西。
传统搜索引擎大致是:
query → 查询理解 → 倒排索引召回 → 排序 → 返回链接列表 → 用户自己筛选
生成式引擎(含带检索的对话产品、答案引擎、搜索里的 AI 摘要)大致是:
query → 意图理解 / 查询改写(常拆成多条子查询)
→ 检索(模型内部知识 + 联网检索 + 站内索引)
→ 片段召回与重排(chunk 级,不是页面级)
→ 归纳合成 → 输出一个答案(可能附引用)
四个差异对工程实践的影响最直接:
1)产出是一个答案,不是一页链接。 「第几名」的含义变了。在一个只列 3 到 5 个候选的答案里,第 6 名和不存在没有区别。
2)召回单位是片段(chunk),不是页面。 检索侧通常把正文切成几百 token 的块再做向量/关键词混合召回。一个写得很好但结论埋在第 8 段的页面,很可能整篇都召不回来。可摘录性开始比关键词密度重要。
3)引用来源是被显式挑选的。 答案引擎会挑几条它认为可信、可核验的来源。信源结构(自有站点、第三方媒体、百科/榜单/问答类入口)直接决定了你有没有资格进入引用列表。
4)输出有随机性。 采样温度、模型版本、检索时序、是否联网,都会让同一个问题在不同时刻得到不同答案。这意味着单次截图不能作为任何结论的证据——这一点后面会反复提到。
二、工程侧真正能动的四件事
2.1 先确认爬虫进得来
这是最容易被忽略、成本又最低的一步。很多站点的 robots.txt 是几年前配的,把 AI 相关 UA 全挡在外面而不自知。
需要先分清三类 UA,用途完全不同(以各厂商公开文档为准):
| 训练抓取 | 采集语料用于模型训练 | GPTBot、ClaudeBot、meta-externalagent(兼做内容索引) |
| 检索抓取 | 为答案引擎建立索引,决定你能否被检索到 | OAI-SearchBot、Claude-SearchBot、PerplexityBot、Meta-WebIndexer |
| 用户实时取用 | 用户当场要求打开某个链接时的抓取 | ChatGPT-User、Claude-User、Perplexity-User、meta-externalfetcher |
另外还有 CCBot(Common Crawl)。它属于非营利的开放网页存档项目,本身不是某家厂商的训练爬虫,但产出的语料被大量模型用于训练,放行与否需要单独判断。
如果你的目标是「被 AI 检索到并引用」,那么至少要放行第二类。只放行训练类而挡住检索类,是很常见的配置错误。
一份最小配置:
# 检索抓取:决定能否进入答案引擎的索引
User-agent: OAI-SearchBot
Disallow: /admin/
Disallow: /api/
Allow: /
User-agent: Claude-SearchBot
Disallow: /admin/
Disallow: /api/
Allow: /
User-agent: PerplexityBot
Disallow: /admin/
Disallow: /api/
Allow: /
# 用户实时取用
User-agent: ChatGPT-User
Allow: /
User-agent: Claude-User
Allow: /
# 训练抓取:按自身版权策略决定放行与否
User-agent: GPTBot
Allow: /
User-agent: ClaudeBot
Allow: /
# Google-Extended 不是爬虫,而是一个 robots.txt 控制令牌:
# 只影响 Gemini 的训练与接地用途,不影响 Googlebot,也不作为搜索排名信号
User-agent: Google-Extended
Allow: /
Sitemap: https://example.com/sitemap.xml
三个必须知道的前提:
• robots.txt 的 group 不会合并。 按 REP,一个 bot 只匹配最具体的那一个 group,不会再去读 User-agent: * 下的规则。所以你一旦为某个 UA 单开了 group,原来写在 * 里的 Disallow: /admin/、Disallow: /api/ 对它就全部失效了——必须在每个 group 里重抄一遍。上面的配置里检索类 UA 就是这么写的。这是照抄配置最容易踩的坑。
• robots.txt 是君子协定,不是访问控制。 真要拦,得在 WAF/网关层按 UA 和 IP 段做(多数厂商也提供 IP 段或 rDNS 反查用于校验)。
• 「用户实时取用」不等于都守 robots。 OpenAI 对 ChatGPT-User 的说法是「由用户发起,robots.txt 规则可能不适用」;Perplexity 对 Perplexity-User 写的是「一般会忽略 robots.txt」;Meta 对 meta-externalfetcher 写的是「可能绕过 robots.txt」。作为对照,Anthropic 文档明确声明三个 Claude bot(含 Claude-User)均遵守 robots.txt。所以别指望靠 robots 实现严格的内容边界。
2.2 用结构化数据降低实体消歧成本
模型要回答「这家公司是谁、做什么」,最省力的路径是直接读到结构化字段,而不是从散文里猜。JSON-LD 的边际成本极低,值得默认部署:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "示例科技",
"url": "https://example.com",
"foundingDate": "2019-03-01",
"description": "面向制造业的设备预测性维护软件供应商。",
"areaServed": "CN",
"sameAs": [
"https://www.wikidata.org/wiki/Q00000000",
"https://github.com/example-tech"
]
}
</script>
配合 FAQPage 尤其值得做——它天然就是「问题 + 短答案」的形态,和片段召回的粒度完全对齐:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "预测性维护系统的部署周期一般多久?",
"acceptedAnswer": {
"@type": "Answer",
"text": "标准部署 6 到 8 周:接入 2 周、冷启动 3 周、验收 1 周。"
}
}]
}
</script>
关于这两段,有两点提醒:
• Google 已经不再展示 FAQ 富结果了,所以这里的收益指向 AI 检索侧的片段对齐,别指望 SERP 富媒体。另外结构化数据政策要求问答内容在页面上对用户可见,只埋 JSON-LD 不显示属于违规用法。
• sameAs 的作用是实体对齐:把「站上这个名字」和「外部权威页面上的那个实体」绑定,有助于降低同名企业被张冠李戴的概率。前提是这些 URL 必须真实存在且确实指向你——编造或指向不存在的页面只会制造噪声。没有 Wikidata 条目就用工商公示页、GitHub 组织页或权威目录页,不必硬凑。
2.3 把正文写成「可摘录」的形状
这一节没有代码,但对召回率的影响可能是最大的。既然召回单位是 chunk,正文就要按 chunk 友好的方式组织:
• 一段只讲一件事,结论前置。 把判断写在段首,论证放后面。被切走的那半段至少还带着结论。
• 不要跨段用代词指代。 「它」「该方案」「上述架构」在被切成独立 chunk 之后会彻底失去指代对象。宁可重复实体名。
• 关键事实自带限定词。 主体 + 数值 + 单位 + 时间窗 + 来源,写在同一句里。「响应时间 200ms」不如「v2.3 在单机 8 核环境下 P99 响应时间为 200ms(2026-03 实测)」。
• 表格和 FAQ 优先。 参数、对比、边界条件用表格;常见问题用问答结构。这两种形态的 chunk 边界最干净。
• 不要假设检索抓取会做 OCR。 参数表画成图,等于把它从索引里删掉。
• 正文别依赖客户端渲染。 目前公开测试的结论是,主流 AI 抓取基本都不渲染 JS。首屏 HTML 里拿不到正文,后面的一切都无从谈起。自查方式是抓一句正文原话,而不是数字节数:
curl -s -A "Mozilla/5.0 (compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot)" \\
https://your.site/page | grep -c "你正文里的一句原话"
# 返回 0 = 首屏 HTML 里没有正文,需要 SSR 或预渲染
2.4 关于 llms.txt,说点实话
llms.txt 是一个社区提出的约定:在站点根目录放一个 Markdown 文件,用结构化的方式告诉模型「这个站点有什么、重点看哪几页」。
# 示例科技
> 面向制造业的设备预测性维护软件供应商。
## 核心文档
– [产品概览](https://example.com/product): 功能边界与适用产线类型
– [部署指南](https://example.com/docs/deploy): 环境要求、部署周期、验收标准
– [常见问题](https://example.com/faq): 价格区间、服务承诺、数据合规
## 可引用的事实
– 成立时间:2019 年 3 月
– 标准部署周期:6–8 周
但必须说清楚现状:它到今天仍然是提案性的社区约定,不是标准,主流厂商没有统一承诺支持。 更进一步,已有公开统计显示,绝大多数部署了 llms.txt 的站点,这个文件几乎收不到任何抓取请求。它的成本很低(写一个文件),做了不亏;但把它当成排名手段就想多了。真正起作用的还是 2.1 到 2.3 那三件事。
三、评测:把「AI 里的表现」变成可复现的数字
这一节是全文重点。没有评测,前面所有优化都是盲改。
3.1 三个基础指标,且互不替代
绝大多数关于「AI 可见度」的争论,根源是把下面几件事混为一谈:
| 提及率 | 提及目标品牌的有效问句数 ÷ 有效问句数 | 模型知不知道它 |
| 描述准确度 | 关键事实全部正确的提及数 ÷ 提及数 | 说到它的时候说对了没有 |
| 幻觉率 | 出现凭空捏造事实的提及数 ÷ 提及数 | 有多少是编出来的 |
三点需要说明:
被提及不等于被推荐,被推荐不等于描述准确。 一个提及率 80%、准确度 60% 的结果,比提及率 50%、准确度 95% 要糟糕得多——前者意味着模型正在大规模地传播关于它的错误信息。只报一个数的评测都不可信。
准确度和幻觉率不是互补关系。 中间还有一段「有偏差但未捏造」的地带:信息过时、口径不同、描述不完整。这类既不计入准确,也不计入幻觉。如果你的两个指标恒等于加起来 100%,说明你其实只有一个指标。
位次这个指标要慎用。 它只在答案确实呈现为有序候选列表时才有意义,而且强依赖竞品表的完整性(见 3.4)。本文的实测部分因此不报位次。
3.2 样本设计比代码重要
下面四条是在实际项目里反复吃亏之后才固化下来的硬规则,任何一条破了,跨期和跨平台的对比就不成立:
1. 问句池要分层且版本化。 按决策阶段分层(认知类 / 比较类 / 验证类 / 采购类),每层固定条数。问句池一旦改动就是新版本,新旧版本的数据不能画在同一张趋势图里。
2. 平台集固定。 中途增删平台,汇总指标会凭空跳变。
3. 多轮采样。 生成是随机的,单轮结果没有统计意义。同一问句至少跑 5–7 轮,指标按全部有效样本汇总。
4. 有效样本规则要预先定义。 拒答、超时、明显跑题的响应算不算分母,必须在跑之前定好,不能看完结果再定。
3.3 一个最小可复现的评测骨架
下面这段是从上面提到的那套实际流程里简化出来的骨架,去掉了平台适配层和存储层,可以直接改改用。核心不在于调用哪家 API,而在于把口径固化成代码。
# geo_eval.py —— 一个最小可复现的 GEO 评测骨架
from __future__ import annotations
import re, time, statistics
from dataclasses import dataclass, field
from typing import Iterable, Callable, Literal
# ———- 1. 实体与观测 ———-
@dataclass(frozen=True)
class Brand:
canonical: str
aliases: tuple[str, …] = () # 简称、英文名、旧名、常见错写
def pattern(self) -> re.Pattern:
names = sorted({self.canonical, *self.aliases}, key=len, reverse=True)
return re.compile("|".join(map(re.escape, names)))
@dataclass
class Observation:
prompt_id: str
platform: str
round_id: int
answer: str
ts: float = 0.0 # 采样时刻,事后复核的锚点
meta: dict = field(default_factory=dict) # 联网开关、模型版本、问句池版本…
valid: bool = True # 拒答 / 跑题 → False
error: str | None = None # 传输失败,与「拒答」区分开
# ———- 2. 抽取 ———-
# 否定语境的粗过滤。注意不要单收「没有」——「没有之一」「没有明显短板」
# 都是褒义搭配,会造成系统性误杀。
NEGATIVE = re.compile(
r"(没有(收录|提供|涉及|相关)|不包括|不属于|并非|未提供|暂未收录|不推荐|除.{0,6}之外)"
)
def mentioned(answer: str, brand: Brand) -> bool:
"""任一次出现处于非否定语境,即算一次有效提及。"""
for m in brand.pattern().finditer(answer):
window = answer[max(0, m.start() – 24): m.end() + 24]
if NEGATIVE.search(window) is None:
return True
return False
def rank_of(answer: str, brand: Brand, competitors: Iterable[Brand]) -> int | None:
"""按首次出现顺序估算候选位次。
只在答案确实呈现为候选列表时才有意义;且只在你登记的竞品集合内排序,
竞品表不全会让名次系统性偏前。返回 None 表示不参与统计。"""
hits = []
for b in (brand, *competitors):
m = b.pattern().search(answer)
if m:
hits.append((m.start(), b.canonical))
if len(hits) < 2:
return None
hits.sort()
return [name for _, name in hits].index(brand.canonical) + 1
# ———- 3. 指标 ———-
Verdict = Literal["accurate", "flawed", "fabricated", "unknown"]
# accurate 关键事实全对
# flawed 有偏差、过时或不完整,但没有捏造
# fabricated 出现凭空捏造的事实(虚构数值 / 资质 / 业务线)
# unknown 无法判定,不计入分母
def metrics(obs: list[Observation],
brand: Brand,
judge: Callable[[str], Verdict]) -> dict:
empty = {"n_valid": 0, "n_mention": 0, "mention_rate": None,
"accuracy": None, "hallucination": None}
valid = [o for o in obs if o.valid and o.error is None]
if not valid:
return empty
hit = [o for o in valid if mentioned(o.answer, brand)]
judged = [v for v in (judge(o.answer) for o in hit) if v != "unknown"]
return {
"n_valid": len(valid),
"n_mention": len(hit),
"mention_rate": round(len(hit) / len(valid), 4),
"accuracy": round(sum(v == "accurate" for v in judged) / len(judged), 4)
if judged else None,
"hallucination": round(sum(v == "fabricated" for v in judged) / len(judged), 4)
if judged else None,
}
# ———- 4. 采样 ———-
ROUNDS, RETRIES = 7, 3 # 生成有随机性,单轮结果没有统计意义
def collect(client, platform: str, prompts, meta: dict) -> list[Observation]:
out = []
for p in prompts:
for r in range(ROUNDS):
for attempt in range(RETRIES):
try:
out.append(Observation(p.id, platform, r, client.ask(p.text),
ts=time.time(), meta=dict(meta)))
break
except Exception as e:
if attempt == RETRIES – 1: # 传输失败不进有效样本分母
out.append(Observation(p.id, platform, r, "", ts=time.time(),
meta=dict(meta), valid=False,
error=type(e).__name__))
time.sleep(2 ** attempt)
time.sleep(1.0) # 按各平台限流调整
return out
client.ask() 各平台自行封装即可。真正需要长期维护的其实是三样东西:别名表、否定词表、以及 judge() 的判定规则——它们决定了指标的真实可信度,比模型接口稳定得多。judge() 建议人工抽检,或用带证据库的判别流程,不要用另一个没有事实依据的模型直接打分。
3.4 几个必踩的坑
• 正则会高估提及率,否定过滤又会反向低估。 品牌名出现在「不包括 X」里会被误算;而「没有之一」「没有明显短板」这类褒义搭配又会被否定词表误杀。两个方向的偏差都要抽样核对,别只防一头。
• 位次强依赖竞品表的完整性。 代码只在你登记的竞品集合内排序。漏登记的候选会让名次系统性偏前——竞品表缺 5 家,你的「第 1」可能实际是第 6。竞品表要按每轮答案里实际出现的实体持续回填,报数时必须同时报「参与排序的候选数」。散文式回答里强行算位次是自欺欺人,宁可返回 None。
• 联网开关会改变一切。 同一个模型开不开联网检索,结果完全不同。这个状态必须写进观测元数据,否则事后无法解释指标跳变。
• 别名表要包含错写。 模型经常把品牌名写错一个字,漏掉这些会系统性低估提及率。
四、一组实测观察
下面是启航创服一个已脱敏客户项目的实测结果:同一组 100 条意图问句,在 7 个国内主流生成式引擎上采样,取数口径为近 30 天。采样与判定跑在该项目的评测流水线上,口径就是 3.1 和 3.2 写的那套。

图 1 同一组问句在七个生成式引擎上的表现差异
| 豆包 | 84% | 97% | 1% |
| 通义千问 | 79% | 96% | 1% |
| 腾讯元宝 | 78% | 96% | 1% |
| 文心一言 | 73% | 94% | 2% |
| 智谱清言 | 71% | 93% | 2% |
| Kimi | 68% | 92% | 3% |
| DeepSeek | 62% | 90% | 4% |
三点值得注意:
提及率的平台间极差达到 22 个百分点。 同一个实体、同一组问句、同一时间窗,最高 84%、最低 62%。这个差异主要来自检索栈而非模型能力——是否默认联网、检索源覆盖了哪些中文垂类站点、索引更新频率,都会直接体现在提及率上。
准确度的极差只有 7 个百分点,幻觉率的相对差异却接近 4 倍。 1% 和 4% 看着都不大,实际含义是:同样 100 次提及,后者会多出 3 条凭空捏造的描述。对需要长期维护公开信息一致性的团队来说,这才是要盯的数。顺带印证了 3.1 的那句话——准确度 + 幻觉率并不等于 100%,中间那段「有偏差但未捏造」才是日常最常见的形态。
所以,任何不交代口径的排名结论都没有意义。 只要允许挑平台、挑时间窗、挑问句子集,同一份原始数据可以讲出完全相反的故事。这也是为什么前面要把「问句池 / 平台集 / 时间窗 / 轮次 / 有效样本规则」全部固化下来——它不是流程洁癖,是让数字能被复核的唯一办法。
五、从一次性脚本到持续运行,会遇到什么
上面那套东西,写完能回答「今天怎么样」。但只要连续跑几个季度,就会撞上四个和算法无关、纯属工程与数据治理的问题:
1. 口径漂移。 换了个人维护,否定词表改了、有效样本规则松了,趋势图上就会出现一个来路不明的跳变。
2. 问句池版本管理。 业务变化必然要改问句。改完之后历史数据还能不能对比、怎么标注断点,需要机制而不是口头约定。
3. 样本留存。 三个月后有人质疑某个数,你得能把当时那条原始回答、时间戳、平台状态原样调出来。只存汇总指标等于没法自证——这也是骨架里 ts 和 meta 两个字段存在的理由。
4. 变更与结果的关联。 改了某个页面、补了某条信源之后,指标动没动?如果内容变更记录和监测记录是两套互不相干的系统,这个问题永远说不清。
这四件事都没什么技术难度,但不上工具就一定会散。第三条尤其容易被低估——3.3 骨架里 ts 和 meta 两个字段,就是被它逼出来的。这套流程后来被沉淀进了 InnoLyceum AI,不过用什么工具其实无所谓:Git 管问句池版本、对象存储留原始响应、一张表关联内容变更和监测记录,自己拼一样能跑起来。结论只有一个:如果打算长期做 GEO,先把口径和样本管起来,比多写几个爬虫重要得多。
六、必须说清楚的局限
• 结论有强时效性。 模型版本、检索栈、索引策略都在变,本文的实测数据只代表那个时间窗内的观察,不构成对任何平台的长期评价。
• 有限样本不等于全平台结论。 100 条问句覆盖的是一个特定行业的决策场景,换个行业结果可能完全不同。
• 引擎侧不可控。 索引与否、引用与否、如何排序,决定权在各平台。任何声称能「保证 AI 排名」「保证永久收录」的说法,在技术上都站不住。
• 别用批量生成的低质内容去污染信源。 短期或许有效,长期是负债——各平台对低质与农场内容的识别都在加强,而且被 AI 反复引用的错误事实,纠正成本远高于制造成本。
参考
• Schema.org 结构化数据词表:https://schema.org
• llms.txt 提案:https://llmstxt.org
• OpenAI 爬虫说明(GPTBot / OAI-SearchBot / ChatGPT-User):https://developers.openai.com/api/docs/bots
• Anthropic 爬虫说明(ClaudeBot / Claude-SearchBot / Claude-User):https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler
• Perplexity 爬虫说明(PerplexityBot / Perplexity-User):https://docs.perplexity.ai/docs/resources/perplexity-crawlers
• Google-Extended 说明:https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers#google-extended
• Meta 爬虫说明(meta-externalagent / Meta-WebIndexer / meta-externalfetcher):https://developers.facebook.com/documentation/sharing/webmasters/web-crawlers
• Common Crawl CCBot:https://commoncrawl.org/ccbot
• 第四章数据:北京启航创服科技有限公司的某个已脱敏客户项目,原始样本因客户保密不公开,口径见 3.1–3.2。
如果你也在做类似的评测,最想交流的其实是第三节那几个坑——尤其是否定语境识别和位次解析,这两块我目前的方案都还不够稳。欢迎在评论区聊聊你的处理方式。
关于本文 文中的指标口径、评测骨架与踩坑记录,来自启航创服 InnoLyceum AI 在企业级 GEO 运营与多平台 AI 可见度监测中的实际项目,不是实验室里的结论。模型与检索栈变化很快,照搬前请先按 3.2 重跑一遍自己的样本。
网硕互联帮助中心




评论前必须登录!
注册