# 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();
}
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 });
}
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" };
2
# 数据流对比
WebFetch(客户端):
模型发 tool_use(WebFetch,url) → 引擎查 registry → 本地 execute(fetch+turndown+callAI)
→ tool_result 回传 → 模型继续 【标准两段往返】
WebSearch(服务端):
请求里声明 web_search_20250305 → 后端自行发起搜索(server_tool_use)
→ 后端拿到结果(web_search_tool_result) → 结果 + 回答都在同一次响应里
→ 客户端只渲染"🔍 搜索中",不执行任何东西 【零往返】
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,把外部进程的工具接进来。