从 RAG 到 Agentic Search:为什么 Coding Agent 更喜欢“边搜边想”?

文章目录

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

调用关系是:

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()

问题路径可能是:

create_order

OrderService

Repository

Retry Wrapper

Database 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()

也就是:

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(关系图)的角度看,这件事情很自然:

Timeout 后再次执行

create_order()

OrderService.create()

OrderRepository.save()

execute_with_retry()

database.commit()

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 各自是什么?

而是:

create_order()

OrderRepository.save()

retry_on_timeout()

database.commit()

也就是说:

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()

调用关系可能是:

Timeout 后再次调用

create_payment()

PaymentService.pay()

payment_client.charge()

execute_with_retry()

发送扣款请求

真正的问题出在:

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)

因为实际执行路径是:

create_order()

save_order()

publish_event()

OrderCreated Event

InventoryConsumer

decrease_stock()

如果 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?

整个过程可能变成:

代码修改

检测变更

重新 Chunking

重新 Embedding

更新 Index

这就是 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(工作区)本身就是一个动态环境。

可以简单表示成:

用户修改

Current Workspace
当前工作区

Agent 修改

Git / Tool 修改

Search / Read
直接读取当前状态

对于 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)里,检索通常是一个相对明确的阶段:

Query
用户问题

Retrieval
检索

Reranking / Filtering
重排序 / 过滤

Context
构建上下文

LLM Reasoning
模型推理

可以简化成:

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
(我还需要找什么?)
   ↓
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(编码智能体)大量使用 grepglobread 时,一个很自然的问题是:

模型已经这么强了,为什么工具看起来反而这么“原始”?

grep 甚至不理解代码语义。

它只是:

给我一个字符串,我帮你找它出现在哪里。

但这恰恰可能是 Agent 工具设计里一个很重要的变化:

工具本身不一定需要非常聪明,真正需要聪明的是“谁来决定什么时候调用什么工具”。

11.1 两种不同的设计思路

一种思路是:

用户问题
   ↓
Smart Retriever
(复杂检索器)
   ↓
尽可能一次找到正确 Context
   ↓
LLM

Retriever(检索器)需要承担很多工作:

  • 理解 Query(查询)
  • 判断用户意图
  • 找到相关内容
  • 排序结果
  • 过滤噪声
  • 选择最终 Context(上下文)

也就是说:

尽量把“检索智能”放在 Retriever 里。

而 Agentic Search(智能体式搜索)可以采用另一种思路:

Agent
推理

grep
文本搜索

glob
文件搜索

read
读取文件

工具本身很简单。

但 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 并不知道具体函数名。项目中的实现可能叫 RateLimiterThrottleMiddleware,甚至完全没有“限流”这个关键词。这种情况下,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 看起来越来越喜欢 grepglobread 这些简单工具,而不是只依赖一次性的检索结果?

全文可以压缩成三个判断。

第一,代码不只是文本。 它还包含 Symbol(符号)、调用、依赖、作用域、控制流等可以被直接利用的程序结构。

第二,Coding Agent 不只需要 Relevance(相关性)。 Debug(调试)需要沿调用和依赖关系追踪问题,代码修改还经常需要 Completeness(完整性):不仅要找到“几个相关位置”,还要尽可能确认“有没有遗漏”。

第三,Retrieval(检索)正在进入 Reasoning Loop(推理循环)。 Agent 可以根据刚刚读到的代码重新生成搜索目标、切换检索工具,再用新的结果修正自己的判断。

因此,与其把变化概括成:

RAG → Search

不如理解为:

Retrieve then Reason
(先检索,再推理)

        ↓

Retrieve while Reasoning
(边检索,边推理)

Coding Agent 并没有放弃 Retrieval。变化的是:检索不再只是推理之前的一次准备,而逐渐成为 Agent 探索、验证和修正判断的过程本身。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐