首页
/ DeerFlow 的 grep 与 glob 工具:为 Coding Agent 构建受控的文件系统检索层

DeerFlow 的 grep 与 glob 工具:为 Coding Agent 构建受控的文件系统检索层

2026-09-04 13:43:24作者:龚格成

本文基于 DeerFlow 仓库中的 RFC 文档 rfc-grep-glob-tools.md,并结合该 RFC 在仓库中已落地的实现源码,系统讲解 DeerFlow 为什么要在 sandbox 工具层新增 glob(按路径模式找文件)与 grep(按内容模式找位置)两个 built-in 只读检索工具、它们的设计边界(参数语义、路径权限、结果硬限制)、底层 Python 实现细节(忽略规则、二进制跳过、截断提示),以及如何在配置中启用和调优。读完后,你将理解 Agent 化文件检索工具区别于 "shell 命令包装" 的核心取舍,并能完整复现 DeerFlow 中 glob -> grep -> read_file -> str_replace 的仓库探索工作流。

问题起点:ls / read_file 还不够

RFC 的出发点很明确:如果 DeerFlow 想更接近 Claude Code 这类 coding agent 的实际工作流,仅有 ls / read_file / write_file / str_replace / bash 还不够。模型在进入修改前,通常还需要两类能力:

  • glob:快速按路径模式找文件,例如 "所有 *.tsx 的 page 文件";
  • grep:快速按内容模式找候选位置,例如 "某个 symbol / 文案 / 配置键在哪里出现"。

RFC 列举了当时的典型痛点:

  1. 模型想找特定后缀文件时,只能反复 ls 多层目录,或者退回 bash find
  2. 模型想找某个符号出现位置时,只能逐文件 read_file,或者退回 bash grep / rg
  3. 一旦退回 bash,工具调用就失去结构化输出,结果也更难做裁剪、分页、审计和跨 sandbox 一致化;
  4. 对没有开启 host bash 的本地模式,bash 甚至可能不可用,此时缺少足够强的只读检索能力。

因此结论是:DeerFlow 缺的不是 "再多一个 shell 命令",而是文件系统检索层。这两类工具的价值不是 "功能上 bash 也能做",而是能以更低 token 成本、更强约束、更稳定的输出格式,替代模型频繁走 bash find / bash grep / rg 的习惯。

目标与非目标

RFC 给出的 Goals

  • 为 agent 提供稳定的路径搜索和内容搜索能力;
  • 减少对 bash 的依赖,特别是在仓库探索阶段;
  • 保持与现有 sandbox 安全模型一致;
  • 输出格式结构化,便于模型后续串联 read_file / str_replace
  • 让本地 sandbox、容器 sandbox、未来 MCP 文件系统工具都能遵守同一语义。

Non-Goals 同样重要,它划定了边界:

  • 不做通用 shell 兼容层;
  • 不暴露完整 grep/find/rg CLI 语法;
  • 不在第一版支持二进制检索、复杂 PCRE 特性、上下文窗口高亮渲染等重功能;
  • 不把它做成 "任意磁盘搜索",仍然只允许在 DeerFlow 已授权的路径内执行。

直接收益可以概括为五点:更低的模型负担(不用拼 find/grep/xargs/quoting 细节)、更稳定的跨环境行为(本地、Docker、AIO sandbox 不必依赖容器里是否装了 rg)、更强的安全与审计(调用参数天然就是 "搜索什么、在哪搜、最多返回多少")、更好的 token 效率(返回命中摘要而非整段文件),以及对 tool_search 友好(高频基础工具值得保留为 built-in)。

工具定义:glob 与 grep 的参数与返回格式

RFC 建议增加两个 built-in sandbox tools,放在 sandbox/tools.py。仓库中这两个工具已经实现,定义见 glob_toolgrep_tool

glob 工具

用途:按路径模式查找文件或目录。当前实现的签名(与 RFC 建议的 schema 基本一致):

@tool("glob", parse_docstring=True)
def glob_tool(
    runtime: Runtime,
    pattern: str,
    path: str,
    description: str = "",
    include_dirs: bool = False,
    max_results: int = _DEFAULT_GLOB_MAX_RESULTS,  # 200
) -> str:
    """Find files or directories that match a glob pattern under a root directory."""

参数语义:

参数 说明
pattern glob 模式,相对于根路径匹配,例如 **/*.pysrc/**/test_*.ts
path 搜索根目录,必须是绝对路径
description 与现有工具保持一致的 UI 展示说明
include_dirs 是否返回目录,默认 False
max_results 最大返回条数,默认 200,防止一次性打爆上下文

返回格式(由 _format_glob_results 生成,见 tools.py):

Found 3 paths under /mnt/user-data/workspace
1. /mnt/user-data/workspace/backend/app.py
2. /mnt/user-data/workspace/backend/tests/test_app.py
3. /mnt/user-data/workspace/scripts/build.py

结果为空时返回 No files matched under <root>;命中超过上限时,首行会追加 (showing first N),并附一句 Results truncated. Narrow the path or pattern to see fewer matches. 的截断提示。

grep 工具

用途:按内容模式搜索文件,返回命中位置摘要。当前实现:

@tool("grep", parse_docstring=True)
def grep_tool(
    runtime: Runtime,
    pattern: str,
    path: str,
    description: str = "",
    glob: str | None = None,
    literal: bool = False,
    case_sensitive: bool = False,
    max_results: int = _DEFAULT_GREP_MAX_RESULTS,  # 100
) -> str:
    """Search for matching lines inside a text file or files under a root directory."""

参数语义:

参数 说明
pattern 搜索词或 Python 正则
path 搜索目标,可以是单个文件或根目录,必须是绝对路径
glob 可选路径过滤,例如 **/*.py,用于缩小扫描范围
literal True 时按普通字符串匹配,不解释为正则(内部用 re.escape 转义)
case_sensitive 是否大小写敏感,默认 False(即默认 re.IGNORECASE
max_results 最大返回命中行数(不是文件数),默认 100

返回格式(_format_grep_results):

Found 4 matches under /mnt/user-data/workspace
/mnt/user-data/workspace/backend/config.py:12: TOOL_GROUPS = [...]
/mnt/user-data/workspace/backend/config.py:48: def load_tool_config(...):
/mnt/user-data/workspace/backend/tools.py:91: "tool_groups"
/mnt/user-data/workspace/backend/tests/test_config.py:22: assert "tool_groups" in data

第一版只返回文件路径 + 行号 + 命中行摘要,不返回上下文块,避免结果过大;模型需要上下文时再调用 read_file(path, start_line, end_line)。截断时同样输出 Results truncated. Narrow the path or add a glob filter. 提示。

设计原则:为什么不做 shell wrapper

这是 RFC 中最有分歧也最关键的一条决策。不建议grep 实现为 subprocess.run("grep ..."),也不建议在容器里直接拼 find / rg 命令。原因:

  • 会引入 shell quoting 和注入面;
  • 会依赖不同 sandbox 镜像是否安装了同一套命令;
  • Windows / macOS / Linux 行为不一致;
  • 很难稳定控制输出条数与格式。

正确方向是:glob 使用 Python 标准库路径遍历,grep 使用 Python 逐文件扫描,输出由 DeerFlow 自己格式化。如果未来为了性能要优先调用 rg,也应该封装在 provider 内部并保证外部语义不变,而不是把 CLI 暴露给模型。

从源码结构看,这个原则得到了完整贯彻:核心检索逻辑放在独立的 search.py 中,只依赖 os.walkrefnmatchPurePosixPath 等标准库,没有任何子进程调用。

实现解析:search.py 中的检索内核

search.py 是两个工具的共享内核,包含四个关键部件。

1. 统一的忽略规则集

RFC 要求 glob 的默认忽略项尽量与 ls 对齐,并 "抽一个共享 ignore 集"。实现中这是一个 50 项的 IGNORE_PATTERNS 列表,覆盖:

  • 版本控制目录:.git.svn.hg.bzr
  • 依赖与虚拟环境:node_modules.venvvenvsite-packages
  • 构建产物:distbuildtargetout.next.nuxt.output.turbo
  • 缓存与临时文件:__pycache__.pytest_cache.mypy_cache.ruff_cache*.log*.tmp*.bak*.swp 等。

值得注意的性能细节:should_ignore_name 在目录树遍历时每个条目都要执行一次,因此实现把纯字面量名称预编译成 frozenset(O(1) 查找),只把含 *?[ 的少数通配模式合并成一条正则,避免每个文件名做约 50 次 fnmatch 调用。os.path.normcase 同时保持了与 fnmatch 一致的大小写行为(POSIX 敏感、Windows 折叠)。

2. glob 匹配:find_glob_matches

find_glob_matches(root, pattern, *, include_dirs, max_results) 的行为:

  • 根目录不存在抛 FileNotFoundError,不是目录抛 NotADirectoryError(对应 RFC "输入根目录不存在/根路径不是目录:返回清晰错误");
  • os.walk 遍历,且通过原地改写 dirs[:] 把忽略目录从遍历中直接剪掉;
  • 模式匹配相对路径(path_matchesPurePosixPath.match,并额外兼容 **/ 前缀模式);
  • 命中数达到 max_results 时立即返回,并通过第二个返回值 truncated=True 告知调用方结果被截断——这正是 RFC "大结果集会被截断并明确提示" 验收标准的落点。

3. grep 匹配:find_grep_matches

find_grep_matchesGrepMatch 数据类(path / line_number / line 三元组,与 RFC 建议的抽象层签名一致)实现了以下行为,逐条对应 RFC 的 "Detailed Behavior":

  • 默认只扫描文本文件;is_binary_file 检查前 8KB 内是否含 \0 字节,命中即跳过;
  • 超过 max_file_size(默认 DEFAULT_MAX_FILE_SIZE_BYTES = 1_000_000,即 RFC 建议的 1MB 上限)的文件直接跳过;
  • literal=True 时先 re.escape,否则把 pattern 当 Python re 编译;编译失败会抛 re.error,由工具层捕获并返回 Error: Invalid regex pattern: ...(对应 "regex 编译失败时返回参数错误");
  • case_sensitive=False 时加 re.IGNORECASE
  • 跳过符号链接,并要求 resolve 后的文件仍位于 root 之下(防越权);
  • 读取用 encoding="utf-8", errors="replace",保证脏字节不会中断扫描;
  • 单行超过 line_summary_length * 10(即 2000 字符)的行直接跳过,注释里写明这是为了防止对 minified / 无换行文件的 ReDoS;
  • 每行命中后经 truncate_line 截断到 200 字符(DEFAULT_LINE_SUMMARY_LENGTH),对应 RFC "单行摘要最大长度 200 字符";
  • 按文件路径、行号的自然遍历顺序输出,保持稳定排序。

4. 从工具到 Sandbox 抽象:RFC 中 Option B 已落地

RFC 给出两个实现选项:Option A(直接在 sandbox/tools.py 实现第一版)与 Option B(先扩展 Sandbox 抽象,新增 glob / grep 抽象方法),结论是 "第一版建议走 Option A,等工具价值验证后再下沉到 Sandbox 抽象层"。

从当前仓库看,项目走完了两步:Sandbox ABC 现在包含 globgrep 两个抽象方法,各 sandbox provider 可以各自实现/优化;工具层调用 sandbox.glob(...) / sandbox.grep(...),各环境(本地、容器、远程)遵守同一语义。这正对应 RFC 的目标之一 "让本地 sandbox、容器 sandbox、未来 MCP 文件系统工具都能遵守同一语义"。

安全模型:沿用路径权限与输出脱敏

RFC 原则 B 要求两个工具复用 ls / read_file 的路径校验逻辑,"它们属于 file:read,不是 bash 的替代越权入口"。在 tools.py 中可以确认这条调用链:

  1. 禁用技能拦截:先检查 _is_disabled_skill_path,被禁用的 skill 目录直接返回错误;
  2. 沙箱初始化ensure_sandbox_initialized(runtime) + ensure_thread_directories_exist(runtime)
  3. 路径解析与权限校验(仅本地 sandbox 分支):_resolve_local_read_path(path, thread_data) 内部调用 validate_local_tool_path(path, thread_data, read_only=True),随后区分两类路径——skills / ACP workspace / 自定义挂载路径交给 sandbox 的 PathMapping 解析,其余 user-data 虚拟路径走 _resolve_and_validate_user_data_path 解析。这保证了 thread workspace / uploads / outputs 虚拟路径、/mnt/skills/.../mnt/acp-workspace/... 均被支持,越权路径与 path traversal 被拒绝;
  4. 输出脱敏:结果回来后用 mask_local_paths_in_output 把宿主真实路径反掩回虚拟路径(glob 对每条 match、grep 对每条 GrepMatch.path),保证 "输出不泄露宿主机真实路径" 的验收标准;
  5. 结果级过滤:即使 root 本身合法,遍历过程中遇到的已禁用 skill 路径仍会被 _drop_disabled_skill_paths 逐条剔除;
  6. 错误脱敏_sanitize_error 会把异常信息中解析出的宿主路径掩码回虚拟路径再返回,避免错误消息成为路径泄露通道。

两个工具还各自捕获了 FileNotFoundError / NotADirectoryError / PermissionError(grep 额外捕获 re.error),统一转成 Error: ... 文本返回,模型可以据此自纠参数。异步侧则通过给 glob_tool / grep_toolcoroutine_glob_tool_async / _grep_tool_async)适配 LangGraph 的异步调用,同步函数体不变。

结果硬限制:默认值、上限与配置覆写

RFC 原则 C 规定 "没有硬限制的 glob/grep 很容易炸上下文",建议第一版:glob.max_results 默认 200、最大 1000;grep.max_results 默认 100、最大 500;单行摘要 200 字符;跳过二进制与超大文件;命中超阈值时返回 "已展示条数 + 被截断事实 + 缩小范围建议"。

实现与这些数值完全对应,见 tools.py

_DEFAULT_GLOB_MAX_RESULTS = 200
_MAX_GLOB_MAX_RESULTS = 1000
_DEFAULT_GREP_MAX_RESULTS = 100
_MAX_GREP_MAX_RESULTS = 500

截断提示文案也符合 RFC 的示例风格("已展示 N 条 + 建议缩小 path/pattern/glob")。

还有一个超出 RFC 文本、但在代码中确认的机制:_resolve_max_results 会读取 config.example.yaml 中该工具配置的 max_results 键(_get_tool_config_int),并与模型请求的 max_resultsmin。也就是说,配置里写死的上限是全局天花板——即使模型在调用时传入更大的 max_results,也会被钳制回配置值;非法值(<=0)回退默认值。这为运维侧限流提供了单一控制点。

启用与配置:file:read 工具组

RFC 的 Suggested Config 与仓库根目录的 config.example.yaml 实际内容一致:

tools:
  - name: glob
    group: file:read
    use: deerflow.sandbox.tools:glob_tool
    max_results: 200

  - name: grep
    group: file:read
    use: deerflow.sandbox.tools:grep_tool
    max_results: 100

要点:

  • 两个工具归属 file:read 组,与 ls / read_file 同权限等级,是只读检索工具而非 bash 的越权入口;
  • max_results 键会被 _resolve_max_results 读入,作为该工具返回上限的全局天花板(glob 天花板 1000、grep 天花板 500,超出部分会被 clamp);
  • use 指向 deerflow.sandbox.tools 下的具体 tool 对象,与仓库中注册位置一一对应。

推荐工作流与 Prompt 引导

RFC 明确了四个工具的互补关系,推荐模型工作流为:

  1. glob 找候选文件;
  2. grep 找候选位置;
  3. read_file 读局部上下文;
  4. str_replace / write_file 执行修改。

边界清晰的好处是利于在系统提示中教模型形成稳定习惯。RFC 同时强调:引入这两个工具时,必须同步更新系统提示——查找文件名模式时优先 glob,查找代码符号、配置项、文案时优先 grep,只有工具不足以完成目标时才退回 bash;否则模型仍会习惯性先调 bash

风险、备选方案与验收标准

RFC 讨论过的风险与缓解(原文四节):

  1. bash 能力重叠——是事实但不是问题;lsread_file 也能被 bash 替代,仍保留,因为结构化工具更适合 agent;
  2. 性能——大仓库上纯 Python grep 可能比 rg 慢;缓解:结果上限 + 文件大小上限(1MB)、强制 root path、glob 过滤缩小扫描范围、必要时在 provider 内部做 rg 优化但保持同一 schema。当前实现还额外加了 "超长行跳过" 防 ReDoS;
  3. 忽略规则不一致——ls 能看到而 glob 看不到的路径会让模型困惑;缓解:统一 ignore 集(即 search.py 中那份共享列表),并在文档中明确 "默认跳过常见依赖和构建目录";
  4. 正则方言过复杂——第一版只支持 Python re,并提供 literal=True 简单模式。

Alternatives Considered 全部被否定,理由值得记录:

  • 完全依赖 bash:会让 DeerFlow 在代码探索体验上持续落后,且削弱无 bash / 受限 bash 场景能力;
  • 只加 glob 不加 grep:只解决 "找文件" 没解决 "找位置",模型最终仍退回 bash grep
  • 只加 grep 不加 globgrep 缺少路径模式过滤时扫描范围经常过大,glob 是它的天然前置;
  • 直接接入 MCP filesystem server:MCP 可作为补充,但 glob / grep 作为基础 coding tool 最好是 built-in,才能在默认安装中稳定可用。

Acceptance Criteria(RFC 原文):

  • config.example.yaml 中可默认启用 globgrep
  • 两个工具归属 file:read 组;
  • 本地 sandbox 下严格遵守现有路径权限;
  • 输出不泄露宿主机真实路径;
  • 大结果集会被截断并明确提示;
  • 模型可以通过 glob -> grep -> read_file -> str_replace 完成典型改码流;
  • 在禁用 host bash 的本地模式下,仓库探索能力明显提升。

对应的回归测试覆盖见 test_sandbox_search_tools.py,针对路径校验、虚拟路径映射、结果截断与二进制跳过等场景做验证。

小结:三条被坚守的边界

RFC 的最终推荐是 "可以加,而且应该加",但明确卡住三个边界,这也正是当前实现可核对到的事实:

  1. grep / glob 必须是 built-in 的只读结构化工具——归属 file:read 组,复用 validate_local_tool_path(read_only=True) 权限模型;
  2. 第一版不做 shell wrapper,不把 CLI 方言直接暴露给模型——检索内核纯标准库实现于 search.py
  3. 先在 sandbox/tools.py 验证价值,再下沉到 Sandbox provider 抽象——当前仓库已完成这一步,Sandbox ABC 中同时存在 glob / grep 抽象方法,各 provider 共享同一对外语义。

按这个方向,DeerFlow 在 coding / repo exploration 场景下的可用性得到提升,且风险可控:检索行为可审计、可限流、跨环境一致,并且与既有文件工具形成清晰的探索—定位—阅读—修改闭环。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384