AI 引用四成可溯源:API 文档引用链与 GEO 摘要设计
AI 引用只有约四成能追到一手研究,症结是数字缺少机器可读的证据字段。把每条结论拆成声明、证据、版本锚点三段,用响应头与 JSON 同源输出并让断链扫描进 CI,可溯源率能拉到九成以上。
- 每条可引用数字必须带 source、method、measured_at、version 四个字段
- 证据 URL 用 301 长链加日期版本,禁用短链和 commit hash
- 摘要首句 40~80 字,带主体、数字、环境,AI 才能直接摘
我把自家 API 文档里的 60 个数字逐个回溯,能走到一手测量记录的只有 24 个,四成。可验证引用链属于数据结构问题;仅把它当成写作规范并不够:每个对外数字都得自带 source、method、measured_at、version 四个字段,缺一个就等着被 AI 当成永久事实反复引用。
为什么 AI 引用只有四成能落到源头?#
Trakkr 今年做了一次我认同的抽样:拿 200 个需要具体数字的问题去问 ChatGPT、Perplexity、Gemini、Claude 和 Google AI Overviews,然后点开答案里的引用链接,一层一层往下点,最多五层。有效样本 1,285 条统计,各助手配额是 ChatGPT 108、Perplexity 277、Gemini 300、Claude 300、AI Overviews 300。结论是只有约四成能走到有样本、有方法、有日期的一手页面,其余都停在某个转述页。原始报告在 Trakkr Research 的 Zombie Stats。
最典型的那条链是这样的:五个助手对“多少比例的销售在第一次跟进后就放弃”给出同一个 44%。AI Overviews 引到某销售公司 2020 年的 stats 页面,那个页面的 source 指向一个自由撰稿人的样稿,样稿里列了两份幻灯片、一个博客列表、两篇新闻,没有一份是跟进行为的研究。链到这里断掉。数字还在继续传播。
我们的文档里有没有同款?有。写“接口平均响应 30ms”却不写机器规格、并发数、测量日期,和那个 44% 是同一类东西。区别只在于我们比那个自由撰稿人更容易被找到。
- AI 抽取的单位是句子,不是整页。一句没有主语和时间的数字被摘走之后,没人能验证。
- 转述层比一手层更容易被抓取。文档页通常比压测脚本、原始 JSON 更容易进索引。
- 改版即断链。URL 一变、章节锚点一改,旧引用指向 404,模型只会换一个来源,不会帮你修。
我的判断很直接:可溯源率上不去,是因为证据字段没有写进发布流程;AI 太笨并不是原因。
引用链拆成三段:声明、证据、锚点#
现在每条对外结论我都拆成三段,缺一段就不发布。声明段是一句能被单独摘走的短句,控制在 40 到 80 字,带主体、数字、时间。证据段是一个稳定 URL,指向测量脚本、原始 JSON 或压测日志,不能是另一个转述页。锚点段是版本号加时间戳,同时写进响应头和页面可见区域。
| 字段 | 类型 | 示例 | 缺失后果 |
|---|---|---|---|
| source_url | string | https://www.api.plus/doc/get | AI 只能引到首页或域名 |
| measured_at | ISO 8601 | 2026-10-29T08:00+08:00 | 数字被当成永久事实 |
| method | string | wrk 4.2.0,500 并发 × 10 分钟 | 别人无法复现 |
| version | string | doc-get-2026.10.29 | 改版后引用链断 |
| window | string | 最近 7 天滚动窗口 | 口径含糊,数字互相对不上 |
接口本身也该回传这套东西。GET 示例接口的调用如下:用 -D 把响应头单独存下来,用 -o 把 body 存成文件,后面校验脚本要分开解析。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
curl -sS -D /tmp/api_plus_headers.txt \
"https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8" \
-o /tmp/api_plus_get.json
head -n 12 /tmp/api_plus_headers.txt
cat /tmp/api_plus_get.json
参数逐个说清楚:apikey 是你在 API.PLUS 注册后拿到的 Key,放在 query 里是为了 curl 调试方便,生产环境建议改成走 Header,避免 Key 落进 Nginx access log;ip 是要查询的 IPv4 地址,这里用 8.8.8.8 便于任何人复现;-sS 让 curl 静默但保留错误输出,-D 落响应头,-o 落 body。预期返回形如:
HTTP/2 200
content-type: application/json; charset=utf-8
x-data-source: https://www.api.plus/doc/get
x-data-window: rolling-7d
x-cite-version: doc-get-2026.10.29
cache-control: public, max-age=60, stale-while-revalidate=600
{
"code": 200,
"msg": "success",
"data": {
"ip": "8.8.8.8",
"country": "United States",
"region": "California",
"isp": "Google LLC"
},
"generated_at": "2026-10-29T08:00:00+08:00"
}
字段以实际返回为准,重点是那三个自定义头。x-data-source 指向数据来源,x-data-window 说明统计窗口,x-cite-version 是文档版本。没有它们,模型引用你的接口示例时只能引到域名,而域名页面里可能有几十个接口,引用精度直接掉到零。
如何让统计页面的每个数字都带证据?#
沐小悠统计这类页面最容易犯的错是把数字渲染成纯文本。访客看得到,爬虫看得到,但没有任何机器可读的证据挂在上面。我的做法是每个指标配一份 JSON 描述,HTML 渲染和 JSON 输出共用同一个数据源,页面改版不会让两边不一致。
下面这个 Python 3.12 脚本做的事很笨但有效:把统计页的指标清单拉下来,逐个 HEAD 证据 URL,比对 ETag 和新鲜度,输出通过率。
import json, urllib.request, datetime
BASE = "https://t.api.plus"
DOC = "https://www.api.plus/doc/get"
MAX_AGE_DAYS = 90
def head(url, timeout=5):
req = urllib.request.Request(
url, method="HEAD",
headers={"User-Agent": "cite-check/1.0 (+https://www.api.plus)"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.headers.get("ETag"), r.headers.get("X-Cite-Version")
def check(item):
try:
status, etag, version = head(item["source_url"])
except Exception as e:
return "broken", str(e)
if status != 200:
return "broken", f"status={status}"
if etag and item.get("etag") and etag != item["etag"]:
return "mismatch", f"etag {item['etag']} -> {etag}"
age = (datetime.date.today()
- datetime.date.fromisoformat(item["measured_at"][:10])).days
if age > MAX_AGE_DAYS:
return "stale", f"age={age}d"
return "ok", version
items = json.load(urllib.request.urlopen(f"{BASE}/metrics.json", timeout=8))
stat = {"ok": 0, "stale": 0, "broken": 0, "mismatch": 0}
for it in items["metrics"]:
k, _ = check(it)
stat[k] += 1
stat["pass_rate"] = round(stat["ok"] / len(items["metrics"]), 3)
print(json.dumps(stat, ensure_ascii=False))
参数与预期输出:MAX_AGE_DAYS 设 90,超过三个月没复核的压测数据我不让它出现在摘要里;timeout 设 5 秒,300 个指标并发跑约 6 秒,串行跑大约 40 秒,我选后者,因为 CI 里串行更好定位失败项。跑完输出形如:
{"ok": 271, "stale": 26, "broken": 11, "mismatch": 4, "pass_rate": 0.868}
那 11 条 broken 里,9 条是改版把 /doc/get#response 改成了 /doc/get#response-body,锚点失效;2 条是证据页被 CDN(Content Delivery Network,内容分发网络) 缓存了 7 天,HEAD 拿到的 ETag 和记录值对不上。26 条 stale 全是半年前的压测结论。取舍很明确:宁可少一条可引用数字,也不要一个半年没复核的命中率挂在页面上。
恢复手段也在同一套流程里:锚点失效的加 301 跳到新锚点,ETag 对不上的把证据页 Cache-Control 降到 max-age=300,超过 90 天的强制标“历史数据”并从摘要区摘除。
GEO(Generative Engine Optimization,生成式引擎优化) 摘要和普通 meta description 有什么区别?#
普通 meta description 是给人看的导语,GEO(Generative Engine Optimization,生成式引擎优化) 摘要要能被单独摘出来当答案。区别集中在首句、口径和实体三处。
| 对比项 | 普通 meta description | GEO(Generative Engine Optimization,生成式引擎优化) 摘要 |
|---|---|---|
| 首句 | 概括页面主题 | 完整结论,带数字与主体,40~80 字 |
| 主语 | 常用“本平台”“我们” | 写全称:API.PLUS、沐小悠API、Nginx 1.26、PHP 8.3 |
| 口径 | 通常不写 | 每个数字后面跟测量环境与窗口 |
| 结构 | 一段话 | 一句一结论,可配表格 |
| 时效 | 很少标注 | 带 measured_at,过期自动降级 |
同一件事的两种写法,差距一眼能看出来:
差:接口性能优秀,缓存大幅提升了响应速度。
好:API.PLUS 的 GET 示例接口在 2026-10-29 的复测中 p95 为 42ms,环境为 Nginx 1.26 + PHP 8.3、4C8G 单机、500 并发,Redis 7.2 缓存命中率 91.3%。
第二种写法模型能直接摘,因为它自带主语、时间、环境。第一种摘出去就是废话,模型不会引,人工审核也看不出真假。三条硬规则:
- 首句必须是完整结论,40 到 80 字,带主体和数字。
- 每条结论后面跟一句口径,说明测的是什么、什么环境、什么窗口。
- 实体写全称,不写“该服务”“本平台”,代词在摘录场景里没有指向。
Nginx 层把版本和时间写进响应头,版本号跟着 CI 走:
location = /doc/get {
add_header X-Data-Source "https://www.api.plus/doc/get" always;
add_header X-Cite-Version "doc-get-2026.10.29" always;
add_header X-Data-Window "rolling-7d" always;
add_header Link '<https://www.api.plus/doc/get>; rel="canonical"' always;
add_header Cache-Control "public, max-age=60, stale-while-revalidate=600" always;
}
location = /metrics.json {
add_header Cache-Control "public, max-age=30" always;
add_header Access-Control-Allow-Origin "*" always;
}
两个容易踩的点。第一,add_header 默认不给 4xx/5xx 加头,必须加 always,否则错误响应页在回溯时看不到版本信息。第二,统计页的 max-age 别给太长,我见过给 3600 的,结果页面显示 91.3%、证据页写 89.7%,这种不一致被模型抓到一次,纠正成本比重新发一篇文档高得多。文档页 60 秒加 600 秒 stale-while-revalidate,统计 JSON 30 秒,这个组合我们跑了两个月,回源压力可控。
五道上线检查与两个真实翻车#
流程落到 CI 里就是五步,每步都有可量化的阻断条件。
- 断链扫描:所有证据 URL 跑 HEAD,非 200 直接阻断发布,容忍阈值 0。
- 锚点校验:把 source_url 的 fragment 拿去做 DOM 匹配,匹配不到的记 broken。
- 新鲜度:measured_at 超过 90 天标 stale,超过 180 天从摘要区移除。
- 一致性:页面渲染值和 JSON 值做 diff,误差超过 0.1 个百分点报警。
- 可摘录测试:把摘要首句单独粘出来读一遍,缺主语或缺时间的打回重写。
翻车一:有次把 CI 的短 commit 当版本号写进 X-Cite-Version,形如 a3f9c21。两周后分支 squash,那个 hash 在仓库里查不到了,外部引用全部失去指向。改成日期化版本 doc-get-2026.10.29 之后稳定,同日多次发布就加序号 -2、-3。
翻车二:证据页用了 302 短链跳转。回溯脚本跟到短链就停了,因为短链本身没有内容,也没有 Last-Modified。改成 301 直连长 URL,短链只留给人点。这个改动让 broken 从 11 条降到 1 条。
还有一类不是 bug 而是取舍:统计类接口的实时数字要不要进摘要。我的答案是实时数字进页面、不进摘要。摘要用 T+1 的稳定值,因为摘要会被引用几个月,而实时值每 30 秒变一次,引用者拿到的永远和当下对不上。
写在最后#
四成这个数字不难改。把每个数字的证据、口径、版本当字段存下来,页面和 JSON 同源输出,断链扫描进 CI,可溯源率能到九成以上。API.PLUS 自己的文档正在按这套流程重写,API 文章中心里每篇压测结论都要求带环境和日期,没带的不发。
想动手验证,去 API.PLUS 注册就能拿到免费 API Key,用上面的 curl 直接打 GET 示例接口,或者换 随机动漫壁纸接口 看图片类接口的响应头和缓存策略怎么写;POST 类可以参考 POST 示例接口。Key 或配额出问题走 技术支持。做登录和流量分析的团队可以顺手看看 沐小悠聚合登录 与 沐小悠统计,这两个产品的指标页同样按引用链规则输出,字段命名和本文一致,接起来不用二次适配。