Skip to main content

搜索:为什么 grep 打败了 RAG

这可能是整个源码里最反直觉的发现。当整个行业都在推进向量数据库和 Embedding 搜索时,Anthropic 选择了最朴素的那一条路。

你可能以为的方案

本节源码:(行业通用 RAG 模式,本文用 GrepTool 反例)

2026 年做 AI 编程工具的代码搜索功能,业界标准方案大概是:

  1. 把代码库切成小块(chunking)
  2. 用 Embedding 模型把每个块变成向量
  3. 存进向量数据库(Pinecone、Weaviate、Chroma 之类的)
  4. 用户查询时,先把查询变成向量,再做相似度搜索
  5. 把最相关的代码块喂给 LLM

这就是 RAG(Retrieval-Augmented Generation)——整个 AI 应用开发领域最热门的架构范式,无数创业公司在做 RAG 相关的工具和基础设施。

Claude Code 实际做的方案

本节源码:src/tools/GrepTool/GrepTool.ts(Grep)+ src/tools/GlobTool/GlobTool.ts(Glob)+ src/tools/FileReadTool/FileReadTool.ts(Read)

源码里搜索相关的工具就两个——Grep(基于 ripgrep 的内容搜索)和 Glob(基于文件名模式匹配)——以及直接读文件的 Read 工具。

没有 Embedding。没有向量数据库。没有语义搜索。就是纯文本的模式匹配

你觉得它搜代码搜得很准?背后就是 grep 加上一个足够聪明的大脑。

为什么这样做反而更好

本节源码:论证来自 RAG vs grep 通用工程取舍,不在单文件中

这个决策背后的逻辑其实很清晰,一旦想通了就觉得理所当然

1. LLM 已经足够聪明

RAG 解决的核心问题是:搜索引擎不够聪明,需要语义理解才能找到相关内容。但当搜索结果的使用者是世界顶级的 LLM 时,情况变了。

Claude Opus 可以从一堆 grep 结果里推断代码之间的关系、推断调用链、识别设计模式。它不需要一个非常聪明的搜索引擎来预先筛选和排序——它自己就是最强的「理解引擎」

与其让每个环节都复杂,不如让一个环节足够强,其他环节保持简单

2. grep 的优势被低估了

grep 做一件事,做得非常好:精确文本匹配。它告诉你「这个字符串在哪几个文件的哪几行出现了」,100% 准确,几乎没有误报,几乎没有延迟。

对比 RAG 的 Embedding 搜索:结果是概率性的,可能返回「语义相关但实际无关」的内容,也可能漏掉「写法不同但实际相关」的内容。还需要不断调整 chunk 大小、选择 Embedding 模型、维护向量索引——每个环节都可能出错。

打个比喻:grep 就像在图书馆里用 ISBN 号查书——精确但要求你知道 ISBN。RAG 就像让一个人帮你找「讲这个主题的书」——灵活但可能拿错。

当你有一个足够聪明的图书管理员(LLM)时,后者的精确性比前者的灵活性更有价值

3. 维护成本的差距是巨大的

grep 零维护。代码改了?grep 直接搜到最新内容。没有索引需要更新、没有向量需要重建、没有数据库需要管理。

RAG 方案的维护成本不可小觑:代码每次变更都要重新 chunk 和 embed、向量索引可能失效、Embedding 模型升级需要重建所有向量、数据库本身需要运维。对于一个每天在几十个不同项目之间切换的工具来说,这种用户体验大打折扣

4. 上下文窗口变大了

RAG 在上下文窗口很小(4K、8K)的时代是必需品——你只能往 prompt 里塞这么多内容,必须精选。但 Claude Code 用的是 100K 甚至 1M token 的上下文窗口。

窗口大了,暴力搜索就变得可行了。多搜几个文件、多读几百行代码,反正放得下。不需要精确的检索来节省每一个 token

Grep 工具的设计细节

本节源码:src/tools/GrepTool/GrepTool.ts

说完了「为什么不用 RAG」,看看 Claude Code 是怎么把 grep 用到极致的。

源码里的 GrepTool 基于 ripgrep(rg),不是 GNU grep。这个选择本身就说明了问题——ripgrep 比传统 grep 快一个数量级,原生支持 .gitignore、Unicode、多线程,而且对大型代码库做了大量优化。

先看 GrepTool 的输入参数定义:

const inputSchema = lazySchema()
=> z.strictObject({
pattern: z.string().describe(
'The regular expression pattern to search for in file contents',
),
path: z.string().optional(),
glob: z.string().optional().describe(
'Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
output_mode: z.enum([
'content', 'files_with_matches', 'count'
]).optional(),
'-B': semanticNumber(z.number().int().nonnegative().optional())
.describe('The number of lines to display before each match'),
'-A': semanticNumber(z.number().int().positive().optional()),
'-C': semanticNumber(z.number().int().positive().optional()),
'-n': semanticBoolean(z.boolean().optional()),
'-i': semanticBoolean(z.boolean().optional()),
type: z.string().optional(),
head_limit: semanticNumber(z.number().int().positive().optional())
.describe('The number of lines to read'),
offset: semanticNumber(z.number().int().nonnegative().optional())
.describe('The line number to start reading from'),
multiline: semanticBoolean(z.boolean().optional()),
})

三种 output_mode 的设计很精妙

  • files_with_matches(默认)——只返回匹配的文件路径列表。这是最省 token 的模式,适合「先摸清哪些文件相关」的第一轮搜索
  • content——返回匹配行及上下文。支持 -A/-B/-C 参数控制上下文行数,支持 -n 显示行号。适合「知道在哪个文件、想看具体代码」的精确搜索
  • count——只返回每个文件的匹配次数。适合「这个函数被调用了多少次」之类的统计场景

三种模式本质上是一个信息密度的梯度:LLM 可以先用 files_with_matches 低成本扫描,定位目标后再用 content 精确读取。比一次性返回所有内容高效得多。

头结果数量限定:head_limit 默认 250

源码注释本身就是一篇小论文:

// Unbounded content-mode greps can fill up to the 20KB persist
// threshold (~6-24K tokens/grep-heavy session).
// 250 is generous enough for exploratory searches while preventing
// context bloat. Pass head_limit=0 explicitly for unlimited.
const DEFAULT_HEAD_LIMIT = 250

不限制结果数的 grep 可能返回几万行,直接吃光上下文窗口。250 是一个平衡点:足够做初步探索,又不至于浪费 token。如果 LLM 真的需要更多结果,可以显式传 head_limit=0 或用 offset 翻页。

Glob 工具:文件发现的第一步

本节源码:src/tools/GlobTool/GlobTool.ts

Grep 负责「文件里搜什么」,Glob 负责「先找到哪些文件」。这是搜索流程的第一步,经常被忽略但其实至关重要

Glob 工具的底层也是 ripgrep,用的是 rg --files --glob 模式——让 ripgrep 列出匹配模式的文件,而不是搜索文件内容。继承了 ripgrep 的所有性能优势。

一个关键的设计细节:结果按修改时间排序。Glob 的默认结果按修改时间排序(ripgrep 的 --sort=modified 是升序,最新的在前),限制最多 100 个文件。LLM 拿到完整的文件列表后,可以根据上下文判断哪些文件最相关。

这不算技术上有多复杂的决策,但它体现了一种产品思维:搜索结果的排序本身就是一种「智能」。不需要向量数据库来判断相关性,让最近改动的文件排在前面,AI 就大概率能优先看到相关的内容。

Read 工具:最被低估的搜索武器

本节源码:src/tools/FileReadTool/FileReadTool.ts

Read 工具表面上是「读文件」,但它其实是整个搜索系统的最后一环,也是能力最容易被低估的一环。

offset 和 limit 参数意味着 LLM 不需要读整个文件。它可以先用 Grep 找到 validateUser 在第 187 行,然后用 Read(file, offset=180, limit=30) 只读周围的代码。对于几千行的大文件,这省掉了大量上下文。

但 Read 真正让人意外的是它的多模态能力

- This tool allows Claude Code to read images
(eg PNG, JPG, etc). When reading an image file
the contents are presented visually.

- This tool can read PDF files (.pdf). For large PDFs
(more than 10 pages), you MUST provide the pages
parameter to read specific page ranges.

- This tool can read Jupyter notebooks (.ipynb files)
and returns all cells with their outputs, combining
code, text, and visualizations.

它能读图片(利用 LLM 的多模态能力直接「看」截图和设计稿)、能分页读 PDF、能解析 Jupyter Notebook(包括输出结果和可视化)。这些能力组合起来,Read 工具就不只是一个文件读取器,而是一个完整的文档理解工具

还有一个藏在代码深处的优化

源码中有一个常量:

export const FILE_UNCHANGED_STUB =
`File unchanged since last read. The content from
the earlier Read tool_result in this conversation
is still current — refer to that instead of
re-reading.`

如果 LLM 在同一个会话里重复读同一个文件,而文件没有变化,Read 工具会返回这条简短提示而不是完整内容。这是一个 token 节省的策略——LLM 在调试过程中经常反复读同一个文件,这个 stub 避免了重复加载。

为什么不让 AI 直接调用 bash grep?

本节源码:src/tools/BashTool/prompt.ts:285("Content search: Use GrepTool (NOT grep or rg)")

源码的 BashTool prompt 明确要求避免使用 grep 命令,而是用 Grep 工具。原因有几个:

  1. Grep 工具有权限校验(checkReadPermissionForTool),确保不会搜索不该搜的文件
  2. 它继承了 ripgrep 的优势——自动排除 .git.svn 等版本控制目录的噪音;它限制每行最大 500 字符(--max-columns 500),防止 base64 或 minified 代码把结果撑爆
  3. 它会把绝对路径转换成相对路径来节省 token

但说到底,最关键的是:Grep 工具把搜索结果按照 LLM 最容易消费的方式组织起来——文件路径相对化、行号、上下文行控制、head_limit 限制。这些都是为 LLM 优化的,不是为人类优化的。

「没有向量数据库」的实证

本节源码:在 Claude Code 全仓库 grep 关键词(512,920 行)

前面说了 Claude Code 没用向量数据库。这不是猜测,是实证。我在 Claude Code 的全部 512,920 行源码中搜索了以下关键词:

关键词匹配数结果
embedding2一处是「embedding a file:// URL」(在 URL 中嵌入链接),一处是「string-embedding」(字符串嵌入 bash 命令)。都是英语单词的通用用法,和向量嵌入无关
vector0没有向量数据库,没有向量搜索,没有向量索引
semantic search0(同上)
pinecone / weaviate / chroma / faiss0没有任何主流向量数据库的引用

证据确凿——Claude Code 的搜索系统里没有任何向量数据库

这不是「还没来得及做」或「下一版再加」。Claude Code 已经是市面上最成功的 AI 编程工具之一,在这个状态下就证明了:不用 RAG/向量数据库,也能做出最好用的代码搜索体验

这个决策能教我们什么

本节源码:通用工程教训,不在单文件中

「手里有锤子不代表所有问题都是钉子」

RAG 是好东西,但它解决的是**「把信息检索的结果展示给人类」**的场景。当用户是 LLM 时,需求结构完全不同:

  • 人类需要模糊匹配——他们不记得准确的关键词,希望「讲这个主题的书」能找到相关的内容
  • LLM可以从精确匹配里推理——它能用自己的能力把零散的 grep 结果拼成完整图景

让一个 LLM 来做推理,往往比训练一个更强的搜索器更划算。

「够好」原则

在设计 AI 应用时,不要默认每个环节都需要最先进的技术。先问一个问题:如果这个环节用最简单的方案,瓶颈在哪里?如果 LLM 自身的能力可以补偿简单方案的不足,那就用简单方案。复杂方案节省的智能成本,可能比它自身的维护成本要贵得多。

几个可推广的判断标准

  • 如果 LLM 是消费方,搜索可以简单——RAG 适合「搜索结果直接展示给人类看」的场景。当搜索结果作为给 LLM 的中间步骤时,精确匹配 + LLM 理解 > 模糊匹配 + 预排序
  • 零维护胜过高精度——如果工具需要在任意环境下即开即用(像 Claude Code 那样),任何需要预处理的方案都是障碍
  • 先做减法,再考虑加法——当你想加一个复杂组件时,先问:不加行不行?如果行,那就不加。Claude Code 证明了,不用向量数据库也能做出最好用的 AI 编程工具

这个规则有边界条件——对于上百万个文档的企业知识库,纯 grep 确实不够用。但对于代码搜索这个特定场景,项目通常在几万到几十万行之间,grep 的速度足够,LLM 的理解力足够,简单方案就是最优方案