我一直在進行每月小型研究:從單一產業中挑選十個買方意圖問題,請四個 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,它們彼此之間仍然無法匹配。以下全部是同一個頁面:

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

刻意將路徑小寫——路徑在許多伺服器上是區分大小寫的,我寧願多算也不要合併兩個真正不同的頁面。

讓我笑出來的一件事:幾個助手會在回傳的 URL 後面附加 ?utm_source=chatgpt.com(或它們自己的等效參數)。工具在標記自己的推薦流量。把它移除,否則你會有引擎特定的重複頁面。

接著是分析,這部分真的很無聊

每題的集合重疊,跨引擎兩兩比較:

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——我寧願被糾正也不願被引用。

(揭露:我在這個領域建立了一個工具。這項研究沒有設限,也不需要註冊。)