# 拆解 Cerebras 企業知識庫 RAG：窄腰設計、檢索推理解耦與 Pipeline 的邊界


<!--more-->

## 前言

Cerebras 在 2026 年 7 月發表了一篇部落格文章，介紹他們內部知識庫系統「Cerebras Knowledge」的架構——這系統每天處理超過 15,000 次查詢，資料橫跨 Slack、Wiki、程式碼倉庫、Jira，服務對象還包含自動化流程與 AI agent，不只是人類。

這篇筆記整理了我讀完文章、又拿自己手上企業 RAG 系統的實戰經驗回頭對照後的心得。全文以「窄腰設計」這個心智模型為主軸，依序拆解資料抽象層、混合檢索與 reranking、查詢 planning、組織 scoping 這幾個階段；最後會補上兩個文章沒有直接講、但討論過程中自己推導出來的判斷——**檢索與推理必須解耦**，以及**這套架構本質上是單輪 pipeline，不是 agentic loop**。

{{< admonition abstract "重點摘要（TL;DR）" >}}
- **窄腰設計（narrow waist）**：異質資料（Slack、Wiki、程式碼、Jira）在寫入端收斂成一張只有五個欄位的 embeddings table，讀取端再從這個單純介面重新展開成各種檢索與編排策略，寫入與讀取因此可以獨立演化。
- **混合檢索靠 RRF 融合，不靠分數正規化**：Reciprocal Rank Fusion 只比較「名次」，天生跳過不同檢索器分數尺度不可比的難題，並用 `k=60` 這個常數把系統調成偏好多路共識、而非單路強訊號。
- **Planner 負責路由，不負責思考夠不夠**：LLM 被用來動態決定「這次該用哪些工具」，但整條處理線是決策一次、平行執行一次、synthesis 一次，沒有「證據不夠、回頭再查一輪」的迴圈——本質上是單輪 pipeline，不是 agentic loop。
{{< /admonition >}}

## 一個貫穿全文的心智模型：窄腰設計

如果只能用一個概念理解這整篇文章，我會選「窄腰設計」（narrow waist）。

想像一個沙漏：上半部是各種異質的資料寫入來源——Slack 討論串、Wiki 頁面、程式碼、Jira 票、甚至自訂資料庫；中間收窄成一個很單純的介面；下半部又重新展開成各種檢索與編排策略。這個「窄腰」在 Cerebras 的系統裡就是一張 embeddings table，只有五個欄位：`document`（要被搜尋的文字）、`embedding`（向量）、`metadata`（一個不透明的 JSON）、`source`（哪裡來的）、`timestamp`。

窄腰的價值不在於「統一」這件事本身好看，而在於**腰以上和腰以下可以各自獨立演化，互不干擾**。想新增一個資料源，只要寫一個 connector 把資料塞進這五個欄位，讀取端完全不用改一行；想換一種檢索策略，也只要在讀取端動手，寫入端毫無感覺。文章裡有一句話講得很直接：不管是 Slack 閒聊還是晶片線路表，只要進了同一張 embeddings table，就能透過同一套介面被查詢——這正是窄腰設計要達成的效果。

值得特別注意的是，這個「窄腰」的形狀在文章裡至少重複出現三次，不是只在資料寫入層出現一次而已：

| 出現位置 | 收斂前（寬） | 收斂後（窄腰） |
|---|---|---|
| 資料寫入層 | Slack、Wiki、程式碼、Jira 等異質來源 | 同一張 embeddings table |
| 讀取融合層 | 多個檢索器各自的排名 | RRF 融合成單一排序 |
| 多工具輸出層 | 不同工具吐出格式各異的結果 | executor 正規化成共同的 evidence schema |

三次出現的模式其實是同一件事：**先讓多樣性在某個點被收斂成一個單純、統一的形狀，再從那個點重新展開**。這個「寫入端追求統一、讀取端擁抱分化」的張力，幾乎是整篇文章所有技術細節的母題，後面每一節其實都在講這個張力在不同層面上的具體實作。

系統的基礎設施選擇也呼應了這個原則：embedding、原始摘要、metadata、以及同步狀態全部放在同一個 Postgres（用 pgvector，3072 維，HNSW 索引）。這個選擇有個複利效應，在處理程式碼嵌入時特別明顯——因為同步狀態跟 embedding 存在同一個資料庫裡，每次 commit 只需要重新處理變動過的那幾個 chunk，不用整個倉庫重算一次。

## 統一資料抽象層：連 Connector 都要收窄

有個陷阱是「形式統一但語意分裂」：如果 embeddings table 為了遷就每個來源，額外加上 `slack_channel`、`code_repo`、`jira_ticket_id` 這種專屬欄位，下游的檢索邏輯還是得針對來源類型寫一堆 if-else，那其實不算真正收窄。Cerebras 的做法是徹底收到底：下游邏輯永遠不需要知道一行資料是從哪來的才能處理它，`source` 欄位只在最後呈現結果給使用者時才派上用場。

判斷窄腰有沒有收乾淨，有個簡單的檢查點：如果下游的檢索或排序邏輯裡出現「if source == 'X' then...」這種分支，就代表窄腰還沒收乾淨。

每個 connector 只需要回答三個問題，而且**只有這三個**：資料怎麼變成 document 加 metadata（一個轉換函式）、怎麼從來源系統把資料拉過來（一個抓取函式）、多久同步一次（甚至可以像 Slack 一樣，每個頻道各自設定自己的頻率）。連 connector 都不知道下游會用什麼方式使用這份資料——這三件事裡完全沒有「怎麼檢索」，把「寫入與讀取解耦」這個原則具體落到資料層。

契約夠窄帶來一個組織層面的紅利：新增資料源這件事可以完全下放給不熟悉核心系統的團隊自己做。開一個 PR、附上一個小型 Python module，只要輸出符合共享 schema 的一行資料，其餘全部自動接上，不需要核心團隊介入。這其實是個蠻好的判準：如果新增一個資料源需要核心團隊介入才能完成，通常代表抽象層本身漏水了。

另外一個容易被忽略的細節是，`metadata` 欄位不只是拿來展示的附加資訊，它同時也是檢索排序訊號的載體——例如後面會提到的 age decay（隨時間衰減的排序機制）就是直接讀 metadata 裡的 timestamp。這代表 metadata 的設計必須在寫 connector 的當下就想清楚未來可能需要哪些排序訊號（時間戳、來源類型、作者或團隊資訊），不然事後想加，代價是要把整個 corpus 重新處理一次。

## 為什麼純向量搜尋接不住 Slack 對話

文章花了不少篇幅講 Slack 訊息怎麼被處理，原因是純向量搜尋在這種資料上有三種很具體的失效模式。

### 三種失效模式

第一種：embedding 的壓縮過程對「這段話有多少實際資訊」不敏感，只捕捉語意的方向。這代表一句廢話跟一句乾貨，只要語意方向都跟查詢接近，分數可能非常相近——向量搜尋分不出「這句話語意相關」跟「這句話有料」的差別。

第二種是個更隱蔽的幾何特性：短訊息在 cosine 相似度計算上會系統性佔便宜。原因是短訊息的向量方向比較集中明確，而長訊息因為包含多個子主題，向量其實是好幾個語意方向的加權平均，方向被稀釋掉了。這不是換一個 embedding 模型就能解決的問題，是這種表示法本身的特性。

第三種是上下文依賴——單獨一句「that fixed it」脫離對話脈絡完全沒有意義，這也解釋了為什麼處理單位要選在整個討論串（thread）的層級，而不是單一則訊息。

面對這三種失效模式，Cerebras 沒有隨便疊加幾種常見訊號了事，而是先精確診斷每種失效的根因，再找一個訊號的數學性質剛好不受那個根因影響：

| 失效模式 | 對應訊號 | 為什麼免疫 |
|---|---|---|
| 語意方向掩蓋內容價值 | IDF（詞彙稀有度） | 直接算詞彙稀有度，跟向量幾何無關 |
| 短訊息佔便宜 | 全文搜尋 | 精確 token 匹配，不受長短影響 |
| 上下文依賴 | 處理單位選 thread | 保留完整對話脈絡 |
| 答案會過期 | Age decay | 直接讀 timestamp，跟語意無關 |

這裡有個容易被忽略但很關鍵的前提：兩種訊號要疊加才有意義，前提是它們的失效模式必須「正交」。如果兩種訊號對同一種輸入都會失效，疊加根本沒解決任何問題，只是把同一個弱點算兩次。

### Distillation：先正規化再 Embed

在真正把文字丟進向量模型之前，系統還做了一步叫 distillation 的處理：不直接 embed 原始的討論串文字，而是先讓 LLM 讀完整條 thread，抽出四樣結構化資料——一句話問題、摘要、解決方案、系統與程式碼參照——正規化成一致格式之後才拿去 embed。原始文字則另外走 Postgres 的全文索引，保持可以被字面搜尋到，但不進向量空間。

這一步為什麼重要，原因在 embedding 這個表示法本身的特性：embedding 品質高度依賴輸入文字的形式一致性。一條討論串可能有 2 個人講 3 句話，也可能有 10 個人吵了 50 則訊息還離題，如果直接把這些長短、參與人數差異極大的原始文字拿去 embed，產生的向量在「什麼代表這個文件」這件事上根本無法互相比較。文章裡提到，討論串正規化成一致格式之後，準確率有實測上的顯著提升——雖然沒有附上具體數字，但這個方向本身已經被驗證過，不是憑空的設計偏好。這裡文章用的詞是「normalized」而不是「summarized」，強調的重點是讓向量在同一個表示空間裡語意一致，而不只是把文字弄短。這也給了一個蠻通用的判準：如果原始資料形式差異很大，該問的不是「要不要摘要」，而是「有沒有一個固定 schema，能把這些異質資料收斂成一致格式」。

### Bursting：把離群但有價值的旁枝訊息撈回來

不過 distillation 是有損壓縮，重要的旁枝訊息可能因為不在主線問答脈絡裡而被摘要忽略掉。系統用一個叫 bursting 的機制補救：把「同一個作者連續發的訊息」單獨抓出來，前面加上這條 thread 的主題當作上下文，單獨 embed 一次。但不是每個 burst 都值得留：必須通過三道品質門檻——IDF 要 ≥ 4.0（確保含有稀有詞，不是純廢話）、長度要 ≥ 200 字元（確保內容量足以撐起一個有意義的向量）、有 reaction 加分（用社交訊號間接驗證這則訊息有價值）。

舉個具體例子：一條有 4 位發言者、5 則訊息的討論串，最後可能產生「1 筆討論串層級的 distillation 產物，加上 N 筆通過品質門檻的 burst」，全部寫進同一張 embeddings table，靠 metadata 標記類型是 `slack_thread` 還是 `slack_burst`。沒通過門檻的 burst（比如純粹的社交填充語）則完全不會被 embed。

這裡有個工程上的心法值得記下來：與其等資料進了系統之後，靠後段昂貴的 rerank 去補救垃圾資料排太前面，不如在寫入時就用低成本規則先過濾一批。寫入時過濾是一次性成本，rerank 卻是每次查詢都要付的成本——越早把噪音擋掉，長期越省。

熟悉 Anthropic 的 Contextual Retrieval 的人可能會覺得 bursting 有點眼熟。Contextual Retrieval 的核心主張是為每個 chunk 生成一段客製化的上下文前綴，避免脫離語境的孤立片段。Bursting 其實是同一個原理的輕量版本：不用為每個 burst 都跑一次 LLM 去生成上下文，直接把整條討論串的主題貼上去當前綴，用便宜很多的方式達到類似的效果。

## 多路檢索排完名之後，怎麼變成一份排序

系統同時用好幾種檢索器打同一個查詢（向量搜尋、全文搜尋、IDF 加權等），但這帶來一個直接的麻煩：不同檢索器的分數完全沒辦法直接比較。BM25 的分數範圍不固定，cosine 相似度卻落在 -1 到 1 之間，單位、尺度、分布形狀都不一樣，加起來沒有意義。常見的解法是把每種分數都正規化到 0 到 1 之間再加權相加，但這也有陷阱：正規化依賴「這批結果裡的最大值、最小值是多少」，同一份文件在不同查詢批次裡的相對權重會因此飄移不定。

### RRF：只看名次，不看分數

Cerebras 的解法是 RRF（Reciprocal Rank Fusion），公式長這樣：

```
RRFscore(d) = Σ  weight_i / (k + rank_i(d))
              i∈檢索器
```

其中 k = 60（這個平滑常數來自 Cormack 等人在 2009 年那篇論文的建議值），weight 預設是 1.0。如果某個檢索器根本沒撈到這份文件，那一項貢獻就是 0——不加分也不扣分。

RRF 最核心的巧思在於：它只用「名次」不用「分數」。名次是所有檢索器都天然具備、而且天然可以互相比較的整數，這一招直接繞開了「分數尺度不可比」這個難題，不需要正規化，也就沒有正規化帶來的飄移問題。

k 這個常數其實是一個設計哲學的旋鈕。k 越大，頭部名次之間的分數差距被壓得越平，系統越傾向讓「多路都上榜、分數慢慢累積」贏過「單路衝到第一名」——也就是偏好共識勝過單一強訊號；k 越小，第一名拿到的獎勵越懸殊，系統越傾向相信單一檢索器給出的強烈信號。選 k=60，等於 Cerebras 選擇了「寧可相信多個獨立訊號的共識，也不要被單一路的巧合命中牽著走」。

實際動筆算一下就能感覺到這個效果：如果 A 文件在兩路檢索裡都排第三名，B 文件在一路排第一名、另一路卻只排到第八名，算出來的 RRF 分數常常是 A 贏——一份「兩邊都還算穩定」的文件，可能贏過一份「單邊很強但另一邊普通」的文件。這正是 RRF 獎勵共識、懲罰單路強訊號的具體數學效果。

### IDF 與 BM25 的關係

這裡有個容易搞混的地方：IDF 跟 BM25 到底是什麼關係？兩者常常被當成同一件事，但角色其實不一樣：

| | BM25 | 文章裡的 IDF 用法 |
|---|---|---|
| 角色 | 完整的 sparse 排序函數 | 單一加權訊號/品質閘門 |
| 輸出 | 一份排名 | 一個乘數，用來調整其他訊號 |
| 用途 | 完整的 sparse retrieval 那一路 | 額外壓低「語意接近但全是廢話詞」的結果 |

簡單說，IDF 本來就是 BM25 內部的一個組成零件，Cerebras 把這個零件單獨抽出來，另外當一個獨立訊號使用。文章裡講的「full-text search」對應到大家熟悉的 sparse retrieval（BM25）那一路；IDF 則是額外抽出來、跨所有路都能用的品質訊號。

{{< admonition tip "實務心得：把 IDF 額外抽出來當獨立訊號，處理對話型資料特別好用" >}}
如果系統已經在做 BM25 加向量的混合檢索，IDF 其實已經隱含在 BM25 內部了，但把 IDF 額外再抽出來當一個獨立的過濾或加權訊號，拿來專門壓低「語意上很接近查詢、但全是廢話詞」的結果，在處理對話型資料時特別好用——像 Slack 這種充滿社交填充語的資料，BM25 排名本身不會主動幫你濾掉那些在向量檢索裡浮上來的廢話，但一個獨立的 IDF 閘門可以額外做這件事，兩者不衝突，可以疊加使用。
{{< /admonition >}}

### 融合排序之後的收尾：去重、Rerank、補回上下文

融合排序算完之後，還有三個收尾動作。第一個是去重跟限額：同一份文件常常被切成好幾個 chunk，如果好幾個 chunk 都排進候選集合前段，會佔滿名額，讓資訊來源的多樣性下降，做法是限制同一份文件最多貢獻幾筆進候選集合。第二個是讓 reranker（一種 cross-encoder 模型）出馬：RRF 排序純粹是統計融合，不理解查詢的實際語意，而 reranker 會把查詢跟每一份候選文件逐一配對，直接輸出相關性分數，能做到 RRF 做不到的語意層面判斷。

{{< admonition info "澄清：Reranker 的「一次呼叫」是什麼意思" >}}
這裡有個技術細節值得澄清一下，免得誤會 reranker 的運作方式。「一次呼叫」reranker，指的是打包成一次批次 API 呼叫，但內部仍然對每個候選文件各自做一次「查詢配文件」的推論——比如 20 個候選就是 20 次配對，不是把所有候選一次塞進同一個 context 處理。這跟後面 synthesis 階段的 LLM 呼叫本質不同：

| | Reranker | Synthesis LLM |
|---|---|---|
| 輸入格式 | 每次只看（查詢，單一文件） | 一次看（查詢，所有候選文件） |
| 模型任務 | 打分數（分類/回歸） | 生成連貫文字 |
| 計算量級 | 小模型，快 | 大模型，context 長，較慢 |

整條處理線其實是「漸進昂貴」：越後面的步驟，單次呼叫成本越高。所以前面用便宜的步驟（RRF 融合、去重限額）盡量把候選集合縮小，把最貴的運算（synthesis）留給少量高信心的候選。
{{< /admonition >}}

最後一個收尾動作是補回上下文：拿到最終 top 10 之後，如果贏家是某份文件裡的一個 section，系統會額外撈回相鄰的兩個 section 一起呈現，避免 chunking 把前提條件或注意事項切到隔壁 chunk 而遺失掉。這個動作只發生在 rerank 之後、只對最終要呈現的少量結果做，避免在候選階段就浪費資源。

{{< admonition warning "RRF 的結構性弱點：投影片檢索的踩坑經驗" >}}
RRF 用久了會踩到一個結構性弱點，我自己在做投影片檢索時遇過。場景是這樣的：每頁投影片先用 VLM（視覺語言模型）產生文字摘要，再拿去 embed，搭配混合檢索。常出現的狀況是，正確答案的那一頁在向量檢索排第一名，但全文搜尋完全沒撈到它（反過來的情況也會發生）。融合完之後，正確答案排名反而不好。暫時的權宜解法是「永遠保留向量跟全文各自的第一名」。

往根因追下去會發現：RRF「獎勵共識」的機制隱含一個假設——每一路都至少「看得到」正確答案，只是排名高低不同。一旦正確答案在某一路完全缺席（連候選都沒進去），RRF 對那一路的貢獻是 0，「共識」機制反而變成「懲罰」，正確答案只靠單路分數撐著，可能輸給一份兩路都排名普通、但兩路都有出現的雜訊文件。投影片場景特別容易踩到這個問題，因為向量跟全文兩路**共用同一個不穩定的中介來源**——VLM 生成摘要時用詞不穩定，同一張圖可能寫「revenue growth」，也可能寫「sales increase」，導致全文搜尋這種對字面詞彙敏感的方法容易因為用詞不巧而完全掛零。兩路訊號源頭一致，正好破壞了前面提到「訊號要正交」這個前提。

解法可以由淺到深疊著用：先把各路的召回深度拉深（比如拉到 top 50 到 100），很多「單路缺席」其實是候選在截斷線之外被切掉，拉深召回可以把它撈回來，而且 RRF 對深處名次的貢獻本來就小，不容易引入額外噪音。再往下，可以修正表示層讓兩路訊號源頭不同——全文搜尋不該只索引 VLM 摘要，應該額外加入投影片頁面的原始文字（OCR 抽出的標題、bullet、圖表標籤），原始文字用詞穩定，不受 VLM 選詞不穩定的影響。最根本的做法是讓 reranker 補 RRF 的結構弱點：各路拉深召回後取聯集（不只信 RRF 分數），交給 cross-encoder 重新逐一打分——reranker 對查詢跟文件直接配對評估，不管來源路數，只要正確答案進了聯集就有機會被排到最前面，天生免疫「單路缺席」的問題。
{{< /admonition >}}

## 讓 LLM 做路由，而不是做苦工：Planner 與 Agent 編排

如果每次查詢都無腦呼叫所有工具，會帶來兩個直接的壞處：不需要的工具也被觸發，拖慢延遲、浪費成本；不相關工具吐出來的雜訊結果，還可能拉低整體排序品質。Planner 存在的第一原理，是把「這次該用哪些工具」從固定規則變成一個動態決策，讓成本跟延遲跟查詢的實際需求成正比。這種依查詢動態決定路由方式的思路，跟我們之前介紹過的 [UniversalRAG](../universal-rag/) 用 Router 依 modality 與 granularity 動態選路的原則，本質上是同一件事的不同應用場景。

### Planner：故意比它服務的工具還便宜

Planner 拿到三樣輸入：一份工具目錄（只需要精簡的能力描述，不需要完整技術文件或內部實作細節）、使用者的查詢本身、以及當前鎖定的查詢範圍（對應後面會講到的 Projects 概念）。輸出則是一組工具選擇清單，交給 executor 平行執行。

Planner 被故意設計得比它服務的工具還便宜——它做的是分類任務（從有限選項裡選一個或幾個），不是生成任務，所以可以用小模型、結構化輸出，推理成本跟延遲都低，是整條處理線裡最便宜的一環。這其實是個蠻重要的架構原則：決策層要故意比執行層便宜，不然「用來省錢的決策」本身如果太貴，設計就自相矛盾了。

對 Planner 而言，工具永遠是個黑盒子——它只需要知道工具的能力描述，不需要知道工具內部是簡單查表，還是需要額外做一輪推理才能給出結果。這讓不同性質的工具可以在同一套 planner 邏輯下被平等看待。

{{< admonition tip "想通的一個問題：SQL 查詢算不算違反單輪 synthesis？" >}}
這點讓我想通一個之前一直卡住的問題。處理需要 SQL 查詢的問題時，常見流程是「先檢索表格 schema，再用一次 LLM 做 SQL reasoning 取出相關數據，最後再用一次 LLM 基於數據產生文字回答」。這乍看跟文章「單次 synthesis 產生最終答案」的做法矛盾——如果檢索工具本身需要內部推理才能拿到證據，單輪 synthesis 架構還適用嗎？

關鍵在區分「查表（lookup）」跟「計算（compute）」。文章列出的工具——搜尋、搜尋 Slack、搜尋程式碼、查詢誰懂這個領域等等——檢索的對象都是已經被 embed 或索引好、預先算好、躺在那裡等人拿的靜態資料，檢索動作本質上是唯讀撈取，撈出來就是最終形態的證據，可以直接進融合排序，不需要額外推理。SQL 場景本質不同：「把相關數據取出來」這件事本身需要當場生成 SQL、執行，才能得到答案，這是一個不可省略的推理步驟，不是唯讀撈取。

判準是看這次 LLM 呼叫的產出性質：如果產出是「證據」（下游還要再加工），可以保留這一步，但要**藏在檢索工具內部**——SQL 生成該留在 `search_sql` 這個工具內部，作為取得查詢結果數據的必要步驟，對外只暴露它吐出的數據列。如果產出是「面向使用者問題的部分答案」（比如針對單一資料源提前生成的摘要文字），那一步就該砍掉，延後到管線最末端的單次 synthesis。對 Planner 而言，`search_sql` 跟其他工具一樣只是一個依能力描述被選用的黑盒子，不需要知道內部是 lookup 還是 compute。
{{< /admonition >}}

### 兩種對外介面：MCP 與 Web UI

系統對外提供兩種介面，共用同一套原語，但設計目的完全不同。MCP 介面刻意不包含 planner，只暴露最基本的檢索原語，工具被設計得盡量簡單、盡量不依賴 LLM，由外部 agent（比如 Claude Code）自己決定呼叫順序跟怎麼組合結果。Web UI 介面則相反，把 planner、executor、synthesizer 全部包起來，對使用者呈現一個單純的問答介面。

這個差異背後的理由很直接：MCP 的使用者是另一個具備推理能力的 agent，不該替它預先做決策——如果工具背後偷藏一層 planner，外部 agent 對工具行為的預期就會失控，因為工具行為不再是輸入的確定性函數，而是取決於一個它看不到的隱藏決策層。Web UI 的使用者是人類，人類不會自己組合工具呼叫，系統必須替他把整套流程包好。

這給了一個值得記住的延伸判斷：如果系統未來想被其他 agent 呼叫使用，就必須把「檢索原語」跟「編排決策」拆成兩層、可以獨立暴露的介面——這其實是「檢索與推理解耦」原則在更高一層（編排層）的重演，跟 [HiRA](../hira/) 與 [Plan-and-Act](../plan-and-act/) 裡 Planner 和 Executor 被拆成獨立角色的設計精神是相通的。

Executor 扛兩個職責：平行執行被選中的工具（前提是工具之間沒有依賴關係，本身是無狀態的檢索原語），以及把不同工具吐出的原始格式正規化成一套共同的 evidence schema（包含分數、新鮮度、來源提示等）。這是窄腰設計在讀取端、多工具輸出整合層的第三次出現。

## 知識庫變大之後：Projects 與 Scoping

「全域搜尋」這個假設，規模一大就會系統性地惡化。corpus 越大，噪音對排序品質的侵蝕越嚴重——原本不相關的文件，只要跟查詢沾到一點語意邊或字面邊，就有機會擠進候選集合前段，把真正該出現的答案往下推。這不是檢索演算法變差了，而是「查全部」這個假設本身，隨著資料量成長會系統性地讓訊噪比變差。麻煩的是這個現象很隱性：小規模測試的時候一切正常，上線之後隨著資料成長悄悄劣化，很難定位到底是哪個技術環節出了問題。

Cerebras 的解法是 Projects：一個 project 就是一組具名的資料源 bundle。關鍵字是「引用」而不是「複製」——同一個資料源可以被多個 project 引用，不需要真的複製一份資料出來。這代表 project 本質上是查詢時的邏輯過濾條件（例如對 metadata 裡的 `source` 欄位做範圍限制），而不是一個新的資料容器。這個設計避開了資料更新一致性的風險，也避開了儲存成本隨 project 數量線性成長的問題，同樣體現了「寫入端與讀取端獨立演化」的原則——新增一個 project，完全不需要碰任何資料寫入邏輯。

Scoping 跟前面兩個機制有直接的交互關係。跟 reranking 的關係是：scope 過濾應該在 RRF 融合之前就先執行，不是等融合、rerank 都做完才篩選，不然會浪費排序運算在 scope 外的資料上，還可能讓 top-K 名額被 scope 外的文件佔掉。跟 planner 的關係是：當前鎖定的 scope 是 planner 決策的輸入之一，不只是後續檢索的過濾條件——它同時限縮「該查哪裡」跟「該不該用某個工具」，比如某個 project 底下沒有掛任何自訂資料庫，`search_sql` 這類工具大概率就不該被考慮。

至於新使用者要怎麼知道該選哪個 scope，Cerebras 的做法是把預設 project 直接綁在使用者的 profile 上。如果要求使用者自行選擇 scope，等於把「系統該懂的組織知識」轉嫁給使用者，這其實是本末倒置——新使用者根本不需要先搞懂公司內部有哪些資料源分組，就能自動拿到高訊號的答案。這是「把系統複雜度留在系統裡、不外洩給使用者」這個原則，在使用者體驗層面的具體體現。

## 這篇文章沒說的：單輪 pipeline 跟 agentic loop 的邊界

以上這些技術細節都很扎實，但讀完整篇文章，我發現有一個邊界文章完全沒有觸及，而且重要到值得單獨拿出來講——這套架構的本質，其實是一個**單輪（single-round）pipeline**，不是 agentic loop。

### 單輪架構的兩個結構性限制

文章描述的完整流程是：查詢進來，planner 決策一次，平行執行一次，synthesis 一次，結束。這個結構性地不支援兩種很常見的複雜需求。第一種是「發現資訊不夠、需要再查一輪」：synthesis 或某個中間步驟得能判斷「證據不夠」，然後回頭再發一次檢索，但文章的 executor 是 fan-out 完就結束，沒有「評估夠不夠、不夠再來一輪」的回饋路徑。第二種是「先查某個、再根據結果查另一個」的序列依賴：文章明確採用平行執行，而平行的前提正是工具彼此獨立、互不依賴，如果工具 B 的輸入依賴工具 A 的輸出，在「平行 fan-out」這個模型下根本沒辦法表達。

這兩個問題表面看是兩件事，往根源追其實是同一件事：**系統缺少「狀態」和「多步推理」**。單輪架構隱含的假設是，這次查詢需要哪些證據，在一開始（planner 決策的那一刻）就能被完全決定；但真實情況常常是，「我還需要查什麼」這件事，往往要看過第一輪結果之後才會知道——這是認知順序上的本質問題，不是工程細節沒做好。

拿一張表把兩種範式的差異攤開來看會更清楚：

| | 文章的架構（RAG pipeline） | 需要迭代/序列時的架構（agentic loop） |
|---|---|---|
| 決策次數 | 一次，決定完不再變 | 多次，每輪結果可能觸發下一輪決策 |
| 工具間關係 | 獨立，可平行 | 可能有依賴，需要序列 |
| 何時停止 | 固定流程跑完就停 | 由判斷邏輯動態決定「夠了嗎」 |
| 本質 | 一個流程（pipeline） | 一個會自己決定下一步的 agent |

嚴格說，這篇文章做的不是「agentic RAG」，而是「用 LLM 做動態路由的傳統 RAG pipeline」。Planner 雖然用了 LLM，但它的智慧僅止於「這次該用哪些工具」，不包含「這次夠不夠、要不要再來一輪」。這裡想強調一個容易混淆的判斷：**用了 LLM 做決策，不等於 agentic**。Agentic 的核心特徵是迴圈跟狀態，而不是決策過程裡有沒有 LLM 參與。

這不代表文章的內容沒有用。文章給的不是「怎麼做迭代」的答案，而是「迭代迴圈裡每一輪該怎麼做好」的答案。如果要建構一個支援迭代跟序列依賴的系統，形狀大致長這樣：

```
while 資訊不夠 or 還有下一步要查:
    agent 決定下一步要查什麼、用什麼工具    ← 文章未觸及
    執行檢索                              ← 文章教如何做好
    評估「這次查到的夠不夠、下一步要查什麼」  ← 文章未觸及
    合併進累積的證據池
synthesis 讀完整個累積證據池產出答案       ← 文章有講(單輪版本)
```

外層的 while 迴圈，以及「決定下一步」的推理，屬於 agentic 系統設計的範疇，文章完全沒有碰。但迴圈內部「怎麼把單輪檢索做好」的技術——資料處理、多路訊號融合排序、單輪內的 planner 工具選擇——完全適用、不會過期。單輪架構的品質，決定了迴圈裡每一次迭代的品質；迴圈存在與否，決定了系統能不能處理需要多步推理的複雜問題。這兩層彼此正交。

### 該往哪個方向補強：兩個判斷問題

如果要判斷該往哪個方向補強，有兩個問題可以問。第一個問題是「資訊不夠」的判斷發生在哪一層：如果是 synthesis 讀完證據後才發現不夠，需要在 synthesis 之後加一個 self-critique 或 sufficiency check 步驟，讓它輸出「還缺什麼」，再回頭餵給 planner 觸發下一輪；如果是使用者追問才觸發，那屬於對話管理的範疇，不是需要迴圈的迭代問題，單輪 pipeline 每次追問重跑一次就夠了。第二個問題是序列依賴到底是「有限幾種固定模式」還是「真的需要 agent 臨場規劃」：如果模式有限（比如固定「先查人、再查該人的 PR」這種兩步流程），不需要完整的 agentic loop，可以把這個序列寫成一個複合工具，內部固定執行順序，對外仍然是 planner 眼中的單一黑盒工具；如果序列依賴是開放式、沒有固定模式、需要 agent 臨場判斷下一步，那才是真正需要 agentic loop 的情況，此時文章整套 planner/executor/synthesis 架構，最多只能降級成 agent loop 裡「單輪執行」的一個子模組。

## 從自己系統的坑回頭看：檢索與推理必須解耦

讀這篇文章對我最大的價值，其實不是上面列的任何一個技術細節，而是拿它回頭對照自己手上系統之後看清楚的一個架構問題。

### 原本系統的問題：檢索與推理耦合

我原本的系統以「領域/客戶」作為分化維度，每個領域綁定固定的幾種檢索方法——比如某個領域固定用三種檢索方法打三種資料庫，再合併結果。這種設計規模一大就暴露出兩個問題。第一，不同領域即使有相同類型的資料或問題，也很難共用檢索方法，因為方法是綁死在領域維度上的。第二，同一個領域內只要新增一種資料類型，就得開發一種新的檢索方法，分化的粒度綁在領域層級，沒辦法對單一查詢的意圖做動態調整。

往根因追下去，這兩個問題共同的病灶是「檢索與推理耦合」。原本的融合流程是這樣的：三種檢索各自呼叫對應的推理方法（比如 SQL 推理、圖文推理），各自產生一份摘要，最後再用一次 LLM 合併三份摘要。等於每一路檢索都內建了完整的推理與摘要流程，檢索跟推理在架構上根本分不開——這導致沒辦法跨領域共用（推理邏輯焊死在檢索上），也沒辦法用動態 planner 取代領域綁定（因為每一路不是純粹的檢索原語，而是一整套完整的問答套件）。

文章帶來的重構方向很清楚：把檢索跟推理解耦，檢索工具只負責吐出原始證據，不做推理、不做摘要，推理只在管線最末端的單次 synthesis 發生。畫成圖大概是這樣：

```
原有架構：
  檢索A → 推理A(如SQL) → 摘要A ┐
  檢索B → 推理B(如圖文) → 摘要B ┼─→ LLM合併 → 最終答案
  檢索C → 推理C        → 摘要C ┘

文章架構：
  檢索A(唯讀撈取) ┐
  檢索B(唯讀撈取) ┼→ 融合排序(RRF, 純數學) → rerank(1次小模型) → 單次synthesis → 答案
  檢索C(唯讀撈取) ┘
```

### 解耦之後的具體收穫

這個轉變帶來幾個具體的好處。跨領域難共用的問題被解決了，因為解耦之後，檢索工具變成乾淨、無狀態、不綁推理的原語，天生可以跨領域共用。新增資料類型要重新開發檢索方法的問題也解決了，配合 planner 機制，新增資料類型只需要新增一個檢索工具、更新它的能力描述，planner 自動就會學會在合適的查詢選用它，不需要動既有的推理邏輯。

更根本的是修復了跨源推理的能力。原架構裡「每一路各自完整推理再摘要」的做法，會導致沒有任何一個 LLM 同時看過全部證據，如果正確答案需要跨源交叉推理（比如「A 的一句話加上 B 的一張圖」），原架構結構性地做不到。文章架構的單次 synthesis 讓所有證據攤在同一個 context 裡一次推理，天生就支援跨源推理。另外原架構「摘要的摘要」（每一路先摘要、最後再合併摘要）是兩層有損壓縮疊加，下游完全無法察覺上游丟失了什麼細節；文章架構只在最末端壓縮一次，證據在此之前保持原始形態，誤差不會累積兩次。

當然不是所有「檢索前的推理」都該被消除。前面提過的 lookup vs compute 判準在這裡同樣適用：如果某類檢索的本質是計算而非查表（像 SQL 生成需要當場推理才能取得數據），那次推理應該保留，但要藏進檢索工具內部，只對外暴露「吐出查詢到的證據」，而不是「產生面向使用者問題的部分答案」。

## 結論

這篇文章示範了「窄腰設計」如何在異質的企業資料場景裡，同時解決寫入端的可擴展性與讀取端的檢索品質問題——這個原則從資料抽象層、讀取融合層、多工具編排層，到組織 scoping 層反覆出現了四次，是最值得內化的單一心智模型。

拿它跟自己現有系統對照過一輪之後，最大的收穫是兩個文章沒有直接寫出來的判斷。第一，檢索與推理必須解耦：把推理徹底移出檢索路徑，只在管線末端的單次 synthesis 發生，才能同時解決「跨領域難共用」與「新資料類型要重複開發」這兩個問題。第二，這整套 planner-executor-synthesis 架構本質上是單輪 pipeline，能把「一輪檢索做扎實」，卻處理不了「證據不足需要迭代」或「查詢之間有序列依賴」這類需要狀態與多步推理的問題——那是 agentic RAG 的範疇，文章完全沒有觸及。

最後留一個閱讀方法論：技術細節的價值不在於照搬，而在於拿自己的實戰場景逐一檢驗背後的第一原理是否成立——就像投影片檢索裡 RRF 因訊號源頭不正交而失效的案例。這種對照式閱讀，比單純理解文章本身，更能帶來可直接落地的架構判斷力。

