Book 第十二章:Web 能力为什么要被严密收口
第三部分:本地执行面

第十二章:Web 能力为什么要被严密收口

解释 Claude Code 怎样把外部信息抓取做成带 provider 能力门控的受控执行面。

1. 为什么这两个工具也该放在一起看

从名字看:

  • WebFetchTool 是抓一个 URL
  • WebSearchTool 是做互联网搜索

但在 Claude Code 里,它们共同构成的是另一条能力线:

让 agent 在“不离开工具协议”的前提下访问外部世界。

这条线和本地文件工具、本地搜索工具的差异很大,因为它多了几层新约束:

  • 联网权限
  • 域名安全
  • 搜索结果引用
  • 外部内容的版权/摘要处理
  • 模型原生能力和 Claude Code 自己运行时之间的边界

所以这两个工具放在一起看,能更清楚地看出 Claude Code 对“联网”这件事的态度。

2. 先说结论

我对这组实现的判断是:

Claude Code 没把联网能力做成一个万能 HTTP 客户端,而是拆成了两个非常克制的只读协议。

它们的边界大概是这样:

  • WebFetchTool 已知 URL,抓页面内容,再用一个小模型按 prompt 做抽取/摘要
  • WebSearchTool 未知 URL,先借模型原生 web search 做多轮搜索,再把结果回流给主回合

这说明 Claude Code 在外部信息访问上坚持了两条原则:

  1. 联网必须带治理
  2. 搜索和抓取不是一回事

3. 源码锚点

建议先看这些位置:

  • src/tools/WebFetchTool/WebFetchTool.ts:66
  • src/tools/WebFetchTool/WebFetchTool.ts:104
  • src/tools/WebFetchTool/WebFetchTool.ts:181
  • src/tools/WebFetchTool/WebFetchTool.ts:208
  • src/tools/WebFetchTool/WebFetchTool.ts:300
  • src/tools/WebFetchTool/utils.ts:63
  • src/tools/WebFetchTool/utils.ts:176
  • src/tools/WebFetchTool/utils.ts:212
  • src/tools/WebFetchTool/utils.ts:347
  • src/tools/WebFetchTool/utils.ts:484
  • src/tools/WebFetchTool/preapproved.ts:1
  • src/tools/WebSearchTool/WebSearchTool.ts:152
  • src/tools/WebSearchTool/WebSearchTool.ts:157
  • src/tools/WebSearchTool/WebSearchTool.ts:209
  • src/tools/WebSearchTool/WebSearchTool.ts:254
  • src/tools/WebSearchTool/WebSearchTool.ts:401
  • src/tools/WebSearchTool/prompt.ts:5

4. 总体结构图

flowchart TD
    A["模型请求外部信息"] --> B{"已知具体 URL 吗"}

    B -->|是| C["WebFetchTool"]
    B -->|否| D["WebSearchTool"]

    C --> C1["URL 校验 + 域名权限"]
    C1 --> C2["preapproved host / rule-based allow-ask-deny"]
    C2 --> C3["domain blocklist preflight"]
    C3 --> C4["HTTP fetch + redirect policy + cache"]
    C4 --> C5["HTML -> markdown / 二进制落盘"]
    C5 --> C6["小模型按 prompt 提取内容"]

    D --> D1["provider / model 是否支持 web search"]
    D1 --> D2["passthrough permission"]
    D2 --> D3["queryModelWithStreaming + server tool schema"]
    D3 --> D4["捕捉 query_update / result progress"]
    D4 --> D5["整合搜索块 + 链接 + 文本说明"]

    C6 --> E["tool_result"]
    D5 --> E

这个图里最关键的一点是:

Claude Code 没有把“联网”做成一个统一大工具,而是按信息获取方式拆成了两条不同协议。

5. WebFetchTool:定点抓取 + 二次抽取

5.1 它不是浏览器,也不是通用 HTTP 客户端

WebFetchTool 的输入非常克制:

  • url
  • prompt

它不让模型自己配 HTTP 方法、headers、body,更不让模型自由上传数据。

它的定位非常明确:

读一个页面,然后回答“从这个页面里提取什么信息”。

5.2 它是只读、可并发、但默认应当 defer

这个工具显式声明:

  • isReadOnly() => true
  • isConcurrencySafe() => true
  • shouldDefer: true

这里的味道很明显:

  • 它不会写本地文件
  • 也不会改远端状态
  • 但因为牵涉外部网络和二次模型摘要,所以运行时更愿意把它放在 defer 工具那一类

5.3 Prompt 已经先把一批错误用法挡掉了

它的 prompt 很值得看,重点有几条:

  • 如果有 MCP 提供的 authenticated web fetch,优先用 MCP
  • authenticated/private URL 可能直接失败
  • GitHub URL 优先用 gh CLI
  • 会自动升级 http -> https
  • 大内容可能被摘要
  • 有 15 分钟缓存
  • 跨 host redirect 不会自动帮你跟到底

这说明作者不想让 WebFetch 变成一个“无脑抓网页工具”,而是想让模型先意识到:

  • 有的 URL 应该走专门工具
  • 有的 URL 根本不适合它

6. WebFetchTool 的权限设计很有意思

6.1 它不是普通 read 权限,而是域名级权限

WebFetchTool 的权限并不走本地文件那套路径权限模型。

它会先把输入转成:

  • domain:<hostname>

然后用这个内容去查:

  • deny rule
  • ask rule
  • allow rule

这说明 Claude Code 对它的授权粒度不是“这个 URL 字符串”,而是:

这个域名能不能抓。

6.2 有一批预批准 host

preapproved.ts 里有一长串预批准域名,基本都是:

  • 官方技术文档站
  • 编程语言官网
  • 云厂商文档
  • 常见框架文档

而且注释写得很清楚:

  • 这些预批准只对 WebFetch 生效
  • 不代表 sandbox 网络权限也能自动放开

这点特别重要,因为它说明 Claude Code 把:

  • “GET 一个技术文档页”

和:

  • “允许任意网络访问”

明确分开了。

6.3 即使没被规则拦住,还要过 domain blocklist preflight

WebFetchTool 在真正抓取前,还会请求:

  • https://api.anthropic.com/api/web/domain_info?...

来确认当前域名是否允许抓取。

如果:

  • 域名被 block
  • 或企业网络环境让这个检查失败

都会产生专门错误。

这说明 Claude Code 不只做本地授权,还把服务端侧的联网策略纳进来了。

7. WebFetchTool 的执行链不是简单 GET

7.1 它自己管理缓存

这里有两层缓存:

  • URL 内容缓存:15 分钟,50MB LRU
  • 域名 preflight 允许缓存:5 分钟

这说明 Claude Code 预期:

  • 同一个 URL 会被重复抓
  • 同一域名的安全检查也会反复发生

所以它把这些成本主动吸收进工具内部了。

7.2 redirect 不是默认全跟

这点很值钱。

WebFetchTool 只允许一类安全 redirect 自动继续:

  • 同 host
  • 或只是在 www. 有无之间切换

如果 redirect 到了另一个 host,它不会帮你直接抓,而是返回一段明确提示,让模型:

  • 再发一次新的 WebFetch

这背后的设计动机很明确:

避免可信域的 open redirect 把请求带到恶意域。

7.3 它对 egress proxy block 有专门识别

如果网络出口代理返回特定 403 头,它会抛:

  • EgressBlockedError

这说明作者不是把所有网络失败都当通用异常,而是尽量识别“为什么失败”。

7.4 HTML 会转 markdown,二进制会额外落盘

抓回来以后:

  • HTML -> turndown -> markdown
  • 二进制内容会持久化到磁盘,并附上路径

这一点很妙。

因为作者没有强行要求所有网页内容都只能以纯文本形式存在。

对 PDF 之类二进制内容,它允许:

  1. 先做一次文本级摘要
  2. 再把原始二进制文件落到本地,供后续别的工具继续分析

7.5 它还会用一个小模型再跑一次抽取

如果不是预批准域名上的小型 markdown 页面,它会调用:

  • queryHaiku(...)

把:

  • 页面内容
  • 用户 prompt

一起喂给次级模型做抽取。

这说明 WebFetch 不是“返回网页文本”,而是:

先抓取,再代理一次阅读。

8. WebFetchTool 的版权和引用约束,也下沉进了 prompt

makeSecondaryModelPrompt(...) 里对非预批准域名有一组非常具体的限制:

  • 精确引用长度限制
  • exact language 要用引号
  • 不要逐字复述文章
  • 不要产出歌词

这说明 Claude Code 没把版权/引用约束全留给主模型,而是连 secondary model 的摘要 prompt 都一起约束了。

这是一种很稳的防线前移。

9. WebSearchTool:借模型原生 web search 做多轮检索

9.1 它不是自己调搜索引擎,而是借模型的 server tool

这点非常关键。

WebSearchTool 的实现不是自己请求某个搜索 API,而是:

  • 构造 web_search_20250305 tool schema
  • 再通过 queryModelWithStreaming(...)
  • 让模型自己在一次 API 调用里驱动多轮 web search

所以它和 WebFetch 的本质差异是:

  • WebFetch 更像 Claude Code 自己实现的工具
  • WebSearch 更像 Claude Code 对模型原生搜索能力的包装器

9.2 它还要先看 provider / model 支不支持

isEnabled() 里会按 provider 判断:

  • firstParty:开
  • vertex:只对支持的 Claude 4 系列开
  • foundry:开
  • 其他:关

这说明 WebSearchTool 不是纯仓库内能力,而是:

跟底层模型和接入提供商绑定的可选能力。

9.3 权限是 passthrough,不是域名级规则

它的 checkPermissions() 返回的是:

  • passthrough

并带一条建议:

  • 如果允许,就把 WebSearchTool 加到本地规则里

也就是说,WebSearch 的授权模型更像:

  • “这个工具整体能不能用”

而不是:

  • “这个域名能不能搜”

这和 WebFetch 很不一样。

10. WebSearchTool 的执行链更像一个小型子回合

10.1 它自己起了一次流式模型调用

call() 里,它会:

  • 构造一条 user message:Perform a web search for the query: ...
  • 设置一个极简 system prompt
  • 启动 queryModelWithStreaming(...)

本质上,Claude Code 在这里又发起了一次内部回合。

10.2 它会实时解析 server tool use 的 partial JSON

这个细节很厉害。

它会盯着 content_block_delta 里的:

  • input_json_delta

从 partial JSON 里正则提取当前 search query,然后给 UI 发进度:

  • query_update

这说明它不是等所有搜索结束才告诉用户发生了什么,而是尽量把内部搜索过程实时外显。

10.3 搜索结果到达时,也会推送进度事件

web_search_tool_result 到来时,它会发:

  • search_results_received

包括:

  • 实际 query
  • 当前结果条数

这说明 WebSearchTool 已经不是“黑盒工具”,而是有一套自己的进度协议。

11. WebSearchTool 的结果整理也很像运行时适配层

11.1 它会把模型流返回的 block 序列重组

原始结果里混着:

  • text
  • server_tool_use
  • web_search_tool_result

它会重新整理成:

  • 文本说明段
  • 搜索结果对象

这一步特别关键,因为 Claude Code 没把原始 block 直接暴露给上层,而是先做了一层协议净化。

11.2 最终 tool_result 会强制提醒“必须带 Sources”

mapToolResultToToolResultBlockParam(...) 最后会追加一段硬提醒:

  • 你必须在回答用户时用 markdown hyperlink 引用这些来源

这和 prompt 里的强制要求一前一后,形成了双保险。

也就是说,Claude Code 不只是“给了链接”,它还强制模型在最终回答里把链接带出去。

11.3 UI 刻意不展示真实搜索内容

renderToolResultMessage(...) 只显示:

  • 做了几次搜索
  • 总共花了多久

而不把详细结果在 UI 里原样铺开。

同时 extractSearchText() 也返回空字符串,避免把“模型看见但 UI 没显示”的内容错误建索引。

这再次说明 Claude Code 对:

  • 模型视图
  • 宿主视图
  • 搜索索引视图

是分得很开的。

12. 两个工具放一起看,最能看出的差异

12.1 WebFetch 是 URL-first

你必须已经知道:

  • 去哪里

然后再问:

  • 从这里提什么

12.2 WebSearch 是 query-first

你先知道的是:

  • 你想找什么

再让模型去发现:

  • 应该看哪些来源

12.3 WebFetch 更像 Claude Code 自己做的协议

它自己管:

  • HTTP
  • redirect
  • cache
  • blocklist
  • HTML 转 markdown
  • 小模型摘要

12.4 WebSearch 更像模型原生能力的包装层

它自己主要做:

  • 兼容性判断
  • 权限接入
  • 进度解析
  • 结果重组
  • 引用要求强化

这两者的职责边界非常清楚。

13. Claude Code 在外部信息工具上做了哪些额外处理

把这两份实现收一下,大概可以总结成七层增强。

13.1 能力拆分增强

  • 定点抓取用 WebFetch
  • 开放搜索用 WebSearch

13.2 权限增强

  • WebFetch 做域名级 allow/ask/deny
  • WebSearch 做工具级 passthrough 授权

13.3 安全增强

  • preapproved 技术站点
  • domain blocklist preflight
  • redirect host 限制
  • egress block 识别

13.4 结果治理增强

  • WebFetch 二次摘要
  • WebSearch 结果块重组
  • UI / 模型 / 索引三视图分离

13.5 性能增强

  • URL 缓存
  • 域名检查缓存
  • 大小限制
  • 小模型摘要

13.6 版权与引用增强

  • WebFetch 次级 prompt 带引用约束
  • WebSearch 强制最终回答附 Sources

13.7 产品边界增强

  • authenticated/private URL 优先 MCP
  • GitHub URL 优先 gh
  • domain filter 支持
  • provider/model 能力门控

这说明 Claude Code 在联网能力上不是“能用就行”,而是做了非常多的运行时治理。

14. 对 C# 版的翻译建议

如果你后面做 C# 版,我建议把外部信息能力至少拆成两套接口,不要合成一个 HttpTool

14.1 URL 抓取层

  • IWebFetchToolDefinition
  • IUrlFetchPermissionService
  • IUrlSafetyPolicy
  • IHttpFetchService
  • IHtmlToMarkdownService
  • ISecondarySummarizer

14.2 Web 搜索层

  • IWebSearchToolDefinition
  • IModelNativeSearchAdapter
  • ISearchProgressProjector
  • ISearchResultAssembler
  • ISourceCitationPolicy

14.3 共享层

  • IExternalContentCache
  • IDomainApprovalPolicy
  • IExternalContentRenderer

这里我最建议你保留三件事:

  1. FetchSearch 分开
  2. 联网安全策略不要只放在最外层
  3. 引用要求要在工具层就前置

因为这三条直接决定系统联网后会不会变得不可控。

15. 我对这组设计的评价

WebFetchToolWebSearchTool 最值得学的,不是它们让 Claude Code 能联网,而是它们展示了 Claude Code 对联网这件事有多克制。

作者没有做:

  • 任意 HTTP 方法
  • 任意 headers/body
  • 万能浏览器会话
  • 搜索结果随便用不带来源

相反,他做的是:

  • 只读
  • 分层
  • 带引用
  • 带安全检查
  • 带 provider 边界

这非常像 Claude Code 整个运行时的一贯风格:

能力可以给,但必须放进治理框架里。