vault backup: 2026-06-09 23:15:17
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
---
|
||||
title: "搜索与导航工具 - 代码库精准定位"
|
||||
description: "解析 Claude Code 的搜索导航工具:Glob 文件匹配、Grep 内容搜索,基于 ripgrep 的高性能代码检索,帮助 AI 在百万行代码中精准定位。"
|
||||
keywords: ["代码搜索", "Glob", "Grep", "ripgrep", "文件搜索"]
|
||||
tags: [claude-code, search, glob, grep, ripgrep, code-navigation]
|
||||
create time: 2026-06-09 22:30
|
||||
---
|
||||
|
||||
## 两种搜索维度
|
||||
# 搜索与导航工具 - 代码库精准定位
|
||||
|
||||
## 概述
|
||||
|
||||
Claude Code 的搜索能力建立在 ripgrep 之上,通过 Glob(按名称找文件)和 Grep(按内容找代码)两个维度帮助 AI 在百万行代码中精准定位。本文还涵盖 ToolSearch 工具发现机制和 Web 搜索/抓取的完整实现。
|
||||
|
||||
## 正文
|
||||
|
||||
### 两种搜索维度
|
||||
|
||||
| 维度 | 工具 | 底层实现 | 适用场景 |
|
||||
|------|------|----------|---------|
|
||||
@@ -13,22 +20,19 @@ keywords: ["代码搜索", "Glob", "Grep", "ripgrep", "文件搜索"]
|
||||
|
||||
两者共享同一个 ripgrep 引擎,通过不同的参数组合实现不同搜索模式。
|
||||
|
||||
## ripgrep 的内嵌方式
|
||||
### 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
|
||||
```mermaid
|
||||
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"]
|
||||
```
|
||||
|
||||
平台适配示例:
|
||||
@@ -41,7 +45,7 @@ vendor/ripgrep/
|
||||
└── x86_64-win32/rg.exe # Windows
|
||||
```
|
||||
|
||||
### macOS 代码签名
|
||||
#### macOS 代码签名
|
||||
|
||||
vendor 模式下的 rg 二进制需要 ad-hoc 签名才能通过 Gatekeeper(`codesignRipgrepIfNecessary()`):
|
||||
|
||||
@@ -55,9 +59,9 @@ codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,ru
|
||||
xattr -d com.apple.quarantine <rg-path>
|
||||
```
|
||||
|
||||
## 搜索结果的设计考量
|
||||
### 搜索结果的设计考量
|
||||
|
||||
### head_limit 与 Token 预算
|
||||
#### head_limit 与 Token 预算
|
||||
|
||||
大型项目的搜索结果可能有数十万条。默认最多返回 250 条匹配——这不是随意选择,而是**token 预算**的约束:
|
||||
|
||||
@@ -68,18 +72,16 @@ xattr -d com.apple.quarantine <rg-path>
|
||||
|
||||
Grep 工具的 `head_limit` 参数让 AI 可以按需调整——搜索小项目时可以用更大的值。
|
||||
|
||||
### 按修改时间排序
|
||||
#### 按修改时间排序
|
||||
|
||||
Glob 默认把**最近修改的文件排在前面**。这不是默认的文件系统排序,而是刻意的设计决策:
|
||||
|
||||
```
|
||||
设计假设:最近修改的文件最可能与当前任务相关
|
||||
实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件
|
||||
```
|
||||
> [!question] 为什么按 mtime 排序?
|
||||
> 设计假设:最近修改的文件最可能与当前任务相关。实际效果:AI 优先看到"活"的代码,而不是沉寂的历史文件。
|
||||
|
||||
在 `packages/builtin-tools/src/tools/GlobTool/` 中,ripgrep 的输出在返回给 AI 前按 mtime 排序。
|
||||
|
||||
### ripgrep 的错误处理
|
||||
#### ripgrep 的错误处理
|
||||
|
||||
ripgrep 执行有专门的错误恢复链(`src/utils/ripgrep.ts`):
|
||||
|
||||
@@ -90,41 +92,45 @@ ripgrep 执行有专门的错误恢复链(`src/utils/ripgrep.ts`):
|
||||
| **缓冲区溢出** | 截断到 20MB,返回已收集的结果 |
|
||||
| **SIGTERM 失效** | 5 秒后升级为 SIGKILL |
|
||||
|
||||
## ToolSearch:在 50+ 工具中发现目标
|
||||
### ToolSearch:在 50+ 工具中发现目标
|
||||
|
||||
当可用工具超过 50 个时(含 MCP 提供的外部工具),AI 可能不知道该用哪个。**ToolSearch**(`packages/builtin-tools/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-N
|
||||
```mermaid
|
||||
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"]
|
||||
```
|
||||
|
||||
### `select:` 直接选择
|
||||
评分规则:
|
||||
- 工具名精确匹配:10 分(MCP: 12 分)
|
||||
- 工具名部分匹配:5 分(MCP: 6 分)
|
||||
- searchHint 匹配:4 分
|
||||
- 描述匹配:2 分
|
||||
|
||||
#### `select:` 直接选择
|
||||
|
||||
AI 也可以用 `select:ToolName` 精确选择已知工具。这比搜索更快,且支持逗号分隔的批量选择(`select:A,B,C`)。
|
||||
|
||||
### 延迟加载(Deferred Tools)
|
||||
#### 延迟加载(Deferred Tools)
|
||||
|
||||
不是所有工具都常驻内存。MCP 工具和低频工具被标记为 `isDeferredTool`,只有在 ToolSearch 选中后才真正加载。这减少了每次 API 调用的 token 开销(工具描述占用大量 token)。
|
||||
|
||||
### 缓存策略
|
||||
#### 缓存策略
|
||||
|
||||
工具描述的获取是 memoized 的——只在延迟工具集合变化时清除缓存:
|
||||
|
||||
@@ -135,7 +141,7 @@ function getDeferredToolsCacheKey(deferredTools: Tools): string {
|
||||
}
|
||||
```
|
||||
|
||||
## Web 搜索与抓取
|
||||
### Web 搜索与抓取
|
||||
|
||||
AI 的信息获取不局限于本地代码:
|
||||
|
||||
@@ -144,24 +150,23 @@ AI 的信息获取不局限于本地代码:
|
||||
|
||||
这让 AI 可以查阅文档、搜索 Stack Overflow、阅读 GitHub issue——和人类开发者的工作方式一致。
|
||||
|
||||
### WebSearch 实现机制
|
||||
#### WebSearch 实现机制
|
||||
|
||||
WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-tools/src/tools/WebSearchTool/adapters/` 中的工厂函数 `createAdapter()` 选择:
|
||||
|
||||
```
|
||||
适配器架构:
|
||||
WebSearchTool.call()
|
||||
→ createAdapter() 选择后端
|
||||
├─ ApiSearchAdapter — Anthropic API 服务端搜索(需官方 API 密钥)
|
||||
├─ BingSearchAdapter — 直接抓取 Bing 搜索页面解析(无需 API 密钥)
|
||||
└─ BraveSearchAdapter — 调用 Brave LLM Context API 解析(需 Brave API 密钥)
|
||||
→ adapter.search(query, options)
|
||||
→ 转换为统一 SearchResult[] 格式返回
|
||||
```mermaid
|
||||
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` 中的工厂函数按以下优先级选择后端:
|
||||
**适配器选择逻辑**(`adapters/index.ts`):
|
||||
|
||||
| 优先级 | 条件 | 适配器 |
|
||||
|--------|------|--------|
|
||||
@@ -177,14 +182,13 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-
|
||||
|
||||
将搜索请求委托给 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[]
|
||||
```mermaid
|
||||
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[]"]
|
||||
```
|
||||
|
||||
| 特性 | 实现 |
|
||||
@@ -198,17 +202,16 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-
|
||||
|
||||
直接抓取 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[]
|
||||
```mermaid
|
||||
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 做区域化定向(如跳转到德语/新加坡市场导致结果不相关)。
|
||||
@@ -227,29 +230,30 @@ WebSearch 通过适配器模式支持三种搜索后端,由 `packages/builtin-
|
||||
| **进度追踪** | 发送 query_update 和 search_results_received 回调 |
|
||||
| **中止支持** | 外部 AbortSignal 传播到 axios 请求 |
|
||||
|
||||
### WebSearchTool 统一接口
|
||||
#### WebSearchTool 统一接口
|
||||
|
||||
`WebSearchTool`(`packages/builtin-tools/src/tools/WebSearchTool/WebSearchTool.ts`)是面向主循环的工具定义,所有 provider 均可使用(`isEnabled()` 始终返回 true)。它将适配器返回的 `SearchResult[]` 转换为内部 `Output` 格式,`mapToolResultToToolResultBlockParam` 将搜索结果格式化为带 markdown 超链接的文本,并附加 "REMINDER" 要求主模型在回复中包含 Sources。
|
||||
|
||||
### WebFetch 实现机制
|
||||
#### 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 提取信息
|
||||
→ 返回处理后的结果
|
||||
```mermaid
|
||||
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["返回处理后的结果"]
|
||||
```
|
||||
|
||||
安全防护多层设计:
|
||||
@@ -266,13 +270,13 @@ WebFetch 是一个完整的 HTTP 客户端 + 内容处理管线:
|
||||
|
||||
预批准域名(`packages/builtin-tools/src/tools/WebFetchTool/preapproved.ts`):
|
||||
|
||||
用户无需手动授权即可抓取的域名列表,包含 ~90 个主流技术文档站点(MDN、Python docs、React docs、AWS docs 等)。列表分为 hostname-only 和 path-prefix 两类,查找复杂度 O(1)。
|
||||
用户无需手动授权即可抓取的域名列表,包含约 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 的流式输出
|
||||
#### ripgrep 的流式输出
|
||||
|
||||
对于交互式场景(如 QuickOpen),ripgrep 支持**流式输出**(`ripGrepStream()`):
|
||||
|
||||
@@ -281,3 +285,10 @@ rg --files → 逐 chunk 到达 → 按行分割 → onLines(lines) 回调
|
||||
```
|
||||
|
||||
不需要等 ripgrep 完成整个搜索——第一批结果在 rg 仍在遍历目录树时就已展示。调用者可以通过 AbortSignal 提前终止搜索(例如找到足够多的结果后)。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[what-are-tools]]
|
||||
- [[file-operations]]
|
||||
- [[shell-execution]]
|
||||
- [[task-management]]
|
||||
|
||||
Reference in New Issue
Block a user