iT邦幫忙

2026 iThome 鐵人賽

DAY 7
1
Claude AI

從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層系列 第 7

檢索是怎麼回事:切塊、向量,找出來而不是記起來

  • 分享至 

  • xImage
  •  

上一篇的結論是一句原則:窗口裡要放「最小的高訊號詞元集合」。

原則很好,但它沒有回答最實際的那個問題:那一小段,是怎麼被找出來的?

這是第三層「外部知識」的第三篇,也是這一層的深水區。今天把檢索拆開:一份文件怎麼被切成塊、一段文字怎麼變成一串數字、「相關」到底是怎麼算出來的。而且會碰到一個洞:這條路上有一段,Claude 不做。


30 秒實驗

這個實驗不用寫程式,也不用金鑰。

第一步:打開你自己的一份長文件,找一句只有這份文件才答得出來的事實,例如某個參數的預設值。

第二步:從文件的第一個字開始往下數 400 個字元(character),在那裡畫一刀;再數 400 個,再畫一刀,一路切到底。空白、換行、全形標點,每一個都算一個字元,因為切塊器就是這樣數的:它拿到的是一長串字元,不是你眼睛看到的那個版面,段落與標題在它眼裡只是幾個換行符號。不想自己數的話,wc -m 會告訴你整份文件有幾個字元;在編輯器裡把一段選起來,狀態列也會報選了幾個字元。

然後看那句話落在第幾刀跟第幾刀之間,問自己兩個問題:

  • 那句話完整地落在同一塊裡,還是剛好被切成兩半?
  • 那一塊裡面,有沒有交代這句話在講什麼?如果那句話寫的是「預設值是 30 秒」,但「這是哪個參數」寫在上一段、而上一段被切到前一塊去了,那這一塊被找出來也沒有用。

你剛剛做的就是切塊(chunking),而你剛剛擔心的兩件事,正是這一篇要量的東西。

真正的切塊器數的不一定是字元,也可能是詞元(token),而兩者差很多:第 4 篇那張圖裡,61 個字元被切成 38 個詞元。單位選哪一個,會改變刀落在哪裡,但不會改變這個實驗要你看的那兩件事。


找出來,不是記起來

第 5 篇借過 2020 年那篇 RAG 論文的分法:參數記憶是模型本身,非參數記憶是一個可以查的索引。當時說「你今天做的上傳,就是最土法的非參數記憶:沒有索引,也沒有挑選,整份送進去」。

今天補的就是被省略的那兩件事。完整的檢索長這樣,三步:

  1. 切塊:把文件切成一段一段。
  2. 向量化(embedding):把每一塊變成一串數字,讓「意思相近」變成「距離相近」。
  3. 查詢時算距離:把問題也變成一串數字,找出最近的幾塊,塞進窗口。

第三步結束之後,故事就回到第 5 篇:那幾塊變成 messages 裡的內容,跟著問題一起送進去做一次推論。模型永遠只看得到送進去的那些字,檢索改變的是「哪些字有資格被送進去」。

所以這一層真正的槓桿在前面兩步,而它們都不在模型裡。


那個洞:向量化這一段,Claude 不做

這件事官方文件寫得很白(2026-09-20 查)。Embeddings 那一頁第一句就是:

Anthropic 沒有自己的向量化模型。

這是這個系列走到現在,第一次出現 Claude 不負責的一段。官方的處理方式是轉介:文件指向 Voyage AI,並且明講「這份指南接下來講的是 Voyage AI,但你應該自己評估多家供應商」。

實務上你會用到的規格是這些(2026-09-20 查):

面向
一般用途模型 voyage-4(品質最好的是 voyage-4-large,最省的是 voyage-4-lite
單次輸入長度 32,000 詞元
向量維度 預設 1024,可選 256/512/2048
領域模型 程式碼 voyage-code-3、金融 voyage-finance-2、法律 voyage-law-2

三個實作上會踩到的細節:

  • 問題和文件要用不同的 input_type 官方要求檢索情境一定要指定 input_type="query""document",不要省略。它做的事很樸素:在你的文字前面偷偷加一句提示詞,查詢那邊加的是 Represent the query for retrieving supporting documents: ,文件那邊是 Represent the document for retrieval: 。同一段文字,當問題和當文件,產生的向量不一樣。
  • Voyage 的向量長度被正規化成 1,所以內積(dot product)跟餘弦相似度(cosine similarity)等價,而內積算得比較快;餘弦相似度與歐氏距離排出來的名次也一樣。
  • 量化(quantization)可以換儲存空間。 int8 省 4 倍,binary 省 32 倍。知識庫大到一定程度,這一條會變成成本問題。

程式碼只有這麼長:

import voyageai

vo = voyageai.Client()   # 讀環境變數 VOYAGE_API_KEY

doc_embds = vo.embed(chunks, model="voyage-4", input_type="document").embeddings
query_embd = vo.embed([query], model="voyage-4", input_type="query").embeddings[0]

# 向量已正規化,內積就是餘弦相似度
similarities = np.dot(doc_embds, query_embd)
best = np.argmax(similarities)

向量到底是什麼

向量化之後,每一塊變成 1024 個浮點數。你可以把它想成在一個 1024 維的空間裡放了一個點。三張圖把整件事走一遍:

左:四段文字各自的十六維權重,一格一個維度;中:同樣四個向量壓到平面上,意思近的方向也近;右:每一段跟問題的餘弦相似度,由高到低排

左邊是四段文字各自的向量,一格一個維度。「文件 A」跟「問題」的花紋比較像,而框起來的那一欄,四段文字都有值。

中間是把同樣四個向量壓到一個平面上。意思近的方向就近,而「近」是用夾角量的:文件 A 跟問題幾乎同方向,講部署測試的文件 C 幾乎垂直。

右邊就是檢索在做的事:把夾角換算成一個數字,由高到低排,取前幾名送進窗口。0.77、0.44、0.07 是從左邊那些權重實算出來的。

這張圖是示意。 圖上的向量是我為了說明捏出來的,不是任何模型的真實輸出,所以別拿那三個數字去對任何一家的結果(投影與相似度倒是從圖上那些數字實算的)。可以帶走的是形狀:一格一個權重、方向決定相關、相似度是算出來的一個數字。

圖上看得到的是形狀,底下這三件事決定了後面所有的取捨:

  • 那 1024 個數字,單獨看沒有任何意義。 沒有哪一維代表「法律」或「情緒」。有意義的只有點跟點之間的相對位置,所以你永遠不會去讀一個向量,只會拿兩個向量來比。
  • 「意思相近就位置相近」不是它自己長出來的,是訓練出來的。 訓練時餵的是成對的句子,告訴模型哪些該靠近、哪些該推遠。所以它學到的「相近」,就是訓練資料裡被判定為相近的那種相近。你的領域如果跟訓練資料差很遠,這個假設就會鬆掉,而這也是為什麼會有程式碼、金融、法律的專用模型。
  • 同一段文字沒有唯一的向量。 換一個模型就換一套座標,兩邊的向量完全不能混用。換模型等於整個索引重算,這件事沒有人會提醒你。

「相關」是怎麼算出來的

查詢的時候做的事只有一件:把問題也變成一個點,然後找離它最近的幾個點。 因為 Voyage 的向量長度被正規化成 1,算距離就是一個內積,快到可以對幾十萬塊即時算完。

這個距離量的是「看起來像同一類文字」,不是「這段話能回答這個問題」。 兩者高度相關,但不相等。所以檢索永遠會回傳「最像的幾塊」,就算那幾塊裡面根本沒有答案,它也會回傳,而且不會告訴你。

上一篇講可分心性的時候提過詞面與語意的推理落差,這就是它在向量端的版本:詞面的相似度算得出來,語意上的可用算不出來,而你拿到的排名是前者。

取幾塊(top-k)也是一個你要自己決定的參數。 取多了,答案比較可能在裡面,但窗口被佔掉更多;取少了則反過來。底下那份論文量的是取前 10 名與前 100 名兩種情況,而這兩種量出來的名次不一樣,等一下就會看到。

維度可以砍,但它同時是天花板

官方文件那張表裡,維度寫的是「預設 1024,可選 256/512/2048」。同一個模型為什麼可以吐出不同長度的向量?

因為它是用 Matryoshka 表徵學習訓練的(Kusupati et al., 2022,名字就是俄羅斯娃娃)。這種訓練法「把資訊以不同的粒度編碼進去」,粗的資訊排在前面、細的排在後面,所以你直接砍掉尾巴、只留前 256 維,剩下的仍然是一個可用的向量。論文在 ImageNet 上報的效果是:同樣的準確度,向量最多可以小 14 倍,檢索也快上同一個量級。官方文件給的截斷範例就是這麼做的:切前 N 維,再重新正規化。

但維度不只是成本,它同時是表達能力的上限。 On the Theoretical Limitations of Embedding-Based Retrieval(Weller et al., ICLR 2026)從學習理論推出一個很硬的結論:「能夠被某個查詢取出來的前 k 名文件組合,數量受限於向量的維度。」 也就是說,不管模型多強,有些文件組合就是不可能被同一個查詢取出來,因為那個空間裝不下那麼多種排法。

作者做了兩件事來證明這不是紙上談兵:他們直接拿測試集去最佳化向量,限制依然存在;又做了一個叫 LIMIT 的資料集,任務本身很簡單,目前最強的模型照樣失敗。他們的結論是這是單一向量這個範式的根本限制,不是資料不夠或模型不夠大。

檢索答不出來的時候,加大模型不一定有用。 它有可能不是「找不夠準」,而是「這個組合根本不在可取出的集合裡」。這也是為什麼實務上不會只靠一個向量,而會再接一層重排序(re-ranking),或混搭關鍵字檢索。

前六篇你只需要一個供應商;從這一篇開始,你的檢索品質綁在另一家公司的模型上,而它換版本的節奏、定價、退役時程都不歸 Claude 管。這不是缺點,是一個要自己做的決定:向量化要不要綁單一供應商,還是用開源模型自己跑。 Voyage 的 voyage-4-nano 是 Apache 2.0 授權、放在 Hugging Face 上,這條路開著。

這一篇講的是通用機制。 切塊、向量、距離,換成任何一家都一樣。Claude 在這條鏈上只負責最後一端:拿到那幾塊之後回答,並且標出處(那是下一篇的主題)。中間這一段是別人的,我照官方文件轉介的那一家寫。


切塊:這一步比你想的更能決定答案

切塊聽起來像雜事,但它決定了被找出來的單位長什麼樣子。到 2026 年,這件事終於有一份夠硬的公開研究:When Is Complex Chunking Worth It?(Caspari et al.,帕紹大學與 IT:U Linz,已被 ACM CIKM 2026 接受,CC BY 授權,程式與資料集都公開)。

它的規模是重點:八種切法、兩個語料(MS MARCO 來的 CoRE,以及維基百科來的 KILT 配 Natural Questions 的問題)、三個開源向量模型(Qwen-0.6B、embeddinggemma-300M、Snowflake-L V2),語料規模從 1 萬份逐級放大(CoRE 實際切到 100 萬份,KILT 切到全部 600 萬份)。量的不只是準不準:索引吞吐量、查詢吞吐量、尖峰記憶體都一起量,而且每一組比較都跑 Fisher 隨機化檢定(一萬次排列、對 28 組兩兩比較做 Bonferroni 校正),確認差距不是雜訊。

底下是 embeddinggemma 在 CoRE 上的 Recall@100(取前 100 名時,該找到的文件有沒有在裡面),按 100 萬份那一欄排序:

切法 1 萬份文件 100 萬份文件
每塊貼上標題(enriched title) 82.55 57.27
固定長度(token,含重疊) 82.73 57.09
句子邊界(sentence) 82.55 56.00
每塊貼上全文摘要(enriched summary) 82.36 55.64
只存一份摘要(summary) 79.82 51.82
晚切(late chunking) 80.18 50.54
語意切塊(semantic) 80.00 48.00
情境化切塊(contextual,用模型替每一塊寫脈絡) 82.18 貴到跑不完

三件事值得記下來:

  • 語料小的時候,切法幾乎不重要。 1 萬份那一欄從 82.73 到 79.82,全部擠在 3 個百分點裡。語料放大到 100 萬份,差距才張開成 9 個百分點(57.27 對 48.00),論文也觀察到「索引越大,切法之間的顯著差異越多」(不是完全單調:最大的 600 萬份反而比 100 萬份少)。你在小資料上比出來的結論,不一定撐得到上線。
  • 貴的切法沒有比較好。 語意、晚切、只存摘要、情境化這四種很少在統計上贏過簡單的 baseline,而它們每一種都要多付模型的錢。最貴的情境化切塊在 CoRE 上根本沒能跑到 100 萬份,作者直接寫明是成本問題。
  • 最穩的改善是最笨的那一個:如果文件有標題,把標題貼在每一塊前面,成本幾乎是零,卻在多數設定下都排在前面。(底下那組例子會讓你看到為什麼:一塊文字帶著自己的標題,才說得出自己在講什麼。)

五種切法,差在誰決定下刀的位置

切塊的方法大致有五種,由笨到聰明排,而越聰明的越貴

切法 誰決定下刀的位置 代價
固定長度(fixed-size) 數字:數到 N 個字元或詞元就切 最快,但完全不看內容
遞迴切塊(recursive) 一組有優先順序的分隔符:段落不行才句子,句子不行才硬切 一樣快,邊界合理得多
結構切塊(structure-based) 文件本來就有的標記:Markdown 標題、HTML 標籤、表格、程式碼區塊 文件沒結構就退化成固定長度
語意切塊(semantic) 相鄰句子的向量距離:話題變了才下刀 整份文件要先跑一輪向量化
代理式切塊(agentic) 模型:讀完整份文件再標出切點 每份文件付一次模型的錢,論文估 1 萬份的語料就要 10 到 15 美元

講再多不如看刀落在哪裡。拿這份文件當例子,問題是「連線逾時預設多久」:

## 連線設定

`request_timeout` 控制一次請求最久等多久,預設值是 30 秒。超過就丟 `TimeoutError`。

## 重試

預設不重試。要開的話設 `max_retries`,上限 5 次,間隔指數成長。

固定長度:跟你剛剛手動做的一樣,數到第 N 個字元就切,不看那裡是什麼。

…控制一次請求最久等多久,預設值是 30 ✂ 秒。超過就丟 `TimeoutError`。

「30」跟「秒」被分到兩塊去,兩塊都答不出那個問題。開頭那個 30 秒實驗,要你比的就是這個。

遞迴:先找空行,找不到才退一步找句號,所以刀只落在邊界上(上面那張表裡最接近它的是「句子邊界」那一列)。塊放得下的時候,標題會跟著本文一起走:

[塊 1] ## 連線設定
       `request_timeout` 控制一次請求最久等多久,預設值是 30 秒。超過就丟 `TimeoutError`。
[塊 2] ## 重試
       預設不重試。要開的話設 `max_retries`,上限 5 次,間隔指數成長。

看起來跟照結構切一樣,但它並不知道那一行是標題,只知道那裡有一個空行。同一節的內容一長、超過塊的上限,它就會挑一個句號切開,而後面那一塊不會再帶著「## 連線設定」。

結構:照標題切,所以每一塊都保證帶著自己的標題,節再長也一樣。這份文件本來就有標題,這一刀幾乎不用調。

語意:把相鄰的句子各自向量化,距離拉開的地方才下刀。

[塊 1] `request_timeout` 控制一次請求最久等多久,預設值是 30 秒。
       超過就丟 `TimeoutError`。
       ↑ 這兩句都在講逾時,向量距離近,不切
✂ 這裡話題換了,距離拉開,下刀
[塊 2] 預設不重試。要開的話設 `max_retries`,上限 5 次,間隔指數成長。

文件沒有標題、段落又亂的時候,這是唯一還找得到邊界的方法。

代理式:把整份文件交給模型,請它讀完再說該切在哪。它切出來的結果通常跟結構切塊很像,差別在於沒有標題可以照著切的時候它也切得出來,因為它讀得懂「30 秒」是在講哪一個參數。代價是每份文件一次模型呼叫。

兩個提醒。第一,那篇論文測的八種裡沒有結構切塊,因為它太依賴文件長什麼樣子,很難用同一組語料公平比較。沒有被測到不代表它差,你的文件如果是 Markdown 或程式碼,它往往是最省事又最準的那個,而且論文裡成績最穩的「每塊貼上標題」,做的正是同一件事的簡化版。

第二,這五種是可以疊的,實務上最常見的組合就是先照結構切一刀,塊還太大再用遞迴切。

要一個起點的話,論文的建議很直白:先用固定長度。 原文說它「因為簡單、索引快、省記憶體,而且成績常常不輸人,在多數大規模檢索場景仍是很強的預設值」;需要保住句子邊界再換句子切塊,代價是索引變慢;文件有標題就把標題貼上去,那是他們測出來最便宜的改善。

但沒有一種切法在所有設定下最好。 同一張表換一個向量模型、換一個語料、換一個語料大小,名次就會動,論文自己的結論句是「最好的方法取決於向量模型、資料集、語料大小與你要的那個指標」。所以切塊參數不能離開你的向量模型與你的語料單獨調。

你量哪一個數字,贏家就換人

上面那張表量的是 Recall@100:取前 100 名,該找到的有沒有被撈進來。論文同時量了另一個指標 NDCG@10:只看前 10 名,而且排得越前面分數越高。

換一個指標,名次就換一批人。 論文的說法是:用 NDCG@10 的時候,那些替每一塊補上文件層級資訊的做法(貼標題、貼摘要)比較強,因為它們擅長把最該排前面的那幾份推上去;改用 Recall@100,固定長度與句子切塊就追了上來。作者的結論是,一個在「直接排名」不夠好的切法,拿去當「第一階段撈回來」可能剛剛好。

這件事對你的意義是:先想清楚你要的是哪一種。撈回來之後還要再排一次(重排序,這一層後面會講),你該看的是召回率;直接把前幾塊送進窗口,你該看的是前幾名準不準。

而前幾名不準的代價,上一篇已經替你算過了:不相關的字進了窗口就要付注意力,就會變成第三種失效「可分心性」的材料。

檢索不是「找得到就好」,是「找得到,而且不要順便拖一堆垃圾進來」。

這份研究也要打幾個折。 它量的是開放領域的英文語料(MS MARCO 與維基百科),你的文件不長那樣;吞吐量與記憶體是他們那套實作、那台機器、那個 FAISS 設定上的數字,只能拿來比大小,不能當成絕對值;最貴的那幾種切法在最大的語料上跑不完,所以大規模的行為只看到一半;而且每一種切法都用同一組固定超參數,沒有各自調到最好,論文自己說這可能低估了個別方法的上限。


這一層的代價

你的系統多了兩個你不太看得到的元件。 切塊器跟向量模型都不在模型裡,也都不會報錯。它們出問題的樣子就是「答案怪怪的」,跟提示詞寫壞了長得一模一樣。

抄來的參數不會是你的最佳解。 那篇論文最實用的結論就是這個:沒有一種切法在所有設定下最好,名次會隨著向量模型、語料、語料大小與你看的指標一起動。你在別人文章裡讀到的塊大小,是別人語料上的答案。

多了一個供應商,就多了一條依賴。 向量化那一段不在 Claude 手上:它的版本、定價與退役時程你都管不到,而換掉它就要把整個索引重算一次。

還有一個問題,這一篇沒有回答:你怎麼知道自己的檢索到底好不好?上面那些數字是別人在 MS MARCO 與維基百科上量的,不是你的


這一篇多了什麼,又多付了什麼

多了什麼能力:知識庫不再是黑箱。你知道它被切成塊、變成向量、按距離取回,也知道這三步各自可以調什麼。

多付了什麼代價:兩個沉默的新元件(切塊器、向量模型)、一個新的供應商依賴,以及一組你還不知道怎麼在自己語料上量的數字。


下一篇

檢索品質決定答案品質:召回率怎麼量、出處怎麼標。

這一篇的數字全是別人在公開語料上量的,而你的語料沒有人量過。

第 4 篇立的規矩是每上一層就用同樣的十題重跑一次,而第 5 篇重跑的時候刻意繞過知識庫、把那份筆記直接附在題目前面,理由寫在文章裡:知識庫會多出「有沒有被搜到」這個變數,而那一層要量的不是它。下一篇要處理的就是那個變數。

兩件事:在還沒有使用者、也沒有標註資料的情況下,怎麼自己生出一組「問題→正確段落」的配對,把召回率從別人的數字變成你的;以及答案回來之後,怎麼讓模型把出處標出來,讓你回得去核對它到底讀了哪一塊。順帶一提,今天官方那句「你應該自己評估多家供應商」沒有給方法,那筆帳也在下一篇還。


延伸閱讀


上一篇
脈絡工程:窗口裡該放什麼,以及它壞掉的四種方式
下一篇
檢索品質決定答案品質:召回率怎麼量、出處怎麼標
系列文
從 LLM 到 Agent:用 Claude 拆解現代 AI 工程的每一層9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言