RAG 负责「从 0 到 1」,精确检索负责「从 1 到 N」


在这里插入图片描述

引言:你有没有遇到过这些问题?

用 Cursor 做多文件重构,改着改着 AI 开始虚构不存在的函数
@codebase 检索整个项目,结果塞进来一堆八竿子打不着的文件;
让 AI 改一个接口签名,它改了三个文件就说「完成了」,结果还有五个调用方没动到……

这些问题的根源不在模型本身,而在 AI IDE 的代码检索层

你可能以为 AI IDE 是这样工作的:

模型理解整个项目 → 精准找到相关文件 → 生成正确修改

真实情况是:

模型只能看到「检索系统喂给它的那部分代码」→ 喂得不准 → 幻觉、漏改、上下文污染全来了

这篇文章,我们跟着一次真实的查询,从你按下回车的那一刻开始,走完 AI IDE 检索的完整流水线。你会看到一个核心洞察:

RAG 负责「从 0 到 1」——在你不知道搜什么的时候,把自然语言变成代码线索;
精确检索(grep / TreeSitter / LSP)负责「从 1 到 N」——有了线索之后,精准挖全。


零、先看全景:一次查询的完整旅程

当你在 Cursor 里敲下「帮我重构用户登录逻辑」,背后是这样一条流水线:

是(干净快照)

否(重构中途·语法破损)

① 用户输入自然语言需求
『帮我重构用户登录逻辑』

② 向量 RAG
自然语言 → 向量 → 召回相关代码片段

🎯 从 0 到 1
你不知道搜什么,RAG 给你起点

③ 模型从召回结果中
提取精确符号名
如 LoginService / GetUser

④ ripgrep 精确搜索
用符号名全局匹配

🎯 从 1 到 N
粗暴但不漏,召回只多不少

⑤ TreeSitter 过滤
剔除注释、字符串里的假匹配

⑥ 当前代码是否干净
可整体解析?

LSP 查询
补全跨文件依赖
谁调用它 / 它依赖谁

跳过 LSP
退回 grep + TreeSitter 结果

⑦ 组装上下文
严格控制 Token 上限

LLM 生成代码修改

注意这个流向:RAG 在最前面当入口,grep / TreeSitter / LSP 在后面做精修。 下面逐段拆解。


一、第一站:向量 RAG —— 把自然语言变成代码线索(从 0 到 1)

1.1 为什么查询的第一站是 RAG?

因为用户说的是人话,不是符号名

用户不会说:

「找到 pkg/auth 目录下所有实现了 Authenticator 接口的类」

用户会说:

「帮我看看登录相关的代码」

后者没有任何精确关键词,grep 根本不知道搜什么字符串,LSP 也不知道你要查哪个符号。
只有 RAG 能接住这种模糊的自然语言查询。

RAG 的核心能力是跨模态映射:把自然语言的语义,映射到代码空间。这就是它作为「入口」的不可替代价值。

1.2 RAG 是怎么工作的?

以 Cursor 的 @codebase 为例,分两个阶段:

索引阶段(打开项目时,后台默默做):

  1. 扫描整个仓库
  2. 把代码切成片段(chunk)——这里就用到了 TreeSitter,按函数/类边界切,保证每段语义完整(后面第三站会详细讲 TreeSitter)
  3. 每个片段生成 embedding(向量)
  4. 存入本地向量库

查询阶段(你输入需求时):

  1. 把自然语言需求转成向量
  2. 用余弦相似度,从向量库召回「语义最接近」的 N 个片段
  3. 把这些片段作为线索,交给模型

1.3 RAG 的三大固有缺陷

RAG 是好入口,但绝不能当最终依据。它有三个绕不开的缺陷:

缺陷 1:同名不同义,向量分不清

// pkg/cache/user.go
type Cache struct { ... }   // 用户缓存

// pkg/db/cache.go
type Cache struct { ... }   // 数据库查询缓存

两个 Cache 语义完全不同,但在向量空间里距离很近,模型分不清哪个是哪个。

缺陷 2:代码结构相似 ≠ 业务语义相关

func Login(...)      { 查库; 校验密码; 发token }  // 用户登录
func AdminLogin(...) { 查库; 校验密码; 发token }  // 管理员登录

两个函数结构高度相似,向量距离极近。你搜「用户权限」,可能把 AdminLogin 也召回。
代码长得像,不代表业务相关。 向量捕捉的是「相似性」,不是「相关性」。

缺陷 3:符号关系彻底丢失
代码的核心是符号间的关系——谁调用谁、谁实现了哪个接口。
但 RAG 把代码切成独立片段各自 embedding,片段之间的调用关系在向量空间里是丢失的
这就是多文件重构漏改的根源:RAG 召回了 A 文件,但没召回 A 依赖的 B 文件。

1.4 被低估的难题:索引过期

RAG 是「提前建索引,查询时召回」,但代码是会变的。

好消息:如果用了 AST-aware chunking(按函数边界切),最容易被误解的「边界漂移」问题其实不存在——

你在函数 A 里加了 100 行,A 的 chunk 内容变了要重算;但后面 B、C 函数内容没变,embedding 不用重算,只需更新「起始行号」这个便宜的元数据指针。

真正难解的是跨文件语义过期
在这里插入图片描述

// model/user.go —— 你把 ID 从 int 改成 string
type User struct { ID string }   // 文件变了 → 重新 embedding ✅

// service/user.go —— 你没动这个文件
func GetUser() *model.User { ... }  // 文件没变 → embedding 不更新 ❌

service/user.go 字面内容一个字没改,增量更新不会碰它。但它引用的 User 定义已经变了——它的真实含义变了,向量却还是旧的

向量索引只能感知「文本变化」,感知不到「语义变化」。 这是 AST-aware chunking 也救不了的根本缺陷。

1.5 RAG 的定位

RAG 是入口,不是终点。它解决「从 0 到 1」——在你不知道搜什么时给你起点。
但它召回不精确、会过期、丢关系,所以拿到线索后,必须交给精确检索去核实和挖深。

拿到 RAG 给的线索后,模型会从中提取出精确符号名(比如 LoginServiceGetUser),进入第二站。


二、第二站:ripgrep —— 有了符号名,精确搜索(从 1 到 N 的主力)

2.1 为什么用 grep 而不继续用 RAG?

因为一旦有了精确符号名,你要的是「一个都不能漏」,而不是「语义最相似」。

任务 RAG grep
「找所有调用 GetUser 的地方」 ❌ 可能漏,可能混进 GetUserInfo ✅ 精确,一个不漏
「用户登录逻辑在哪」 ✅ 语义匹配能找到 ❌ 不知道搜什么关键词

RAG 擅长模糊语义,grep 擅长精确字符串。有了符号名,就该 grep 上场。

2.2 grep 为什么是精确检索的主力?

因为它零前提、永不宕机。ripgrep 把代码当纯文本,不在乎:

  • 代码语法对不对
  • 文件是不是写了一半
  • 项目能不能编译

只要文件在磁盘上,它就能搜。 这在 Agent 重构场景极其重要——代码经常处于「改了一半」的破损态,这时 LSP 崩了、AST 解析失败了,只有 grep 还能干活。

2.3 grep 的缺陷:召回只多不少

grep 分不清代码符号和普通文本。搜 User 会命中:

type User struct { ID int }   // ✅ 真正的定义
// 注释:User 相关逻辑         // ❌ 注释
fmt.Println("User 登录成功")  // ❌ 字符串
const UserName = "test"       // ❌ 别的标识符

大量噪声塞进上下文 → 污染、稀释、幻觉。
所以 grep 之后,需要第三站来降噪。


三、第三站:TreeSitter —— 给 grep 结果戴上「语法眼镜」

3.1 先厘清:TreeSitter 和 AST 是什么关系?

AST 是概念(抽象语法树),TreeSitter 是能生成 AST 的具体工具。

  • AST ≈「房子的设计图纸」——通用概念
  • TreeSitter ≈「自动画图仪」——你给它源码,它吐出图纸

TreeSitter 是 GitHub 开发的开源增量解析库,支持几十种语言,一套 API 通吃。

3.2 TreeSitter 在流水线里其实出现了两次

这是很多人忽略的点——TreeSitter 一身二任:

① 索引阶段(第一站 RAG 里):当切刀
决定代码怎么切成 chunk。按函数/类边界切(AST-aware chunking),保证每个向量片段是完整语义单元,而不是把函数拦腰切断。切块质量 = RAG 召回质量的上限。

② 查询阶段(这一站):当过滤器
把 grep 的结果降噪。用语法树判断:只保留「真正是代码符号」的匹配,剔除注释节点、字符串字面量节点里的假命中。

回到刚才的例子,grep 搜 User 命中一堆噪声,TreeSitter 一过滤,注释和字符串里的 User 全部被剔除,只剩真正的结构体定义和引用。

3.3 TreeSitter 的能力边界:只能看单文件

TreeSitter 有个致命局限:只能孤立解析单个文件
service/user.go 里看到 model.User,它知道这是个类型引用,但不知道 User 定义在哪个文件

它能看懂一页纸的语法,但不知道这页引用的概念写在另一张纸上。
想跨文件?进第四站——LSP。


四、第四站:LSP —— 跨文件依赖,但只在代码干净时用

4.1 LSP 能做什么

LSP(Language Server Protocol,如 gopls / tsserver / pyright)会加载整个项目,建立全局符号表,回答:

  • User 定义在哪?」→ 跳转定义
  • 「哪些地方用了 GetUser?」→ 查找所有引用

LSP 是唯一能精确回答「跨文件依赖」的一层,把「从 1 到 N」做到极致。

4.2 为什么 Agent 不敢默认开 LSP?

因为它有个致命前提:项目代码必须整体可解析

model/user.go     ✅ 语法完整 → 符号表里有 User
service/user.go   ❌ 语法错误 → 解析失败 → 符号表里没有 GetUser
api/handler.go    ✅ 但它调用了 service.GetUser

service/user.go 一坏,LSP 就不知道里面有 GetUser,连 handler.go 里的调用也查不到。
一个文件坏了,整条依赖链的查询都失效。

4.3 Agent 重构的天然矛盾

  • Agent 逐文件改 → 中间状态必然有语法错误
  • 有语法错误 → LSP 解析失败 → 结果不可信
  • 结果不可信 → AI 基于错误信息继续改 → 错上加错

所以主流 Agent 只在特定时机用 LSP:

  • 重构启动前(代码干净):用 LSP 批量查引用,生成待修改清单
  • 迭代中途:关掉 LSP,退回 grep + TreeSitter
  • 一轮改完、修好编译错误后:再用 LSP 核验

五、终点站(其实没人到):全局 AST 依赖图谱

5.1 理想很美好

一次性扫描全仓库,用 AST 提取所有符号 + 完整调用链,存成一张图:

model.User ← service.GetUser ← api.ListUserHandler ← router.Register

AI 要改 User,查图就知道所有影响范围,一个不漏。

5.2 现实很骨感

索引过期问题,比 RAG 严重得多。

  • RAG 过期:召回旧片段,模型可能还能发现不对
  • 图谱过期:整个依赖关系都错了,AI 基于错误的图做决策,后果更严重

Agent 每改一个文件图就过时;想准就得增量重解析,大仓库开销巨大;代码破损时增量解析直接失败,图谱「冻住」。

全局图谱适合静态代码审计、漏洞扫描,不适合持续被改写的 Agent 场景。


六、把整条流水线串起来看

五站能力对比:

站点 角色 解决什么 前提条件 代码破损时
① 向量 RAG 自然语言入口 从 0 到 1:给起点 可用(但可能过期)
② ripgrep 精确搜索主力 从 1 到 N:不漏 ✅ 完全可用
③ TreeSitter 切刀 + 过滤器 降噪 + 保证切块质量 单文件语法完整 部分可用
④ LSP 跨文件语义 补全依赖链 整个项目可解析 ❌ 失效
⑤ 全局 AST 图谱 完整调用网络 理想的全依赖 稳定快照 ❌ 迅速过期

主流产品选型:

产品 RAG 角色 TreeSitter LSP grep
Cursor 入口 + 代码库检索 切块 + 过滤 基本不依赖 辅助
Windsurf Cascade 文件集基础 结构分析 有限 辅助
Claude Code 默认无 默认无 可选插件 主力
JetBrains AI 有限 内置 PSI 深度集成 较少

七、实践建议

1. 慎用 @codebase,精准投喂
RAG 召回不稳定。已知范围就手动 @src/service/user,别让它全库瞎召回。

2. 善用「从 1 到 N」:知道符号名就别用自然语言
❌「看看用户登录相关逻辑」(RAG 模糊召回)
✅「分析 LoginService 的所有方法及其调用方」(精确符号,走 grep/LSP)

3. 重构前先拉清单
代码干净时,先让 AI 输出「待修改文件清单」,确认后再动手——利用干净窗口期把变更范围锁死。

4. 拆分会话
按 model 层 / service 层 / API 层分会话,每个新会话都是干净上下文,避免历史错误污染。

5. 别信「已完成全部修改」
底层是 RAG + grep,漏改是常态,重要重构自己 review。


结语

跟着一次查询走下来,你会发现 AI IDE 的检索一点都不「魔法」,而是一套务实的接力:

  • RAG 先跑第一棒——把人话变成代码线索,从 0 到 1
  • grep 接第二棒——有了符号名精确搜,稳但糙
  • TreeSitter 降噪——既当 RAG 的切刀,又当 grep 的滤网
  • LSP 挑时机补依赖——强但脆
  • 全局图谱——美但虚,还在路上

一句话记住:

RAG 给你起点,精确检索帮你挖全。
不是模型不行,是检索喂给它的东西不对。

Logo

一站式 AI 云服务平台

更多推荐