
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 到 11.1 为什么查询的第一站是 RAG因为用户说的是人话不是符号名。用户不会说「找到pkg/auth目录下所有实现了Authenticator接口的类」用户会说「帮我看看登录相关的代码」后者没有任何精确关键词grep 根本不知道搜什么字符串LSP 也不知道你要查哪个符号。只有 RAG 能接住这种模糊的自然语言查询。RAG 的核心能力是跨模态映射把自然语言的语义映射到代码空间。这就是它作为「入口」的不可替代价值。1.2 RAG 是怎么工作的以 Cursor 的codebase为例分两个阶段索引阶段打开项目时后台默默做扫描整个仓库把代码切成片段chunk——这里就用到了 TreeSitter按函数/类边界切保证每段语义完整后面第三站会详细讲 TreeSitter每个片段生成 embedding向量存入本地向量库查询阶段你输入需求时把自然语言需求转成向量用余弦相似度从向量库召回「语义最接近」的 N 个片段把这些片段作为线索交给模型1.3 RAG 的三大固有缺陷RAG 是好入口但绝不能当最终依据。它有三个绕不开的缺陷缺陷 1同名不同义向量分不清// pkg/cache/user.gotypeCachestruct{...}// 用户缓存// pkg/db/cache.gotypeCachestruct{...}// 数据库查询缓存两个Cache语义完全不同但在向量空间里距离很近模型分不清哪个是哪个。缺陷 2代码结构相似 ≠ 业务语义相关funcLogin(...){查库;校验密码;发token}// 用户登录funcAdminLogin(...){查库;校验密码;发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 改成 stringtypeUserstruct{IDstring}// 文件变了 → 重新 embedding ✅// service/user.go —— 你没动这个文件funcGetUser()*model.User{...}// 文件没变 → embedding 不更新 ❌service/user.go字面内容一个字没改增量更新不会碰它。但它引用的User定义已经变了——它的真实含义变了向量却还是旧的。向量索引只能感知「文本变化」感知不到「语义变化」。这是 AST-aware chunking 也救不了的根本缺陷。1.5 RAG 的定位RAG 是入口不是终点。它解决「从 0 到 1」——在你不知道搜什么时给你起点。但它召回不精确、会过期、丢关系所以拿到线索后必须交给精确检索去核实和挖深。拿到 RAG 给的线索后模型会从中提取出精确符号名比如LoginService、GetUser进入第二站。二、第二站ripgrep —— 有了符号名精确搜索从 1 到 N 的主力2.1 为什么用 grep 而不继续用 RAG因为一旦有了精确符号名你要的是「一个都不能漏」而不是「语义最相似」。任务RAGgrep「找所有调用GetUser的地方」❌ 可能漏可能混进GetUserInfo✅ 精确一个不漏「用户登录逻辑在哪」✅ 语义匹配能找到❌ 不知道搜什么关键词RAG 擅长模糊语义grep 擅长精确字符串。有了符号名就该 grep 上场。2.2 grep 为什么是精确检索的主力因为它零前提、永不宕机。ripgrep 把代码当纯文本不在乎代码语法对不对文件是不是写了一半项目能不能编译只要文件在磁盘上它就能搜。这在 Agent 重构场景极其重要——代码经常处于「改了一半」的破损态这时 LSP 崩了、AST 解析失败了只有 grep 还能干活。2.3 grep 的缺陷召回只多不少grep 分不清代码符号和普通文本。搜User会命中typeUserstruct{IDint}// ✅ 真正的定义// 注释User 相关逻辑 // ❌ 注释fmt.Println(User 登录成功)// ❌ 字符串constUserNametest// ❌ 别的标识符大量噪声塞进上下文 → 污染、稀释、幻觉。所以 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 能做什么LSPLanguage 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.GetUserservice/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.RegisterAI 要改User查图就知道所有影响范围一个不漏。5.2 现实很骨感索引过期问题比 RAG 严重得多。RAG 过期召回旧片段模型可能还能发现不对图谱过期整个依赖关系都错了AI 基于错误的图做决策后果更严重Agent 每改一个文件图就过时想准就得增量重解析大仓库开销巨大代码破损时增量解析直接失败图谱「冻住」。全局图谱适合静态代码审计、漏洞扫描不适合持续被改写的 Agent 场景。六、把整条流水线串起来看五站能力对比站点角色解决什么前提条件代码破损时① 向量 RAG自然语言入口从 0 到 1给起点无可用但可能过期② ripgrep精确搜索主力从 1 到 N不漏无✅ 完全可用③ TreeSitter切刀 过滤器降噪 保证切块质量单文件语法完整部分可用④ LSP跨文件语义补全依赖链整个项目可解析❌ 失效⑤ 全局 AST 图谱完整调用网络理想的全依赖稳定快照❌ 迅速过期主流产品选型产品RAG 角色TreeSitterLSPgrepCursor入口 代码库检索切块 过滤基本不依赖辅助WindsurfCascade 文件集基础结构分析有限辅助Claude Code默认无默认无可选插件主力JetBrains AI有限内置 PSI深度集成较少七、实践建议1. 慎用codebase精准投喂RAG 召回不稳定。已知范围就手动src/service/user别让它全库瞎召回。2. 善用「从 1 到 N」知道符号名就别用自然语言❌「看看用户登录相关逻辑」RAG 模糊召回✅「分析LoginService的所有方法及其调用方」精确符号走 grep/LSP3. 重构前先拉清单代码干净时先让 AI 输出「待修改文件清单」确认后再动手——利用干净窗口期把变更范围锁死。4. 拆分会话按 model 层 / service 层 / API 层分会话每个新会话都是干净上下文避免历史错误污染。5. 别信「已完成全部修改」底层是 RAG grep漏改是常态重要重构自己 review。结语跟着一次查询走下来你会发现 AI IDE 的检索一点都不「魔法」而是一套务实的接力RAG 先跑第一棒——把人话变成代码线索从 0 到 1grep 接第二棒——有了符号名精确搜稳但糙TreeSitter 降噪——既当 RAG 的切刀又当 grep 的滤网LSP 挑时机补依赖——强但脆全局图谱——美但虚还在路上一句话记住RAG 给你起点精确检索帮你挖全。不是模型不行是检索喂给它的东西不对。