Skip to content

第 11 章 Agent 怎样找到任务相关的代码 ​

本章要解决的问题

第 10 章让 grep、glob、read 等工具能在同一套调度规则下执行,但工具只提供能力,不会自动告诉模型先查哪里、何时扩大范围、哪个候选必须读原文确认。这也是第 6 章留下的问题:上下文的结构已经确定,内容从哪里来还没有解决。项目稍大,就不可能把全部代码放进一次请求。

本章回答:模型怎样逐步找到当前任务需要的代码。先看四个 Agent 都在使用的 Shell 或 glob、grep、read 路线,再说明什么时候值得增加 LSP、混合检索或嵌入索引。

11.1 模型怎样从目录逐步找到所需代码? ​

先看模型可以用哪些工具找代码。常见能力可以分成四类:

工具找什么适合局限
Glob文件名模式(**/*.test.ts)摸清项目结构的第一步不看内容
Grep/ripgrep内容正则已知要找什么文本(函数名/报错)找不到"换个说法"的语义
语义检索向量+BM25+重排序"处理用户认证的代码在哪"这种概念级查询要建索引、要嵌入服务
符号级(LSP)定义 vs 引用重构:精确区分"定义处"和"字符串里的同名"要语言服务器、较重

没有语义索引时,常见顺序是:先用 glob/list 了解目录,再用 grep 定位文字,然后 read 当前文件,需要追踪定义和引用时再用 LSP。概念查询较多、这条路线经常需要很多轮时,再考虑语义检索。

语义检索(RAG):按"意思"找代码 ​

grep 的根本局限是只能找文本上匹配的内容——搜"用户认证"找不到名叫 check_credentials 的函数。语义检索(即 RAG,检索增强生成,在代码场景下的形态)补的就是这块。一套可落地的方案有四个部件:

text
分块    按函数/类等语义单元切,不按固定字符数盲切;
        每块带元数据(路径/符号/语言/内容哈希)
稠密    嵌入向量:语义相近 → 向量相近,负责"换个说法也能找到"
稀疏    BM25 关键词:按词频与稀有度打分,负责精确匹配标识符和报错原文
合并    两路并行召回,重排序模型(reranker)对候选精排,取 top-k

优缺点一句话版:

擅长概念级查询("处理用户输入验证的代码在哪");陌生代码库探索;跨文件语义召回
代价索引基础设施(嵌入服务 + 向量库);代码变更后索引过期,要增量重建;代码可能外发给嵌入服务(取决于部署方式)
不适用已知确切函数名或报错文本——grep 一轮就到,语义检索反而绕路

它与 grep/read 不是二选一:常见组合是索引出候选,grep/read 验证——索引只给路径和位置,下结论前仍要读磁盘上的真实文件(上面的分页与版本规则同样适用于索引结果)。

符号级检索(LSP):分清"定义"与"引用" ​

文本搜索的另一个盲区:它分不清 authenticate 在第 42 行是函数定义、第 189 行是调用、注释里那处只是同名文本。LSP(Language Server Protocol,语言服务器协议)就是编辑器"跳转到定义 / 查找所有引用"背后那套机制,让 Agent 拿到符号级的精确答案。

最有价值的场景是重构:重命名一个函数绝不能靠文本替换——函数名可能出现在注释、字符串甚至无关变量里;只有符号级查找能定位真正的定义和全部调用点。这也是 grok-build 和 dsh 在 grep/glob 之外保留可选 LSP 工具的原因,而 codex 连 LSP 都不给——多数场景下 shell 里的 rg 已经够用。

擅长重构(改名/移动/找调用链);精确区分定义与引用
代价每种语言一个语言服务器,进程重、启动慢;构建配置复杂的项目里服务器可能起不来或给出过时符号表
不适用语义查询(LSP 不理解"处理认证的代码");纯文本扫描(直接 grep 更轻)

检索和读取结果怎样分页,又不读到过期内容? ​

目录、搜索结果和文件内容都可能超过单次工具输出上限。不要只返回一段被截断的文本;工具结果至少应包含:

text
items 或 content   当前页内容
truncated          是否还有未返回内容
nextCursor         下一页游标;没有下一页时为空
sourceRevision     工作区或索引版本
contentHash        当前文件或结果来源的内容哈希

游标应绑定查询条件和 sourceRevision,不能只是裸行号。分页期间文件或索引版本变化时,旧游标应失效,让 Agent 从新版本重新查询;否则前后两页可能来自不同文件状态。索引只能给候选路径和位置,做修改或下最终结论前仍要读取磁盘上的当前文件。

11.2 四个 Agent 为什么都用现场检索,什么时候才需要索引? ​

11.1 列出了从 grep/read 到 LSP 和语义检索的能力光谱。接下来不是继续增加工具,而是回答选型问题:为什么四个参考实现都先用当场搜索,哪些任务足以让索引的成本变得值得。

代码检索有哪两种做法? ​

Cursor 的做法(建索引):为代码库建嵌入索引,从而找到用词不同但含义相关的跨文件代码。代价是需要索引基础设施、代码变化后索引会过期,而且生成嵌入时可能需要外发代码。

不建嵌入索引的做法:让模型反复调用 grep、glob 和 read,逐步缩小查找范围。这样模型最终读到的是磁盘上的当前代码,不需要维护向量索引。代价是可能增加工具轮次和延迟,也不擅长“表达不同但意思相近”的查询。是否会外发代码仍取决于模型部署方式,不能由“不建索引”直接推出。

四个 Agent 现在实际怎样检索? ​

我们翻了四个源码样本,结论高度一致——提供给模型的检索工具都是朴素的现场搜索+读取,没有一个给模型接入嵌入索引:

text
codex:      干脆没有专用检索工具——模型用 shell 跑 rg/find/cat
            ("shell 即万能工具"的极简主义)
grok-build: 专用 grep(ripgrep 封装)+ read_file + list_dir + 可选 LSP 工具
dsh:        read / grep / glob(ripgrep 驱动)+ lsp 工具
pi:         默认 read + bash;只读模式可启用 grep / find / ls
            工具直接读取当前文件,不维护嵌入索引

两个容易误读的地方,考据时专门核实过:

  • grok 有 xai-codebase-graph(tree-sitter 作用域图:定义/引用/导入,增量索引,缓存在 ~/.grok/indexes/)——但它不在模型的工具列表里。它服务的是 Web 客户端的导航 UI(用户在浏览器里点"跳转到定义"),走的是 ACP 扩展协议,模型看不到它。
  • codex 的 agent-graph-store 名字像代码图,实际只保存多 Agent 的 parent/child 关系——它与代码检索无关;不要从名字推导出防止循环创建或限制深度的能力:关系存储层不做环校验,V2 spawn 也不按最大深度拒绝(第 16 章详述)。

产业实证的读法

本次抽查的四个 CLI 形态项目(codex/grok/dsh/pi)均未给模型暴露嵌入索引——但这只是四个样本,不是全行业结论:Cursor 官方明确把语义索引用于 Agent 的上下文与性能,说明其他产品会选择不同做法。是否值得建索引,取决于谁消费索引:

  • 服务人:grok 的图谱给了人类用户(IDE 式点击导航)
  • 服务 IDE/Agent 产品:Cursor 的嵌入索引同时也喂给它的 Agent
  • 这四个 CLI Agent 的共同选择:给模型真文件,不给模型二手索引

下面是基于实现的作者推断,不是四个项目团队公开给出的理由:CLI 更容易直接读取磁盘当前内容,无需处理索引更新;有些企业不允许向额外的嵌入服务发送代码;索引的延迟和收益也要与多轮 grep/read 的成本实测。深度集成 IDE、超大仓库或概念查询较多的产品可能作出相反选择。

那什么时候该建索引? ​

  • 你的产品是 IDE 形态(用户在编辑器里,召回要快、要跨文件)——Cursor 的选择
  • 你的用户是人,需要导航/探索 UI——grok 的选择(给 UI 建图)
  • 代码库大到需要十几轮 grep/read,且查询偏概念级——考虑混合使用:索引返回候选文件,read 再读取真实内容确认
决定建索引后,最少要实现什么?

建索引不能只是“把文件随便切成 500 token 然后做 embedding”。对代码更实用的起点是:

text
切块      文件 / 符号 / AST 节点 / Markdown 标题,保留父级上下文
元数据    path + symbol + parent heading + language + source kind(原文/生成内容)
          + git/workspace revision + content hash;多租户产品还要带访问范围
召回      BM25 找标识符和报错原文;向量找概念;RRF 等简单方法合并
精排      可选 reranker,只处理已召回的小候选集
确认      返回路径与行号,Agent 必须 read 当前真实文件后才能下结论
更新      文件变更增量重建;删除写 tombstone;查询结果声明 index revision

多租户或托管产品必须在召回和排序前先按用户、仓库和分支过滤权限,不能先找出全局 top-k 再把越权结果删掉,否则既可能泄露元数据,也会让用户得到不足 k 条的偏斜结果。

指标名称要分清:前 k 个结果里至少出现一个相关项是 hit@k;全部相关项中有多少进入前 k 个结果是 recall@k;返回的 k 项中有多少相关是 precision@k;第一个相关项平均排在多靠前的位置可用 MRR。还要监控索引多久没有更新、无结果率,以及“候选经真实文件读取后仍被确认”的比例。最后一项持续下降,通常说明索引过期或无关结果在变多。这个方案适合某些 IDE 或超大仓库,不能反过来写成 codex、grok-build、dsh 当前都使用向量索引。

11.3 把镜头拉远:不建索引在 RAG 光谱上的位置 ​

"grep + read 多跳"不是孤例,它处在检索技术光谱的一个明确位置上。这条光谱大致是:

传统 RAG(一次检索,直接生成)。 文档切块、建嵌入索引,查询时召回 top-k 拼进提示词。它先检索一次再生成答案,适合信息需求明确、单一的简单问题。一些实验中,这类问题使用传统 RAG 响应更快,质量与允许模型反复检索的方案相近;面对需要分解和交叉验证的复杂问题时,一次检索往往不够。

让 Agent 自己决定何时检索。 检索不再只在回答前执行一次,而是成为 Agent 随时可调用的工具。模型可以反复查找、换关键词并交叉验证。四个实现的现场检索就是代码场景下的例子:它们检索的是本地代码而不是嵌入库,依靠文件系统找到候选文件。

用文件系统组织知识(OpenViking 一类方案)。 把记忆、资源和技能映射成虚拟文件系统里的目录和文件,再用文件链接连接相关内容。它有一个容易忽视的前提:条目之间必须真的建立链接。否则最终只会得到一堆难以查找的文件夹;写入知识时需要明确要求模型建立这些关系。

结构化索引(RAPTOR / GraphRAG)。 多层摘要树、实体关系图谱,擅长跨文档综合与多跳关系推理。代价是索引构建需要大量模型调用——判断标准很简单:查询经常需要"跨文档综合"才值得投入,只是"找到包含某信息的片段"用混合检索就够。

还有一个值得思考的问题:如果模型能力持续变强,分块和索引会不会最终被“全量输入”取代?四个样本当前都没有这样做:窗口再大,代码库仍可能更大,无关内容也会增加成本和干扰。

四家没做的事,不代表其他场景不需要。超大仓库、跨仓库问答、自然语言找代码时,可以从 grep/read 升级到混合检索,最后再考虑结构化索引。


本章小结 ​

  • 四家都不建嵌入索引:Shell 或 grep/glob 多跳 + 读文件确认,靠文件系统当索引
  • 这条路线省掉了索引维护、基础设施和代码外发,代价是每次现场检索的 token 与轮次
  • 升级选项一,语义检索(RAG):稠密向量 + BM25 混合召回 + 重排,擅长概念级查询;代价是索引基础设施、过期重建与代码外发风险
  • 升级选项二,LSP 符号级检索:精确区分定义与引用,重构必备;代价是语言服务器进程与项目配置依赖
  • 什么时候升级:超大仓库、跨仓库问答、自然语言找代码;从 grep/read → 混合检索 → 结构化索引逐级上
  • RAG 光谱上,四家做的是"智能体化检索"的代码版;传统 RAG 适合单一明确的信息需求,OpenViking/GraphRAG 适合跨文档综合

检索让 Agent 只读取相关代码,但每次 read、grep 和工具结果仍会进入历史。下一章先讨论上下文的分层与渐进式加载——更好的办法是让不相关的内容一开始就不进上下文;第 13 章再处理上下文接近上限时先裁掉什么、何时压缩,以及压缩对缓存的影响。

基于 MIT 协议发布,欢迎转载与贡献。