从 RAG 到 Agentic Search:为什么 Coding Agent 更喜欢“边搜边想”?
从 RAG 到 Agentic Search:为什么 Coding Agent 更喜欢“边搜边想”?
文章目录
- 从 RAG 到 Agentic Search:为什么 Coding Agent 更喜欢“边搜边想”?
-
- 1. 一个反常现象:为什么 Coding Agent 不直接用传统 RAG?
- 2. 先把 RAG 说准确:它并不只是 Embedding + Vector DB + Top-K
- 3. 代码和普通文档,根本不是同一种“信息”
- 4. 代码不是一堆文本,而是一张 Graph(关系图)
- 5. 当代码被切成 Chunk,我们丢掉了什么?
- 6. 代码里的“精确名字”,往往比“语义相似”更重要
- 7. 语义相似还不够:Debug 经常需要寻找“关系”
- 8. Coding Agent 面对的是一个不断变化的代码仓库
- 9. Top-K 很难回答一个关键问题:还有没有遗漏?
- 10. 真正的变化:从“先检索再推理”到“边搜边想”
- 11. 为什么 Agent 越聪明,工具反而可以越简单?
- 12. 所以代码 RAG 已经没用了吗?
- 13. 结语:真正改变的不是 Retrieval,而是 Retrieval 在系统中的位置
1. 一个反常现象:为什么 Coding Agent 不直接用传统 RAG?
过去几年,只要提到“让大模型理解一大堆外部资料”,很多人的第一反应都是 RAG(Retrieval-Augmented Generation,检索增强生成)。
面对几十万份文档,我们通常不会把所有内容一次性塞进大模型,而是先建立索引:
文档
↓
Chunking(切块)
↓
Embedding(向量化)
↓
Index / Vector DB(索引 / 向量数据库)
等用户真正提问时,再从中找出最相关的内容交给大模型。
因此,当 Coding Agent(编码智能体)需要理解一个包含几千甚至几万个文件的代码仓库时,一个非常自然的想法就是:
代码也是文本,为什么不把整个仓库做成 RAG?
例如:
代码仓库
↓
Chunking(切块)
↓
Embedding(向量化)
↓
Vector DB(向量数据库)
↓
检索相关代码
↓
Coding Agent
从直觉上看,这似乎非常合理。
但真正观察很多 Coding Agent 的工作方式,会发现一个有些反常的现象:
它们经常大量使用的并不是复杂的 Vector Search(向量搜索),而是一些看起来非常“传统”的工具:
grep
(搜索文件内容)
glob
(按照文件路径或文件名模式查找文件)
read
(读取文件)
Symbol Search
(符号搜索,例如查找函数、类、变量)
Find References
(查找某个符号的所有引用)
也就是说,一个非常先进的大模型,进入代码仓库以后,做的事情有时候像一个经验丰富的程序员:
先搜一下
↓
打开一个文件
↓
看懂之后再搜另一个名字
↓
继续读代码
↓
沿着调用关系往下找
而不是:
用户提问
↓
一次检索出几个最相关的代码块
↓
直接开始回答或修改
这就引出了本文真正想讨论的问题:
为什么代码仓库这个场景,会让 Coding Agent 更倾向于“边搜边想”,而不是只依赖一次性的 RAG 检索?
这里需要先澄清一点。
本文并不是想证明:
RAG 不行,Search 更好。
这种说法太粗糙。
真正值得讨论的是两种不同的检索范式:
Pipeline Retrieval
(流水线式检索)
检索 → 准备上下文 → 推理
和:
Agent-controlled Retrieval
(由 Agent 动态控制的检索)
推理 → 搜索 → 阅读 → 再推理 → 再搜索
后者最大的区别,是搜索本身进入了推理循环。
Agent 不需要在第一次检索时就猜中所有相关代码,而是可以根据刚刚看到的代码,决定下一步应该搜什么。
例如用户提出:
“登录过期以后,有时候没有跳回登录页。”
Agent 一开始可能并不知道问题在哪。
它可以先搜索:
401
Unauthorized
SessionExpired
然后发现:
AuthInterceptor
读取这个文件后,又发现:
SessionExpired
于是继续搜索 SessionExpired 的处理位置,最终找到 Router(路由模块)。
整个过程更像:

这里最重要的一点是:
下一次 Search(搜索)的内容,是上一次 Reasoning(推理)的结果。
因此,我们真正需要理解的,并不是简单的:
Vector DB(向量数据库)和 grep 谁更强?
而是:
为什么代码这个场景特别适合让 Retrieval(检索)和 Reasoning(推理)交替进行?
要回答这个问题,我们首先得把 RAG 本身说准确。
2. 先把 RAG 说准确:它并不只是 Embedding + Vector DB + Top-K
很多介绍 RAG 的文章,会把它画成这样:
Query
(用户问题)
↓
Embedding
(向量化)
↓
Vector DB
(向量数据库)
↓
Top-K
(取最相似的前 K 条)
↓
LLM
(大语言模型)
这个图不能说错,但它描述的更准确名称应该是:
Basic Vector RAG(基础向量 RAG)的一种简化流程。
如果直接把它说成“RAG 的完整流程”,就过于简单了。
2.1 Basic Vector RAG(基础向量 RAG)到底做了什么?
一个基础的 Vector RAG(向量 RAG),通常至少包含两个阶段。
第一阶段是 Indexing(建立索引)。

例如有一篇很长的文档:
完整文档
↓
切成多个 Chunk(文本块)
↓
每个 Chunk 生成一个 Embedding(向量表示)
↓
存入 Vector DB(向量数据库)
Embedding(向量化)的作用,可以简单理解为:
把一段文本转换成一串数字,让语义相似的文本在向量空间中更加接近。
例如:
“如何重置密码?”
和:
“忘记密码以后怎么办?”
虽然文字不一样,但它们的语义比较接近,因此对应的向量通常也会比较接近。
第二阶段才是用户真正提问时发生的 Retrieval(检索)。

这里的 Top-K,可以简单理解为:
从所有候选内容中,取相似度最高的前 K 条。
例如:
Top-3
就是取最相关的前三条。
所以最基础的流程可以概括成:
先把资料建立索引
用户提出问题
↓
找到相关资料
↓
把资料放进 Context(上下文)
↓
让 LLM 根据这些资料回答
这就是 RAG 最核心的思想:
模型不只依赖自己参数中已有的知识,而是在生成答案之前,先从外部数据源取回相关信息。
2.2 真实工程里的 RAG 通常比这个复杂
实际工程中的 RAG,往往不会简单到“向量搜索一下,再把 Top-K 直接塞给模型”。一个更完整的 Pipeline(流水线)可能包含:

其中几个概念尤其重要:
- Query Rewrite(查询改写):把不适合直接搜索的问题改写成更明确的 Query(查询)。
- Hybrid Search(混合搜索):结合 Semantic Search(语义搜索)与 Keyword Search(关键词搜索),而不是只依赖向量相似度。
- Reranking(重排序):先召回较多候选内容,再用更精细的模型重新排序,挑出真正值得放进 Context(上下文)的结果。
例如,搜索明确的函数名 refreshAccessToken 时,Keyword Search(关键词搜索)可能比 Semantic Search(语义搜索)更直接;而寻找“有没有类似的限流实现”时,语义检索又可能更有优势。
所以,严谨来说:
RAG 并不等于 Embedding + Vector DB + Top-K。它是一种“先从外部信息源取回相关内容,再增强模型生成”的整体思路。
2.3 真正的区别不在 Vector DB 和 grep
如果只把两种方案理解成:
Vector DB
VS
grep
其实会把问题说得太浅。
因为现代 RAG 完全可以加入:
Keyword Search(关键词搜索)
Hybrid Search(混合搜索)
Reranking(重排序)
Query Rewrite(查询改写)
Metadata Filtering(元数据过滤)
因此,Coding Agent 真正带来的变化,并不是:
把一个高级的 Vector DB 换成一个原始的 grep。
而是检索的控制方式发生了变化。
典型 Pipeline RAG(流水线式 RAG)更像:
用户问题
↓
检索阶段
↓
构建 Context
↓
LLM 进行主要推理
而 Agentic Search(智能体式搜索)更像:
用户问题
↓
Agent 推理
↓
搜索
↓
读取结果
↓
Agent 再推理
↓
换一个搜索目标
↓
继续读取
↓
……
可以把两者压缩成非常简单的对比:
Pipeline Retrieval:
Retrieve → Reason
检索 → 推理
而 Agentic Search:
Retrieve ↔ Reason
检索 ↔ 推理
真正值得研究的,不是“RAG 要不要向量数据库”,而是 Retrieval(检索)应该发生在推理之前,还是应该进入 Agent 的 Reasoning Loop(推理循环)。
3. 代码和普通文档,根本不是同一种“信息”
如果把代码仓库直接当成“另一种文档库”,很容易得出一个直觉:
既然文档可以做 RAG,那么代码也可以切块、向量化、检索。
这个直觉不能说错,但它忽略了一个关键区别:
代码当然可以被当作文本,但代码并不只是文本。
普通文档最重要的信息,通常来自“它表达了什么”。
而代码除了表达“什么意思”,还包含非常明确的结构信息。
例如一段普通文本:
用户登录失败后,系统会要求重新认证。
你最关心的是这句话的 Semantic Meaning(语义含义)。
但如果变成代码:
def handle_request(request):
if token_expired(request.token):
redirect_to_login()
我们关心的就不只是:
这段代码“表达了登录过期后重新登录”。
还会关心很多结构问题:
token_expired() 定义在哪里?
谁调用了 handle_request()?
redirect_to_login() 最终调用了什么?
这个函数属于哪个模块?
有没有其他地方也处理 token expired?
这个函数的测试在哪里?
这些问题已经不再只是 Semantic Search(语义搜索)的问题。
它们涉及的是:
- Symbol(符号)
- Scope(作用域)
- Call Relationship(调用关系)
- Dependency Relationship(依赖关系)
- Control Flow(控制流)
3.1 文档主要关心“说了什么”,代码还关心“怎么连接”
假设有两段文档:
A:用户可以通过邮箱重置密码。
B:忘记密码后,可以发送重置链接到注册邮箱。
虽然两句话不完全一样,但语义非常接近。
因此 Semantic Search(语义搜索)很容易判断:
A 和 B 是相关内容。
这正是 Embedding(向量化)非常擅长的事情。
但代码中的“相关”,经常完全不是这种关系。
例如:
def create_order():
save_order()
另一个文件中:
def save_order():
db.commit()
这两个函数的代码文字可能并不相似。
甚至它们可能:
- 不在同一个文件;
- 不在同一个目录;
- 函数名也没有共同单词。
但它们存在一个非常明确的关系:
create_order()
↓
save_order()
也就是 Call Relationship(调用关系)。
对于程序理解来说,这种关系通常比“两个函数语义像不像”更加重要。
对于文档,两个片段语义相似,往往意味着它们相关;但对于代码,两个函数真正相关的原因,很多时候不是它们写得像,而是它们之间存在调用或依赖关系。
这也是代码检索和普通文档检索最本质的区别之一。
3.2 代码里的名字,不只是文字,而是 Symbol(符号)
再看一个 Python 示例:
from auth import refresh_token
def handle_unauthorized(user):
refresh_token(user)
这里的:
refresh_token
不是普通文本里的一个单词。
它是一个 Symbol(符号)。
这个符号背后可能对应:
定义位置
↓
auth/token.py
调用位置
↓
middleware.py
api.py
worker.py
测试
↓
tests/test_token.py
所以当 Coding Agent(编码智能体)看到 refresh_token 后,下一步最有价值的问题可能不是:
哪些代码在语义上和“刷新 Token”相似?
而是:
这个 Symbol 到底定义在哪里,谁引用了它?
这就解释了为什么代码工具里经常会出现:
Go to Definition
(跳转到定义)
Find References
(查找所有引用)
Symbol Search
(符号搜索)
这些能力。
它们利用的是代码自身的结构,而不是纯文本语义。
3.3 同样是“相关”,代码里至少存在几种完全不同的关系
假设我们有这样一组 Python 文件:
app.py
service.py
repository.py
database.py
调用关系是:
对于 repository.py 来说,可能同时存在几种不同的“相关”。
第一种:Semantic Similarity(语义相似)
另一个文件也在处理“保存订单”。
def persist_order(order):
...
它和 save_order() 在语义上很像。
第二种:Call Relationship(调用关系)
def create_order(order):
save_order(order)
create_order() 和 save_order() 有直接调用关系。
第三种:Dependency Relationship(依赖关系)
from database import connection
repository.py 依赖 database.py。
第四种:Structural Relationship(结构关系)
例如:
class UserRepository(BaseRepository):
...
这里还有 Inheritance(继承)关系。
也就是说,“相关代码”这四个字本身就可能表示完全不同的东西:
| 问题 | 真正需要寻找的关系 |
|---|---|
| 有没有类似的缓存实现? | Semantic Similarity(语义相似) |
| 谁调用了这个函数? | Call Relationship(调用关系) |
| 这个模块依赖谁? | Dependency Relationship(依赖关系) |
| 谁继承了这个基类? | Inheritance Relationship(继承关系) |
如果全部统一处理成:
计算 Embedding
↓
找最相似的 Top-K
很多信息其实没有被充分利用。
3.4 代码是一种“半结构化甚至高度结构化”的信息
严格来说,把代码简单称为 Structured Data(结构化数据)也不完全准确。
因为传统意义上的结构化数据通常是:
数据库表
Excel 表格
JSON 字段
而代码仍然拥有大量自由文本特征。
所以更准确的说法是:
代码是一种具有非常强结构约束的文本。
例如 Python 代码:
class PaymentService:
def pay(self, order):
return self.client.charge(order)
人类看到的是文本。
但 Parser(解析器)可以把它解析成明确的结构:
Class
└── PaymentService
└── Method
└── pay
└── Call
└── self.client.charge
这已经和普通文章完全不同了。
基于这些结构,还可以进一步构建:
- AST(Abstract Syntax Tree,抽象语法树)
- Call Graph(调用图)
- Dependency Graph(依赖图)
- Symbol Table(符号表)
因此代码天然包含大量机器可以直接利用的结构。
3.5 为什么这一点会影响 Coding Agent 的检索方式?
假设用户说:
“创建订单的时候偶尔会重复写数据库。”
如果只根据 Semantic Similarity(语义相似度)检索,系统可能优先找到:
create_order()
OrderService
OrderModel
OrderValidator
因为这些东西和“订单”在语义上非常接近。
但真正的问题可能在:
retry_request()
甚至:
transaction.commit()
问题路径可能是:
Retry Wrapper(重试包装器)和“订单重复写入”在文字上未必相似。
但它恰恰处在真正的执行路径上。
所以 Debug(调试)时,Agent 很多时候真正需要的是:
沿着程序结构不断寻找下一个节点。
而不是单纯寻找:
和用户问题最相似的几个代码块。
3.6 核心结论
可以把普通文档和代码简单对比成:
| 普通文档 | 代码 |
|---|---|
| 主要承载自然语言语义 | 同时承载语义和程序结构 |
| 相关性经常来自语义相似 | 相关性可能来自调用、依赖、继承等关系 |
| 文本本身是主要信息 | 文本背后的结构同样重要 |
| Semantic Search 很重要 | Semantic + Symbol + Structure 都重要 |
因此:
代码检索不仅是 Information Retrieval(信息检索),很多时候也是 Program Analysis(程序分析)。
因此,代码检索不能只讨论:
“哪种搜索算法更准确?”
还需要考虑:
代码中的结构,究竟应该怎样参与检索?
4. 代码不是一堆文本,而是一张 Graph(关系图)
代码中的“相关”不只来自 Semantic Similarity(语义相似度),还可能来自调用、依赖、继承等关系。把这些关系连接起来,一个代码仓库其实很像一张 Graph(图)。
假设有这样一个简单的 Python 项目:
project/
├── app.py
├── order_service.py
├── order_repository.py
└── database.py
代码可能是:
# app.py
from order_service import create_order
def checkout(order):
create_order(order)
# order_service.py
from order_repository import save_order
def create_order(order):
save_order(order)
# order_repository.py
from database import commit
def save_order(order):
commit(order)
如果只把它当作文本,我们看到的是三个独立的代码片段。
但从程序结构来看,它实际上形成了一条明确的执行关系:
也就是:
checkout()
↓
create_order()
↓
save_order()
↓
commit()
这里每个函数都可以看作一个 Node(节点),函数之间的调用关系则可以看作 Edge(边)。
这就是代码天然具有的 Graph Structure(图结构)。
4.1 一个代码仓库里同时存在很多种“边”
代码里的 Graph 并不只有函数调用。
例如:
from database import connection
表示一种 Import Relationship(导入关系)。
class UserRepository(BaseRepository):
pass
表示一种 Inheritance Relationship(继承关系)。
def create_user():
save_user()
表示一种 Call Relationship(调用关系)。
于是,一个真实代码仓库更接近下面这种结构:

因此,当 Coding Agent(编码智能体)试图理解一段代码时,它经常不是在问:
哪段代码和当前代码最像?
而是在问:
它调用了谁?谁又调用了它?它依赖了什么?这个类型从哪里继承?这个变量最终在哪里被修改?
这些都是关系问题。
4.2 Debug(调试)本质上经常是在 Graph 上追踪路径
例如用户发现:
“创建订单后,数据库偶尔会重复写入。”
入口可能是:
def create_order(order):
order_service.create(order)
但 Bug 并不一定存在于 create_order()。
Coding Agent 可能需要沿着执行路径继续向下寻找:
create_order()
↓
OrderService.create()
↓
OrderRepository.save()
↓
execute_with_retry()
↓
database.commit()
假如真正的问题出在:
def execute_with_retry(fn):
try:
return fn()
except TimeoutError:
return fn()
那么真正需要关注的是:
execute_with_retry()
而不是名字里包含 order 的函数。
从 Graph(关系图)的角度看,这件事情很自然:
Bug 的定位过程实际上是在寻找一条:
从现象到真正原因的程序路径。
这与单纯寻找 Semantic Similarity(语义相似)的代码块,是两种不同的问题。
4.3 AST(抽象语法树)让代码的结构可以被机器直接读取
代码的另一个特殊之处,是它不仅“存在结构”,而且这些结构通常可以被 Parser(解析器)直接提取。
例如:
result = client.request(url)
对于人来说,这是一行文本。
对于代码解析器来说,它可以表示成类似:
Assignment
├── Target
│ └── result
└── Call
├── Object
│ └── client
├── Method
│ └── request
└── Argument
└── url
这就是 AST(Abstract Syntax Tree,抽象语法树)。
代码工具可以基于这些结构继续建立:
- Symbol Table(符号表)
- Call Graph(调用图)
- Dependency Graph(依赖图)
- Type Relationship(类型关系)
因此,代码检索天然拥有一种普通文档很少具备的优势:
我们不需要完全依赖模型“猜”两个代码片段之间有没有关系,很多关系本身就可以通过程序分析直接得到。
4.4 LSP(语言服务器协议)其实也在利用这些结构
程序员在 IDE 里经常使用:
Go to Definition
(跳转到定义)
Find References
(查找引用)
Go to Implementation
(跳转到实现)
这些能力背后并不是简单的 Semantic Search(语义搜索)。
例如看到:
refresh_token(user)
点击“跳转到定义”以后,IDE 可以直接找到:
def refresh_token(user):
...
这种能力依赖 Symbol(符号)、Scope(作用域)、类型信息等代码结构。
因此,对 Coding Agent 来说,LSP(Language Server Protocol,语言服务器协议)、Symbol Search(符号搜索)等工具非常有价值。
它们提供的不是:
“我觉得这两段代码可能比较像。”
而是:
“这个调用引用的就是这个定义。”
两种信息的确定性完全不同。
4.5 “文本距离”与“程序距离”不是一回事
代码还有一个非常重要的特征:
两个代码片段在文件里离得很远,不代表它们在程序逻辑上离得很远。
例如:
src/api/order.py
和:
src/infrastructure/database/transaction.py
目录距离可能很远。
但实际执行关系可能只有两三跳:
order.py
↓
service.py
↓
transaction.py
反过来,同一个文件里上下相邻的两个函数,也可能完全没有调用或依赖关系。
因此,对于代码:
Text Distance
(文本距离)
和:
Program Distance
(程序关系上的距离)
是两个完全不同的概念。
这也是为什么只按照文件位置切块,再按照文本相似度检索,可能无法完整表达代码之间真正的关系。
4.6 核心区别
可以把两种视角简单放在一起:
| 文本视角 | 程序结构视角 |
|---|---|
| 代码由很多文本片段组成 | 代码由节点和关系组成 |
| 关注关键词和语义 | 关注调用、依赖、继承、引用 |
| 哪些内容和问题比较像 | 哪些节点真正连接在一起 |
| Semantic Search(语义搜索) | Program Analysis(程序分析) |
| 文本距离 | 程序关系距离 |
所以,一个更准确的理解是:
代码仓库既是一组文本,也是一张可以被遍历的关系图。
而对于 Coding Agent,理解代码经常意味着:
找到一个节点,然后沿着调用、依赖、引用等关系继续向外探索。
这也是代码场景中 Search(搜索)、Symbol Search(符号搜索)、LSP(语言服务器协议)和 Call Graph(调用图)如此重要的原因。
5. 当代码被切成 Chunk,我们丢掉了什么?
RAG(Retrieval-Augmented Generation,检索增强生成)通常不会把整个代码仓库一次性送进模型,而是先进行 Chunking(切块)。
原因很直接:
一个大型仓库太大,必须先拆成更小的检索单元。
例如一个 Python 文件:
class OrderService:
def create_order(self, order):
self.validate(order)
self.repository.save(order)
def validate(self, order):
...
def cancel_order(self, order_id):
...
系统可能按照函数、Token 数量或固定长度切成:
Chunk A
create_order()
Chunk B
validate()
Chunk C
cancel_order()
从 Retrieval(检索)的角度看,这样做很方便:
代码仓库
↓
Chunking(切块)
↓
每个 Chunk 建立索引
↓
根据 Query(查询)召回相关 Chunk
问题在于:
切块方便了检索,却可能把原本属于一个程序结构的信息拆散。
5.1 一个函数本身,往往不是完整上下文
假设检索命中了:
def create_order(self, order):
self.repository.save(order)
如果只看到这个 Chunk,我们还不知道:
self.repository 到底是什么?
save() 的真正实现在哪里?
有没有装饰器包裹 create_order()?
OrderService 是怎么被初始化的?
save() 内部有没有事务或重试?
真正的问题可能并不在当前 Chunk 里。
例如:
class OrderRepository:
@retry_on_timeout
def save(self, order):
database.commit(order)
甚至问题还可能继续藏在:
def retry_on_timeout(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TimeoutError:
return func(*args, **kwargs)
return wrapper
用户问的是:
“为什么订单偶尔重复写入?”
最开始命中的可能是 create_order()。
但真正关键的信息却分布在三个完全不同的位置:
create_order()
↓
repository.save()
↓
@retry_on_timeout
↓
database.commit()
这说明代码中的 Context(上下文),经常不是一个 Chunk 能完整表达的。
5.2 固定长度切块尤其容易遇到边界问题
如果按照固定 Token 数量切代码,问题会更加明显。
例如:
def process_payment(order):
result = payment_client.charge(order)
if result.failed:
raise PaymentError(result.message)
save_payment_result(
order_id=order.id,
result=result,
)
假设切块边界刚好落在这里:
Chunk A
def process_payment(order):
result = payment_client.charge(order)
if result.failed:
raise PaymentError(result.message)
Chunk B
save_payment_result(
order_id=order.id,
result=result,
)
对于人类来说,它显然是同一个函数。
但对于一个只看到 Chunk B 的检索系统来说:
save_payment_result(
order_id=order.id,
result=result,
)
很多信息已经丢失:
result从哪里来?- 当前处于什么函数?
- 前面做过什么判断?
- 为什么执行到这里?
因此,在代码场景中,Chunk Boundary(切块边界)本身就可能破坏语义。
5.3 按函数切块更合理,但仍然解决不了关系问题
一种自然改进是:
不按固定 Token 切,而是按照 Function(函数)、Class(类)或 AST(抽象语法树)进行结构化切块。
这确实比粗暴的固定长度切块更合理。
例如:
Chunk A = create_order()
Chunk B = OrderRepository.save()
Chunk C = retry_on_timeout()
但问题依然存在。
因为真正重要的不是:
这三个 Chunk 各自是什么?
而是:
也就是说:
Chunk 保留了节点,却不一定保留节点之间的关系。
如果检索只根据每个 Chunk 自己的 Embedding(向量表示)判断相关性,那么调用关系、依赖关系等信息仍然需要额外机制来补充。
5.4 文本相邻,不代表程序逻辑相关
普通文档中,相邻段落通常具有比较强的上下文关系。
代码却不一定。
例如同一个文件:
def create_user():
...
def export_report():
...
def delete_cache():
...
三个函数可能只是碰巧写在同一个文件里。
它们在 Text Distance(文本距离)上很近,但逻辑上可能完全无关。
反过来:
api/order.py
services/order_service.py
repositories/order_repository.py
database/transaction.py
四个文件在目录结构上相隔很远,却可能构成一条连续执行路径:
API
↓
Service
↓
Repository
↓
Database
所以:
代码里的“附近”,不能简单理解成文本位置上的附近。
5.5 Semantic Similarity(语义相似度)也无法完全恢复这些关系
假设用户搜索:
“订单保存失败”
Embedding(向量化)可能优先召回:
save_order()
create_order()
OrderRepository
OrderError
这些结果都很合理。
但真正决定问题的代码可能是:
def execute_with_retry(func):
...
或者:
def commit_transaction():
...
它们和“订单保存失败”的 Semantic Similarity(语义相似度)可能并不高。
但在程序结构中:
save_order()
↓
execute_with_retry()
↓
commit_transaction()
它们反而极其重要。
因此,问题不只是:
Chunking 会不会把一段函数切断?
更深层的问题是:
即使 Chunk 本身切得很好,把代码表示成彼此独立的文本块之后,仍然可能弱化代码之间真正重要的结构关系。
5.6 更准确地说:Chunking 不是错误,而是一种信息压缩
这里不能简单得出:
“代码不能 Chunking。”
Chunking 仍然非常有价值。
它可以:
- 控制 Context Window(上下文窗口)大小;
- 降低检索成本;
- 缩小候选范围;
- 让大仓库变得可搜索。
真正的问题是:
Chunking 本质上是一种信息压缩。
原始代码可能具有:
文本内容
+
文件层级
+
函数边界
+
符号定义
+
调用关系
+
依赖关系
+
类型关系
如果最后只保留:
Chunk 文本
+
Embedding
那么一部分结构信息就被舍弃了。
可以把这个过程简单理解为:

因此,更成熟的代码检索系统通常不会只问:
“应该怎么切 Chunk?”
还会问:
除了 Chunk 本身,我们还应该保留哪些结构信息?
5.7 核心结论
Chunking(切块)解决的是:
如何把大型代码仓库拆成可以检索和放进 Context(上下文)的单元。
但它并不能天然解决:
这些代码单元之间到底是什么关系。
所以代码场景中的关键问题不是:
要不要 Chunking?
而是:
Chunk 之后,
还保留了多少程序结构?
对于普通文档,Chunk 往往就是主要的信息单元;对于代码,Chunk 更像 Graph(关系图)里的一个节点,而节点之间的边同样重要。
6. 代码里的“精确名字”,往往比“语义相似”更重要
代码和普通文档还有一个很大的区别:
代码里存在大量稳定、明确、可直接搜索的 Symbol(符号)。
例如:
refresh_token()
UserService
OrderRepository
MAX_RETRY_COUNT
PaymentError
这些名字不是普通文本里的自然语言描述,而是程序的一部分。
当 Coding Agent(编码智能体)已经看到一个明确的函数名、类名或变量名之后,很多时候最有效的下一步并不是继续做 Semantic Search(语义搜索),而是直接做 Exact Match(精确匹配)。
6.1 已经知道函数名时,为什么还要找“相似代码”?
假设 Agent 读到了:
def handle_unauthorized(user):
refresh_token(user)
现在它想知道:
refresh_token()到底在哪里定义?
最直接的方法就是搜索这个名字:
rg "refresh_token"
可能得到:
auth/token.py:12:def refresh_token(user):
middleware.py:28: refresh_token(user)
tests/test_token.py:41: refresh_token(mock_user)
这一次搜索同时告诉了 Agent:
- 定义在哪里;
- 哪里调用了它;
- 测试里有没有出现;
- 仓库中一共有多少匹配。
而如果使用 Semantic Search(语义搜索),问题就变成:
哪些代码和“refresh token”在语义上比较接近?
可能返回:
validate_token()
create_access_token()
parse_token()
check_token_expiration()
这些函数都“很像”,但它们并不能回答:
谁真正使用了
refresh_token()?
这就是代码场景里一个很典型的区别:
Semantic Search(语义搜索)
→ 找“意思接近”的内容
Exact Match(精确匹配)
→ 找“就是这个东西”的内容
6.2 Symbol(符号)让代码检索具有很强的确定性
在自然语言里,一个概念可能有很多种说法。
例如:
登录过期
会话失效
认证超时
Token 失效
所以只搜关键词可能漏掉很多相关内容。
但一旦 Agent 从代码中发现:
SessionExpiredError
情况就不同了。
它可以直接搜索:
rg "SessionExpiredError"
然后得到所有出现位置。
此时问题已经从:
“哪里可能在处理登录过期?”
变成了:
“这个具体异常在哪里定义、抛出和捕获?”
搜索空间一下就缩小了。
可以把这个过程理解为:
自然语言问题
↓
第一次搜索
↓
发现明确 Symbol(符号)
↓
精确搜索 Symbol
↓
快速缩小范围
这也是为什么 Coding Agent 的搜索往往会越搜越准。
第一次可能只能搜:
unauthorized
expired
401
但一旦发现:
SessionExpiredError
下一轮搜索就可以非常精确。
6.3 Symbol Search(符号搜索)比纯文本搜索更进一步
grep 很强,但它本质上仍然是字符串搜索。
例如:
def save_user(user):
...
代码中也可能有:
logger.info("save_user completed")
如果只是:
rg "save_user"
两个位置都会返回。
但 Symbol Search(符号搜索)可以进一步理解:
哪一个是函数定义?哪一个是真正的函数调用?
例如:
save_user
├── Definition(定义)
│ └── repository/user.py
│
└── References(引用)
├── service/user.py
├── api/user.py
└── tests/test_user.py
这就是 IDE 中常见的:
- Go to Definition(跳转到定义)
- Find References(查找引用)
- Find Implementations(查找实现)
相比纯文本相似度,这些信息更加确定。
6.4 Debug(调试)过程中,“知道名字”本身就是一种巨大优势
假设用户说:
“支付失败以后,有时候还是会继续扣款。”
Agent 一开始只能从自然语言出发:
payment
failed
retry
charge
它可能搜索后发现:
def charge_with_retry(order):
...
此时情况发生了变化。
因为现在 Agent 已经获得了一个非常明确的入口:
charge_with_retry
接下来它可以直接搜索:
rg "charge_with_retry"
找到:
payment/service.py
payment/worker.py
tests/test_payment.py
然后继续发现:
retry_payment()
再搜索:
rg "retry_payment"
整个过程变成:
模糊问题
↓
关键词搜索
↓
发现具体 Symbol
↓
精确搜索
↓
发现新的 Symbol
↓
继续精确搜索
所以 Coding Agent 的检索,并不是一直停留在“用户最开始说了什么”。
它会不断从代码中获取新的、更加精确的搜索词。
6.5 代码检索里,“相似”与“相同”应该分开处理
可以简单对比:
| 问题 | 更适合的方式 |
|---|---|
| 有没有类似的缓存实现? | Semantic Search(语义搜索) |
refresh_token() 定义在哪里? |
Symbol Search(符号搜索) |
谁调用了 save_order()? |
Find References(查找引用) |
仓库里还有没有 old_api()? |
Exact Match(精确匹配) |
| 哪些代码在处理“用户登录失败”? | Semantic / Keyword Search(语义 / 关键词搜索) |
因此不能简单说:
Semantic Search 越智能,就越应该替代 grep。
更准确的是:
不同类型的问题,需要不同类型的检索。
当问题本身很模糊时,Semantic Search(语义搜索)很有价值。
但当 Agent 已经获得了明确的函数名、类名、异常名或配置项时,Exact Match(精确匹配)和 Symbol Search(符号搜索)往往更直接、更便宜,也更容易验证结果。
自然语言阶段需要“理解意思”,进入代码以后,很多问题会迅速变成“找到这个确定的符号,以及它和谁发生关系”。
7. 语义相似还不够:Debug 经常需要寻找“关系”
如果用户问:
“项目里有没有类似的限流实现?”
那么 Semantic Search(语义搜索)非常合适。
因为这个问题本身就是在找:
哪些代码和“限流”这个概念比较相似?
但 Debug(调试)经常不是这种问题。
Debug 更常见的是:
某个现象到底是沿着哪条调用链产生的?
这时候,“语义相似”就不一定是最重要的信号。
7.1 一个典型例子:支付为什么重复扣款?
假设用户告诉 Coding Agent(编码智能体):
“支付偶尔会扣两次钱。”
如果根据 Semantic Similarity(语义相似度)搜索,最容易召回的代码可能是:
PaymentService
PaymentPage
PaymentResult
PaymentError
create_payment()
这些结果都很合理。
因为它们和:
支付
扣款
payment
在语义上非常接近。
但真正的 Bug 可能是:
def execute_with_retry(func):
try:
return func()
except TimeoutError:
return func()
调用关系可能是:
真正的问题出在:
execute_with_retry()
而不是名字里带 payment 的函数。
这就暴露了 Semantic Similarity(语义相似度)的一个天然限制:
和用户描述最像的代码,不一定是真正导致问题的代码。
7.2 Debug 更关心“谁连着谁”
假设 Agent 已经发现:
def pay(order):
return payment_client.charge(order)
这时候最有价值的问题通常不是:
还有哪些函数也在“处理支付”?
而是:
charge()到底调用了什么?
继续往下读:
def charge(order):
return execute_with_retry(
lambda: gateway.send(order)
)
现在又出现了一个新的 Symbol(符号):
execute_with_retry
于是搜索目标发生了变化。
整个过程更像:
pay()
↓
charge()
↓
execute_with_retry()
↓
gateway.send()
这其实是在追踪 Call Relationship(调用关系)。
对于 Debug 来说,真正重要的问题经常是:
谁调用了它?
它又调用了谁?
它依赖了哪个模块?
状态在哪里被修改?
异常在哪里被捕获?
这些都属于关系问题。
7.3 “语义上不像”,不代表“程序上不相关”
再看一个更明显的例子。
用户说:
“创建订单以后,有时候库存没有扣减。”
语义搜索可能很容易找到:
OrderService
create_order()
OrderRepository
OrderValidator
但真正的问题可能藏在:
def publish_event(event):
queue.send(event)
因为实际执行路径是:
如果 publish_event() 偶尔失败,那么:
订单已经创建
但:
库存扣减没有发生
publish_event() 和“库存扣减失败”在文字上可能并不像。
可是在程序结构上,它却位于关键路径中。
所以可以得到一个非常重要的区别:
Semantic Similarity
(语义相似度)
关注:
“这段代码和问题像不像?”
而 Debug 常常需要:
Program Relationship
(程序关系)
关注:
“这段代码和问题之间有没有执行、调用或依赖关系?”
7.4 这并不意味着 RAG 只能做“相似搜索”
这里需要避免一个过度简化。
成熟的 RAG 系统完全可以结合:
- Keyword Search(关键词搜索)
- Symbol Search(符号搜索)
- Metadata Filtering(元数据过滤)
- Graph Retrieval(图检索)
- Hybrid Search(混合搜索)
所以问题并不是:
“RAG 只能找相似,因此不适合代码。”
更准确的是:
如果一个代码检索系统主要依赖 Semantic Similarity(语义相似度)来判断相关性,那么它很难单独覆盖 Debug 中大量基于调用、依赖和执行路径的问题。
代码中的相关性,本身就是多维度的。
| 用户问题 | 更重要的信号 |
|---|---|
| 有没有类似的缓存实现? | Semantic Similarity(语义相似度) |
| 谁调用了这个函数? | Call Relationship(调用关系) |
| 这个模块为什么被加载? | Dependency Relationship(依赖关系) |
| 这个异常最终在哪里处理? | Control Flow(控制流) |
| 这个字段在哪里被修改? | Reference / Data Flow(引用 / 数据流) |
所以:
Semantic Search(语义搜索)更擅长回答“哪些内容可能相关”,而 Debug 经常进一步要求回答“这些代码之间到底是什么关系”。
对于 Coding Agent 来说,真正有效的检索往往不是只找到一个结果,而是:
找到节点
↓
沿关系继续走
↓
发现新的节点
↓
继续验证
8. Coding Agent 面对的是一个不断变化的代码仓库
文档知识库通常有一个特点:
建立索引以后,内容在一段时间内相对稳定。
但 Coding Agent(编码智能体)处理的工作区完全不同。
它可能刚刚:
修改了 service.py
新增了 retry.py
删除了 legacy.py
切换了 Git Branch(Git 分支)
几秒之后,代码仓库的状态就已经和刚开始不同了。
这对基于 Index(索引)的检索系统提出了一个很现实的问题:
索引里的代码,还是不是当前真实代码?
8.1 代码仓库不是静态知识库
假设原来的代码是:
def get_user(user_id):
return database.query(user_id)
系统已经完成:
Chunking
(切块)
↓
Embedding
(向量化)
↓
Vector DB
(向量数据库)
然后 Agent 把代码改成:
def get_user(user_id):
return cache.get(user_id) or database.query(user_id)
此时磁盘上的真实代码已经变了。
但如果 Vector Index(向量索引)还没有更新,检索系统里保存的仍然可能是旧版本:
def get_user(user_id):
return database.query(user_id)
于是出现:
Workspace
(当前工作区)
≠
Index
(索引内容)
这就是 Index Freshness(索引新鲜度)问题。
8.2 每次修改都会带来同步成本
假设 Agent 连续进行了这些操作:
修改 A.py
修改 B.py
删除 C.py
新增 D.py
基于索引的系统需要判断:
A.py 哪些 Chunk 失效?
B.py 是否需要重新切块?
C.py 对应的向量是否需要删除?
D.py 什么时候生成 Embedding?
整个过程可能变成:
这就是 Index Synchronization(索引同步)。
它当然可以做,而且成熟系统也会使用增量索引、文件监听等方式降低成本。
但从 Coding Agent 的角度看,这仍然意味着:
系统必须额外维护“索引状态”和“真实工作区状态”的一致性。
8.3 Git Branch(Git 分支)会让问题更明显
例如:
main
分支里:
def login():
...
切换到:
feature/new-auth
以后,可能已经变成:
def login():
return auth_service.authenticate()
甚至文件结构都发生变化:
旧版本:
auth.py
新版本:
auth/
├── service.py
└── token.py
如果索引更新不及时,Agent 就可能出现一种非常危险的情况:
它检索到了一段确实存在过,但当前分支已经不存在的代码。
对于问答系统来说,这可能只是答案稍微过时。
对于 Coding Agent 来说,它可能直接导致:
找错文件
修改错位置
误判调用关系
生成无法运行的 Patch(补丁)
8.4 grep / read 的一个优势:直接读取当前真实状态
如果 Agent 使用:
rg "refresh_token"
或者直接:
read auth/token.py
它读取的是:
当前文件系统里真实存在的内容。
没有额外的:
代码
↓
重新切块
↓
重新生成向量
↓
更新数据库
这个同步链路。
因此对于高频修改中的代码仓库:
Search / Read
(搜索 / 读取)
天然具有一个很简单的优势:
看到的就是当前工作区。
这不是说 Search 一定比索引技术高级。
恰恰相反,它很“朴素”。
但 Coding Agent 在修改代码时,非常看重这种确定性。
8.5 还存在一种更现实的情况:用户和 Agent 都在改代码
Coding Agent 工作时,代码不一定只由 Agent 修改。
例如:
Agent 修改 service.py
同时:
用户手动修改 config.py
IDE 自动格式化其他文件
测试工具生成临时配置
Git 操作切换文件状态
此时 Workspace(工作区)本身就是一个动态环境。
可以简单表示成:
对于 Coding Agent 来说:
代码仓库不是一个需要“查询”的静态知识库,而是它正在直接操作的工作环境。
这两个场景非常不一样。
8.6 索引仍然有价值,问题在于不能把它当作唯一真相
大型代码仓库仍然可能非常需要 Index(索引)。
例如:
Symbol Index
(符号索引)
Semantic Index
(语义索引)
Dependency Index
(依赖索引)
它们可以显著缩小搜索范围,提高检索效率。
真正需要注意的是:
Coding Agent 最终修改的是当前 Workspace(工作区),而不是 Index(索引)。
因此比较合理的设计往往是:
Index
(帮助快速找到候选位置)
+
Search / Read
(确认当前真实代码)
而不是:
Index Result
(索引结果)
=
Current Truth
(当前真实状态)
尤其在 Agent 连续修改代码的情况下,最终验证通常仍然需要回到当前文件系统。
对于 Coding Agent,检索系统不仅要“找得准”,还要确保自己看到的是“现在的代码”。
9. Top-K 很难回答一个关键问题:还有没有遗漏?
在普通问答里,Retrieval(检索)的目标通常是:
找到足够相关的内容,帮助模型回答问题。
所以 Top-K Retrieval(前 K 条检索)非常常见。
例如:
Top-5
1. user_service.py
2. auth.py
3. token.py
4. middleware.py
5. tests/test_auth.py
对于“解释登录流程”这种问题,这通常已经够用了。
但 Coding Agent(编码智能体)在修改代码时,经常面对另一类问题:
我是不是已经把所有需要修改的地方都处理完了?
这时候,Top-K 的能力边界就很明显。
9.1 “找到相关结果”和“找到所有结果”不是一回事
假设我们要把旧接口:
old_api()
全部迁移成:
new_api()
修改完成以后,Agent 必须确认:
项目里还有没有
old_api()?
使用 Exact Search(精确搜索):
rg "old_api"
如果返回:
0 matches
这条结果非常有价值。
因为它表达的是:
在当前搜索范围里,我没有发现其他匹配项。
而如果使用 Vector Search(向量搜索):
Top-5
1. api/client.py
2. api/service.py
3. tests/test_api.py
4. legacy/helper.py
5. docs/migration.md
它只能告诉我们:
这是最相关的前 5 条。
却不能回答:
第 6 个地方还有没有?
这两类任务本质上不同:
Relevant Retrieval
(相关内容检索)
目标:
找几个最有价值的结果
和:
Exhaustive Search
(全量搜索)
目标:
尽可能找出所有满足条件的结果
9.2 Coding Agent 不只需要 Relevance(相关性)
对于普通 RAG 问答:
“这个项目怎么做用户鉴权?”
模型可能只需要几个最相关的文件,就能生成一个不错的回答。
重点是:
Relevance
(相关性)
但代码修改经常还要求:
Completeness
(完整性)
例如:
- 修改一个公共函数签名;
- 删除一个废弃 API;
- 重命名一个配置项;
- 替换一个旧类;
- 修改所有调用者。
假设:
def send_message(text):
...
改成:
def send_message(user_id, text):
...
Agent 不只是要找到“几个典型调用”。
它需要知道:
调用点 A
调用点 B
调用点 C
调用点 D
……
因为只漏掉一个地方,程序就可能在运行时出错。
所以可以得到一个很重要的区别:
生成答案往往主要要求相关性,而修改代码很多时候还要求完整性。
9.3 Find References(查找引用)为什么对代码特别重要
如果 Agent 发现了一个明确的 Symbol(符号):
save_order()
它经常需要执行的不是:
找几个和
save_order()语义相近的函数。
而是:
谁真正引用了它?
这就是 Find References(查找引用)。
例如:
save_order()
Definition(定义)
└── repository.py
References(引用)
├── order_service.py
├── retry_worker.py
├── admin.py
└── tests/test_order.py
这里最重要的不是“排序”。
而是:
引用集合是否完整。
这也是 LSP(语言服务器协议)、Symbol Index(符号索引)这类工具在 Coding Agent 中非常有价值的原因。
9.4 0 matches 为什么是一条很强的信息?
假设 Agent 完成迁移后再次搜索:
rg "old_api"
返回:
0 matches
它并不意味着:
“世界上绝对不存在任何遗漏。”
例如动态调用、字符串拼接、反射等情况仍可能逃过简单文本搜索。
但至少在当前搜索规则和搜索范围下,它提供了一个非常明确的验证信号:
没有发现剩余匹配。
这比:
Top-5 里已经没有 old_api
强得多。
因为 Top-K 的“没有出现”只能说明:
它没有进入前 K 个结果。
而不是:
它不存在。
可以简单画成:
Top-K Retrieval
(前 K 条检索)
全部结果
├── A
├── B
├── C
├── D
├── E
├── F
├── G
└── H
只返回:
A B C D E
看不到 F/G/H
而 Exact Search(精确搜索)更接近:
扫描搜索范围
↓
找到所有匹配
↓
返回完整匹配集合
9.5 不同阶段需要不同的 Retrieval(检索)目标
Coding Agent 的一个任务,甚至可能同时需要这两类能力。
比如用户说:
“把项目里的旧缓存实现迁移掉。”
第一阶段需要找到:
哪些代码可能和旧缓存有关?
这时候 Semantic Search(语义搜索)可能很好用。
“旧缓存实现”
↓
Semantic Search
↓
找到 cache.py / redis_adapter.py / memory_store.py
但当 Agent 确认真正需要删除的是:
LegacyCache
后面的任务就变成:
LegacyCache
↓
Find References
↓
确认所有引用
↓
修改
↓
再次搜索
↓
0 matches
也就是说,同一个任务可能经历:
模糊探索
↓
Semantic Retrieval
(语义检索)
确定目标
↓
Exact / Symbol Search
(精确 / 符号搜索)
修改完成
↓
Exhaustive Verification
(全量验证)
Coding Agent 的检索目标会随着任务推进而改变:前期需要“找到可能相关的地方”,后期往往需要“确认所有相关地方都已经处理”。
10. 真正的变化:从“先检索再推理”到“边搜边想”
前面的差异最终都指向一个更核心的问题:
谁来决定下一次 Retrieval(检索)应该搜什么?
在典型的 Pipeline RAG(流水线式 RAG)里,检索通常是一个相对明确的阶段:
可以简化成:
Retrieve → Reason
检索 → 推理
而 Agentic Search(智能体式搜索)最大的变化是:
Retrieval(检索)不再只发生在主要推理之前,而是被放进了 Reasoning Loop(推理循环)。
10.1 为什么第一次搜索往往是最不准确的一次?
假设用户告诉 Agent:
“登录过期后,有时候页面不会跳回登录页。”
Agent 一开始拥有的信息非常少。
它甚至不知道:
- 登录逻辑在哪;
- 项目如何表示“过期”;
- 是抛异常还是返回状态码;
- 是前端路由还是后端鉴权出问题。
所以第一轮搜索可能只能很模糊:
401
unauthorized
expired
login
假设搜索后发现:
class SessionExpiredError(Exception):
pass
现在 Agent 的信息量已经不同了。
它获得了一个新的 Symbol(符号):
SessionExpiredError
于是第二次搜索可以直接变成:
rg "SessionExpiredError"
然后找到:
auth.py
middleware.py
router.py
再读取 router.py,发现:
def handle_session_expired():
redirect_to_login()
现在它又得到了新的搜索目标:
handle_session_expired
redirect_to_login
搜索过程实际上是:
模糊问题
↓
第一次 Search
↓
获得新信息
↓
Reasoning
↓
产生更具体的 Query
↓
第二次 Search
↓
获得更多信息
↓
继续 Reasoning
因此:
Agent 的后一次搜索,通常建立在前一次搜索结果之上。
10.2 Search → Read → Reason 是一个循环
Coding Agent 的工作方式可以画成:
更完整一点:
用户问题
↓
Reason
(我现在知道什么?)
↓
Search
(我还需要找什么?)
↓
Read
(结果实际写了什么?)
↓
Reason
(这些信息意味着什么?)
↓
Search
(下一步应该搜什么?)
↓
……
这里的 Search(搜索)可能并不总是同一种工具。
第一次:
Keyword Search
(关键词搜索)
第二次可能变成:
Symbol Search
(符号搜索)
第三次:
Find References
(查找引用)
第四次:
grep
(文本搜索)
Agent 会根据当前已经获得的信息选择工具。
10.3 Retrieval 不再只是“准备 Context”
传统 RAG 里,一个很自然的设计思想是:
先找到相关 Context
↓
再让模型处理问题
这里的 Retrieval(检索)主要负责:
给模型准备材料。
而 Agentic Search 中,检索本身还承担另一个作用:
帮助 Agent 决定问题到底是什么。
例如最初用户只知道:
“数据库偶尔重复写。”
Agent 搜索后才发现:
Retry Wrapper
(重试包装器)
继续读取后发现:
TimeoutError
然后再发现:
请求实际上成功了
但客户端超时
↓
Retry 再次执行
↓
重复写入
这里不是:
先检索出答案
↓
再推理
而是:
检索
↓
改变当前判断
↓
再次检索
↓
再次改变判断
也就是:
Retrieval ↔ Reasoning
检索 ↔ 推理
检索不再只是推理之前的准备,而开始成为推理过程本身的一部分。
10.4 Agent 可以动态改变搜索策略
假设 Agent 最初搜索:
"payment failed"
没有找到特别有价值的结果。
它读到代码后发现项目根本不用 failed,而是:
raise GatewayTimeout()
于是搜索策略马上改变:
payment failed
↓
GatewayTimeout
↓
Find References
↓
retry
↓
execute_with_retry
这和固定 Retrieval Pipeline(检索流水线)有一个明显区别:
搜索目标不是用户问题一次决定的,而是在整个任务中不断更新。
可以理解成:

这也是“边搜边想”真正重要的地方。
不是简单地:
Agent 多搜索几次。
而是:
每一次 Retrieval(检索)都可能改变下一步 Reasoning(推理),而新的 Reasoning 又会决定下一次 Retrieval。
10.5 Agentic Search 也不意味着放弃 RAG
Agentic Search(智能体式搜索)并不要求所有检索都使用 grep。
Agent 完全可以拥有多个 Retriever(检索器):
Coding Agent
│
┌──────────────┼──────────────┐
↓ ↓ ↓
grep Symbol Search Semantic Search
文本搜索 符号搜索 语义搜索
│ │ │
└──────────────┼──────────────┘
↓
Read / Reason
阅读 / 推理
例如:
“有没有类似实现?”
↓
Semantic Search
(语义搜索)
“这个函数定义在哪?”
↓
Symbol Search
(符号搜索)
“这个名字还有没有残留?”
↓
Exact Search
(精确搜索)
变化的核心不是某一种 Retriever 被淘汰,而是:
Retriever 从固定流水线中的一个步骤,变成 Agent 可以按需调用的工具。
10.6 两种范式真正的区别
可以把两者压缩成下面这张表:
| Pipeline Retrieval(流水线式检索) | Agentic Search(智能体式搜索) |
|---|---|
| 先检索,再进行主要推理 | 检索和推理交替发生 |
| Query 主要来自用户问题 | Query 可以来自上一步代码结果 |
| 检索策略相对固定 | Agent 可以动态切换工具 |
| 目标通常是准备 Context | 还用于探索、验证和修正假设 |
| Retrieval → Reasoning | Retrieval ↔ Reasoning |
所以,“边搜边想”并不是一个形象但空泛的说法。
它描述的是一个非常具体的架构变化:
Retrieval(检索)从模型推理之前的前置步骤,进入了 Agent 的 Reasoning Loop(推理循环)。
11. 为什么 Agent 越聪明,工具反而可以越简单?
看到 Coding Agent(编码智能体)大量使用 grep、glob、read 时,一个很自然的问题是:
模型已经这么强了,为什么工具看起来反而这么“原始”?
grep 甚至不理解代码语义。
它只是:
给我一个字符串,我帮你找它出现在哪里。
但这恰恰可能是 Agent 工具设计里一个很重要的变化:
工具本身不一定需要非常聪明,真正需要聪明的是“谁来决定什么时候调用什么工具”。
11.1 两种不同的设计思路
一种思路是:
用户问题
↓
Smart Retriever
(复杂检索器)
↓
尽可能一次找到正确 Context
↓
LLM
Retriever(检索器)需要承担很多工作:
- 理解 Query(查询)
- 判断用户意图
- 找到相关内容
- 排序结果
- 过滤噪声
- 选择最终 Context(上下文)
也就是说:
尽量把“检索智能”放在 Retriever 里。
而 Agentic Search(智能体式搜索)可以采用另一种思路:
工具本身很简单。
但 Agent 可以决定:
现在应该搜哪个关键词?
应该找文件还是找函数?
这个结果值不值得继续读?
下一步应该沿哪个 Symbol(符号)追踪?
什么时候需要换一种搜索方式?
复杂性从工具内部,转移到了 Agent 的 Reasoning Loop(推理循环)中。
11.2 简单工具有一个很重要的优势:行为容易预测
假设工具叫:
semantic_code_search()
(语义代码搜索)
输入:
“登录过期处理”
返回什么,取决于:
- Embedding Model(向量模型)
- Index(索引)
- 相似度算法
- Chunking(切块)
- Reranking(重排序)
- Top-K 参数
Agent 很难准确预测结果。
而:
rg "SessionExpiredError"
行为非常明确:
找出搜索范围中出现
SessionExpiredError的位置。
对 Agent 来说,这种 Predictability(可预测性)非常重要。
因为它可以据此规划下一步。
11.3 工具还需要容易组合
一个 Coding Agent 的过程可能是:
glob
(先找到 auth 相关文件)
↓
grep
(搜索 SessionExpiredError)
↓
read
(读取相关代码)
↓
Find References
(查找引用)
↓
运行测试
每一个工具都只解决一个相对简单的问题。
但组合起来以后,就形成了一条完整工作流。
这与 Unix 工具的一种设计思想很相似:
一个工具做好一件事,再通过组合解决复杂问题。
对于 Agent 来说,好的工具往往具有几个特点:
| 特征 | 含义 |
|---|---|
| Simple(简单) | 输入输出容易理解 |
| Predictable(可预测) | Agent 能预判工具行为 |
| Composable(可组合) | 可以和其他工具连续使用 |
| Verifiable(可验证) | 结果容易再次确认 |
| Cheap(低成本) | 可以高频调用 |
所以:
模型越强,并不意味着每个工具都必须越来越复杂。
反而可能意味着:
Agent 可以使用更多简单、可靠的小工具,通过推理把它们组合起来。
11.4 “智能”并没有消失,而是换了位置
这并不是说复杂 Retriever(检索器)没有价值。
真正的变化是系统可以重新分配“智能”。
过去可能更像:
用户
↓
Smart Retrieval
(聪明的检索系统)
↓
LLM
现在可以变成:
用户
↓
Smart Agent
(聪明的 Agent)
↓
Simple / Specialized Tools
(简单或专业化工具)
Agent 可以调用:
grep
Symbol Search(符号搜索)
Semantic Search(语义搜索)
LSP(语言服务器协议)
Call Graph(调用图)
关键不再是:
哪一个工具足够聪明,可以一次解决所有检索问题?
而是:
Agent 能不能根据当前状态,选择正确的工具。
可以把这个观点压缩成一句话:
工具可以很简单,但 Agent 对工具的选择必须足够聪明。
12. 所以代码 RAG 已经没用了吗?
不是。
前面的讨论真正说明的是:代码检索不能只依赖一种 Retrieval(检索)方式。 不同问题需要不同工具,Semantic Search(语义搜索)仍然有它非常适合的场景。
例如,当用户问:
“项目里有没有类似的限流实现?”
Agent 并不知道具体函数名。项目中的实现可能叫 RateLimiter、ThrottleMiddleware,甚至完全没有“限流”这个关键词。这种情况下,Semantic Search(语义搜索)就很有价值。
但当 Agent 已经发现明确的 Symbol(符号)后,检索目标会发生变化。
| 当前问题 | 更适合的方式 |
|---|---|
| 有没有类似实现? | Semantic Search(语义搜索) |
| 函数在哪里定义? | Symbol Search(符号搜索) |
| 谁调用了这个函数? | Find References(查找引用) |
| 模块之间怎么连接? | Call Graph / Dependency Graph(调用图 / 依赖图) |
| 是否还有旧 API 残留? | Exact Search(精确搜索) |
因此,一个更合理的 Coding Agent 往往拥有多种 Retrieval(检索)能力:

这更接近 Hybrid Retrieval(混合检索):不是选出一个“最强搜索工具”,而是让 Agent 根据当前任务动态选择合适的检索方式。
12.1 Agentic Search 和 RAG 并不对立
Agentic Search(智能体式搜索)并不要求放弃 RAG。Agent 完全可以把 Semantic Retriever(语义检索器)当成自己的一个工具:需要寻找概念相似代码时调用语义检索,发现具体函数后切换到 Symbol Search(符号搜索),修改完成后再通过 Exact Search(精确搜索)进行验证。
这种设计也可以被称为 Agentic RAG(智能体式 RAG)。
真正发生变化的,不是 RAG 被 Search 淘汰,而是 Retrieval(检索)从固定流水线中的一个步骤,变成 Agent 可以按需调用的一组能力。
13. 结语:真正改变的不是 Retrieval,而是 Retrieval 在系统中的位置
回到最开始的问题:为什么 Coding Agent 看起来越来越喜欢 grep、glob、read 这些简单工具,而不是只依赖一次性的检索结果?
全文可以压缩成三个判断。
第一,代码不只是文本。 它还包含 Symbol(符号)、调用、依赖、作用域、控制流等可以被直接利用的程序结构。
第二,Coding Agent 不只需要 Relevance(相关性)。 Debug(调试)需要沿调用和依赖关系追踪问题,代码修改还经常需要 Completeness(完整性):不仅要找到“几个相关位置”,还要尽可能确认“有没有遗漏”。
第三,Retrieval(检索)正在进入 Reasoning Loop(推理循环)。 Agent 可以根据刚刚读到的代码重新生成搜索目标、切换检索工具,再用新的结果修正自己的判断。
因此,与其把变化概括成:
RAG → Search
不如理解为:
Retrieve then Reason
(先检索,再推理)
↓
Retrieve while Reasoning
(边检索,边推理)
Coding Agent 并没有放弃 Retrieval。变化的是:检索不再只是推理之前的一次准备,而逐渐成为 Agent 探索、验证和修正判断的过程本身。
更多推荐





所有评论(0)