Agent文件系统检索核心:Grep和Glob工具
Agent文件系统检索核心:Grep和Glob工具
今天看到一篇文章,说在Harness的定义组件中,Agent的文件系统是核心之一
在文件系统的帮助下,Agent表现出来的搜索能力是非常出色的,用户和开发者不定义搜索路径,只提供输入驱动,而具体的搜索路径是由Agent根据每一次的工具调用动态决定的
Agent的文件检索能力的实现关键,就来源于基础工具:Grep和Glob
最近也在仔细读ClaudeCode的源码,具体的代码实现中有更多“补丁设计”,感觉对于新手不是很友好,读的时候,我觉得opencode或者gemini-cli的实现或许更友好一些,设计和实现都比较干净,如果有相同困惑的佬可以考虑看看
grep和glob这两个工具在很多Agent SDK都是自带的,一方面你可以不用从0开发,可以直接使用框架自带的方法,另外一方面也说明该工具的重要性,我觉得可以成为大模型一样开发claw模式下的基础设施工具
附加一张claudecode的SDK的图说明一下

本文节选自「大模型应用开发 - 上下文工程与运行空间实践指南」 上下文工程是设计原则,Agent Harness 是构建目标。完整内容已发布在 GitHub,欢迎查看
👉 https://github.com/WakeUp-Jin/Practical-Guide-to-Context-Engineering
一、Glob 工具实现
glob 工具是存在降级策略的,为了提高执行效率和降低运行占用量

实现 glob 工具有两种方式,这两种方式“各有各的”好处
- glob 依赖包:返回的是完整的文件的信息(存在文件元信息等,例如文件大小,文件修改时间),所以不需要额外的操作,并且 glob 依赖包是天然的 Node 环境的包
- ripgrep 命令:返回的是文件路径,没有任何文件元信息,所以需要在操作读取文件信息的操作,
stat方法,但是 ripgrep 检索的速度是比 glob 快的,但是 ripgrep 是 Rust 实现的,所以运行时需要加载一个二进制文件
关于 glob 工具执行的总时间,这里有一个形象的公式:
总时间=检索时间+ N*单文件的处理时间
- glob 依赖包的实现方式,后面的那个单文件处理时间完全可以忽略不计,所以其检索时间就约等于总时间
- ripgrep 命令的实现方式,检索时间是比 glob 依赖包的方式更快的,但是其需要单文件的处理时间,也就是 stat 方法的调用时间
我的建议和总结是:
- 🚀如果是追求开发方便,那么我是建议直接使用 glob 实现,会快很多,并且不需要考虑外部文件的执行
- 🪐如果是追求可操作的检索效率,那么是可以考虑使用 ripgrep 来实现,检索工具不可能只实现 glob,也会考虑使用 grep 的,要实现 grep,ripgrep 这个命令是优先考虑的,这么一看也不算是另外单独引入一个外部依赖
- 🌴如果是追求稳定,那么可以考虑降级策略,先使用 ripgrep,如果 ripgrep 这个环境不存在或者下载失败,那么就可以降级为 glob,保证了程序或者项目可以运行
二、Grep 工具的实现

目前 grep 有四种的方式实现,按照优先级排序,保证系统的稳定的可用性,使用降级策略保证 grep 工具执行成功,我们会先验证这些策略的可用性,再考虑优先级高的先使用
- ripgrep:是使用 Rust 编写的二进制文件,检索速度非常非常快
- git grep 命令:这个是直接读取.git/index 中已缓存的文件列表,跳过耗时的目录遍历操作
- 系统的 grep 命令:大部分是传统的 C 实现的,单线程递归搜索,速度还可以,大部分 Unix 系统都有,不过 windows 系统是没有的
- js 实现的 grep 命令:是纯 JS 实现的,是一个保底方案,用 glob 获取文件列表,逐个读取文件内容,逐行正则匹配,速度最慢
三、Ripgrep 自动下载机制

ripgrep 命令的执行需要完整的路径,在 Node 子进程中使用 spawn 的时候,需要完整的路径才可以成功执行命令
▼typescript复制代码async function grepWithRipgrep(pattern, cwd, options) { // 获取 ripgrep 路径 const rgPath = await Ripgrep.filepath(options.binDir); // 返回:/usr/bin/rg 或 ~/.reason/bin/rg // 使用路径执行命令 const proc = spawn(rgPath, [ '--line-number', '--no-heading', pattern, ], { cwd }); }
所以目前的判断获取的策略是这样的:
- 先看看内存缓存中是否存在,如果没有就进入下一级,有就返回
- 再看看系统是否有安装 ripgrep,如果有就返回并且赋值给缓存,下一次就可以直接缓存取啦,如果没有就下一级
- 然后在看看本地路径是否安装了 ripgrep 的二进制文件,要是有安装的话,和上面同理,如果没有就开始进行下载文件到相应的目录中
四、超时控制
实现这个需求,在 Node 中会使用到中止控制器AbortController/AbortSignal,这里有一个简单的例子:
▼typescript复制代码const controller = new AbortController(); function customTask(signal: AbortSignal): Promise<string> { return new Promise((resolve, reject) => { //初始状态的检查 if (signal.aborted) { reject(new Error('Task aborted')); return; } const timer = setTimeout(() => resolve('done'), 5000); // 使用 { once: true } 自动清理监听器 const abortHandler = () => { clearTimeout(timer); reject(new Error('Task aborted')); }; signal.addEventListener('abort', abortHandler, { once: true }); // 或者手动清理(如果需要在 resolve 时也清理) const cleanup = () => { signal.removeEventListener('abort', abortHandler); }; // 修改 timer 回调 const timerCallback = () => { cleanup(); resolve('done'); }; setTimeout(timerCallback, 5000); }); } customTask(controller.signal); // 3秒后取消 setTimeout(() => { controller.abort(); }, 3000);
AbortController:这个是控制器,用来发送“取消”信号AbortSignal:这个是信号,传递给异步操作,让它们可以被取消
关于 AbortSignal 这个对象,有一些属性是值得理解一下的,会让你在异步操作使用更加熟练
▼typescript复制代码interface AbortSignal{ readonly aborted:boolean //是否已经被取消 readonly reason:any //取消的原因 //监听取消事件 addEventListener( type:'abort', listener:(event:Event)=>void, options?:{once?:boolean} ):void //移除取消事件监听器 removeEventListener( type:'abort', listener:(event:Event) => void ):void //用于检查信息是否已取消 throwIfAborted():void }
那我们开始整理超时控制的函数式如何写的,主要就是三步:
- 封装取消函数,这个函数返回信号对象
- 创建一个包装器,传入要取消的函数操作,用于包装任何的异步操作
- 传入参数给异步操作
▼typescript复制代码//完整的核心代码如下 //1、创建取消函数,返回信号对象 export function createTimeoutSignal( timeoutMs: number, externalSignal?: AbortSignal ): { signal: AbortSignal; cleanup: () => void; isTimeout: () => boolean; } { const controller = new AbortController(); let timedOut = false; // 超时定时器 const timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs); // 监听外部中止信号 const abortHandler = () => { clearTimeout(timeoutId); controller.abort(); }; externalSignal?.addEventListener('abort', abortHandler, { once: true }); // 清理函数 const cleanup = () => { clearTimeout(timeoutId); externalSignal?.removeEventListener('abort', abortHandler); }; return { signal: controller.signal, cleanup, isTimeout: () => timedOut, }; } //2、创建异步操作包装器 export async function withTimeout<T>( promiseFactory: (signal: AbortSignal) => Promise<T>, timeoutMs: number, operation: string, externalSignal?: AbortSignal ): Promise<T> { // 1. 提前检查 if (externalSignal?.aborted) { throw createAbortError(); } // 2. 创建超时信号 const { signal, cleanup, isTimeout } = createTimeoutSignal(timeoutMs, externalSignal); try { // 3. 执行操作,传入信号 const result = await promiseFactory(signal); // 4. 成功完成,清理资源 cleanup(); return result; } catch (error) { // 5. 失败,清理资源 cleanup(); // 6. 如果是超时导致的中止,抛出 TimeoutError if (isTimeout() && isAbortError(error)) { throw createTimeoutError(operation, timeoutMs); } // 7. 其他情况原样抛出 throw error; } } //3、传递取消信号为进程执行的异步函数 - 简化版 function spawnAsync(command: string, args: string[], signal?: AbortSignal): Promise<void> { return new Promise((resolve, reject) => { const proc = child_process.spawn(command, args); // 监听取消信号 signal?.addEventListener('abort', () => { proc.kill(); reject(new Error('Aborted')); }, { once: true }); proc.on('close', (code) => { code === 0 ? resolve() : reject(new Error(`Exit code ${code}`)); }); proc.on('error', reject); }); } await withTimeout( (signal) => spawnAsync('long-command', [], { signal }), 5000, // 5秒超时 'command execution', userCancelSignal // 用户可手动取消 );
