Caret Up

什么是代码检索

顾名思义,代码检索就是把代码库中你想要的代码找出来。

你可能会问:现在 LLM 这么强了,为什么不直接把整个代码库丢给它看?原因有四:

  1. LLM 的上下文窗口有限
  2. 过多的上下文会导致成本激增
  3. 上下文过大会分散模型注意力
  4. 需要预留上下文给系统提示、工具调用等

那这个「找东西」的场景,听起来不是天然就适合 RAG 吗?别急,我们具体问题具体分析。

RAG 的工作原理

具体的工作原理可以参考 RAG 的工作原理 一文,这里不再赘述。

简单来说,从原始代码到可用的 RAG 系统,需要经过以下几步:

  1. 对代码进行分片(chunking),可以按函数、按行数等
  2. 对分片后的代码片段进行向量化(embedding)
  3. 建立索引(indexing)
  4. 召回指定片段(retrieval)

需要注意的是,召回过程依赖的是相似度计算。这在企业知识库、客服机器人等领域非常好用,但在代码场景下,则会水土不服。

RAG 的水土不服

理想很丰满,现实很骨感。为什么 RAG 在代码场景下会水土不服?主要有以下几点:

1. 代码切分的边界不好处理

文章即使按段落切分,每一段之间也不影响阅读。但代码不同 —— 它有严格的语法结构和上下文关联,粗暴切分会直接破坏语义。

如果按函数切分,遇到几千行的巨型函数,召回效果会大打折扣。

如果按行数切分,一个 if-else 块可能被拦腰截断:一个 chunk 里是 if,另一个 chunk 里是 else,分开之后两个 chunk 都不知道在说什么。

更糟糕的是,如果函数 A 调用了函数 B,而核心逻辑实现在 B 里,那么 A 被向量化之后,根本无法体现出「A 包含了 B 的功能」。调用链再深几层,这个问题会更严重。

2. 向量无法精确匹配

向量召回的本质是找「相似的」,而不是「准确的」。但代码场景要求的恰恰是准确性。

比如用户说「帮我找到 getUserById 调用的地方」,向量召回可能给你返回一堆 getUserInfogetUserByNamefetchUserInfoqueryUser —— 只要跟 user 沾边的全来了。而用户只是想精确匹配 getUserById 这一个函数名。

3. 代码变化与索引

RAG 场景下,必须先对代码库做向量化和索引,然后才能进行召回。而 chunking、embedding、indexing 每一步都需要消耗时间和计算资源。

每次代码变动后,如果重建索引,就得重新完整跑一遍全流程。尤其是在 vibe coding 盛行的今天,每天代码变动轻松破百行,重建成本很高。

不重建呢?索引落后于实际代码,直接导致基于过期信息生成 bug。

更尴尬的是冷启动场景:打开一个新项目时如果先建索引,要么在后台跑 —— 用户提问时索引还没好,什么都召不回;要么在前台跑 —— 直接卡住用户的操作流程。左右为难。

4. 冷启动慢得要命

这一点上面已经提到了,但值得单独拎出来说。

想象一下:你刚 clone 一个项目,打开 Claude Code 想立刻干活。结果它告诉你「正在建立索引,预计 15 分钟」—— 就这一句话,劝退率至少 50%。

在百万行级别的代码库上跑一遍 chunking + embedding + indexing,十几分钟甚至更久都算快的。而 grep 呢?敲下去就是毫秒级响应,开箱即用。

用户不会等你,更不会理解你为什么还在「热身」。

5. 黑盒,不可解释

这点我觉得最要命。

RAG 召回了 Top-K 个代码片段,但为什么是这 K 个?为什么不是另外 K 个?答案只有向量空间的距离能告诉你,而向量空间里的「距离」对普通人来说跟玄学差不多。

一旦召回漏了关键代码,导致模型给出了错误的修改建议,你怎么 debug?你只能怀疑是 embedding 模型不行、chunking 策略不对、Top-K 太小……每一个环节都是嫌疑犯,但你没有证据。

而 grep 呢?每一步的搜索关键词、匹配行号、命中文件路径全都看得见。用户完全可以跟着模型的检索路径一步步检查:「嗯,你搜了这个关键字,找到了这个文件,读了这一行,逻辑是对的。」出问题了一目了然,透明可审计。

返璞归真

那么,不用 RAG,如何进行代码检索呢?

Claude Code 给出的答案是:返璞归真,现用现找,直接摈弃 RAG,通过提供三个工具来解决:

  1. Glob:按文件名查找,类似于 find
  2. Grep:按内容关键字查找,类似于 grep
  3. Read:按需读取文件内容,类似于 cat

本质上,还是把工具的使用权交给模型自己决定。这种「土办法」,真的能代替 RAG 吗?

Grep:基于 ripgrep,但绝不是简单封装

你可能会想:既然 Claude Code 支持直接调用 Bash 工具,为什么还要多此一举,给它提供封装好的 Grep 工具呢?

1. 权限统一管理

直接调用 Bash 的风险太高了,谁也说不准模型会不会脑子一抽跑一个 rm。虽然可以通过中断调用的方式向用户确认,但作为一个高频读操作,不对用户造成打扰才是最优解。把 grep 包装成一个工具,相当于在 Bash 之上加了一道保险。

2. 输出格式可控

直接跑 bash 的 grep,返回的就是一坨文本,还需要模型自己解析处理,浪费 Token 不说,还可能出现幻觉。而对 grep 进行封装后,可以结构化地输出「行号」、「上下文」、「文件组」,并且可以通过参数控制「只返回匹配的文件名」「只返回匹配数量」「返回匹配的文件和内容」等粒度。让模型按需选择,减少 Token 浪费。

3. 性能

Grep 底层使用的是 ripgrep 而不是传统的 grep,支持多线程并行,自动尊重 .gitignore(省去了搜索 node_modules 这类宇宙黑洞),性能极强。

那么,Claude Code 是如何保证模型一定会调用 Grep 的呢?从之前泄露的 Claude Code 源码中可以窥见,有这样一段 Prompt:

1
2
3
4
5
6
7
A powerful search tool built on ripgrep

- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash 
  command. The Grep tool has been optimized for correct permissions and access.
- Output modes: "content" shows matching lines, "files_with_matches" shows 
  only file paths (default), "count" shows match counts
- Use Agent tool for open-ended searches requiring multiple rounds

用「ALWAYS use …」这种强硬的措辞,通过 system prompt 强调模型走专用工具,而不是抄 bash 的近路。

另外,注意最后一行:「Use Agent tool for open-ended searches requiring multiple rounds」—— 指示模型,多轮搜索请使用 Agent 工具。这就涉及到多 Agent 和上下文隔离的优势了,后面细讲。

Glob:按文件名查找,按修改时间排序

Grep 是按内容查找,Glob 是按文件名查找。

举个例子:我想看某个目录下有哪些 *.go 文件,用 Grep 显然不合适。

Glob 还有几个设计上的小巧思:

  1. 结果按修改时间倒序排列。很好理解 —— 最近修改过的文件,大概率跟当前任务最相关
  2. 结果有 100 个文件的硬上限。超出会截断,避免上下文爆炸。如果模型还想看更多,可以收紧 pattern 再搜一次

Read:按需读取

找到文件之后,怎么看?Read 就是干这个的。

但它有一个反直觉的设计:默认只读 2000 行,超出会截断。

如果想读超过 2000 行的文件怎么办?别急,Read 支持传入 offsetlimit,分段读取。比如先 offset=0, limit=2000 扫一眼文件的大致结构,心里有数了,再 offset=3500, limit=500 精准定位到要看的那一段。

这套设计的核心思想就一句话:模型应该按需读取,不要贪心。

源码里工具描述是这么写的:

By default, it reads up to 2000 lines starting from the beginning of the file. When you already know which part of the file you need, only read that part. This can be important for larger files.

翻译过来就是:如果你已经知道要看哪一部分,就别把整个文件都吃进去。对大文件来说,这点尤其重要。

这本质上是在引导模型养成节约 Token 的习惯。不光是省钱,也是在保护模型的注意力不被无关信息冲散。

还有一个非常关键的细节:Read 每次都直接 stat 磁盘文件、读取最新内容,不缓存、不索引、不预处理。

这意味着什么?你刚改完文件,下一秒 Read 就能看到最新内容。没有索引层,就没有索引滞后。这就是 Claude Code 实时性的根基 —— 直接从磁盘拿,中间没有任何人替它「翻译」过。

三件套的组合:一步一步来,不走捷径

讲完了单个工具,最关键的来了:它们怎么协同?

假设你对 Claude Code 说:「这个项目的登录功能在哪实现的?」

它的检索过程大概是三步走:

  1. 先用 Glob 找候选项。 跑一个 **/*login*.{ts,tsx,js},拉回来 5 个候选文件
  2. 再用 Grep 搜关键字。 在这些候选文件里搜 passport|auth|login,精准定位到几个命中行
  3. 最后用 Read 看详情。 读取命中文件的相关行段,理解具体实现

注意这个过程的关键特征:每一步都看到上一步的结果,再决定下一步做什么。

这里没有 RAG 那种「一次性召回所有相关代码」的动作。不是先把料备齐了再一口气下锅,而是边炒边尝,尝完再决定下一把加什么调料。

这就是两种范式最本质的区别,后面第七节还会展开。

不过你可能会想:如果一个简单的「查登录在哪」要三步,那更复杂的任务呢?比如「调研一下整个项目的认证模块流程」,三件套自己循环够用吗?

派个子 Agent 去干活

来,想象这么一个场景:你跟 Claude Code 说「调研一下这个项目的认证模块整体流程。」

这种「调研类」任务的特点是:要看的东西多,要 grep 好几个关键词、读好几个文件、来回比对、最后总结成一段结论。整个过程可能要走十几个工具调用。

如果让主 Agent 自己一边 grep 一边 read 吭哧吭哧地干,会发生什么?

主 Agent 的上下文很快就会被一堆 grep 输出和文件片段塞得满满当当。等它好不容易理清了认证流程,要回头给你写代码的时候,发现真正要解决的问题已经被检索过程中的中间结果挤到角落里了。模型注意力被冲散,回答质量直线下降。

这就是大型探索任务的第一大敌:上下文污染。

Claude Code 的解法很巧妙:派一个子 Agent 出去探索,主 Agent 只收结论。

打个比方:你做战略决策需要看大量市场数据,你不会自己一头扎进 Excel 里翻几个小时。你会把这事派给助理:「帮我调研一下,明天给我一份精简报告。」助理看了一堆资料,最后只把结论给你。你的注意力(对应主 Agent 的上下文)就被保护起来了。

具体是怎么实现的呢:

  1. 主 Agent 通过 Agent 工具派子 Agent。 子 Agent 是一个独立的运行实例,有自己的对话上下文,跟主 Agent 完全隔离。
  2. 子 Agent 的工具池是精简的。 只有只读工具:Grep、Glob、Read、Bash(只读),没有 Edit、没有 Write,也不能再派子 Agent(防止层层套娃,无限递归)。源码里管这种 Agent 叫 Explore Agent。
  3. 子 Agent 在自己的上下文里随便折腾。 它可以 grep 几十次、read 几十次,中间产生的所有脏数据都留在它自己的上下文里,不会污染主 Agent。
  4. 子 Agent 完事后,只把最终结论返回。 主 Agent 的上下文里只多了一段「认证流程是这样的:……」的精简结论,中间那些搜索过程全被压缩没了。

这就是「上下文压缩」的精髓:用一个隔离的子 Agent 把脏活累活干了,主 Agent 只接收干净的结果。

源码里的引导规则也很直白:

For simple, directed codebase searches use Grep/Glob/Read directly. For broader codebase exploration and deep research, use the Agent tool with subagent_type=Explore. … use this only when … your task will clearly require more than 3 queries.

简单定向搜索(你知道要找啥)→ 直接用 Grep/Glob/Read 三件套。开放式探索(你不确定要找啥)→ 派 Explore 子 Agent。临界点就是「预期超过 3 次查询」。

这是个非常实用的工程经验:少于 3 次就别折腾派 Agent,多于 3 次就别污染主 Agent 的上下文。

另外还有一个加分项:子 Agent 可以并行派多个。 比如你要同时调研「认证模块」「支付模块」「订单模块」,主 Agent 可以一次性派出三个子 Agent 各干各的,完了同时回来报告。这种并发探索能极大缩短整体时延。

到这里,Claude Code 检索体系的层次感就出来了:

  • 底层: Grep / Glob / Read 三件套,处理简单定向检索
  • 中层: Explore 子 Agent,处理开放式探索和上下文隔离
  • 上层: 主 Agent,编排整体任务

每一层有自己的职责,互不干扰。

再深一层:LLM 自己驱动多轮迭代

你心里可能还有一个疑问:三件套加子 Agent 都讲了,但到底是什么让 Claude Code 能「自己探索」?好像还差一层没说透。

差的就是「多轮迭代」这一层。

我打个比方你就懂了:

  • RAG 的范式是「考试发卷子」。 用户提问 → 系统一次性召回 Top-K 个片段 → 模型基于这些片段一次性生成答案。中间没有循环,没有反悔,没有「等等我再看看」。这是一锤子买卖。
  • Claude Code 的范式是「现场探案」。 用户提问 → 模型说「我先 Grep 一下」→ 系统执行 Grep 返回结果 → 模型看到结果说「嗯,UserService 看起来比较像,我 Read 一下」→ 系统执行 Read 返回内容 → 模型说「找到了,逻辑是这样的……」

一个是提前把材料备好丢给模型让它一气呵成,另一个是边查边推理、每一步都基于上一步的结果调整方向。

源码里这个循环的逻辑大概长这样:

1
2
3
4
5
6
7
8
while (true) {
  const response = await callLLM(messages);
  if (没有 tool_use) break; // 模型不再调工具了,循环结束
  for (const toolUse of response.toolUses) {
    const result = await executeTool(toolUse);
    messages.push(result); // 把结果回填到对话历史
  }
}

每一轮都是:模型说话 → 可能带 tool_use → 执行工具 → 把结果回填到对话历史 → 模型继续说。直到模型自己说「我搞定了」,循环才停下来。

这个看似简单的 while 循环,其实是整个 Agent 范式的灵魂:它给了模型每一步根据上一步结果调整方向的能力。

  • Grep 结果是空的?换个关键字再搜。
  • Read 出来的代码逻辑跟你想的不一样?再 Grep 几个相关函数看看。
  • 这个文件引用了另一个文件?跟过去读一下那个文件。

这种「走一步看一步」的能力,是 RAG 的「一次召回」永远给不了的。RAG 召回错了就是错了,模型只能将错就错。Agent 召回错了,下一轮自己就调整了。

所以 Claude Code 用看起来很原始的 grep + read,能做出 RAG 做不到的事,根本原因就在这层 LLM-driven 的多轮迭代上。

grep 本身不稀奇,但「让 LLM 自己决定每一轮 grep 什么」就稀奇了。

总结:为什么 Claude Code 不用 RAG

讲到这里,答案已经呼之欲出了。六个原因,串一遍:

  1. 冷启动。 grep 毫秒级响应,开箱即用;RAG 要先建索引,分钟级冷启动,劝退一半用户。
  2. 实时性。 grep 每次现读磁盘最新版本;RAG 索引会滞后,改了文件得重建。
  3. 精确性。 grep 是确定性的字符正则匹配,找 getUserById 就只返回它;RAG 是向量近似匹配,一堆相似函数糊在一起,你得甄别好久。
  4. Token 经济。 grep + Read 按需读取,模型只看真正需要的几行;RAG 一上来就给整个代码库做 embedding,存储和计算成本都不小。说人话就是:省钱。
  5. 可解释性。 grep 每一步检索过程都对用户透明可审计;RAG 的 Top-K 召回是黑盒,出了 bug 你连往哪查都不知道。
  6. 决策权。 grep 让 LLM 自己决定每一轮搜什么、读什么,多轮迭代逐步逼近答案;RAG 是一次性把材料丢给模型,模型只能将错就错。

但如果再往深一层想,我觉得这背后还有更根本的东西:两种方案代表了两种不同的设计哲学。

RAG 派的潜台词是:LLM 不够强,所以我们要用工程手段帮它把材料准备好。 chunking、embedding、向量召回,本质上都是在「替模型做决定」—— 我们认为你应该看这些,我们就给你这些。

Claude Code 派的潜台词是:LLM 已经足够强,工程的职责是给它准备好工具,把决策权还给它。 grep 不替模型做任何决定,它只是个工具。用还是不用、什么时候用、怎么用,全是模型说了算。

Anthropic 赌的是「模型会越来越强」,所以他们选择信任模型的判断能力。这是一个长期主义的选择。

RAG 是不是该淘汰了

讲到这儿,可能有人要说了:「你这不是在黑 RAG 吗?我们项目还在用呢!」

别急,我没说 RAG 该淘汰。RAG 仍然有自己的舞台,只是不在 Claude Code 这种场景里。

什么场景 RAG 更合适?

  1. 巨型代码库加跨仓库检索。 大公司几十个 monorepo、上千万行代码,靠 grep 满世界搜性能扛不住。提前建好索引的 RAG 在这时候反而是更务实的选择。
  2. 纯语义查询。 比如「找一下处理用户认证相关的代码」这种模糊描述,靠关键字 grep 反而不好搜,向量召回在这里反倒有优势。
  3. 多人协作的知识库类查询。 代码 + 文档 + Wiki 混合检索,RAG 是合适的。

而 Claude Code 这套方案,最适合的是 单项目、探索式开发、需要精确性、要求实时性 的场景 —— 正好是大部分 AI 编程工具的主战场。

工具是为场景服务的,没有银弹。

最后留一个开放问题:如果未来 LLM 的上下文窗口能到 1 亿 token,整个代码库都能一把塞进去,那 RAG 还有意义吗?grep 还有意义吗?想清楚了可以在评论区聊聊。

写在最后

三句话精炼一下全文的核心观点:

  1. 代码场景下,RAG 有切片破坏结构、向量近似不准、索引滞后、冷启动慢、不可解释这五大硬伤。
  2. Claude Code 用 Grep + Glob + Read 三件套 + 子 Agent 探索的设计,本质上是把检索决策权还给 LLM 自己,配合多轮迭代循环实现精准定位。
  3. 更深层看,是 Anthropic 信任 LLM 的判断能力,押注模型会越来越强,所以选择了「不替模型做决定」的设计哲学。

表面看是技术选型,往深了看,是对 Agent 设计哲学的理解。

说到底,Agent 不是带工具的聊天机器人,而是会自己做决策的执行体。工程师的职责是给它一套好用的工具,而不是替它做决策。