# Step 46: 网页工具 —— WebSearch(服务端)+ WebFetch(客户端)

一句话导读:Phase 5 开篇加两个联网工具,但它们的机制根本不同——WebFetch 是"我们自己执行"的客户端工具,WebSearch 是"后端就地执行"的服务端工具,一个进 registry、一个压根不进。搞懂这个区别,是理解 Claude Code 工具体系的分水岭。


# 一、这一步做了什么(What)

加两个联网工具(对应真实源码 tools/WebSearchTool/ 和 tools/WebFetchTool/):

WebFetch WebSearch
类型 客户端工具(我们自己执行) 服务端工具(后端执行)
进 registry? 进(有 execute()) 不进(没有 execute)
怎么跑 我们 fetch(url) → 转 markdown → callAI 提取 在请求 tools 里声明,后端就地搜
结果从哪来 我们 return 同一次响应里直接出现 web_search_tool_result 块

实测:DeepSeek 的 Anthropic 兼容接口支持 web_search_20250305 服务端工具。


# 二、面试官视角:客户端工具 vs 服务端工具(Why)

面试题:都是"联网",为什么 WebFetch 要我们自己写 execute 去 fetch,而 WebSearch 我们一行执行代码都不写?这两类工具的边界在哪?

区别在于**"这个能力谁有资格执行、结果在哪里产生"**。

  • 客户端工具(WebFetch):能力在我们这一侧。抓某个具体 URL 是我们的运行环境能做的事——有 fetch、有网络。所以它有 execute()、进 registry,模型调用它时,引擎在本地跑 execute、把结果作为 tool_result 回传,走的是标准工具往返。

  • 服务端工具(WebSearch):能力在模型后端那一侧。全网搜索需要一个搜索引擎基础设施,那是 Anthropic/DeepSeek 后端才有的东西,客户端造不出来。所以我们不写 execute,只在请求的 tools 数组里声明"我允许你用 web_search",后端在生成响应的过程中就地搜、就地把结果塞进同一次响应。客户端从头到尾不执行任何东西。

维度 客户端工具 服务端工具
execute 在哪 我们写,本地跑 没有,后端跑
进 registry 进 不进
交互轮次 标准两段:tool_use → 我们跑 → tool_result 回传 零往返:结果和回答在同一次响应里
典型代表 WebFetch / Read / Bash WebSearch

一句话:能力在客户端就写 execute 进 registry,能力在后端就只声明不执行——这是 Claude Code 工具体系里一条根本性的分界线。


# 三、原理:它是怎么工作的(How)

# WebFetch(客户端):一个"自己会调 LLM"的工具

tools/webFetch.ts 三步走——抓页面、转 markdown、用小模型按 prompt 提取:

export const WebFetchTool: ToolDef = {
  name: "WebFetch",
  async execute(input): Promise<ToolResult> {
    const url = String(input?.url ?? ""), prompt = String(input?.prompt ?? "");
    // ① 抓页面(带 UA + 15s 超时)
    const res = await fetch(url, { signal: ctrl.signal,
      headers: { "User-Agent": "Mozilla/5.0 (compatible; mini-claude/1.0)" } });
    const html = await res.text();
    // ② turndown 把 HTML 转 markdown(真实源码 WebFetchTool 同款)
    let text = htmlToMarkdown(html);
    if (text.length > 8000) text = text.slice(0, 8000) + "\n…(已截断)";
    // ③ 用模型按 prompt 从正文里提取(真实版也是这一步)
    const answer = await callAI(
      [userText(`下面是网页 ${url} 的正文……要求:${prompt}\n\n网页内容:\n${text}`)],
      "你是一个网页内容提取助手,只根据给定的网页内容回答,不要编造。");
    return { content: [{ type: "text", text: answer }] };
  },
};

function htmlToMarkdown(html: string): string {
  turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
  turndown.remove(["script", "style", "noscript", "iframe"]); // 整段去噪
  return turndown.turndown(html).trim();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

WebFetch 特别之处:它自己会调 LLM(第 ③ 步 callAI),和 Agent 工具一样是"套娃"型工具——用一个小模型把长网页压成 prompt 要的那几点,避免把整页 HTML 塞回主对话。

# WebSearch(服务端):只声明,不执行

它根本不是普通工具,而是在发给 API 的 tools 数组里加一项声明:

// utils/api.ts —— streamTurn 里
const apiTools: any[] = [...tools];
if (webSearch) {
  apiTools.push({ type: "web_search_20250305", name: "web_search", max_uses: 3 });
}
1
2
3
4
5

然后在流式解析时,识别后端回传的两种新块(但绝不当普通工具处理):

if (currentType === "server_tool_use")        yield { type: "web_search", phase: "start" };
else if (currentType === "web_search_tool_result") yield { type: "web_search", phase: "result" };
1
2

# 数据流对比

WebFetch(客户端):
  模型发 tool_use(WebFetch,url) → 引擎查 registry → 本地 execute(fetch+turndown+callAI)
    → tool_result 回传 → 模型继续          【标准两段往返】

WebSearch(服务端):
  请求里声明 web_search_20250305 → 后端自行发起搜索(server_tool_use)
    → 后端拿到结果(web_search_tool_result) → 结果 + 回答都在同一次响应里
    → 客户端只渲染"🔍 搜索中",不执行任何东西   【零往返】
1
2
3
4
5
6
7
8

# 题眼:为什么 server_tool_use 绝不能进普通工具路径?

引擎处理 tool_use 的标准流程是"拿 name 去 registry 查 execute 再跑"。但 server_tool_use 是后端已经替我们跑完的搜索,它在 registry 里没有对应的 execute。如果误把它当普通 tool_call 处理,引擎会去 registry 找 web_search——找不到,报「工具未找到」。所以必须单独识别这两种块、只做渲染不做执行。这个坑正是"服务端工具不进 registry"这条规则的直接后果。


# 四、深入追问(面试常见 follow-up)

Q:WebFetch 里为什么要先 turndown 转 markdown,直接把 HTML 喂给模型不行吗? A:HTML 里 <script>、<style>、一堆嵌套标签属性全是噪音,既占 token 又干扰模型抓正文。turndown 把它转成带 #/**/[]() 的 markdown 并 remove(['script','style',...]) 去噪,信息密度高得多,模型提取更准。真实 WebFetchTool 也用 turndown,机制一致。

Q:WebSearch 走后端,那 WebFetch 抓网页也走 DeepSeek 代理吗? A:不。DeepSeek 代理只代理 LLM 请求,不代理普通网页抓取。WebFetch 的 fetch(url) 走的是直连公网——所以它会受公司防火墙、目标站的 UA 校验、403 等影响,而 WebSearch 因为在后端跑就没这些客户端网络问题。这也是两类工具在部署环境上的一个实际差异。

Q:服务端工具零往返、还省客户端网络,那为什么不把所有联网都做成服务端工具? A:因为能力得后端真有才行。全网搜索后端有基础设施,但"抓取你内网某个具体 URL""读你本地某个文件"这类,后端根本够不着——只有客户端环境能做。所以分界不是"哪个更省",而是"能力物理上在哪一侧"。

Q:WebFetch 自己调 LLM,会不会有递归/成本失控风险? A:它调的是一次性的小模型提取,不是完整 agent loop(没有工具、不递归),就单轮「给正文 + prompt → 出要点」。加上 8000 字截断兜底,成本可控。它和 Agent 工具的相似点是"内部会调模型",但 Agent 是完整子引擎、WebFetch 只是单次提取,量级完全不同。


# 五、踩坑 / 设计权衡

  • server_tool_use 误当工具:最容易踩的坑,处理块时若漏了单独识别,直接报「工具未找到」。根因是没建立"服务端工具不进 registry"的认知。
  • WebFetch 的 403/超时:直连公网意味着不可控。加了 UA 伪装 + 15s 超时 + res.ok 检查兜底,但公司网络下仍可能抓不到——这是客户端工具的固有代价。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
WebFetch: fetch + turndown + callAI WebFetchTool:axios + turndown + 小模型提取(机制一致)
WebSearch: 声明服务端工具 + 解析块 WebSearchTool:web_search_20250305 服务端工具
web_search 事件(start/result) 真实版解析 server_tool_use / web_search_tool_result
server_tool_use 不进 registry 一致
还缺:搜索结果引用渲染、域名允许/拒绝列表、HTTP→HTTPS 升级 真实版都有

# 七、一句话总结

两类工具,分界在"能力物理上在哪一侧":WebFetch 能力在客户端——写 execute、进 registry、标准两段往返、自己还会调小模型提取正文;WebSearch 能力在后端——只在请求里声明、不进 registry、结果和回答在同一次响应里,客户端只渲染不执行,误当普通工具就会报「工具未找到」。

# 下一节预告

联网工具接通后,下一步是 step47 MCP 协议入门——连接一个 stdio server,把外部进程的工具接进来。