1. 为什么 Agent 需要 glob 和 grep 这双“眼睛”
你让 Agent 干一件事:“把项目里所有用到get_weather的地方改成get_city_weather”。如果它没有代码检索能力,只能一个文件一个文件读:先读tools.py,再读agent_loop.py,再读main.py……100 个文件就读 100 次,token 烧得飞快,还容易漏。
代码检索工具就是给 Agent 装上的“眼睛”。glob负责按文件名模式找文件,grep负责按内容正则匹配行。两者配合,Agent 能在秒级定位到“哪个文件、哪一行、写了什么”,然后精准调用编辑工具去改。
这一节我们聚焦 DeepSeek Agent Harness 第 9 章第 42 节的核心工具集开发,把glob和grep两个工具从零实现、注册、验证跑通。同时,所有模型调用统一走 TaoToken 的 Key/API 通道,避免在多个环境里散落凭据。读完你就能独立跑通一条完整的 Agent 代码检索链路:Agent 自己决定调glob还是grep,拿到结构化结果,再基于结果回答或继续操作。
适合谁看:正在给 Agent 写工具集、想让 Agent 具备代码库导航能力的开发者;已经跑通 bash 执行器、下一步要补检索能力的同学;以及想用统一 Key 通道管理模型调用的团队。
先说清楚两个工具的分工,避免混用:
| 工具 | 匹配对象 | 典型输入 | 返回 |
|---|---|---|---|
| glob | 文件名/路径 | **/*.py、test_*.py | 文件路径列表 |
| grep | 文件内容 | def test_、import.*requests | 文件:行号: 内容 |
glob回答“有哪些文件”,grep回答“哪一行匹配”。Agent 通常先glob缩小范围,再grep精确定位,最后read_file或edit_file。这条链路跑通,Agent 才算真正能“看懂”代码库结构。
2. TaoToken 统一 Key 通道前置准备
在写检索工具之前,先把模型调用通道理顺。Agent 的每一轮循环都要调模型,如果 Key 散落在.env、shell 变量、IDE 配置里,换机器或换模型时非常痛苦。TaoToken 提供统一的 API 通道,把凭据集中管理,Agent 代码里只认一个 Base URL 和一个 Key。
你需要准备三样东西,我称之为“三件套”:
- Base URL:
https://taotoken.net/api - API Key:在控制台创建,形如
sk-... - Model ID:比如
deepseek-chat或你实际使用的模型标识
获取 Key 的入口在控制台的 API Keys 页面,创建后复制保存。注意 Key 只在创建时完整显示一次,丢了就重新建一个。
拿到三件套后,推荐用环境变量注入,而不是硬编码进代码。在项目根目录建一个.env文件:
# .env —— 统一模型调用凭据 TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的实际Key TAOTOKEN_MODEL=deepseek-chat然后在 Agent 的 client 初始化处读取。如果你用的是 OpenAI 兼容的 SDK,配置大致如下:
# deep_pilot/client.py —— 统一客户端 import os from openai import OpenAI client = OpenAI( base_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) MODEL_ID = os.environ.get("TAOTOKEN_MODEL", "deepseek-chat")这样做的价值在于:检索工具本身不关心模型从哪来,它只负责返回结构化文本;模型调用统一走一个通道,换模型只改TAOTOKEN_MODEL一个变量。团队协作时,每个人本地.env填自己的 Key,代码仓库里只留.env.example。
注意:
.env一定要加进.gitignore,别把 Key 提交上去。这是最常见的凭据泄露来源。
如果你更习惯用配置文件而不是环境变量,也可以写一个settings.toml:
# settings.toml —— 统一通道配置 [taotoken] base_url = "https://taotoken.net/api" model = "deepseek-chat" # api_key 建议仍从环境变量读取,不写进文件代码里用tomllib读取 base_url 和 model,api_key 继续走环境变量。两种方式都行,关键是“一个通道、一处配置”。
前置准备做完,接下来进入正题:把glob和grep写成可注册的工具。
3. 可复制的 glob/grep 工具注册配置
工具注册的核心是装饰器@tool,它把普通函数变成 Agent 可调用的工具,并自动提取函数签名和 docstring 作为参数说明。先看glob的实现。
glob基于pathlib.Path.glob(),支持*、**、?等通配符。关键点有三个:递归匹配用**/*.py;结果转相对路径方便阅读;限制最多 100 个防止上下文爆炸。
# deep_pilot/search_tools.py —— 代码检索工具 v0.4 from __future__ import annotations import re from pathlib import Path from deep_pilot.tool_registry import tool SKIP_DIRS = {".git", "node_modules", ".venv", "__pycache__", "dist", "build", ".pytest_cache", ".tox"} SKIP_EXTS = {".pyc", ".pyo", ".so", ".dll", ".exe", ".jpg", ".jpeg", ".png", ".gif", ".pdf", ".zip"} @tool def glob(pattern: str, cwd: str = ".") -> str: """ 按文件名模式匹配文件。 参数: - pattern: glob模式,如 "*.py"、"**/*.md"、"test_*.py" - cwd: 工作目录,默认当前目录 返回:匹配的文件列表,每行一个,最多100个 """ cwd_path = Path(cwd).resolve() try: matches = [] for m in cwd_path.glob(pattern): if any(part.startswith(".") or part in SKIP_DIRS for part in m.parts): continue matches.append(m) if len(matches) >= 100: break if not matches: return f"未找到匹配 '{pattern}' 的文件" lines = [str(m.relative_to(cwd_path)) for m in matches] result = "\n".join(lines) if len(matches) >= 100: result += "\n\n[已达到最大结果数 100,可能还有更多匹配]" return result except Exception as e: return f"错误:glob失败 - {str(e)}"再看grep。Python 没有内置 grep,用re模块实现。它遍历文件、逐行匹配、返回文件:行号: 内容。默认最多 50 条,可配置。跳过隐藏目录、跳过目录集合、跳过二进制扩展名。
@tool def grep(pattern: str, path: str = ".", ignore_case: bool = False, max_results: int = 50) -> str: """ 按正则表达式搜索文件内容。 参数: - pattern: 正则表达式模式,如 "def test_"、"import.*requests" - path: 搜索路径(文件或目录),默认当前目录 - ignore_case: 是否忽略大小写,默认False - max_results: 最大结果数,默认50 返回:匹配结果,格式为"文件:行号: 内容" """ search_path = Path(path).resolve() flags = re.IGNORECASE if ignore_case else 0 try: regex = re.compile(pattern, flags) except re.error as e: return f"错误:无效的正则表达式 - {str(e)}" results = [] if search_path.is_file(): files_to_search = [search_path] else: files_to_search = [] for f in search_path.rglob("*"): if not f.is_file(): continue if any(part.startswith(".") or part in SKIP_DIRS for part in f.parts): continue if f.suffix.lower() in SKIP_EXTS: continue files_to_search.append(f) for file_path in files_to_search: try: with open(file_path, "r", encoding="utf-8", errors="ignore") as f: for line_num, line in enumerate(f, start=1): if regex.search(line): if search_path.is_dir(): rel = file_path.relative_to(search_path) else: rel = file_path.name results.append( f"{rel}:{line_num}: {line.rstrip()}") if len(results) >= max_results: break if len(results) >= max_results: break except Exception: continue if not results: return f"未找到匹配 '{pattern}' 的内容" result = "\n".join(results) if len(results) >= max_results: result += f"\n\n[已达到最大结果数 {max_results},可能还有更多匹配]" return result两个工具写完后,在tools.py里导入注册:
# deep_pilot/tools.py —— v0.4 加入检索工具 from deep_pilot.file_tools import read_file, write_file, edit_file from deep_pilot.bash_tools import run_bash from deep_pilot.search_tools import glob, grep到这里,工具注册配置就完成了。@tool装饰器会自动把函数名、参数、docstring 注册进工具表,Agent 在循环里就能看到glob和grep两个可调用项。
如果你用 Cline MCP 或类似工具链,配置思路一致,把 Base URL、Key、Model ID 三件套填进对应位置即可。比如 MCP 的 settings 片段:
{ "mcpServers": { "deep-pilot": { "command": "uv", "args": ["run", "python", "-m", "deep_pilot.server"], "env": { "TAOTOKEN_BASE_URL": "https://taotoken.net/api", "TAOTOKEN_API_KEY": "sk-你的实际Key", "TAOTOKEN_MODEL": "deepseek-chat" } } } }注意这里 Base URL 用的是https://taotoken.net/api,不带任何多余路径。Key 和 Model ID 与前面环境变量保持一致,避免出现“配置了但没生效”的问题。
4. 验证请求:一次端到端检索跑通
工具注册好,接下来验证 Agent 能不能自主调用。写一个测试脚本,跑三个场景:找所有 Python 文件、搜索特定函数、搜索测试函数。
# 实测脚本 from deep_pilot.agent_loop import run print("=== 测试1:找所有Python文件 ===") print(run("找一下项目里所有的 .py 文件")) print("=== 测试2:搜索 get_weather 函数 ===") print(run("找一下代码里所有用到 get_weather 的地方")) print("=== 测试3:搜索所有测试函数 ===") print(run("找一下所有的测试函数(def test_)"))预期控制台输出(精简):
=== 测试1:找所有Python文件 === INFO | agent_loop | Loop 第 1 轮 INFO | agent_loop | → 调用工具: glob({"pattern": "**/*.py"}) INFO | agent_loop | ← 工具结果: deep_pilot/__init__.py deep_pilot/agent_loop.py deep_pilot/bash_tools.py deep_pilot/client.py deep_pilot/file_tools.py deep_pilot/search_tools.py deep_pilot/tools.py test_math.py INFO | agent_loop | Loop 第 2 轮 Agent回答: 项目里有8个Python文件,deep_pilot/ 下7个模块,加一个 test_math.py。 === 测试2:搜索 get_weather 函数 === INFO | agent_loop | → 调用工具: grep({"pattern": "get_weather"}) INFO | agent_loop | ← 工具结果: deep_pilot/tools.py:12: def get_weather(city: str) -> str: deep_pilot/tools.py:14: """获取指定城市今天的天气信息。""" Agent回答: get_weather 定义在 deep_pilot/tools.py 第12-14行。 === 测试3:搜索所有测试函数 === INFO | agent_loop | → 调用工具: grep({"pattern": "def test_"}) INFO | agent_loop | ← 工具结果: test_math.py:5: def test_add(): Agent回答: 找到1个测试函数:test_math.py 第5行的 test_add()。三个测试都通过,说明链路完整:Agent 自己判断该用glob还是grep,构造参数,拿到结构化结果,再用自然语言总结。注意 Agent 的回答是结构化的,它理解了检索结果,而不是原样复读。
这里有个细节值得说:测试 1 里 Agent 调的是glob({"pattern": "**/*.py"}),它自己把“所有 .py 文件”翻译成了递归通配符。测试 2 和 3 里它调grep,参数是纯字符串模式。这说明 docstring 里的参数说明被模型正确理解了。
如果你想验证模型通道是否正常,可以单独发一次请求:
curl https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-chat", "messages": [{"role": "user", "content": "回复ok"}] }'返回里有choices[0].message.content就说明通道通了。这一步能快速区分“是模型通道问题”还是“是工具实现问题”。
5. 本篇常见错误排查
跑检索链路时,最容易撞上几类报错。逐个对照。
401 Unauthorized。通常是 Key 没读到或写错。检查.env是否被加载,TAOTOKEN_API_KEY是否以sk-开头。如果你用os.environ["TAOTOKEN_API_KEY"]直接取,Key 不存在会抛KeyError;用.get()则可能拿到None,请求时变成 401。建议启动时打印一行Key loaded: True/False做自检。
local proxy failed / connection error。这类报错多半是 Base URL 写错。确认是https://taotoken.net/api,不要多加/v1或结尾斜杠。有些 SDK 会自动拼/chat/completions,多一层路径就 404。另外检查本机网络是否能正常访问该域名,公司网络有时会拦截。
reading 'choices' of undefined。这是解析响应时choices字段不存在。常见原因是请求体格式不对,比如model字段拼错、messages不是数组。也可能是返回了错误对象但代码直接取choices。加一层判断:
data = resp.json() if "choices" not in data: raise RuntimeError(f"响应异常: {data}") content = data["choices"][0]["message"]["content"]OAuth / token expired。如果你用的是带 OAuth 的客户端(比如某些 CLI 工具),token 过期会报这个。重新走一次授权,或改用 API Key 方式。TaoToken 的 API Key 通道不涉及 OAuth 刷新,配置好就一直可用,除非你主动删除 Key。
grep 返回空但明明有匹配。检查三点:文件是否在SKIP_DIRS里(比如.venv下的文件会被跳过);文件扩展名是否在SKIP_EXTS里;正则是否写错,比如def test_里的下划线在正则里是普通字符没问题,但import.*requests如果写成import.requests就匹配不到。可以先用grep -i忽略大小写试一次。
glob 匹配不到递归文件。*.py只匹配当前目录,**/*.py才递归。这是最常见的混淆点。另外Path.glob对隐藏文件默认也会匹配,我们在代码里手动跳过了以.开头的部分。
结果被截断。看到[已达到最大结果数]提示是正常的,说明结果太多。可以缩小path范围,或提高max_results,但要注意上下文预算。检索结果太多会挤占模型推理空间,宁可分多次查。
排查顺序建议:先确认 Key 和 Base URL(通道层),再确认工具是否注册成功(注册层),最后确认参数和正则(业务层)。大部分问题在前两层。
6. 把检索能力接进你的 Agent 工作流
工具跑通只是第一步,真正有价值的是把它接进日常工作流。几个实用建议。
第一,让 Agent 先glob再grep。面对“改所有用到 X 的地方”这类任务,先glob("**/*.py")拿到文件清单,再对清单里的文件grep,比直接全库grep更可控。你可以在系统提示里写一句“搜索代码时优先用 glob 缩小范围,再用 grep 定位”。
第二,给grep的max_results设一个合理默认值。50 是个不错的起点,大项目可以降到 30,小项目可以升到 100。关键是让 Agent 知道结果可能被截断,它会自己决定要不要再查一次。
第三,把检索结果和编辑工具串起来。Agent 拿到文件:行号: 内容后,可以直接调edit_file改那一行。这条“检索→定位→编辑”的链路,是代码 Agent 最核心的能力闭环。
第四,统一 Key 通道要贯穿始终。检索工具本身不调模型,但 Agent 循环调模型。把 Base URL、Key、Model ID 三件套集中管理,换模型、换环境、团队协作都省事。TaoToken 的 API 通道在这里扮演的就是“一个入口管所有调用”的角色。
如果你想让 Agent 长期跑编码任务,可以考虑用 Coding Plan 管理调用额度;如果只是验证模型对话是否正常,用模型对话页面快速测一次即可。接入文档里有完整的参数说明和示例。
最后留一个我踩过的坑:早期我把SKIP_DIRS写成了只跳过.git,结果 Agent 在node_modules里搜了几万行,上下文直接爆掉。后来把常见依赖目录和构建产物全加进去,检索才稳定。你的项目如果有特殊的大目录,记得补进SKIP_DIRS。
检索工具是 Agent 的“眼睛”,装上之后它才从“盲人摸象”变成“火眼金睛”。下一节我们补联网能力,让 Agent 能查文档、查 API、查最新信息。