第十二章:Web 能力为什么要被严密收口
解释 Claude Code 怎样把外部信息抓取做成带 provider 能力门控的受控执行面。
1. 为什么这两个工具也该放在一起看
从名字看:
WebFetchTool是抓一个 URLWebSearchTool是做互联网搜索
但在 Claude Code 里,它们共同构成的是另一条能力线:
让 agent 在“不离开工具协议”的前提下访问外部世界。
这条线和本地文件工具、本地搜索工具的差异很大,因为它多了几层新约束:
- 联网权限
- 域名安全
- 搜索结果引用
- 外部内容的版权/摘要处理
- 模型原生能力和 Claude Code 自己运行时之间的边界
所以这两个工具放在一起看,能更清楚地看出 Claude Code 对“联网”这件事的态度。
2. 先说结论
我对这组实现的判断是:
Claude Code 没把联网能力做成一个万能 HTTP 客户端,而是拆成了两个非常克制的只读协议。
它们的边界大概是这样:
WebFetchTool已知 URL,抓页面内容,再用一个小模型按 prompt 做抽取/摘要WebSearchTool未知 URL,先借模型原生 web search 做多轮搜索,再把结果回流给主回合
这说明 Claude Code 在外部信息访问上坚持了两条原则:
- 联网必须带治理
- 搜索和抓取不是一回事
3. 源码锚点
建议先看这些位置:
src/tools/WebFetchTool/WebFetchTool.ts:66src/tools/WebFetchTool/WebFetchTool.ts:104src/tools/WebFetchTool/WebFetchTool.ts:181src/tools/WebFetchTool/WebFetchTool.ts:208src/tools/WebFetchTool/WebFetchTool.ts:300src/tools/WebFetchTool/utils.ts:63src/tools/WebFetchTool/utils.ts:176src/tools/WebFetchTool/utils.ts:212src/tools/WebFetchTool/utils.ts:347src/tools/WebFetchTool/utils.ts:484src/tools/WebFetchTool/preapproved.ts:1src/tools/WebSearchTool/WebSearchTool.ts:152src/tools/WebSearchTool/WebSearchTool.ts:157src/tools/WebSearchTool/WebSearchTool.ts:209src/tools/WebSearchTool/WebSearchTool.ts:254src/tools/WebSearchTool/WebSearchTool.ts:401src/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 的输入非常克制:
urlprompt
它不让模型自己配 HTTP 方法、headers、body,更不让模型自由上传数据。
它的定位非常明确:
读一个页面,然后回答“从这个页面里提取什么信息”。
5.2 它是只读、可并发、但默认应当 defer
这个工具显式声明:
isReadOnly() => trueisConcurrencySafe() => trueshouldDefer: true
这里的味道很明显:
- 它不会写本地文件
- 也不会改远端状态
- 但因为牵涉外部网络和二次模型摘要,所以运行时更愿意把它放在 defer 工具那一类
5.3 Prompt 已经先把一批错误用法挡掉了
它的 prompt 很值得看,重点有几条:
- 如果有 MCP 提供的 authenticated web fetch,优先用 MCP
- authenticated/private URL 可能直接失败
- GitHub URL 优先用
ghCLI - 会自动升级
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 之类二进制内容,它允许:
- 先做一次文本级摘要
- 再把原始二进制文件落到本地,供后续别的工具继续分析
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_20250305tool 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_useweb_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/denyWebSearch做工具级 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 抓取层
IWebFetchToolDefinitionIUrlFetchPermissionServiceIUrlSafetyPolicyIHttpFetchServiceIHtmlToMarkdownServiceISecondarySummarizer
14.2 Web 搜索层
IWebSearchToolDefinitionIModelNativeSearchAdapterISearchProgressProjectorISearchResultAssemblerISourceCitationPolicy
14.3 共享层
IExternalContentCacheIDomainApprovalPolicyIExternalContentRenderer
这里我最建议你保留三件事:
Fetch和Search分开- 联网安全策略不要只放在最外层
- 引用要求要在工具层就前置
因为这三条直接决定系统联网后会不会变得不可控。
15. 我对这组设计的评价
WebFetchTool 和 WebSearchTool 最值得学的,不是它们让 Claude Code 能联网,而是它们展示了 Claude Code 对联网这件事有多克制。
作者没有做:
- 任意 HTTP 方法
- 任意 headers/body
- 万能浏览器会话
- 搜索结果随便用不带来源
相反,他做的是:
- 只读
- 分层
- 带引用
- 带安全检查
- 带 provider 边界
这非常像 Claude Code 整个运行时的一贯风格:
能力可以给,但必须放进治理框架里。