Files

13 KiB
Raw Permalink Blame History

tags, create time
tags create time
claude-code
search
glob
grep
ripgrep
code-navigation
2026-06-09 22:30

搜索与导航工具 - 代码库精准定位

概述

Claude Code 的搜索能力建立在 ripgrep 之上,通过 Glob(按名称找文件)和 Grep(按内容找代码)两个维度帮助 AI 在百万行代码中精准定位。本文还涵盖 ToolSearch 工具发现机制和 Web 搜索/抓取的完整实现。

正文

两种搜索维度

维度 工具 底层实现 适用场景
按名称找文件 Glob ripgrep --files + glob 过滤 "找到所有测试文件"、"找 config 开头的文件"
按内容找代码 Grep ripgrep 正则搜索 "哪里定义了这个函数"、"谁在调用这个 API"

两者共享同一个 ripgrep 引擎,通过不同的参数组合实现不同搜索模式。

ripgrep 的内嵌方式

Claude Code 不依赖系统安装的 ripgrep——它在 src/utils/ripgrep.ts 中实现了三级降级策略:

flowchart TD
    A{"USE_BUILTIN_RIPGREP?"} -->|"false"| B["系统 ripgrep: PATH 中的 rg 二进制"]
    A -->|"true"| C{"Bun 内嵌?"}
    C -->|是| D["process.execPath 自身, argv0='rg'"]
    C -->|否| E["vendor/ripgrep/{arch}-{platform}/rg"]
    B --> F["安全考虑: 只用命令名 rg, 不用完整路径"]
    D --> G["Bun 将 rg 静态编译进二进制"]
    E --> H["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    # Windows

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>

搜索结果的设计考量

head_limit 与 Token 预算

大型项目的搜索结果可能有数十万条。默认最多返回 250 条匹配——这不是随意选择,而是token 预算的约束:

  • 每条匹配行约 50-100 token
  • 250 条 ≈ 12,500-25,000 token
  • 这大约占 200k 上下文窗口的 6-12%
  • 超过这个比例,AI 的推理质量会下降

Grep 工具的 head_limit 参数让 AI 可以按需调整——搜索小项目时可以用更大的值。

按修改时间排序

Glob 默认把最近修改的文件排在前面。这不是默认的文件系统排序,而是刻意的设计决策:

[!question] 为什么按 mtime 排序? 设计假设:最近修改的文件最可能与当前任务相关。实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件。

在 packages/builtin-tools/src/tools/GlobTool/ 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。

ripgrep 的错误处理

ripgrep 执行有专门的错误恢复链(src/utils/ripgrep.ts):

错误 处理
EAGAIN(资源不足) 自动以单线程模式 -j 1 重试
超时(默认 20s,WSL 60s) 返回已有部分结果,丢弃可能不完整的最后一行
缓冲区溢出 截断到 20MB,返回已收集的结果
SIGTERM 失效 5 秒后升级为 SIGKILL

ToolSearch:在 50+ 工具中发现目标

当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。ToolSearch(packages/builtin-tools/src/tools/ToolSearchTool/)提供了工具发现机制。

搜索算法

ToolSearch 实现了基于关键词的加权搜索(searchToolsWithKeywords()):

flowchart TD
    A["输入: query = database connection"] --> B{"精确匹配工具名?"}
    B -->|是| C["快速路径返回"]
    B -->|否| D{"MCP 前缀匹配?"}
    D -->|是| E["匹配所有相关工具"]
    D -->|否| F["关键词拆分: database, connection"]
    F --> G["工具名解析"]
    G --> G1["MCP: mcp__server__action → server, action"]
    G --> G2["普通: FileEditTool → file, edit, tool"]
    G1 --> H["加权评分"]
    G2 --> H
    H --> I["必选词过滤: +database 前缀"]
    I --> J["按分数排序, 返回 top-N"]

评分规则:

  • 工具名精确匹配:10 分(MCP: 12 分)
  • 工具名部分匹配:5 分(MCP: 6 分)
  • searchHint 匹配:4 分
  • 描述匹配:2 分

select: 直接选择

AI 也可以用 select:ToolName 精确选择已知工具。这比搜索更快,且支持逗号分隔的批量选择(select:A,B,C)。

延迟加载(Deferred Tools)

不是所有工具都常驻内存。MCP 工具和低频工具被标记为 isDeferredTool,只有在 ToolSearch 选中后才真正加载。这减少了每次 API 调用的 token 开销(工具描述占用大量 token)。

缓存策略

工具描述的获取是 memoized 的——只在延迟工具集合变化时清除缓存:

// 工具名排序后拼接作为缓存 key
function getDeferredToolsCacheKey(deferredTools: Tools): string {
  return deferredTools.map(t => t.name).sort().join(',')
}

Web 搜索与抓取

AI 的信息获取不局限于本地代码:

  • WebSearch(packages/builtin-tools/src/tools/WebSearchTool/):调用 Anthropic API 的 web_search_20250305 server tool 搜索互联网
  • WebFetch(packages/builtin-tools/src/tools/WebFetchTool/):抓取特定 URL 内容,转换为 Markdown 供 AI 阅读

这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。

WebSearch 实现机制

WebSearch 通过适配器模式支持三种搜索后端,由 packages/builtin-tools/src/tools/WebSearchTool/adapters/ 中的工厂函数 createAdapter() 选择:

flowchart LR
    A["WebSearchTool.call()"] --> B["createAdapter() 选择后端"]
    B --> C["ApiSearchAdapter"]
    B --> D["BingSearchAdapter"]
    B --> E["BraveSearchAdapter"]
    C --> F["adapter.search(query, options)"]
    D --> F
    E --> F
    F --> G["转换为统一 SearchResult[] 格式返回"]

适配器选择逻辑(adapters/index.ts):

优先级 条件 适配器
1 环境变量 WEB_SEARCH_ADAPTER=api ApiSearchAdapter
2 环境变量 WEB_SEARCH_ADAPTER=bing BingSearchAdapter
3 环境变量 WEB_SEARCH_ADAPTER=brave BraveSearchAdapter
4 API Base URL 指向 Anthropic 官方 ApiSearchAdapter
5 第三方代理 / 非官方端点 BingSearchAdapter

适配器是无状态的,同一会话内缓存复用。

ApiSearchAdapter — API 服务端搜索

将搜索请求委托给 Anthropic API 的 web_search_20250305 server tool:

flowchart TD
    A["ApiSearchAdapter.search(query, options)"] --> B["queryModelWithStreaming()"]
    B --> C["携带 extraToolSchemas: BetaWebSearchTool20250305"]
    C --> D["API 服务端执行搜索, 返回流式事件"]
    D --> E["server_tool_use / web_search_tool_result / text 交替返回"]
    E --> F["extractSearchResults() 提取 SearchResult[]"]
特性 实现
模型选择 Feature flag tengu_plum_vx3 控制用 Haiku(强制 tool_choice)还是主模型
搜索上限 每次调用最多 8 次搜索(max_uses: 8)
域过滤 支持 allowedDomains / blockedDomains
进度追踪 流式解析 input_json_delta 提取 query,实时回调 onProgress

BingSearchAdapter — Bing 搜索页面解析

直接抓取 Bing 搜索 HTML 并用正则提取结果,无需 API 密钥:

flowchart TD
    A["BingSearchAdapter.search(query, options)"] --> B["axios.get(bing.com/search?q=...)"]
    B --> C["extractBingResults(html)"]
    C --> D["正则匹配 li class=b_algo 块"]
    D --> E["提取 h2>a 标题和 URL"]
    E --> F["resolveBingUrl() 解码 Bing 重定向链接"]
    F --> G["extractSnippet() 三级降级提取摘要"]
    G --> H["客户端域过滤"]
    H --> I["返回 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())按优先级尝试三个来源:

  1. <p class="b_lineclamp..."> — 带行截断的摘要段落
  2. <div class="b_caption"> 内的 <p> — 普通摘要段落
  3. <div class="b_caption"> 的直接文本内容 — 兜底方案
特性 实现
超时 30 秒(FETCH_TIMEOUT_MS)
域过滤 支持 allowedDomains / blockedDomains,含子域名匹配
进度追踪 发送 query_update 和 search_results_received 回调
中止支持 外部 AbortSignal 传播到 axios 请求

WebSearchTool 统一接口

WebSearchTool(packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts)是面向主循环的工具定义,所有 provider 均可使用(isEnabled() 始终返回 true)。它将适配器返回的 SearchResult[] 转换为内部 Output 格式,mapToolResultToToolResultBlockParam 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。

WebFetch 实现机制

WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:

flowchart TD
    A["WebFetchTool.call({ url, prompt })"] --> B["getURLMarkdownContent(url)"]
    B --> B1["validateURL() 长度/协议/公网域名检查"]
    B1 --> B2["URL_CACHE 命中检查 (15 分钟 TTL LRU)"]
    B2 --> B3["checkDomainBlocklist() 域名预检"]
    B3 --> B4["getWithPermittedRedirects() axios 请求"]
    B4 --> B5{"内容类型?"}
    B5 -->|"HTML"| B6["Turndown 转 Markdown"]
    B5 -->|"非 HTML"| B7["原始文本"]
    B5 -->|"二进制"| B8["persistBinaryContent() 保存到磁盘"]
    B6 --> C["applyPromptToMarkdown()"]
    B7 --> C
    C --> D["截断到 100K 字符"]
    D --> E["queryHaiku() 用小模型按 prompt 提取信息"]
    E --> F["返回处理后的结果"]

安全防护多层设计:

层级 机制 说明
域名预检 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 检测企业代理拦截

预批准域名(packages/builtin-tools/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 的流式输出

对于交互式场景(如 QuickOpen),ripgrep 支持流式输出(ripGrepStream()):

rg --files → 逐 chunk 到达 → 按行分割 → onLines(lines) 回调

不需要等 ripgrep 完成整个搜索——第一批结果在 rg 仍在遍历目录树时就已展示。调用者可以通过 AbortSignal 提前终止搜索(例如找到足够多的结果后)。

关联笔记