我一直在进行一项小型月度研究:从一个行业中选取十个购买意图问题,向四位 AI 助手提出相同问题,并记录每个助手实际读取的每个来源。

分析是最简单的部分。设置重叠、统计域名,完成。

从四个不同的 API 中提取引用,使其具备可比性——这才是我浪费了一个周末的地方。之所以写下来,是因为当我需要时,我在任何地方都找不到这些内容。

问题

所有四家提供商都会告诉你它们使用了哪些 URL。但它们在何处放置、如何命名,甚至“URL”是什么上,都不一致。

以下大致是每个提供商给你的内容。(2026 年中期的形状——它们会变化,复制之前请查看文档。)

OpenAI,使用网页搜索工具的 Responses API。引用以文本输出的注释形式出现:

const urls = [];
for (const item of resp.output ?? []) {
  for (const block of item.content ?? []) {
    for (const a of block.annotations ?? []) {
      if (a.type === "url_citation") urls.push(a.url);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Anthropic,使用网页搜索工具的 Messages API。搜索结果作为独立于文本的内容块类型返回:

const urls = [];
for (const block of msg.content ?? []) {
  if (block.type === "web_search_tool_result") {
    for (const r of block.content ?? []) {
      if (r.url) urls.push(r.url);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Gemini,使用 Google Search grounding。它位于候选项下方的 grounding metadata 中:

const urls = (resp.candidates?.[0]?.groundingMetadata?.groundingChunks ?? [])
  .map(c => c.web?.uri)
  .filter(Boolean);

Enter fullscreen mode Exit fullscreen mode

Perplexity 是最友好的——响应上有一个扁平数组:

const urls = resp.citations ?? resp.search_results?.map(r => r.url) ?? [];

Enter fullscreen mode Exit fullscreen mode

四种不同的嵌套深度,三种不同的同一概念的词语(“annotation”、“search result”、“grounding chunk”),其中一个根本不给你 URL。稍后会详细说明。

所以:每个提供商一个适配器,规范化为 { engine, question, urls[] },永远不要让提供商形状的数据越过这个边界。事后看来显而易见。我一开始没有这样做,后来后悔了。

陷阱 1:Gemini 的 grounding URI 不是来源

这是可能会悄无声息地破坏我的数据的问题。

Gemini 的 groundingChunks[].web.uri 不是发布者的 URL。它是通过 Google 自己的 grounding 端点的重定向。如果你按字面意思理解,Gemini 的每条引用看起来都来自同一个域名,而你的“Gemini 阅读了哪些网站”分析结果将只返回一个网站。

你必须解析它:

async function resolveFinal(url) {
  try {
    const r = await fetch(url, { method: "HEAD", redirect: "follow" });
    return r.url;
  } catch {
    return url; // keep the original, flag it, move on
  }
}

Enter fullscreen mode Exit fullscreen mode

从大量执行中得出的两点注意事项。HEAD 就足够了,而且比 GET 便宜得多——你只需要最终 URL。并积极缓存,因为相同的重定向会反复出现,你不希望对任何东西造成冲击。

我注意到这一点是因为 Gemini 的数据在第一次运行时看起来很荒谬。值得构建一个当一个引擎的域名多样性接近零时发出警告的合理性检查——这几乎总是一个提取错误,而不是一个发现。

陷阱 2:同一页面有五种“服装”

一旦你有了真实的 URL,它们仍然不会相互匹配。以下所有 URL 都是同一页面:

https://www.example.com/clinic/
http://example.com/clinic
https://example.com/clinic?utm_source=chatgpt.com
https://example.com/clinic#reviews
https://example.com/Clinic

Enter fullscreen mode Exit fullscreen mode

如果你在比较集合,同一页面的五种拼写意味着五个“不同”的来源,而你的重叠数字会比实际情况低。如果你的整个发现是“它们的重叠出奇地少”,这正是你不希望非强制性错误指向的方向。

我最终使用的规范化器:

const TRACKING = /^(utm_|fbclid|gclid|msclkid|ref|source$)/i;

function canonical(raw) {
  const u = new URL(raw);
  u.protocol = "https:";
  u.hostname = u.hostname.replace(/^www\./, "").toLowerCase();
  u.hash = "";  for (const k of [...u.searchParams.keys()]) {
    if (TRACKING.test(k)) u.searchParams.delete(k);
  }
  u.pathname = u.pathname.replace(/\/+$/, "") || "/";
  return u.toString();
}

Enter fullscreen mode Exit fullscreen mode

故意将路径小写——在许多服务器上路径是区分大小写的,我宁愿过度计数也不愿合并两个真正不同的页面。

一个让我发笑的点:几个助手会将 ?utm_source=chatgpt.com(或它们自己的等效项)附加到它们返回的 URL 上。这些工具正在标记自己的引荐流量。删除它,否则你将拥有同一页面的引擎特定重复。

然后是分析,这真的很无聊

每个问题的集合重叠,跨引擎成对进行:

const jaccard = (a, b) => {
  const A = new Set(a), B = new Set(b);
  const inter = [...A].filter(x => B.has(x)).length;
  const union = new Set([...A, ...B]).size;
  return union ? inter / union : 0;
};

Enter fullscreen mode Exit fullscreen mode

Jaccard 在这里有一个真正的缺陷:它将只被读取一次的来源与每个引擎在每个问题上都读取的来源完全相同地对待。我在加权它方面来回摇摆,尚未说服自己。如果你有更好的“这两个系统是否查阅了相同证据”的指标,我真的很想听听。

数据呈现的样子

本月的运行是美容和美学诊所。十个问题,九个城市,四个引擎。

  • 214 个不同网站,仅在十个问题中被读取
  • 75% 的网站仅被一个引擎读取,其他引擎未读取
  • 同一问题上任意两个引擎之间的重叠:7%
  • 十个问题中恰好有 1 个问题产生了所有四个引擎都读取的网站
  • 它们读取的内容中约 13% 是目录;其余是企业自己的网站

最后一点在不同行业之间发生了变化,这出乎我的意料——在上个月的律师事务所运行中,目录域名主导了列表的顶部。

我立即产生的反对意见,你可能也有:LLM 是非确定性的,所以两次调用当然不同——这可能是噪声。

所以我对每个引擎的子集重新运行了三次,并将每个引擎与自身进行了比较:

  • 同一引擎,再次询问:51% 重叠
  • 不同引擎,同一运行:7%

与自身的一致性比与竞争对手的一致性高约 7.5 倍。确实存在运行到运行的方差,但它远小于引擎之间的差距。差异是结构性的,而不是抽样的。

如果你想从另一个方向来探讨这个问题,我单独写了一篇单个助手是否能看到给定网站——同样的问题,一次一个引擎。

我无法声称的内容

一个垂直领域的十个问题是样本,而不是普查。方差控制是重新运行两次的问题三次,而不是整个集合。“读取”仅意味着 URL 出现在该响应的引用中——我看不到权重,或者页面中有多少内容实际上很重要。所有这些都是 2026 年 7 月的快照;这些系统会在你面前发生变化。

给任何在此基础上构建的人的要点

如果你正在编写任何比较跨提供商检索的内容,请为提取和规范化分配大部分时间,而不是分析。我的粗略分配最终是 80/20 偏向于无聊的部分。

并尽早构建合理性检查。我的两个真正错误——重定向错误和跟踪参数错误——在输出中是不可见的。代码运行良好。数据只是错误的,方向恰好迎合了我的假设。这是值得偏执的那种错误。

如果只带走一点,更广泛的观点是:将“AI 可见性”视为单一渠道在接触数据后无法成立。四个引擎从同一个问题中构建了几乎完全独立的阅读列表,所以这是四个问题,而不是一个。

本月运行的完整报告,包括逐个问题的细分,在这里。如果您想检查我的计数,我很乐意分享原始 JSONL——我宁愿被纠正也不愿被引用。

(披露:我在此领域构建一个工具。该研究未设门槛,也无需注册。)