在前面的文章中,我们分析了 Claude Code 的文件系统工具、代码搜索工具和 Shell 执行工具。这些工具解决了"本地环境交互"的问题,但当模型需要获取最新文档、查询当前版本信息或研究最新技术动态时,就必须突破本地边界,接入互联网。本文将深入解析 Claude Code 的两个联网工具——WebFetchTool 和 WebSearchTool——的设计哲学、实现细节与安全策略。
一、工具定位与协作关系
WebFetchTool 和 WebSearchTool 分别解决联网能力的两个不同维度:
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)是核心入口,其处理流程如下:
- URL 验证:检查 URL 长度(最大 2000 字符)、协议合法性,拒绝包含用户名/密码的 URL(
utils.ts:139-162) - 协议升级:HTTP 自动升级为 HTTPS(
utils.ts:208-211) - 域名安全检查:通过 Anthropic API 查询域名是否被阻止(
utils.ts:119-133) - HTTP 请求:使用 axios 获取内容,自定义重定向处理(
utils.ts:244-302) - 内容类型判断: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.com→www.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_domains 和 blocked_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),采用四级检查:
- 预批准域名:如果 URL 的主机名在
PREAPPROVED_HOSTS集合中,自动允许(无需用户确认) - deny 规则:如果用户设置了针对该域名的 deny 规则,直接拒绝
- allow 规则:如果用户设置了针对该域名的 allow 规则,自动允许
- 默认行为:首次访问新域名时,向用户请求权限
预批准域名列表(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/malware(preapproved.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_LENGTH | 10 MB | 单请求最大内容长度 |
FETCH_TIMEOUT_MS | 60 秒 | HTTP 请求超时 |
DOMAIN_CHECK_TIMEOUT_MS | 10 秒 | 域名安全检查超时 |
MAX_REDIRECTS | 10 跳 | 最大重定向次数 |
MAX_MARKDOWN_LENGTH | 100,000 字符 | 返回内容长度上限 |
MAX_URL_LENGTH | 2000 字符 | 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 在需要最新信息时,既能利用搜索引擎的广度,又能对关键页面进行深度处理——最终返回给用户的,不仅是答案,还有可验证的来源链接。