Agent 里的代码检索:Grep vs RAG 全方位解析
文章目录
前言
这篇文章讲清楚 Grep 和 RAG 在 AI Agent 里到底是怎么工作的——从原理到参数传递、返回结果,再到两者怎么配合使用。
如果你在做 Agent 开发,或者好奇 Claude Code / Cursor 是怎么"读懂"你的代码库的,这篇文章给你一个完整的认知框架。
一、先搞清楚 Agent 为什么需要检索
LLM 的上下文窗口是有限的。哪怕 Claude 有 100K token,一个中型 Java 项目轻松几十万行,全塞进去根本不现实。
所以 Agent 在回答你之前,需要先"找到相关代码",再把这部分内容送进 LLM。这个"找"的过程,就是检索。
用户提问
↓
检索层(Grep / RAG)
↓
取出相关代码片段
↓
拼成 Prompt 送给 LLM
↓
LLM 回答
检索的质量直接决定 LLM 回答的质量。找错了,LLM 再强也没用。
二、Grep:最古老也最直接的检索
Grep 是什么
Grep 是 Unix 上的文本搜索工具,1974 年诞生,比 LLM 早了半个世纪。
它的工作原理极其简单:逐行扫描文件,找出匹配正则表达式的行。
grep -r "getUserById" ./src
# 返回结果
src/service/UserService.java:23: public User getUserById(Long id) {
src/controller/UserController.java:45: User user = userService.getUserById(userId);
src/test/UserServiceTest.java:12: User result = service.getUserById(1L);
就这么简单。没有任何"智能",纯粹的字符串匹配。
Grep 在 Agent 里怎么工作
以 Claude Code 为例,当你问"帮我看看 getUserById 这个方法有没有问题",Agent 内部大概是这样的:
第一步:Agent 生成 tool call
LLM 决定调用 grep 工具,生成如下参数:
{
"tool": "grep",
"params": {
"pattern": "getUserById",
"path": "./src",
"flags": ["-r", "-n", "--include=*.java"]
}
}
参数说明:
pattern:要搜索的正则表达式或字符串path:搜索范围,缩小范围能节省 tokenflags:-r递归搜索子目录-n显示行号-l只返回文件名(不返回内容,省 token)--include=*.java只搜 Java 文件
第二步:工具执行,返回结果
{
"matches": [
{
"file": "src/service/UserService.java",
"line": 23,
"content": " public User getUserById(Long id) {"
},
{
"file": "src/controller/UserController.java",
"line": 45,
"content": " User user = userService.getUserById(userId);"
}
],
"total_matches": 3
}
第三步:Agent 拿到结果,再读具体文件
光有行号不够,Agent 还会调 read_file 工具,读取定义所在函数的上下文(比如前后 30 行),然后一起塞进 Prompt。
第四步:最终 Prompt 大概长这样
用户问题:帮我看看 getUserById 这个方法有没有问题
相关代码(来自 UserService.java:20-35):
```java
@Service
public class UserService {
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
请分析这段代码是否有问题。
### Grep 的优势和局限
**优势:**
- **精确**:找到的就是你要的,零噪音
- **快**:毫秒级,不需要任何预处理
- **透明**:你能完全看懂它在搜什么
- **无依赖**:不需要向量数据库、不需要 embedding 模型
**局限:**
你问:“处理支付失败的重试逻辑在哪里?”
Grep 能搜什么?
→ “支付失败” ✓(找中文注释还行)
→ “retry” ✓(英文关键词)
→ “处理支付失败的逻辑” ✗(语义描述,没有对应的字符串)
Grep 是**字面量匹配**,不理解语义。你得猜出代码里用了什么词,才能搜到。
---
## 三、RAG:让检索理解语义
### RAG 是什么
RAG = **Retrieval-Augmented Generation**,检索增强生成。
核心思路是:把文档/代码转成向量(embedding),存进向量数据库,查询时把问题也转成向量,用**向量相似度**找最相关的内容。
预处理阶段(提前做)
代码文件 → 切块 → Embedding 模型 → 向量 → 向量数据库
↑
查询阶段(实时做) |
用户问题 → Embedding 模型 → 查询向量 → 相似度搜索
↓
Top-K 相关片段
↓
拼 Prompt → LLM
关键在 **Embedding**:它把文字变成一串数字(比如 1536 维的向量),语义相近的文字在向量空间里距离也近。
“处理支付失败” 的向量 → [0.23, -0.15, 0.87, …]
“payment retry logic” → [0.25, -0.13, 0.84, …] ← 向量很接近!
所以即使你用中文问,也能找到英文代码里的相关逻辑。
### RAG 在 Agent 里怎么工作
**预处理阶段**(项目打开时/代码变更时触发):
- 遍历代码文件
- 按一定策略切块(chunk)
- 每块调 embedding API,得到向量
- 存入向量数据库(Faiss / Pinecone / Chroma 等)
切块策略很关键,后面细讲。
**查询阶段**(用户提问时):
**第一步:问题向量化**
```json
{
"input": "处理支付失败的重试逻辑",
"model": "text-embedding-3-small"
}
返回:
{
"embedding": [0.023, -0.156, 0.872, ...(1536维)]
}
第二步:向量数据库检索
results = vector_db.search(
query_vector=query_embedding,
top_k=5, # 返回最相似的 5 个片段
threshold=0.75 # 相似度阈值,低于这个就不要
)
返回结果:
[
{
"score": 0.92,
"file": "src/payment/PaymentRetryService.java",
"chunk": "public void retryFailedPayment(Payment payment) {\n int maxRetries = 3;\n for (int i = 0; i < maxRetries; i++) { ...",
"start_line": 45,
"end_line": 78
},
{
"score": 0.87,
"file": "src/payment/PaymentConfig.java",
"chunk": "// 支付失败重试间隔配置\npublic static final int RETRY_INTERVAL_SECONDS = 30;",
"start_line": 12,
"end_line": 15
}
]
第三步:拼 Prompt
用户问题:处理支付失败的重试逻辑在哪里?
相关代码片段(相似度 0.92):
[PaymentRetryService.java:45-78]
public void retryFailedPayment(Payment payment) {
...
}
相关代码片段(相似度 0.87):
[PaymentConfig.java:12-15]
// 支付失败重试间隔配置
...
切块策略:RAG 质量的关键
切块切得好不好,直接影响 RAG 的噪音多少。
按固定行数切(差):
第 1 块:第 1-50 行
第 2 块:第 51-100 行 ← 可能把一个函数从中间切断
函数被切断,embedding 的语义就乱了。
按 AST 节点切(好):
第 1 块:UserService 类的 getUserById 方法(完整)
第 2 块:UserService 类的 saveUser 方法(完整)
第 3 块:UserService 类的类注释 + 字段声明
按函数/类边界切,每块都是完整的语义单元,embedding 质量高得多。
Cursor 的 RAG 噪音相对少,主要原因就是用了 AST-aware chunking。
四、两者的本质区别
Grep RAG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
匹配方式 字面量 / 正则 语义相似度
适合场景 "找 getUserById" "找处理支付失败的逻辑"
精确度 高(找到就是) 中(可能有噪音)
速度 极快(毫秒) 较慢(需 embedding)
预处理 无 需要建索引
token消耗 高(返回原始文本) 中(返回精选片段)
依赖 无 向量数据库 + embedding 模型
Grep 是关键词搜索引擎,RAG 是语义搜索引擎。两者解决的是不同类型的问题。
五、实际 Agent 里是怎么配合用的
主流 Agent 的策略不是"选一个",而是分层检索:
用户问题
↓
问题分类
├── 精确查询(找某个符号/文件)
│ ↓
│ Grep
│ ↓
│ 精确结果
│
└── 语义查询(找某个功能/逻辑)
↓
RAG
↓
候选片段(Top-K)
↓
Grep 二次验证(确认准确位置)
↓
精确结果
Claude Code 的实际做法
Claude Code 主要用 grep,但它很聪明地控制了 token 消耗:
- 先用
-lflag 只搜文件名,不返回内容 - 确定文件后,只读相关区域(不是整文件)
- 多次 grep 缩小范围,再精读
# 第一步:找有哪些文件
grep -rl "getUserById" ./src
# 返回:src/service/UserService.java
# 第二步:在该文件里找精确位置
grep -n "getUserById" src/service/UserService.java
# 返回:23: public User getUserById(Long id) {
# 第三步:读取第 20-40 行的上下文
# 只读这 20 行,而不是整个文件
Cursor 的实际做法
Cursor 以 RAG 为主,grep 为辅:
- 用户提问 → 先走 RAG,拿到语义相关片段
- 对 RAG 结果里的符号,用 grep/AST 找精确定义
- 两路结果合并送给 LLM
六、为什么主流还是 Grep
看完 RAG 这么强大,你可能会问:为啥 Claude Code 不用 RAG?
原因很现实:
1. 冷启动问题
RAG 需要先对整个项目建索引。一个中型项目,embedding 几千个文件,可能要几分钟。用户打开项目就要等,体验很差。
2. 索引维护成本
代码随时在改。改了一个文件,索引就过期了。增量更新索引比听起来复杂。
3. 部署复杂
需要运行向量数据库,占内存,还要维护 embedding 模型版本。grep 只需要一行 shell 命令。
4. 噪音问题依然存在
RAG 的 Top-K 结果里,相似度排第 3 的片段可能完全不相关,但还是被塞进了 Prompt,浪费 token 还可能干扰 LLM。
5. Grep 对大多数任务够用
80% 的 Agent 任务是精确符号查询,grep 完全胜任。
七、实战建议:自己做 Agent 怎么选
小项目(< 5万行)
直接用 grep,够快够准,不引入额外复杂度。
中大型项目(> 10万行)
用混合策略:
精确查询 → grep
语义查询 → RAG(AST-aware chunking)
↓
命中结果 → grep 精确定位
提升 grep 效率的几个技巧:
# 只返回文件名,减少 token
grep -rl "pattern" ./src
# 限制文件类型
grep -r "pattern" --include="*.java" ./src
# 排除干扰目录
grep -r "pattern" --exclude-dir={.git,target,node_modules} ./src
# 显示上下文(前后 3 行)
grep -n -C 3 "pattern" ./src/UserService.java
提升 RAG 质量的几个技巧:
- 用 tree-sitter 做 AST-aware chunking,按函数/类切,不按行数切
- 给每个 chunk 加元数据(文件名、类名、方法名),参与 embedding
- 设合理的相似度阈值,低于 0.75 的结果直接丢弃
- hybrid search:向量相似度 + BM25 关键词搜索取并集,再重排
总结
- Grep = 精确、快、无依赖,适合符号级精确查询
- RAG = 语义理解强,适合功能描述式查询,但有噪音和索引成本
- 实际 Agent 两者配合用,Grep 做精确查,RAG 做语义理解
- 主流工具现状:Claude Code 以 Grep 为主,Cursor 以 RAG 为主
- 自己做 Agent:小项目用 Grep,大项目上混合策略
检索层做好了,LLM 才能发挥真正的价值。垃圾进,垃圾出——这条规律在 Agent 里同样成立。
更多推荐


所有评论(0)