RAG 已死?不,是 Grep 強勢回歸!

近來「RAG 已死」的說法甚囂塵上,例如《長上下文窗口、Agent 崛起,RAG 已死?》、《The RAG Obituary: Killed by Agents》等文章都在討論這件事。而像 Claude Code、Codex 這類新一代的 Agent CLI,也紛紛放棄了 embedding 技術,官方甚至直接承認:不建索引、不用向量資料庫,全靠 LLM 驅動的 Grep 就夠用了。RAG 真的不適合現今的 Agent 了嗎?圍繞著這個問題,我們展開了深入的調研,同時針對 Claude Code 等尖端解決方案的原始碼進行拆解,最終形成本文,力求回答 RAG 在 Agent 時代是否還佔有一席之地這個核心關切。

01、從 Claude Code 的 Grep 談起

Claude Code 的創建者 Boris Cherny 在多個公開場合都提到過這個令許多人意外的事實:Claude Code 不使用 RAG,不使用 embedding,也不建立索引。其核心搜尋機制是靠 LLM 驅動的 Grep(Grep 是 Unix 系統下的文字搜尋工具,給予一組正則表達式,它就會在檔案中逐行尋找匹配的內容)。

在 X/Twitter 上,他說得很直接:

「Early versions of Claude Code used RAG + a local vector db, but we found pretty quickly that agentic search generally works better.(早期版本的 Claude Code 使用了 RAG 加上本地向量資料庫,但我們很快發現,智慧體式的搜尋通常效果更好)」

在 Pragmatic Engineer 的採訪中,他更進一步說明:

「Plain glob and grep, driven by the model, beat everything.(單純由模型驅動的 glob 和 grep,擊敗了一切)」

Anthropic 官方的 Context Engineering 部落格也確認了這套架構:Claude Code 使用 Grep 和 Glob 將程式碼動態載入到上下文(context)中。這個選擇並非憑空想像,Boris 在 Pragmatic Engineer 的採訪中提到,他在 Meta 時曾觀察到 Instagram 的工程師在 IDE 的 click-to-definition 功能故障後,所有人都退回使用手動 Grep 來搜尋程式碼。不過他也在 Latent Space 播客中坦承,放棄 RAG 的決策部分是基於直覺。儘管 Anthropic 表明了他們不使用 RAG,而是採用 agentic search 加上 Grep,但 Grep 具體如何調用、LLM 怎麼決定搜尋什麼、工具調用循環的長相如何,這些實作細節都沒有公開。

2026 年 3 月,Claude Code CLI 的一份原始碼快照因外洩而被公開。我去看了一下,發現確實如 Boris 所說,原始碼中完全沒有任何與 embedding、vector、similarity search 相關的實作。但更有意思的是,這套零索引的內容搜尋機制是怎麼具體實作的。

這篇文章就是基於這份原始碼以及一些行業實踐,從實作層面拆解 Claude Code 的程式碼搜尋機制及其背後的設計哲學。本文會先拆解 Claude Code 如何驅動多輪的 Grep 循環(第二章),再看暴力搜尋為什麼在本地專案上速度夠快(第三章),最後與 Cursor 等業界方案進行對比,討論 Grep 方案的真實成本與收益(第四章)。為了讓整套機制不至於停留在抽象的層次,我們用一個貫穿全文的例子來看。Claude Code 除了終端機 CLI 模式外,還有網頁版和桌面版,而這些模式下它會透過一個叫做 bridge 的遠端控制系統運行,由伺服器端的 session 執行實際工作。假設你在讀這份原始碼時產生了一個好奇:當 LLM 調用 GrepTool 進行搜尋時,bridge 是怎麼追蹤和記錄這次工具調用的? 你請 Claude Code 幫你從原始碼中找出答案,接下來會發生什麼事?

02、LLM 驅動的多輪循環搜尋機制

Claude Code 的程式碼搜尋可以用一句話概括:LLM 自己決定要搜尋什麼、要用什麼工具搜尋、搜到後要不要繼續搜,直到資訊充足為止。 這裡沒有預設的搜尋流程,沒有固定的工具調用順序,一切都由 LLM 在執行期間決定。這一章會先說明搜尋循環是如何運作的、循環中有哪些工具可用,然後深入最核心的 GrepTool,看它如何控制回傳的資訊量。最後用開頭提出的那個實戰問題,完整走一遍多輪搜尋的過程。

2.1 搜尋循環與工具

整套搜尋機制的核心是一個循環:將使用者輸入內容和可用工具列表傳給 LLM,LLM 會回傳文字或工具調用請求。如果是工具調用,執行工具後會把結果附加到對話歷史中,然後帶著更新後的完整歷史再次調用 LLM。LLM 會基於不斷增長的上下文來決定下一步該做什麼,直到它認為資訊已足夠、直接生成文字回覆而不再調用工具,循環就自然結束了。這個循環也有強制退出的機制:達到最大輪次上限、超出預算限制、使用者中斷、或是工具調用被權限拒絕。

圖片

這個循環對所有工具都是平等的。LLM 可以在任何時候調用任何工具,甚至可以在一次回應中同時調用多個工具。 沒有那種硬性規定的「必須先搜尋再讀取」。與程式碼搜尋相關的核心工具有以下四個:

工具

底層實作

作用

GrepTool

ripgrep (rg)

正則搜尋檔案內容

GlobTool

glob 模式匹配

按檔案名稱/路徑模式尋找檔案

FileReadTool

Node.js fs

讀取指定檔案的指定行範圍

AgentTool

獨立 LLM 對話

啟動子 agent 進行多步探索

此外還有 LSP(Language Server Protocol)工具,用「go to definition」、「find references」等語意精確的操作來補足 Grep 的不足。但核心的搜尋架構是建立在這四個工具之上。

其中 AgentTool 比較特殊:它不是直接搜尋檔案,而是啟動一個獨立的子 agent,讓子 agent 在自己的 context window 裡完成一整套搜尋任務,最後只把結論回傳給主對話。子 agent 有多種類型,與搜尋最相關的是 Explore 類型:它只配備搜尋和讀取工具(Grep、Glob、Read),不能編輯檔案、不能執行指令、不能巢狀啟動新的 Agent,是一個純唯讀的搜尋專家。

子 agent 的核心價值在於 context 隔離。它從零開始構建自己的對話歷史,不繼承主對話的訊息,這意味著它搜尋過程中產生的大量 grep 結果、程式碼片段都會留在自己的 context 裡,主對話只會收到一段總結性的文字結論。對於需要大範圍搜尋的任務,如果在主對話裡直接搜尋,幾輪 grep/read 下來,context 可能就被中間結果塞滿了。因此交給子 agent 處理後,主對話的 context 只會增加一條結論訊息。

2.2 GrepTool 的資訊量控制

在一般的實踐中,LLM 最常用的模式是「先定位,再深入」:先用 Grep/Glob 找到相關檔案,再用 Read 讀取具體內容。但 Grep 搜到檔案後,是不是每次都得再接一個 Read 才能使用? 不一定。關鍵在於 GrepTool 有三種輸出模式,回傳的資訊量完全不同:

  • files_with_matches 模式(預設):只回傳匹配的檔案路徑列表,不包含任何程式碼內容。例如搜尋 "class.*Transport" 會回傳 cli/transports/WebSocketTransport.ts、cli/transports/SSETransport.ts 這樣的路徑。LLM 拿到的只有檔案名稱,所以在這種模式下通常需要再接 Read 才能看到具體程式碼。 這也是為什麼預設模式會設計成只回傳檔案名稱,也就是故意控制資訊量,避免一次 Grep 就讓大量程式碼湧入 context window,讓 LLM 自己判斷哪些檔案值得深入讀取。另外還有一個保護機制:head_limit 預設為 250,即使搜到 10,000 條匹配也只回傳前 250 條,防止搜尋結果淹沒 context。

  • content 模式:回傳匹配行及其上下文程式碼。例如 Grep({pattern: "TOOL_VERBS", output_mode: "content", "-C": 5}) 會直接回傳匹配行前後各 5 行的程式碼片段。對於很多情境,例如確認一個常數的值、看一個函式簽章、檢查某個 import 是否存在,這些片段就夠用了,不需要再 Read 整個檔案。

  • count 模式:只回傳每個檔案的匹配數量,用於快速評估搜尋詞在專案中的分佈密度,不回傳具體內容。

所以實際的工具組合方式是靈活的:Grep(預設模式)→ Read 是最常見的路徑,但 Grep(content 模式)也可以獨立使用,LLM 也可以直接調用 Read(如果已經知道檔案路徑),或者一次同時發起多個 Grep 進行平行搜尋。這種靈活性是刻意的設計。其思想是用軟引導代替硬約束:system prompt 會建議 LLM 先 Grep 定位再 Read 深入,GrepTool 的預設輸出模式也會自然引導這個流程,但不在程式碼中堵死其他路徑,讓 LLM 根據具體情況來判斷。

2.3 實戰:追蹤搜尋工具的執行記錄

回到開頭的範例。當然這個問題是我刻意挑選的,因為答案分散在多個檔案中,需要多輪搜尋才能拼湊出全貌,目的是為了展示多輪搜尋的完整過程。我把這個問題丟給了 Claude Code,以下是真實的搜尋過程。

第 1 輪:廣撒網。 LLM 將問題中的 GrepTool追蹤 翻譯成 grep 的關鍵詞,用預設的 files_with_matches 模式掃過一遍:

Grep({pattern: "GrepTool|tool.*track|tool.*activity", glob: "*.ts"})→ 回傳 4 個檔案:structuredIO.ts, sessionRunner.ts, bridgeUI.ts, bridgeStatusUtil.ts

4 個檔案,其中 3 個在 bridge/ 目錄下,1 個在 cli/ 下。問題問的是 bridge 系統,LLM 將重點放在 bridge/ 下的檔案。sessionRunner.ts(session + runner = 會話執行器)最可能包含工具執行追蹤的邏輯。

圖片

第 2 輪:看上下文。 Grep 切換到 content 模式,查看 GrepTool 在 sessionRunner.ts 中的上下文:

Grep({pattern: "GrepTool|tool.*activity", path: "bridge/sessionRunner.ts", output_mode: "content", "-C": 5})

回傳的程式碼片段中能看到一張映射表的尾部,顯示 GrepTool: 'Searching'BashTool: 'Running',但上文被截斷了。LLM 判斷需要 Read 整段程式碼才能看齊全。

圖片

第 3 輪:調用 Read。 使用 Read 打開 sessionRunner.ts 的完整上下文,一次看到三個關鍵結構:

  • 第一個是工具名→動詞映射表TOOL_VERBS,共 18 個條目,這裡列出與搜尋相關的部分)。每個搜尋工具(Grep、Glob)在這裡都被映射成 Searching。注意有兩套命名,比如內部名 Grep 和外部 SDK 名 GrepTool。這表明工具名稱是硬編碼在映射表裡的,不是動態註冊的。

Grep: 'Searching',   GrepTool: 'Searching',
Glob: 'Searching',   GlobTool: 'Searching',
Read: 'Reading',      FileReadTool: 'Reading',
Edit: 'Editing',      FileEditTool: 'Editing',
Bash: 'Running',      BashTool: 'Running',
// 還有 Write, MultiEdit, WebFetch, WebSearch, Task, NotebookEditTool, LSP 等
  • 第二個是摘要生成函式,它將動詞和搜尋目標拼接在一起:動詞來自上面的映射表,目標則從工具調用的輸入中提取(優先取 file_path,其次 pattern、command、url 等)。所以一次 GrepTool({pattern: "reconnect|backoff"}) 調用的摘要就是 Searching reconnect|backoff。

  • 第三個是活動解析器:它從 session 的 stdout 中逐行解析 JSON,當發現工具調用事件時,調用上面的摘要函式生成摘要,打包成一條活動事件。

到這裡已經知道要怎麼追蹤、怎麼記錄了,但活動事件生成之後去了哪裡?

圖片

第 4 輪:追蹤使用方。 Grep 搜尋 SessionActivity 被誰引用,一次追出整條鏈:

Grep({pattern: "SessionActivity|currentActivity", path: "bridge/", output_mode: "content", "-C": 2})

三個檔案同時浮出水面:

  • bridge/types.ts :活動事件的型別定義,只有 3 個欄位(類型、摘要、時間戳記)。每個 session 維護一個環形緩衝區和一個當前活動指標。

  • bridge/bridgeMain.ts :一個計時器會週期性地輪詢每個 session 的當前活動,並維護最近 5 次工具調用的軌跡,例如 Searching → Reading → Searching → Editing 這樣的歷史。

  • bridge/bridgeUI.ts :收到工具啟動事件後,快取摘要文字並渲染到 bridge 的狀態面板。

這樣就拼出了完整的追蹤鏈:session 進程輸出工具調用的 JSON → 活動解析器提取並生成摘要 → bridge 主進程定時輪詢獲取最新活動 → UI 模組渲染到狀態面板。

圖片

03、性能原理:暴力搜尋為什麼夠快

上一章展示了 Claude Code 如何用多輪 Grep 搜尋程式碼。但這引出一個顯而易見的問題:每輪 Grep 都是在專案檔案中進行暴力掃描,一個大一點的專案可能有幾萬個檔案,暴力掃描不會慢嗎?

Grep 在今日已經是一個大家族。誕生於 1973 年的 GNU grep 是最經典的版本,逐檔案遞迴、不認得 .gitignore、預設為單執行緒。而 Claude Code 的 GrepTool 底層調用的並不是它,而是 ripgrep,在 2016 年由 Andrew Gallant 用 Rust 重寫的現代實作,預設遵守 .gitignore、自動跳過二進位檔案、多執行緒平行處理、用 SIMD 加速匹配,為在大型程式碼庫裡快速搜尋這個場景從頭設計。

原始碼佐證:tools/GrepTool/GrepTool.ts:21import { ripGrep } from '../../utils/ripgrep.js' ,所以真正幹活的是 ripgrep,不是系統內建的 grep。

這一章解釋為什麼 ripgrep 的暴力掃描在開發者本地專案上速度夠快:五層過濾怎麼把搜尋範圍從幾萬個檔案縮小到幾十個,SIMD 和 Boyer-Moore 怎麼加速檔案內匹配,以及程式碼搜尋和向量檢索在資料規模上的本質差異。3.4 節還有一組用 Claude Code 自己的原始碼做的 ripgrep vs GNU grep 實測數據,可以直接看到差距。

3.1 ripgrep 的五層過濾

Claude Code 原始碼分析:基於 2026 年 3 月 31 日因外流而公開的 Claude Code CLI 原始碼快照。

Cursor 相關資料:Engineer's Codex: How Cursor Indexes Codebases Fast、Cursor Agent system prompt(2025 年 3 月外流版本)、Turbopuffer 客戶案例: Cursor。 https://read.engineerscodex.com/p/how-cursor-indexes-codebases-fast https://turbopuffer.com/customers/cursor

OpenAI Codex 相關資料:

相關文章推薦

分享網址
AINews·AI 新聞聚合平台
© 2026 AINews. All rights reserved.