搜索与导航工具
两种搜索维度
Section titled “两种搜索维度”| 维度 | 工具 | 底层实现 | 适用场景 |
|---|---|---|---|
| 按名称找文件 | Glob | ripgrep --files + glob 过滤 | ”找到所有测试文件”、“找 config 开头的文件” |
| 按内容找代码 | Grep | ripgrep 正则搜索 | ”哪里定义了这个函数”、“谁在调用这个 API” |
两者共享同一个 ripgrep 引擎,通过不同的参数组合实现不同搜索模式。
ripgrep 的内嵌方式
Section titled “ripgrep 的内嵌方式”Claude Code 不依赖系统安装的 ripgrep——它在 src/utils/ripgrep.ts 中实现了三级降级策略:
优先级 1: 系统 ripgrep (USE_BUILTIN_RIPGREP=false) → 使用 PATH 中的 rg 二进制 → 安全考虑:只用命令名 'rg',不用完整路径,防止 PATH 劫持
优先级 2: 内嵌模式 (bundled/native build) → process.execPath 自身,argv0='rg' → Bun 将 rg 静态编译进二进制,通过 argv0 分发
优先级 3: vendor 目录 (npm build) → vendor/ripgrep/{arch}-{platform}/rg → macOS 需要 codesign 签名 + 移除 quarantine xattr平台适配示例:
vendor/ripgrep/ ├── x86_64-darwin/rg # macOS Intel ├── arm64-darwin/rg # macOS Apple Silicon ├── x86_64-linux/rg # Linux Intel ├── arm64-linux/rg # Linux ARM └── x86_64-win32/rg.exe # WindowsmacOS 代码签名
Section titled “macOS 代码签名”vendor 模式下的 rg 二进制需要 ad-hoc 签名才能通过 Gatekeeper(codesignRipgrepIfNecessary()):
// 首次使用时执行:// 1. 检查是否已是有效签名codesign -vv -d <rg-path>// 2. 如果只是 linker-signed,重新签名codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime <rg-path>// 3. 移除隔离属性xattr -d com.apple.quarantine <rg-path>搜索结果的设计考量
Section titled “搜索结果的设计考量”head_limit 与 Token 预算
Section titled “head_limit 与 Token 预算”大型项目的搜索结果可能有数十万条。默认最多返回 250 条匹配——这不是随意选择,而是token 预算的约束:
- 每条匹配行约 50-100 token
- 250 条 ≈ 12,500-25,000 token
- 这大约占 200k 上下文窗口的 6-12%
- 超过这个比例,AI 的推理质量会下降
Grep 工具的 head_limit 参数让 AI 可以按需调整——搜索小项目时可以用更大的值。
按修改时间排序
Section titled “按修改时间排序”Glob 默认把最近修改的文件排在前面。这不是默认的文件系统排序,而是刻意的设计决策:
设计假设:最近修改的文件最可能与当前任务相关实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件在 src/tools/GlobTool/ 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
ripgrep 的错误处理
Section titled “ripgrep 的错误处理”ripgrep 执行有专门的错误恢复链(src/utils/ripgrep.ts):
| 错误 | 处理 |
|---|---|
| EAGAIN(资源不足) | 自动以单线程模式 -j 1 重试 |
| 超时(默认 20s,WSL 60s) | 返回已有部分结果,丢弃可能不完整的最后一行 |
| 缓冲区溢出 | 截断到 20MB,返回已收集的结果 |
| SIGTERM 失效 | 5 秒后升级为 SIGKILL |
ToolSearch:在 50+ 工具中发现目标
Section titled “ToolSearch:在 50+ 工具中发现目标”当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。ToolSearch(src/tools/ToolSearchTool/)提供了工具发现机制。
ToolSearch 实现了基于关键词的加权搜索(searchToolsWithKeywords()):
输入: query = "database connection" ↓1. 精确匹配: 检查是否有工具名完全匹配(快速路径)2. MCP 前缀匹配: "mcp__postgres" → 匹配所有 postgres 相关工具3. 关键词拆分: ["database", "connection"]4. 工具名解析: - MCP 工具: "mcp__server__action" → ["server", "action"] - 普通工具: "FileEditTool" → ["file", "edit", "tool"]5. 加权评分: - 工具名精确匹配: 10 分(MCP: 12 分) - 工具名部分匹配: 5 分(MCP: 6 分) - searchHint 匹配: 4 分 - 描述匹配: 2 分6. 必选词过滤: "+database" 前缀表示必须包含7. 按分数排序,返回 top-Nselect: 直接选择
Section titled “select: 直接选择”AI 也可以用 select:ToolName 精确选择已知工具。这比搜索更快,且支持逗号分隔的批量选择(select:A,B,C)。
延迟加载(Deferred Tools)
Section titled “延迟加载(Deferred Tools)”不是所有工具都常驻内存。MCP 工具和低频工具被标记为 isDeferredTool,只有在 ToolSearch 选中后才真正加载。这减少了每次 API 调用的 token 开销(工具描述占用大量 token)。
工具描述的获取是 memoized 的——只在延迟工具集合变化时清除缓存:
// 工具名排序后拼接作为缓存 keyfunction getDeferredToolsCacheKey(deferredTools: Tools): string { return deferredTools.map(t => t.name).sort().join(',')}Web 搜索与抓取
Section titled “Web 搜索与抓取”AI 的信息获取不局限于本地代码:
- WebSearch(
src/tools/WebSearchTool/):调用 Anthropic API 的web_search_20250305server tool 搜索互联网 - WebFetch(
src/tools/WebFetchTool/):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
WebSearch 实现机制
Section titled “WebSearch 实现机制”WebSearch 通过适配器模式支持两种搜索后端,由 src/tools/WebSearchTool/adapters/ 中的工厂函数 createAdapter() 选择:
适配器架构: WebSearchTool.call() → createAdapter() 选择后端 ├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥) └─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥) → adapter.search(query, options) → 转换为统一 SearchResult[] 格式返回适配器选择逻辑
Section titled “适配器选择逻辑”adapters/index.ts 中的工厂函数按以下优先级选择后端:
| 优先级 | 条件 | 适配器 |
|---|---|---|
| 1 | 环境变量 WEB_SEARCH_ADAPTER=api | ApiSearchAdapter |
| 2 | 环境变量 WEB_SEARCH_ADAPTER=bing | BingSearchAdapter |
| 3 | API Base URL 指向 Anthropic 官方 | ApiSearchAdapter |
| 4 | 第三方代理 / 非官方端点 | BingSearchAdapter |
适配器是无状态的,同一会话内缓存复用。
ApiSearchAdapter — API 服务端搜索
Section titled “ApiSearchAdapter — API 服务端搜索”将搜索请求委托给 Anthropic API 的 web_search_20250305 server tool:
调用链: ApiSearchAdapter.search(query, options) → queryModelWithStreaming() 发起独立的 API 调用 → 携带 extraToolSchemas: [BetaWebSearchTool20250305] → API 服务端执行搜索,返回流式事件 → server_tool_use / web_search_tool_result / text 交替返回 → extractSearchResults() 从 content blocks 提取 SearchResult[]| 特性 | 实现 |
|---|---|
| 模型选择 | Feature flag tengu_plum_vx3 控制用 Haiku(强制 tool_choice)还是主模型 |
| 搜索上限 | 每次调用最多 8 次搜索(max_uses: 8) |
| 域过滤 | 支持 allowedDomains / blockedDomains |
| 进度追踪 | 流式解析 input_json_delta 提取 query,实时回调 onProgress |
BingSearchAdapter — Bing 搜索页面解析
Section titled “BingSearchAdapter — Bing 搜索页面解析”直接抓取 Bing 搜索 HTML 并用正则提取结果,无需 API 密钥:
调用链: BingSearchAdapter.search(query, options) → axios.get(bing.com/search?q=...) — 使用浏览器级别 headers 绕过反爬 → extractBingResults(html) → 正则匹配 <li class="b_algo"> 块 → 提取 <h2><a> 标题和 URL → resolveBingUrl() 解码 Bing 重定向链接 → extractSnippet() 三级降级提取摘要 → 客户端域过滤 (allowedDomains / blockedDomains) → 返回 SearchResult[]反爬策略:Bing 对非浏览器 UA 返回需要 JS 渲染的空页面。适配器使用完整的 Edge 浏览器请求头(包含 Sec-Ch-Ua、Sec-Fetch-* 等现代浏览器标头)确保获得完整 HTML。同时使用 setmkt=en-US 参数统一市场定位,避免 Bing 基于用户 IP 做区域化定向(如跳转到德语/新加坡市场导致结果不相关)。
URL 解码:Bing 搜索结果中的 URL 为重定向格式(bing.com/ck/a?...&u=a1aHR0cHM6Ly9...),resolveBingUrl() 从 u 参数中 base64 解码出真实目标 URL(a1 前缀 = https,a0 = http)。
摘要提取(extractSnippet())按优先级尝试三个来源:
<p class="b_lineclamp...">— 带行截断的摘要段落<div class="b_caption">内的<p>— 普通摘要段落<div class="b_caption">的直接文本内容 — 兜底方案
| 特性 | 实现 |
|---|---|
| 超时 | 30 秒(FETCH_TIMEOUT_MS) |
| 域过滤 | 支持 allowedDomains / blockedDomains,含子域名匹配 |
| 进度追踪 | 发送 query_update 和 search_results_received 回调 |
| 中止支持 | 外部 AbortSignal 传播到 axios 请求 |
WebSearchTool 统一接口
Section titled “WebSearchTool 统一接口”WebSearchTool(src/tools/WebSearchTool/WebSearchTool.ts)是面向主循环的工具定义,所有 provider 均可使用(isEnabled() 始终返回 true)。它将适配器返回的 SearchResult[] 转换为内部 Output 格式,mapToolResultToToolResultBlockParam 将搜索结果格式化为带 markdown 超链接的文本,并附加 “REMINDER” 要求主模型在回复中包含 Sources。
WebFetch 实现机制
Section titled “WebFetch 实现机制”WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
调用链: WebFetchTool.call({ url, prompt }) → getURLMarkdownContent(url) → validateURL() — 长度≤2000、无用户名密码、公网域名 → URL_CACHE 命中检查(15 分钟 TTL LRU,50MB 上限) → checkDomainBlocklist() — 调用 api.anthropic.com/api/web/domain_info 预检 → getWithPermittedRedirects() — axios 请求,自定义重定向处理 → HTML → Turndown 转 Markdown(懒加载单例,~1.4MB) → 非 HTML → 原始文本 → 二进制(PDF 等)→ persistBinaryContent() 保存到磁盘 → applyPromptToMarkdown() → 截断到 100K 字符 → queryHaiku() 用小模型按 prompt 提取信息 → 返回处理后的结果安全防护多层设计:
| 层级 | 机制 | 说明 |
|---|---|---|
| 域名预检 | checkDomainBlocklist() | 调用 api.anthropic.com/api/web/domain_info?domain=…,5 分钟缓存 |
| 重定向控制 | isPermittedRedirect() | 仅允许同 host(±www)重定向,跨域重定向返回提示让 AI 重新调用 |
| 重定向深度 | MAX_REDIRECTS = 10 | 防止重定向循环无限挂起 |
| 内容大小 | MAX_HTTP_CONTENT_LENGTH = 10MB | 单次响应上限 |
| 请求超时 | FETCH_TIMEOUT_MS = 60s | 主请求超时;域名预检 10s |
| URL 验证 | validateURL() | 长度、协议、用户名密码、公网域名检查 |
| egress 检测 | X-Proxy-Error: blocked-by-allowlist | 检测企业代理拦截 |
预批准域名(src/tools/WebFetchTool/preapproved.ts):
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
对预批准域名,WebFetch 跳过 Haiku 摘要步骤(如果内容是 Markdown 且 < 100K 字符),直接返回原文——因为技术文档本身的结构化程度已经足够好。
权限模型方面,WebFetch 按 hostname 生成 domain:xxx 规则匹配用户的 allow/deny/ask 规则,支持用户对特定域名配置永久允许或拒绝。
ripgrep 的流式输出
Section titled “ripgrep 的流式输出”对于交互式场景(如 QuickOpen),ripgrep 支持流式输出(ripGrepStream()):
rg --files → 逐 chunk 到达 → 按行分割 → onLines(lines) 回调不需要等 ripgrep 完成整个搜索——第一批结果在 rg 仍在遍历目录树时就已展示。调用者可以通过 AbortSignal 提前终止搜索(例如找到足够多的结果后)。