☰
代码Agent省Token实战:弃RAG向量库,用CLI精确定位提效四成
2026/10/15 8:08:53 网站建设 项目流程

团队代码 Agent 上线时,我第一反应就是把代码库灌进向量库,做一套标准 RAG,觉得这才是“大厂标配”。真跑到生产环境后,崩溃得很统一:每处理一个稍微复杂的问题,几十个工具调用里一大半 token 都花在“被迫看过一大段没用代码”上,定位还经常偏。后来我把向量库从关键路径上整个撤掉,换成了几个精简 CLI 命令——精确检索、按行读取、语法级定位——同一批任务类型下 token 消耗平均降了四成,代码定位的准度反而变好了。这篇就把这笔账和落地细节摊开讲,给正在做 AI 编程工具、或者自己搭 Agent 工作流的朋友一个参考。

这个现象在关注 codex cli、claude code 这类工具的人那里应该不陌生:它们大多数都没给代码库建向量索引,靠的是在本地 shell 里调用 rg、sed、git 拿上下文。以前我觉得这是“还没做 RAG 的过渡方案”,现在我自己踩完坑才理解,这才是更符合代码 Agent 工作方式的默认解。

1. 先说清楚:代码 Agent 的 token 到底烧在哪

1.1 一次需求闭环里的 token 流向

很多人以为 Agent 费 token 是因为模型“想得多、写得多”,实际上模型输出在整条链路里只占小头。真实的大头在输入侧,也就是每次工具调用时,整个对话历史都会被重新发送给模型。这里面的构成大致是这样的:

  • 系统提示词:固定开销,包含角色设定、工具定义、约束规则,工具越多越长。
  • 工具描述:每个工具都会在系统提示里挂一份 schema,字段写得越复杂,token 吃越多。
  • 工具调用返回内容:RAG 检索结果、grep 输出、sed 读出来的代码片段,这些会作为“观察结果”拼进上下文。
  • 历史消息累积:上一轮的对话原文会原封不动带到下一轮,随着交互加深,这部分指数膨胀。

其中工具描述和历史累积是“结构性开销”,谁都逃不掉,但如果检索返回的内容又长又臭,就会把历史和后续每一轮的开销一起顶上去。换句话说,你在某一步喂进去 3000 个 token 的无关代码,后续十轮对话里这 3000 token 就一直跟着你,变成十倍、二十倍的成本。

1.2 核心矛盾:上下文是复利式增长的

LLM 的注意力机制决定了模型理论上会“看过”喂进去所有 token,但实际效果无限接近垃圾进垃圾出。检索结果里 80% 是无关代码时,模型不光多花计算量,还会被噪声带偏。

更麻烦的是,长上下文的截断机制会加剧浪费。比如对话历史到了 10k token,你觉得不够用了开始删早轮消息,删掉的可能正好是任务开始时最有价值的需求描述,于是 Agent 开始“失忆”,下一步就会重新去检索一遍。这块的成本在日志上看起来是跳崖式猛涨,但挖到根上,往往是某一轮塞进来太多低信息密度的检索结果导致的。

所以,要给代码 Agent 省 token,第一原则不是压模型输出,而是压喂进去的输入;第二原则是让每次检索返回的信息密度足够高。

2. 为什么向量库在代码场景里是“高射炮打蚊子”

2.1 向量检索的三个结构性缺陷

向量库本身没问题,但用在代码 Agent 场景里,它有三个躲不开的缺陷。

第一个是召回噪声。embedding 检索按“语义相似度”排序,top-5 里经常混进两三条只是长得像、实际毫无关联的 chunk。比如搜某个业务函数,却召回了一个 SQL 查询模板,因为两个片段里都提到了同一个表名。这些噪声 chunk 会让 Agent 做出错误判断,然后多绕好几轮。

第二个是粒度错位。向量库的分块策略是“一刀切”。代码跟散文不一样,一个函数从第 40 行到第 120 行才是完整逻辑,分块不会看这个边界。常见结果是函数被拦腰切成两半,检索命中的是后半段,Agent 拿到了一个没有上下文函数体,还得再补一次超窗读取。为了不让它截断,工程上普遍会设“重叠窗口”,chunk 从 512 token 直接膨胀到 900 token。

第三个是维护成本。代码每天都在变,向量索引要跟着增量更新、重建;函数一改名,旧索引里全是失效数据。这个成本很多人做着做着就躺平了,索引越老,召回越烂,Agent 就越需要加大 k 值来防漏,token 成本进一步上涨。

2.2 代码不是自然语言,模糊检索从根上就是错位

自然语言检索的核心诉求是“语义相似”,比如搜“怎么退款”,能命中“退货流程”就算成功。但代码场景的诉求恰恰相反:我要知道某个函数被谁调用了、某个变量在哪里定义、某个文件里哪一行抛了错。这些是精确的、可验证的事实,本质上是图结构里的导航问题。

代码本身的可导航性极好:有 AST、有符号表、有调用图、有 git 历史。这些结构比 embedding 更可靠,而且输出天然是短小的定位信息。用自然语言的模糊匹配去驱动精确导航,相当于开着导航仪却靠路牌认路,纯属给自己加难度。

2.3 一个真实翻车现场

我在给团队 Agent 配检索时遇到过这样的事:在某个 Python 项目里搜get_user_by_email这个函数,向量检索返回 top-5 chunks,其中有三个是 ORM 查询示例和数据库迁移文件,真正包含函数定义体的那个 chunk 只排到第 4 位,还因为分块截断,只有函数的前 10 行。

那次对话的记录里,检索返回总共耗了 2700 个 token,最后 Agent 自己思考了一下,还是调了 grep 去搜函数名,才拿到完整的定义。等于花了两轮工具调用的成本干一件事。这种案例出现多了,我就开始想:为什么一开始不用 grep?

3. 精简 CLI 的省 token 原理:精确获取代替模糊召回

3.1 代码检索的正确姿势是用了几十年的老工具

代码库里的检索,正确的工具集其实是那些存在了几十年的 POSIX 命令行:

  • rg:正则精确搜索,秒级遍历整个仓库,返回文件名:行号:内容的紧凑格式。
  • sed配合行号:精确读取某个文件的某几行,不会给你前后文一堆填充物。
  • tree或fd:快速摸清目录结构,让 Agent 知道项目大概长什么样。
  • git log、git show、git blame:拿变更历史、查某次提交影响了哪些文件。
  • ctags、ast-grep:按符号名、AST 节点类型定位定义和引用,比正则更强一层。

这套工具的特点是:输出短、准、可验证。不存在“语义相似”这种模糊地带,命中就是命中,行号锁死,绝不多送你 1000 行无关代码。对 Agent 来说,这是信息密度极高的上下文。

3.2 小步迭代的 token 数学

CLI 方案和向量方案最根本的区别不在工具形式,而在“一次拿多少”。

向量方案是一轮大检索,一次塞给你 5 个 chunk、每块可能还带扩展窗口,总共 3000 到 4000 token。而 CLI 方案鼓励 Agent 用 3 到 5 轮小步操作去逼近目标:第一轮rg拿文件名和行号,比如 100 token,然后第二轮sed精准读取那 30 行代码,比如 300 token。两轮加起来不到 500 token,而且每一步都能根据返回结果修正方向。

这里有个隐藏在注意力开销里的数学:如果 Agent 一开始拿到的就是精确定位信息,它就不需要“再想一想检索结果里哪个能用”,每次调用返回的 token 利用率远高于向量方案。我统计过,CLI 方案下工具返回内容大约有 85% 会直接影响 Agent 的下一步动作,而向量方案里这个比例通常只有三成左右。

3.3 工具输出格式本身就是成本

很多人在设计 Agent 工具时喜欢返回结构化的 JSON,字段名冗长,还有一堆元数据。这在业务系统里没问题,但在 token 计费的世界里,每多一个“query_id”: “abc123”都是在烧钱。

我给 Agent 设计的输出格式刻意保持“反结构化”:文件路径 + 行号 + 纯文本内容,中间用冒号和制表符分隔。行号前缀还能让 Agent 快速引用“第 152 行的位置有问题”,不需要它去解析多层嵌套的 JSON。工具描述同样搞精简,schema 里只留四个字段,不搞枚举校验。

实际跑下来,只“输出格式精简”这一项,就比完整 JSON schema 少了大约 40% 的工具调用开销。

4. 40% 的省 token 账是怎么算出来的

4.1 一张基于日志的对比表

我拿自己团队的一个真实任务集做基准:中等规模代码库,大约 300 个文件,任务类型是“修 bug + 加小功能”,平均每个任务经历 15 轮左右的 Agent 动作循环。数据直接从工具调用的 token 日志里统计,大概是下面这个量级:

指标(一次 15 轮的中型任务)向量库 + RAG精简 CLI
平均对话上下文长度(每轮累计输入)约 10k token约 5k token
15 轮累计输入 token约 150k约 75k
模型输出 token约 35k约 30k
总 token 消耗约 185k约 105k
相对节省—约 43%

我不是说任何任务都能省这么多,但这个量级在我们的真实日志里反复出现。省下来的主来源就一个:上下文历史的复利。向量方案每轮多塞 3000 到 4000 token 的检索结果,累积 15 轮就是 5 万多的差异,而且历史越长,越容易触发截断丢失,又反向要求 Agent 多做一次检索。

4.2 两个容易被忽略的隐藏开销

第一是超窗。为了防止分块把函数切开,向量方案普遍给 chunk 加前后重叠区间,实际塞进 context 的往往是原始 chunk 的 1.5 到 2 倍。这个损耗在配置里看不见,但账全算在 token 上。

第二是“二次定位”轮次。向量检索如果给了一个不完整结果,Agent 不会意识到自己缺东西,而是尝试基于错误信息继续作答。一次生成可能就花掉几百 token,等模型发现自己编不下去,又回头重新检索。这种失败轮次在日志里占比很高,而 CLI 方案因为每次返回都是精确且可核对的,几乎不存在这种“基于幻觉结果继续推理”的情况。

4.3 什么任务省得多,什么任务省不动

也要给向量库一点公道。不同任务类型,这个方案的收益差异很大:

  • 符号定位、代码审查、改 bug、查引用关系:CLI 方案优势最明显,省 40% 到 50% 都正常。
  • 自然语言描述型需求,比如“优化这个服务里所有不够健壮的地方”:精确检索无从下手,只能做语义联想,这时候向量库兜底反而更省轮次。
  • 超大 monorepo 或者多语言混合项目,Agent 连“该搜哪个目录”都不知道:先导航再搜索的成本会增加,CLI 方案省下来的比例会缩水。

所以千万不要只看一个数字就迷信任何方案。我在后面会讲怎么判断何时该保留向量库。

5. 实操:给代码 Agent 配一套 CLI 上下文工具箱

5.1 核心工具清单与使用场景

以下是我在项目里实际部署的的最小工具箱,每个工具都对应明确的 Agent 场景:

工具场景典型单次输出
rg按符号、字符串、正则定位文件和行号几十到几百 token
sed按行号读取指定文件区间几百 token
tree/fd摸清目录结构、定位文件路径几十到几百 token
git log/git show查提交记录、变更内容、作者信息几百 token
ast-grep按 AST 节点找函数定义、类定义、调用点每条命中几行

这套工具组合覆盖了代码 Agent 80% 以上的“找东西”场景。只要你把需求拆对,每一轮的返回量都在可控范围内。

5.2 一个快速可用的 bash 封装脚本

为了让不熟悉终端的 Agent 也能稳定调用,我会把这些命令包装成带参数校验的 bash 函数。下面是一个可以用来读代码片段的示例,核心逻辑是“限制读取行数,限制输出大小,给出行号前缀”:

#!/usr/bin/env bash agent_fetch() { local file="$1" local start="$2" local end="$3" local max_lines=80 if [ ! -f "$file" ]; then echo "ERR: $file 不存在,请先确认文件路径" return 1 fi local total=$((end - start + 1)) if [ "$total" -gt "$max_lines" ]; then echo "WARN: 目标区间 ${total} 行超过上限,只输出前 ${max_lines} 行" sed -n "${start},$((start + max_lines - 1))p" "$file" | cat -n else sed -n "${start},${end}p" "$file" | cat -n fi }

同理可以写agent_search包装 rg,强制限制-m 20的命中数,默认关闭上下文行,只输出文件名:行号:内容:

agent_search() { local pattern="$1" local dir="${2:-.}" rg -n --no-heading -m 20 "$pattern" "$dir" | head -40 }

输出里永远不要带大段前后文。让 Agent 看到行号和内容后,自己决定要不要用agent_fetch去取完整函数体。这一步设计,是整个方案省 token 的胜负手。

5.3 在常见 Agent 框架里的接入思路

如果你用的是 Codex CLI 这类自带本地沙箱的工具,思路更简单:它本身就能执行 shell 命令,你只需要在系统提示里给它立规矩,写清楚“优先使用 rg 定位,再用 sed 按行读取,不要一次读取超过 80 行代码”。

如果是走自定义工具调用链路,就把上面的agent_fetch、agent_search注册成两个普通工具,然后在 schema 里把描述写得克制一点。我用的是下面这种风格:

{ "name": "agent_search", "description": "在代码库中按正则精确检索符号、函数名、文件名,返回文件名、行号与单行内容。适合定位代码位置,不适合做语义相似检索。", "parameters": { "pattern": "string", "dir": "string" } }

这里刻意的把“不适合做语义相似检索”写进去,是为了减少模型在意图不清晰时乱用的概率。一句话的约束,省掉的是无数次无效检索的 token。

6. 常见问题与避坑实录

6.1 rg 搜不到,但不代表代码里不存在

踩过最典型的坑是词形和重命名问题。函数名是驼峰还是下划线,是缩写还是全称,搜索词差一个字母结果就是零。比如搜getUser,实际代码里写的是fetch_user。解法不是加大检索范围,而是让 Agent 换工具:用git log -S找历史里的旧名,或者用ast-grep按 AST 节点类型列函数定义,全部列出来再人工(对 Agent 来说就是再读取一次目录)挑。

另外中文注释、常量拼写、模板字符串都会导致 rg 失效,这时候先让 Agent 查一下最近的 git 提交,往往比盲目换关键词更省。

6.2 模糊需求场景会碰壁,必要时留一个轻量向量兜底

我上面说过,CLI 方案的前提是“你能把需求拆成精确的检索关键词”。但真实产品里总会出现“把这个服务里所有不健壮的错误处理都改了”这种模糊指令。让 Agent 一口气从“不健壮”这种语义出发去 grep,是完全不现实的。

对这种场景,我保留了一个很轻量的向量索引,只索引每个文件的头部注释和公开函数签名,不做全文分块。好处是:向量层只负责把自然语言需求粗粒度映射到具体文件和函数名,真正的代码细节仍然靠 CLI 精确拿。这个混合方案在模糊需求上省 token 效果不如纯 CLI 明显,但比纯向量方案还是好很多,因为兜底层不会把大段代码塞进上下文。

6.3 别把 grep 结果堆成山,需要限制工具行为

如果你不限制 rg 的输出行数,它会一次性吐出 2000 行命中结果,一秒就把前面省的全吐回去。所以我的封装里强制加了-m 20和head -40这两个上限。

同样的道理也适用于 sed 读取,所以我给agent_fetch加了 80 行上限,超过范围就只给前面 80 行并提示总长度,让 Agent 决定是否分两段取。让工具“先说位置,再按需给内容”,而不是“一次全给”,这是整个方案的核心纪律。

6.4 用日志量化调优,别靠感觉

最后说一个方法论层面的心得:所有优化都要落到 token 日志上。我每次工具调用时都会记录输入 token、输出 token、历史累积长度三个值,跑同一批基准任务集,然后对比方案改造前后的总量。

有一次我看日志发现某个任务的总 token 异常高,挖了两层发现是一个内部工具返回了 JSON 格式字段名,每个字段 20 个字符长,加上转译字符后单次调用多花了 400 token,还没算跟着复利放大。这种问题靠“感觉”是看不出来的,但日志会诚实地告诉你。

结尾前的一点体会

这个方案做下来,我最大的感触不是“向量库没用”,而是“向量库在代码场景里用错了位置”。代码的天性是精确结构、可导航、可验证,给它套上自然语言的模糊检索,等于让 Agent 在信息密度很低的大片噪声里找东西。CLI 工具箱之所以省 token,本质上是在还原代码本身的秩序:先定位,再按需读取,绝不把整座山搬给模型。

最后分享一个小细节:我现在把所有 CLI 工具的输出规则统一成“结果足够小时返回内容体,结果太大时只返回文件路径 + 行号 + 命中数”。这一个小小的前置判断,让 Agent 失控读取的次数降了七成,token 账单也肉眼可见地温和了。如果你也在做类似的事,建议从这个规则开始改造,你会在第一周就在日志里看到差异。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询