DeerFlow 的 grep 与 glob 工具:为 Coding Agent 构建受控的文件系统检索层
本文基于 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 列举了当时的典型痛点:
- 模型想找特定后缀文件时,只能反复
ls多层目录,或者退回bash find; - 模型想找某个符号出现位置时,只能逐文件
read_file,或者退回bash grep/rg; - 一旦退回
bash,工具调用就失去结构化输出,结果也更难做裁剪、分页、审计和跨 sandbox 一致化; - 对没有开启 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_tool 与 grep_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 模式,相对于根路径匹配,例如 **/*.py、src/**/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.walk、re、fnmatch、PurePosixPath 等标准库,没有任何子进程调用。
实现解析:search.py 中的检索内核
search.py 是两个工具的共享内核,包含四个关键部件。
1. 统一的忽略规则集
RFC 要求 glob 的默认忽略项尽量与 ls 对齐,并 "抽一个共享 ignore 集"。实现中这是一个 50 项的 IGNORE_PATTERNS 列表,覆盖:
- 版本控制目录:
.git、.svn、.hg、.bzr; - 依赖与虚拟环境:
node_modules、.venv、venv、site-packages; - 构建产物:
dist、build、target、out、.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_matches用PurePosixPath.match,并额外兼容**/前缀模式); - 命中数达到
max_results时立即返回,并通过第二个返回值truncated=True告知调用方结果被截断——这正是 RFC "大结果集会被截断并明确提示" 验收标准的落点。
3. grep 匹配:find_grep_matches
find_grep_matches 与 GrepMatch 数据类(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当 Pythonre编译;编译失败会抛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 现在包含 glob 与 grep 两个抽象方法,各 sandbox provider 可以各自实现/优化;工具层调用 sandbox.glob(...) / sandbox.grep(...),各环境(本地、容器、远程)遵守同一语义。这正对应 RFC 的目标之一 "让本地 sandbox、容器 sandbox、未来 MCP 文件系统工具都能遵守同一语义"。
安全模型:沿用路径权限与输出脱敏
RFC 原则 B 要求两个工具复用 ls / read_file 的路径校验逻辑,"它们属于 file:read,不是 bash 的替代越权入口"。在 tools.py 中可以确认这条调用链:
- 禁用技能拦截:先检查
_is_disabled_skill_path,被禁用的 skill 目录直接返回错误; - 沙箱初始化:
ensure_sandbox_initialized(runtime)+ensure_thread_directories_exist(runtime); - 路径解析与权限校验(仅本地 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 被拒绝; - 输出脱敏:结果回来后用
mask_local_paths_in_output把宿主真实路径反掩回虚拟路径(glob 对每条 match、grep 对每条GrepMatch.path),保证 "输出不泄露宿主机真实路径" 的验收标准; - 结果级过滤:即使 root 本身合法,遍历过程中遇到的已禁用 skill 路径仍会被
_drop_disabled_skill_paths逐条剔除; - 错误脱敏:
_sanitize_error会把异常信息中解析出的宿主路径掩码回虚拟路径再返回,避免错误消息成为路径泄露通道。
两个工具还各自捕获了 FileNotFoundError / NotADirectoryError / PermissionError(grep 额外捕获 re.error),统一转成 Error: ... 文本返回,模型可以据此自纠参数。异步侧则通过给 glob_tool / grep_tool 挂 coroutine(_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_results 取 min。也就是说,配置里写死的上限是全局天花板——即使模型在调用时传入更大的 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 明确了四个工具的互补关系,推荐模型工作流为:
glob找候选文件;grep找候选位置;read_file读局部上下文;str_replace/write_file执行修改。
边界清晰的好处是利于在系统提示中教模型形成稳定习惯。RFC 同时强调:引入这两个工具时,必须同步更新系统提示——查找文件名模式时优先 glob,查找代码符号、配置项、文案时优先 grep,只有工具不足以完成目标时才退回 bash;否则模型仍会习惯性先调 bash。
风险、备选方案与验收标准
RFC 讨论过的风险与缓解(原文四节):
- 与
bash能力重叠——是事实但不是问题;ls和read_file也能被bash替代,仍保留,因为结构化工具更适合 agent; - 性能——大仓库上纯 Python
grep可能比rg慢;缓解:结果上限 + 文件大小上限(1MB)、强制 root path、glob过滤缩小扫描范围、必要时在 provider 内部做rg优化但保持同一 schema。当前实现还额外加了 "超长行跳过" 防 ReDoS; - 忽略规则不一致——
ls能看到而glob看不到的路径会让模型困惑;缓解:统一 ignore 集(即 search.py 中那份共享列表),并在文档中明确 "默认跳过常见依赖和构建目录"; - 正则方言过复杂——第一版只支持 Python
re,并提供literal=True简单模式。
Alternatives Considered 全部被否定,理由值得记录:
- 完全依赖
bash:会让 DeerFlow 在代码探索体验上持续落后,且削弱无 bash / 受限 bash 场景能力; - 只加
glob不加grep:只解决 "找文件" 没解决 "找位置",模型最终仍退回bash grep; - 只加
grep不加glob:grep缺少路径模式过滤时扫描范围经常过大,glob是它的天然前置; - 直接接入 MCP filesystem server:MCP 可作为补充,但
glob/grep作为基础 coding tool 最好是 built-in,才能在默认安装中稳定可用。
Acceptance Criteria(RFC 原文):
config.example.yaml中可默认启用glob与grep;- 两个工具归属
file:read组; - 本地 sandbox 下严格遵守现有路径权限;
- 输出不泄露宿主机真实路径;
- 大结果集会被截断并明确提示;
- 模型可以通过
glob -> grep -> read_file -> str_replace完成典型改码流; - 在禁用 host bash 的本地模式下,仓库探索能力明显提升。
对应的回归测试覆盖见 test_sandbox_search_tools.py,针对路径校验、虚拟路径映射、结果截断与二进制跳过等场景做验证。
小结:三条被坚守的边界
RFC 的最终推荐是 "可以加,而且应该加",但明确卡住三个边界,这也正是当前实现可核对到的事实:
grep/glob必须是 built-in 的只读结构化工具——归属file:read组,复用validate_local_tool_path(read_only=True)权限模型;- 第一版不做 shell wrapper,不把 CLI 方言直接暴露给模型——检索内核纯标准库实现于
search.py; - 先在
sandbox/tools.py验证价值,再下沉到Sandboxprovider 抽象——当前仓库已完成这一步,SandboxABC 中同时存在glob/grep抽象方法,各 provider 共享同一对外语义。
按这个方向,DeerFlow 在 coding / repo exploration 场景下的可用性得到提升,且风险可控:检索行为可审计、可限流、跨环境一致,并且与既有文件工具形成清晰的探索—定位—阅读—修改闭环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00