WebFetchTool 与 WebSearchTool:联网能力

📑 目录

在前面的文章中,我们分析了 Claude Code 的文件系统工具、代码搜索工具和 Shell 执行工具。这些工具解决了"本地环境交互"的问题,但当模型需要获取最新文档、查询当前版本信息或研究最新技术动态时,就必须突破本地边界,接入互联网。本文将深入解析 Claude Code 的两个联网工具——WebFetchToolWebSearchTool——的设计哲学、实现细节与安全策略。

一、工具定位与协作关系

WebFetchToolWebSearchTool 分别解决联网能力的两个不同维度:

  • WebFetchTool:定向网页抓取。给定一个具体 URL,获取其内容并转换为模型可读的格式。
  • WebSearchTool:泛化联网搜索。给定一个查询词,通过 Anthropic 的 Web Search 能力返回搜索结果。

在真实使用场景中,这两个工具往往一前一后配合使用:WebSearchTool 负责"找入口",找到相关的文档页面或资料链接;WebFetchTool 负责"读正文",抓取具体页面的详细内容。这种分工让 Claude Code 既能利用搜索引擎的广泛覆盖能力,又能对特定页面进行深度内容提取。

flowchart LR
    A[用户提问] --> B{需要联网?}
    B -->|是| C[WebSearchTool
搜索相关页面] C --> D[获取搜索结果
标题/URL/摘要] D --> E[WebFetchTool
抓取指定页面] E --> F[HTML → Markdown
内容提取] F --> G[Haiku 模型
按 prompt 处理] G --> H[返回结构化结果
附来源链接] B -->|否| I[本地工具处理]

二、WebFetchTool 架构解析

WebFetchTool 位于 src/tools/WebFetchTool/ 目录下,由 5 个文件组成(总计约 1131 行),涵盖工具定义、内容处理、缓存管理、预批准域名列表和 UI 渲染。

2.1 输入输出 Schema

WebFetchTool 的接口设计非常精简,只有两个输入字段(WebFetchTool.ts:27-32):

const inputSchema = lazySchema(() =>
  z.strictObject({
    url: z.string().url().describe('The URL to fetch content from'),
    prompt: z.string().describe('The prompt to run on the fetched content'),
  }),
)

输出则包含完整的状态信息(WebFetchTool.ts:34-48):

const outputSchema = lazySchema(() =>
  z.object({
    bytes: z.number().describe('Size of the fetched content in bytes'),
    code: z.number().describe('HTTP response code'),
    codeText: z.string().describe('HTTP response code text'),
    result: z.string().describe('Processed result from applying the prompt to the content'),
    durationMs: z.number().describe('Time taken to fetch and process the content'),
    url: z.string().describe('The URL that was fetched'),
  }),
)

这种设计让工具调用方不仅能拿到处理后的内容,还能精确知道请求耗时、原始响应大小和 HTTP 状态码,便于调试和性能分析。

2.2 URL 获取与内容解析

实际的内容获取逻辑在 utils.ts 中实现。getURLMarkdownContent() 函数(utils.ts:185-280)是核心入口,其处理流程如下:

  1. URL 验证:检查 URL 长度(最大 2000 字符)、协议合法性,拒绝包含用户名/密码的 URL(utils.ts:139-162
  2. 协议升级:HTTP 自动升级为 HTTPS(utils.ts:208-211
  3. 域名安全检查:通过 Anthropic API 查询域名是否被阻止(utils.ts:119-133
  4. HTTP 请求:使用 axios 获取内容,自定义重定向处理(utils.ts:244-302
  5. 内容类型判断:HTML 通过 Turndown 转为 Markdown,二进制文件持久化到磁盘(utils.ts:260-276
sequenceDiagram
    participant U as WebFetchTool.call()
    participant V as validateURL()
    participant D as checkDomainBlocklist()
    participant H as getWithPermittedRedirects()
    participant T as TurndownService
    participant C as URL_CACHE
    U->>V: 验证 URL 格式
    V-->>U: valid / invalid
    U->>D: 查询域名安全状态
    D->>D: 检查 DOMAIN_CHECK_CACHE
    D-->>U: allowed / blocked / check_failed
    U->>C: 检查缓存
    alt 缓存命中
        C-->>U: 返回缓存内容
    else 缓存未命中
        U->>H: 发起 HTTP GET
        H->>H: 处理重定向(最多10跳)
        H-->>U: AxiosResponse
        U->>T: HTML → Markdown
        T-->>U: markdownContent
        U->>C: 写入缓存
    end

2.3 HTML 到 Markdown 的转换

WebFetchTool 使用 turndown 库将 HTML 转为 Markdown。源码中采用惰性初始化策略(utils.ts:79-86),直到第一次需要处理 HTML 时才加载这个约 1.4MB 的依赖:

let turndownServicePromise: Promise<InstanceType<TurndownCtor>> | undefined
function getTurndownService(): Promise<InstanceType<TurndownCtor>> {
  return (turndownServicePromise ??= import('turndown').then(m => {
    const Turndown = (m as unknown as { default: TurndownCtor }).default
    return new Turndown()
  }))
}

转换后,内容会经过二次处理。applyPromptToMarkdown() 函数(utils.ts:363-402)将 Markdown 内容和用户 prompt 一起交给 Haiku 模型,让模型按照 prompt 的要求提取关键信息。这里有两个值得注意的设计:

  • 内容截断:如果 Markdown 超过 100,000 字符,会被截断并附加提示(utils.ts:365-369
  • 预批准域名差异化:对预批准域名(如官方文档),允许直接引用内容;对其他域名,则限制引用长度(最多 125 字符),并禁止逐字复制(prompt.ts:34-43

2.4 重定向安全策略

WebFetchTool 不自动跟随跨域重定向,这是出于安全考虑(utils.ts:163-187)。isPermittedRedirect() 函数只允许以下重定向:

  • 添加或移除 www. 子域名(如 example.comwww.example.com
  • 同主机下的路径/查询参数变化
  • 协议和端口必须保持不变
  • 禁止包含用户名/密码的重定向 URL

如果重定向到不同主机,工具会返回一个特殊的 RedirectInfo 对象(WebFetchTool.ts:114-134),提示用户需要手动用新 URL 再次调用:

REDIRECT DETECTED: The URL redirects to a different host.
Original URL: https://docs.claude.com/...
Redirect URL: https://docs.anthropic.com/...
Status: 301 Moved Permanently

这种设计有效防止了开放重定向漏洞被利用来绕过域名白名单。

三、WebSearchTool 架构解析

WebSearchTool 位于 src/tools/WebSearchTool/ 目录下,它的实现方式与 WebFetchTool 有本质不同——它并非直接调用搜索引擎 API,而是借助 Anthropic 官方的 web_search_20250305 server tool 能力。

3.1 搜索查询构建

WebSearchTool 的输入 schema 同样非常简洁(WebSearchTool.ts:24-33):

const inputSchema = lazySchema(() =>
  z.strictObject({
    query: z.string().min(2).describe('The search query to use'),
    allowed_domains: z.array(z.string()).optional()
      .describe('Only include search results from these domains'),
    blocked_domains: z.array(z.string()).optional()
      .describe('Never include search results from these domains'),
  }),
)

值得注意的是,allowed_domainsblocked_domains 互斥,不能同时指定(WebSearchTool.ts:246-253)。

工具内部通过 makeToolSchema() 构造 Anthropic 的 beta web search schema(WebSearchTool.ts:64-72):

function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
  return {
    type: 'web_search_20250305',
    name: 'web_search',
    allowed_domains: input.allowed_domains,
    blocked_domains: input.blocked_domains,
    max_uses: 8, // Hardcoded to 8 searches maximum
  }
}

max_uses: 8 是一个硬编码的限制,意味着单次 WebSearchTool 调用最多触发 8 次内部搜索。

3.2 包装型工具的调用方式

WebSearchTool 是一个典型的"包装型工具"。它的 call() 方法(WebSearchTool.ts:255-350)内部重新发起了一次模型调用:

const queryStream = queryModelWithStreaming({
  messages: [userMessage],
  systemPrompt: asSystemPrompt([
    'You are an assistant for performing a web search tool use',
  ]),
  tools: [],
  options: {
    extraToolSchemas: [toolSchema],
    querySource: 'web_search_tool',
    // ...
  },
})

关键点:

  • 外层是 Claude Code 的标准工具循环
  • 内层通过 queryModelWithStreaming 发起一次专门用于搜索的模型请求
  • 搜索工具通过 extraToolSchemas 注入,而非直接调用搜索引擎 HTTP API
  • 根据 feature flag tengu_plum_vx3,可以选择使用 Haiku 模型执行搜索

3.3 搜索结果解析与重组

底层模型返回的搜索结果不是简单的 JSON 数组,而是一串混合内容块(WebSearchTool.ts:74-127):

  • server_tool_use:模型发起 server tool 调用的信号
  • web_search_tool_result:实际的搜索结果
  • text:模型的文本解释
  • citation 相关块:引用信息

makeOutputFromSearchResponse() 函数负责把这些混合块整理成结构化的 Output

function makeOutputFromSearchResponse(
  result: BetaContentBlock[],
  query: string,
  durationSeconds: number,
): Output {
  const results: (SearchResult | string)[] = []
  // ... 遍历 block,分类处理 text / server_tool_use / web_search_tool_result
  return { query, results, durationSeconds }
}

输出 schema 的设计也很独特(WebSearchTool.ts:43-55):

const outputSchema = lazySchema(() =>
  z.object({
    query: z.string().describe('The search query that was executed'),
    results: z.array(z.union([searchResultSchema(), z.string()]))
      .describe('Search results and/or text commentary from the model'),
    durationSeconds: z.number().describe('Time taken to complete the search operation'),
  }),
)

results 数组中的元素可以是搜索命中结果({tool_use_id, content: [{title, url}]}),也可以是字符串形式的模型解释文本。这种混合结构让 WebSearchTool 不仅是数据管道,还能携带模型的中间推理过程。

3.4 来源约束与 Prompt 工程

WebSearchTool 的 prompt 对"来源标注"有严格要求(prompt.ts:8-28):

CRITICAL REQUIREMENT - You MUST follow this:
  - After answering the user's question, you MUST include a "Sources:" section at the end of your response
  - In the Sources section, list all relevant URLs from the search results as markdown hyperlinks: [Title](URL)
  - This is MANDATORY - never skip including sources in your response

此外,prompt 还强制要求搜索时带上当前年份(prompt.ts:30-34):

IMPORTANT - Use the correct year in search queries:
  - The current month is ${currentMonthYear}. You MUST use this year when searching for recent information

这是一个非常典型的产品化修补——避免模型搜索"latest React docs"时召回到过时的内容。

四、内容处理与过滤机制

4.1 无关内容过滤

WebFetchTool 的 HTML → Markdown 转换过程中,turndown 库会自动剥离 <script><style> 标签。此外,Anthropic 的 getWebFetchUserAgent() 函数(utils/http.ts:33-36)使用特定的 User-Agent 标识:

export function getWebFetchUserAgent(): string {
  return `Claude-User (${getClaudeCodeUserAgent()}; +https://support.anthropic.com/)`
}

Claude-User 是 Anthropic 公开记录的爬虫标识,网站运营者可以在 robots.txt 中匹配。claude-code 后缀则让网站能够区分本地 CLI 流量和 claude.ai 服务器端抓取。

4.2 关键内容提取

对于抓取的网页内容,WebFetchTool 不直接返回原始 Markdown,而是通过 applyPromptToMarkdown() 让 Haiku 模型按用户 prompt 进行提取。这个过程实现了"按需提取"——用户问什么,模型就提取什么,而不是把整页内容塞进上下文。

对于预批准域名和非预批准域名,提取策略有显著差异(prompt.ts:34-43):

  • 预批准域名:允许包含代码示例、文档摘录,引用更自由
  • 非预批准域名:严格限制引用长度(125 字符上限),要求使用引号标注原文,禁止逐字复制大段内容

五、缓存机制

5.1 双层缓存策略

WebFetchTool 实现了两层 LRU 缓存(utils.ts:88-107):

// URL 内容缓存:15 分钟 TTL,50MB 大小限制
const CACHE_TTL_MS = 15 * 60 * 1000
const MAX_CACHE_SIZE_BYTES = 50 * 1024 * 1024
const URL_CACHE = new LRUCache<string, CacheEntry>({
  maxSize: MAX_CACHE_SIZE_BYTES,
  ttl: CACHE_TTL_MS,
})

// 域名预检缓存:5 分钟 TTL,128 条上限
const DOMAIN_CHECK_CACHE = new LRUCache<string, true>({
  max: 128,
  ttl: 5 * 60 * 1000,
})

URL_CACHE 以完整 URL 为键,缓存抓取后的内容;DOMAIN_CHECK_CACHE 以主机名为键,缓存域名安全检查结果。分离这两层缓存的原因是:同一域名下的不同路径会触发相同的预检请求,如果只用 URL_CACHE 会造成不必要的 HTTP 往返。

5.2 缓存失效与清理

缓存提供了显式清理接口(utils.ts:109-112):

export function clearWebFetchCache(): void {
  URL_CACHE.clear()
  DOMAIN_CHECK_CACHE.clear()
}

LRUCache 会自动处理过期驱逐(TTL)和大小限制(LRU)。当缓存项超过 15 分钟或总大小超过 50MB 时,最久未使用的条目会被自动移除。

5.3 重复请求优化

在工具描述中(prompt.ts:16-18),Anthropic 明确告诉模型缓存的存在:

- Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL

这意味着如果 Claude 在短时间内多次访问同一 URL(例如分页浏览或重试),第二次会直接命中缓存,无需重新发起 HTTP 请求。

六、安全限制

6.1 域名白名单与黑名单

WebFetchTool 的权限模型以域名为粒度(WebFetchTool.ts:104-180),采用四级检查:

  1. 预批准域名:如果 URL 的主机名在 PREAPPROVED_HOSTS 集合中,自动允许(无需用户确认)
  2. deny 规则:如果用户设置了针对该域名的 deny 规则,直接拒绝
  3. allow 规则:如果用户设置了针对该域名的 allow 规则,自动允许
  4. 默认行为:首次访问新域名时,向用户请求权限

预批准域名列表(preapproved.ts:17-110)涵盖了主流编程语言文档、框架官网、数据库文档、云服务文档等,包括 Python、Node.js、React、Docker、AWS、Kubernetes 等。这个列表经过精心筛选,既覆盖了开发者的常见需求,又避免了过多开放带来的风险。

export const PREAPPROVED_HOSTS = new Set([
  'platform.claude.com',
  'docs.python.org',
  'developer.mozilla.org',
  'react.dev',
  'nodejs.org',
  'docs.aws.amazon.com',
  'kubernetes.io',
  // ... 共 80+ 个域名
])

值得注意的是,预批准域名中有少数带路径前缀的条目(如 github.com/anthropics),isPreapprovedHost() 函数会严格检查路径边界,防止 /anthropics 匹配到 /anthropics-evil/malwarepreapproved.ts:113-130)。

6.2 内容类型限制

WebFetchTool 通过 Accept 请求头声明支持的内容类型(utils.ts:256-259):

headers: {
  Accept: 'text/markdown, text/html, */*',
  'User-Agent': getWebFetchUserAgent(),
}

对于二进制内容(如 PDF),工具会将其保存到临时文件并返回文件路径(utils.ts:260-276),而不是尝试解码为文本。isBinaryContentType() 函数负责判断内容类型。

6.3 大小与超时限制

WebFetchTool 设置了多层资源消耗控制(utils.ts:97-108):

限制项说明
MAX_HTTP_CONTENT_LENGTH10 MB单请求最大内容长度
FETCH_TIMEOUT_MS60 秒HTTP 请求超时
DOMAIN_CHECK_TIMEOUT_MS10 秒域名安全检查超时
MAX_REDIRECTS10 跳最大重定向次数
MAX_MARKDOWN_LENGTH100,000 字符返回内容长度上限
MAX_URL_LENGTH2000 字符URL 长度上限

这些限制遵循 PSR(Product Security Review)要求,防止单个请求或用户消耗过多系统资源。

6.4 企业安全策略适配

对于企业客户,WebFetchTool 提供了 skipWebFetchPreflight 设置(utils.ts:217-218)。当企业网络策略阻止出站连接到 claude.ai 时,可以跳过域名黑名单预检,依靠域名白名单和用户审批作为安全边界。

此外,工具还能检测 egress 代理阻止(utils.ts:288-296):如果代理返回 403 且带有 X-Proxy-Error: blocked-by-allowlist 头,会抛出 EgressBlockedError,向用户明确报告网络出口限制。

七、环境适配与可用性

WebSearchTool 并非在所有环境中都可用,它的 isEnabled() 方法(WebSearchTool.ts:150-175)会根据 API provider 和模型版本进行判断:

isEnabled() {
  const provider = getAPIProvider()
  const model = getMainLoopModel()

  if (provider === 'firstParty') return true
  if (provider === 'vertex') {
    const supportsWebSearch =
      model.includes('claude-opus-4') ||
      model.includes('claude-sonnet-4') ||
      model.includes('claude-haiku-4')
    return supportsWebSearch
  }
  if (provider === 'foundry') return true
  return false
}
  • firstParty(Anthropic 官方 API):完全支持
  • vertex(Google Vertex AI):仅 Claude 4 系列模型支持
  • foundry:支持

这说明联网搜索能力不是 CLI 客户端的独立功能,而是依赖于底层模型和平台是否具备 web_search_20250305 server tool。

八、总结

Claude Code 的联网工具展现了终端 AI Agent 在网络访问上的精心权衡:

WebFetchTool 是一个完整的网页抓取管道,包含 URL 验证、域名安全检查、自定义重定向策略、HTML → Markdown 转换、Haiku 二次提取、双层 LRU 缓存和资源消耗控制。它的安全模型以"用户审批 + 预批准白名单 + 域名黑名单预检"三层机制为核心,既保证了开发者的便捷性,又防止了未授权的网络访问。

WebSearchTool 则是一个更高级的包装型工具,它将 Anthropic 官方的 Web Search server tool 接入 Claude Code 的工具循环。通过内部模型调用、混合内容块解析和严格的来源标注要求,它把"搜索引擎"包装成了 Agent 工作流中的一等公民,而非简单的数据查询接口。

两个工具的配合关系可以总结为:WebSearchTool 找信息源,WebFetchTool 读具体内容。这种分工让 Claude Code 在需要最新信息时,既能利用搜索引擎的广度,又能对关键页面进行深度处理——最终返回给用户的,不仅是答案,还有可验证的来源链接。