ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

代码Agent省四成Token:精简CLI替代向量库的工程实践

代码Agent省四成Token:精简CLI替代向量库的工程实践

1. 代码 Agent 的上下文困境:为什么向量库成了负担

1.1 一个被忽视的成本黑洞

做代码 Agent 的人迟早会撞上同一堵墙:上下文窗口不够用。你给它一个中等规模的项目,它要理解目录结构、要读关键文件、要记住之前的修改、还要在生成代码时参考相关模块。这些信息全塞进 prompt 里,Token 消耗就像开了闸的水龙头。

大多数人的第一反应是上向量库。把代码切片、做 embedding、存进向量数据库、查询时做相似度检索,听起来很优雅。我一开始也是这么干的,用了一套本地向量库方案,代码分块策略调了好几轮,embedding 模型换了三个,检索效果确实比全文塞进去好一些。但跑了一段时间之后,我算了一笔账,发现事情没那么简单。

向量库方案的成本不只是检索那一下的 Token。它有一整套隐性开销:embedding 计算本身要消耗算力,索引维护要时间,检索出来的片段往往还需要二次排序和截断,而且检索质量不稳定——有时候最相关的代码片段没被召回,Agent 就得反复查询,反而增加了交互轮次。更关键的是,向量库的检索是"语义相似",但代码 Agent 需要的是"结构精确"。你问它某个函数的调用链,向量检索可能给你返回一堆语义相近但实际不相关的代码,这种噪声会直接污染 Agent 的判断。

1.2 代码 Agent 真正需要的是什么

我后来想明白一件事:代码 Agent 和通用问答 Agent 的需求本质不同。通用问答需要的是"语义相关性",你问天气它给你天气信息就行。但代码 Agent 需要的是"结构精确性"——它要知道这个函数定义在哪、被谁调用、依赖哪些模块、修改会影响哪些文件。这些信息用向量相似度去近似,本身就是一种信息损失。

代码天然就是结构化的。目录树、import 关系、函数签名、类型定义,这些都是确定性的、可精确索引的信息。用向量库去处理结构化信息,就像用模糊搜索去查数据库主键,能用,但没必要,而且容易出错。

所以当我看到"精简 CLI 给代码 Agent 省掉四成 Token"这个说法时,第一反应是:终于有人把这件事说透了。核心思路不是"不要检索",而是"用对的方式检索"。CLI 工具天然适合做这件事,因为它直接操作文件系统,能精确获取结构信息,不需要经过 embedding 这一层有损压缩。

1.3 四成 Token 到底省在哪

省 Token 的本质是减少无效上下文。向量库方案里,无效上下文主要来自三个地方:

第一,检索噪声。向量检索返回的 top-k 片段里,真正有用的可能只有一两个,其余都是语义相近但实际无关的内容。这些噪声片段白白占用 Token。

第二,重复检索。因为检索质量不稳定,Agent 经常需要换关键词重新查,每次查询都是一轮完整的 LLM 调用,Token 消耗翻倍。

第三,索引维护的上下文。为了让向量库工作,你需要在 prompt 里告诉 Agent 怎么用检索工具、返回格式是什么、怎么处理检索结果,这些指令本身也占 Token。

精简 CLI 方案把这三点全砍掉了。它用确定性的文件操作替代模糊检索,一次查询就能拿到精确结果,不需要反复试错,也不需要复杂的工具使用指令。四成这个数字,我实测下来是靠谱的,项目越大、结构越复杂,省得越多。

2. 精简 CLI 的检索哲学:精确优于相似

2.1 从"猜"到"查"的范式转换

向量库的工作方式是"猜"——它根据语义相似度猜测哪些代码可能相关。CLI 的工作方式是"查"——它根据确定性的规则精确查找。这个区别听起来简单,但影响是深远的。

举个例子。假设 Agent 需要修改一个用户认证相关的函数。向量库方案会拿"用户认证"这个 query 去做相似度检索,可能返回登录逻辑、权限校验、session 管理、甚至一些命名里带 auth 但实际无关的工具函数。Agent 拿到这一堆东西,得自己判断哪些真正相关。

CLI 方案怎么做?它可以直接执行类似grep -r "function authenticate" --include="*.ts"这样的命令,精确找到函数定义。然后通过grep -r "authenticate(" --include="*.ts"找到所有调用点。再通过目录结构判断哪些文件属于认证模块。每一步都是确定性的,结果精确,没有噪声。

这种精确性带来的 Token 节省是直接的:Agent 不需要在噪声里筛选,不需要反复确认,拿到结果就能用。

2.2 CLI 作为 Agent 的"手和眼"

我习惯把 CLI 工具理解为 Agent 的"手和眼"。LLM 本身是"大脑",它负责推理和决策,但它不能直接操作文件系统。向量库是一种"记忆外挂",它把信息压缩存储,需要时检索出来喂给大脑。而 CLI 是"感官延伸",它让大脑能直接感知和操作真实环境。

这个区别很关键。记忆外挂的问题是,存储和检索都有信息损失。你压缩得越狠,损失越大;检索越模糊,噪声越多。而感官延伸没有这个问题——Agent 通过 CLI 看到的文件内容就是文件本身,没有经过任何有损转换。

具体到代码 Agent 场景,CLI 能提供的能力包括:

  • 精确文件定位:通过find、fd等工具按名称、路径、类型精确定位文件
  • 内容搜索:通过grep、rg等工具按正则或字面量精确搜索代码内容
  • 结构分析:通过tree、ls -R等工具获取目录结构
  • 依赖追踪:通过分析 import/require 语句追踪模块依赖
  • 变更感知:通过git diff、git log感知代码变更历史

这些能力组合起来,Agent 就能在不依赖向量库的情况下,精确理解代码库结构。

2.3 为什么"精简"是关键

注意标题里的"精简"两个字。不是随便一个 CLI 都能省 Token,关键在于精简。一个臃肿的 CLI 工具,输出一大堆无关信息,反而比向量库更费 Token。

精简的核心原则是:只返回 Agent 需要的信息,不多不少。这要求 CLI 工具的输出格式对 LLM 友好——结构化、简洁、无冗余。

我见过一些 CLI 工具,输出带一堆颜色代码、进度条、装饰性字符,这些对 LLM 来说全是噪声。精简 CLI 应该做到:

  • 输出纯文本,无 ANSI 转义序列
  • 结果结构化,便于 LLM 解析
  • 支持精确过滤,减少无关内容
  • 输出长度可控,避免截断关键信息

提示:如果你在用的 CLI 工具输出带颜色或格式控制字符,记得加--no-color或类似参数。LLM 不需要这些装饰,它们只会浪费 Token。

3. 把 CLI 接入代码 Agent 的实操路径

3.1 工具选型:哪些 CLI 值得接入

不是所有 CLI 都适合接入代码 Agent。我的筛选标准有三条:输出精简、行为确定、覆盖高频需求。

按这个标准,我实际接入过并且觉得好用的工具包括:

工具用途为什么适合
rg(ripgrep)代码内容搜索速度快,输出干净,支持精确过滤
fd文件查找语法简洁,默认忽略 .gitignore
tree目录结构输出直观,可控制深度
git变更感知diff/log 输出结构化
ast-grep语法结构搜索按 AST 模式匹配,比正则精确

这些工具的共同特点是:输出格式稳定、行为可预测、支持精确控制。Agent 调用它们时,不需要复杂的参数说明,也不需要处理各种边界情况。

相比之下,我不建议接入的工具有:输出带大量装饰的、行为依赖交互式终端的、输出格式不稳定的。这些工具会让 Agent 的调用逻辑变复杂,反而增加 Token 消耗。

3.2 工具描述的精简写法

接入 CLI 工具时,工具描述(tool description)的写法直接影响 Token 消耗。很多人写工具描述像写文档,恨不得把所有参数都列一遍。但 Agent 不需要那么详细——它需要的是"什么时候用"和"怎么用"。

我的写法是:一句话说明用途,一句话说明关键参数,一个示例。

比如rg的工具描述:

用途:在代码库中搜索文本内容。 关键参数:pattern(搜索模式),path(搜索路径,默认当前目录),type(文件类型过滤)。 示例:搜索所有 TypeScript 文件中的 "authenticate" 函数调用:rg "authenticate\(" --type ts

这种写法比完整文档省很多 Token,而且 Agent 理解起来更直接。它不需要知道rg的所有参数,只需要知道最常用的那几个。

注意:工具描述里不要写"返回格式"的详细说明。Agent 看到实际输出后自然能理解格式,提前描述反而浪费 Token。

3.3 调用链的设计:让 Agent 自己决定查什么

接入 CLI 之后,关键问题是:怎么让 Agent 知道该调用哪个工具、传什么参数?

我的做法是给 Agent 一个"探索策略"而不是"固定流程"。具体来说,在 system prompt 里告诉它:

  • 需要了解项目结构时,先用tree看目录
  • 需要找特定文件时,用fd按名称查找
  • 需要找代码内容时,用rg搜索
  • 需要了解变更时,用git diff

但不规定具体顺序。让 Agent 根据当前任务自己决定下一步查什么。这样做的 Token 优势很明显:Agent 只查它真正需要的信息,不会因为固定流程而查一堆用不上的东西。

实测下来,这种"策略引导 + 自主决策"的方式,比"固定流程"省 20% 到 30% 的 Token。因为固定流程里有很多步骤是冗余的——比如一个简单的函数修改任务,不需要先看完整目录树。

3.4 输出截断与分页的处理

CLI 工具的输出可能很长,比如rg搜到一个关键词在几百个文件里出现。直接把全部输出塞给 Agent,Token 直接爆炸。

我的处理方式是:在 CLI 调用层做截断和分页。具体策略:

  • 默认限制输出行数(比如 50 行)
  • 如果结果被截断,在输出末尾加提示:"结果已截断,共 N 条,显示前 50 条"
  • Agent 如果需要更多,可以调整参数重新查询

这个策略的关键是:让 Agent 知道结果被截断了,并且知道怎么获取完整结果。这样它不会因为信息不全而做出错误判断,也不会盲目地要求全部输出。

提示:截断阈值不要设太小。我试过 20 行,Agent 经常需要重新查询,反而费 Token。50 行是个比较平衡的值。

4. 实测对比:向量库方案 vs 精简 CLI 方案

4.1 测试环境与任务设计

为了验证"省四成 Token"这个说法,我设计了一组对比测试。测试项目是一个中等规模的 TypeScript 项目,约 200 个文件,包含前端组件、后端 API、工具函数和测试文件。

测试任务选了三个典型场景:

  1. 定位修改:找到某个函数的定义并修改其实现
  2. 影响分析:分析修改某个接口会影响哪些调用方
  3. 新增功能:在现有模块基础上新增一个类似功能

每个任务分别用向量库方案和精简 CLI 方案跑 5 次,取平均 Token 消耗。

4.2 Token 消耗数据对比

测试结果如下:

任务类型向量库方案 (Token)精简 CLI 方案 (Token)节省比例
定位修改12,4007,20042%
影响分析18,60010,80042%
新增功能24,20015,10038%
平均18,40011,03340%

四成的节省比例是靠谱的。而且我注意到一个规律:任务越复杂、涉及的文件越多,CLI 方案的节省比例越高。因为复杂任务里,向量库的检索噪声和重复查询问题更严重。

4.3 为什么 CLI 方案更稳定

除了 Token 节省,CLI 方案还有一个隐性优势:结果稳定性。

向量库方案的检索结果受 embedding 模型、分块策略、相似度阈值等多个因素影响,同样的 query 在不同时间可能返回不同结果。这种不确定性会让 Agent 的行为变得难以预测。

CLI 方案的结果是确定性的。同样的rg命令,任何时候执行都返回同样的结果。Agent 的行为因此更可预测,调试也更容易。

我遇到过好几次向量库方案"抽风"的情况:同一个任务,前一天跑得好好的,第二天因为检索结果变了,Agent 就找不到正确的文件了。换成 CLI 方案之后,这种问题再没出现过。

4.4 什么情况下向量库仍然有价值

说了这么多 CLI 的好处,也得客观说一句:向量库不是完全没用。

在以下场景,向量库仍然有优势:

  • 语义搜索:当你不知道具体关键词,只知道大概意思时,向量检索更合适
  • 跨语言检索:代码和注释用不同语言时,向量检索能跨越语言障碍
  • 大规模代码库:文件数量极大时,精确搜索可能返回太多结果,向量检索能快速缩小范围

我的实际做法是混合使用:CLI 做精确检索,向量库做语义补充。但主力是 CLI,向量库只在特定场景下启用。这样既能享受 CLI 的精确和节省,又保留了向量库的语义能力。

5. 踩过的坑与优化心得

5.1 工具输出格式的坑

最开始接入 CLI 时,我直接用了默认输出格式。结果发现 Agent 经常解析错误——比如git diff的输出带颜色代码,Agent 把颜色代码当成了内容的一部分。

解决办法是统一加--no-color和--no-pager参数。另外,有些工具的输出带行号或文件名前缀,这些对 Agent 来说可能是噪声,也可能是有用信息,需要根据场景决定是否保留。

我的经验是:搜索类工具保留文件名和行号(Agent 需要知道结果在哪),内容展示类工具去掉行号(Agent 只需要内容本身)。

5.2 参数传递的坑

Agent 调用 CLI 时,参数传递经常出问题。最常见的是路径问题——Agent 可能传相对路径,也可能传绝对路径,还可能传带空格的路径。

我的处理方式是在工具层做路径规范化:统一转成相对于项目根目录的路径,并且对含空格的路径做转义处理。这样 Agent 不需要关心路径格式,传什么都能正常工作。

另一个坑是参数注入。Agent 生成的参数可能包含特殊字符,直接拼接到命令里可能导致命令执行失败或安全问题。我的做法是用参数数组而不是字符串拼接,避免 shell 解析问题。

5.3 结果缓存的坑

为了进一步省 Token,我试过对 CLI 结果做缓存。同样的查询在短时间内重复执行时,直接返回缓存结果。

这个优化在大多数情况下有效,但有一个坑:代码变更后缓存没失效。Agent 修改了文件,然后查询同一个内容,拿到的是修改前的缓存结果,导致判断错误。

解决办法是在文件变更时主动清除相关缓存。具体来说,监听文件系统变更事件,或者在做修改操作后手动清除缓存。这个逻辑不复杂,但很容易忘,我踩过好几次才养成习惯。

5.4 工具数量的坑

一开始我接入了十几个 CLI 工具,觉得工具越多 Agent 能力越强。实际跑下来发现,工具太多反而不好——Agent 在选择工具上花费更多推理,而且工具描述本身也占 Token。

后来我精简到 5 个核心工具:rg、fd、tree、git、ast-grep。这 5 个覆盖了 90% 以上的需求,Agent 的选择逻辑也简单了很多。

提示:工具不是越多越好。每增加一个工具,Agent 就多一个选择负担,工具描述也多占一份 Token。保持精简,只留真正高频的。

5.5 一个反直觉的发现

最后分享一个反直觉的发现:CLI 方案的 Token 节省,不完全来自"少查了信息",更多来自"少犯了错误"。

向量库方案里,Agent 经常因为检索结果不准确而做出错误判断,然后需要多轮交互来纠正。这些纠正轮次消耗的 Token 往往比检索本身还多。CLI 方案因为结果精确,Agent 一次就能拿到正确信息,减少了试错轮次,这才是 Token 节省的大头。

所以优化方向不应该是"怎么让检索返回更少内容",而应该是"怎么让 Agent 一次就拿到正确内容"。精确性比简洁性更重要。

6. 从零搭建精简 CLI 检索层的步骤

6.1 环境准备与工具安装

搭建精简 CLI 检索层的第一步是安装核心工具。我推荐的工具组合和安装方式如下:

# macOS 上用 Homebrew brew install ripgrep fd tree ast-grep # Ubuntu/Debian sudo apt install ripgrep fd-find tree # ast-grep 需要通过 npm 或 cargo 安装 npm install -g @ast-grep/cli # Windows 上用 scoop 或 winget scoop install ripgrep fd tree

安装完成后,验证一下版本:

rg --version fd --version tree --version ast-grep --version

这些工具都是跨平台的,Windows、macOS、Linux 上都能用。如果你在 Windows 上用,建议在 WSL 里跑,路径处理和输出格式会更一致。

6.2 工具封装层的设计

直接让 Agent 调用原始 CLI 工具会有很多问题:参数格式不统一、输出格式不一致、错误处理缺失。所以我建议加一层封装。

封装层的核心职责:

  • 参数规范化:统一路径格式、处理特殊字符、设置默认值
  • 输出清洗:去掉颜色代码、控制字符、分页提示
  • 错误处理:捕获命令执行错误,返回结构化错误信息
  • 结果截断:限制输出长度,添加截断提示

封装层可以用任何语言写,Python、Node.js、Go 都行。我用的是 Node.js,因为和 Agent 框架集成方便。

一个简单的封装示例:

async function searchCode(pattern, options = {}) { const args = ['--no-color', '--no-heading']; if (options.type) args.push('--type', options.type); if (options.maxCount) args.push('--max-count', options.maxCount); args.push(pattern); if (options.path) args.push(options.path); const result = await execFile('rg', args, { maxBuffer: 1024 * 1024 }); const lines = result.stdout.split('\n').filter(Boolean); if (lines.length > 50) { return lines.slice(0, 50).join('\n') + `\n[结果已截断,共 ${lines.length} 条]`; } return lines.join('\n'); }

这个封装做了三件事:设置默认参数、限制输出行数、添加截断提示。Agent 调用时只需要传 pattern 和可选的 type/path,不需要关心底层参数。

6.3 工具描述的编写

封装好工具之后,需要给每个工具写描述,让 Agent 知道怎么用。描述要精简,但信息要完整。

我用的模板是:

工具名:search_code 用途:在代码库中搜索文本内容,支持按文件类型过滤。 参数: - pattern(必填):搜索模式,支持正则表达式 - type(可选):文件类型,如 ts、py、go - path(可选):搜索路径,默认当前目录 返回:匹配的行,格式为"文件路径:行号:内容",最多返回 50 条。 示例:search_code("authenticate\\(", { type: "ts" })

这个描述包含了 Agent 需要的所有信息:什么时候用、传什么参数、返回什么格式、怎么调用。没有多余的说明,Token 消耗可控。

6.4 与 Agent 框架的集成

最后一步是把封装好的工具集成到 Agent 框架里。不同的框架集成方式不同,但核心逻辑是一样的:注册工具、提供描述、处理调用。

以常见的 function calling 为例,你需要:

  1. 定义工具的 JSON Schema
  2. 在每次 LLM 调用时传入工具列表
  3. 解析 LLM 返回的工具调用请求
  4. 执行工具并返回结果
  5. 把结果加入对话上下文

关键点是第 5 步:工具结果加入上下文时,要控制长度。我通常会在结果前后加简短说明,比如"搜索结果如下:"和"以上是搜索结果",帮助 Agent 理解上下文边界。

集成完成后,建议先用简单任务测试:让 Agent 找一个已知的函数定义。如果它能正确调用工具并返回结果,说明集成成功。

6.5 调优与迭代

集成完成后,需要根据实际使用情况调优。我关注的指标有三个:

  • 工具调用准确率:Agent 是否在正确的场景调用了正确的工具
  • 结果利用率:工具返回的结果里,有多少被 Agent 实际用到了
  • Token 消耗:每个任务的 Token 消耗是否在预期范围内

如果工具调用准确率低,说明工具描述不够清晰,需要调整描述。如果结果利用率低,说明返回了太多无关信息,需要收紧过滤条件。如果 Token 消耗超标,需要检查是否有重复查询或冗余上下文。

调优是一个持续的过程。我的经验是,前两周需要频繁调整,之后基本稳定。稳定后每周检查一次指标,有异常再调整。

7. 这套方案适合谁,不适合谁

7.1 适合的场景

这套精简 CLI 方案最适合以下场景:

  • 中小规模代码库:文件数量在几百到几千之间,精确搜索能快速定位
  • 结构化良好的项目:目录清晰、命名规范、依赖关系明确
  • 频繁的代码修改任务:Agent 需要反复查找和修改代码
  • 对 Token 成本敏感的场景:比如按量计费的 API 调用

如果你在做代码审查、自动重构、bug 修复这类任务,这套方案能明显降低 Token 消耗。

7.2 不适合的场景

以下场景可能不适合纯 CLI 方案:

  • 超大规模代码库:文件数量上万时,精确搜索可能返回太多结果
  • 语义搜索需求:当你只知道大概意思、不知道具体关键词时
  • 非结构化内容:文档、注释为主的检索,向量方案更合适
  • 跨语言检索:代码和查询语言不同时,向量方案能跨越语言障碍

这些场景下,建议混合使用 CLI 和向量库,各取所长。

7.3 一个实用的判断标准

如果你不确定该用哪种方案,可以用这个标准判断:你的查询能不能用精确的关键词或正则表达?

能,就用 CLI。不能,就用向量库。

比如"找到所有调用 authenticate 函数的地方",这是精确查询,用 CLI。"找到所有和用户认证相关的代码",这是语义查询,用向量库。

实际使用中,大部分代码 Agent 任务都是精确查询。这也是为什么 CLI 方案能省这么多 Token——它覆盖了大多数实际需求。

8. 最后的经验分享

这套方案我用了大半年,从最初的怀疑到现在的依赖,中间踩了不少坑,也积累了一些心得。

最大的体会是:不要为了技术而技术。向量库是个好东西,但它不是万能的。在代码 Agent 这个场景里,精确的结构化检索往往比模糊的语义检索更有效。CLI 工具虽然"土",但它直接、可靠、省 Token。

另一个体会是:精简是一种能力。接入工具很容易,但控制工具数量和输出长度很难。我见过太多 Agent 项目,工具列表几十个,每个工具描述几百字,光工具描述就占了几千 Token。这种"丰富"其实是负担。

最后一个建议:先跑通再优化。不要一开始就追求完美的工具封装和描述。先用最简的方式跑通,看看实际 Token 消耗和效果,再针对性优化。我最初就是用几个原始 CLI 命令直接跑,跑通之后才发现哪些地方需要封装、哪些地方需要精简。

这套方案不是银弹,但它在我实际使用中确实省了四成左右的 Token,而且结果更稳定、调试更容易。如果你也在做代码 Agent,不妨试试把向量库换成精简 CLI,可能会打开一个新思路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进