介绍
Claude Code(含桌面版)自带的 Web Search 联网搜索功能,在接入第三方模型或 API 中转后经常会失效——要么报错,要么搜出来是空的,最坑的是模型假装搜到了、实际在编内容。原因不在客户端设置,而在上游服务商有没有实现这个工具:web_search 是 Anthropic 定义的”服务器端工具”,搜索由厂商自己的基础设施执行,纯做协议转换的中转商没有搜索后端,这个工具就永远跑不起来。
好在这个问题有成熟解法。本文给出四种经过实测的方案:
- DeepSeek 官方通道——零安装,官方原生支持,代价是只能用 DeepSeek 模型
- Firecrawl 官方 MCP + Skills——独立于模型厂商的抓取服务,任意模型下可用
- 自建 MiMo 搜索 MCP——把 MiMo 的联网搜索包装成自己的 MCP server
- 项目内 CLI 脚本——不依赖 MCP,任何能跑 Node 的环境都能用
四种方案可以叠加使用,文末有对比表和选择建议。

先搞懂原理:为什么第三方模型搜不了
Claude 的 web_search(工具 ID web_search_20250305)是一个服务器端工具:模型只能”请求搜索”,真正执行搜索、把结果注入对话的是上游厂商的服务器。所以:
- 服务商自己实现了这个工具(有自己的搜索后端)→ 原生可用
- 纯协议中转(只把 Anthropic 格式翻译成 OpenAI 格式转发)→ 工具声明被透传或丢弃,没人执行
这解释了另外两个常见疑问:
- 为什么 WebFetch 还能用? WebFetch 是客户端工具——由 Claude Code 自己抓取网页、转成文本再交给模型,不依赖上游做任何事,所以在中转下照样能跑。只有 WebSearch 会挂。
- 为什么换客户端设置没用? 上游格式、模型映射、effort 这类的调整都不解决——这是上游能力问题,不是配置问题。
三种失败表现(从明显到隐蔽)
| 表现 | 说明 |
|---|---|
| 显式报错 | 中转把服务器工具转成普通函数时描述字段为 null,上游直接返回 400 |
| 静默空结果 | 只回显你的查询词、没有任何结果块,也不报错 |
| 假装搜到了(最坑) | HTTP 200 一切正常,模型用散文写一段”搜索结果”——实际全是编的 |
判断标准:真结果一定带 URL / 来源链接。 没有来源的”搜索结果”都是编的。另一个实用提醒:搜索能力只取决于当前生效的那个 provider——换 provider 后要重新测,别从单次失败下结论说”这台机器用不了搜索”。
方案一:DeepSeek 官方通道(零安装)
DeepSeek 官方 API 原生实现了 Claude Code 的 Web Search。官方文档原话:
The DeepSeek API natively supports the Web Search feature in Claude Code. When using Claude Code, if the model determines that your question requires a web search, it will invoke the Web Search tool and perform the search through the API provided by DeepSeek.
配置:把 Claude Code / 桌面版的 API 端点切到 DeepSeek 官方即可:
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_AUTH_TOKEN=<你的 DeepSeek API Key>
(模型映射等完整环境变量见 DeepSeek 官方文档。如果用 CC Switch 这类切换器,选 DeepSeek 官方那条 profile 即可。)
优点:
- 零安装、零维护,模型自己决定什么时候搜,体验和官方完全一致
- 搜索结果会带来源链接,可直接核对
缺点:
- 锁定 DeepSeek 模型——想要别的模型就用不了这条路
- 搜索会产生额外 token 费用:搜索内容要经模型总结一遍,官方文档明确提示
适合:就用 DeepSeek 模型、图省事的人。
方案二:Firecrawl 官方 MCP + Skills
Firecrawl 是一个独立的网页抓取服务,通过 MCP 接入 Claude 后,与当前用什么模型完全无关——任意 provider 下都能用。能力面也最宽:搜索、抓单页、整站爬取、定时监控、文档解析等。
2.1 接入 MCP
在终端执行(user scope = 所有项目可用):
claude mcp add --transport http -s user firecrawl https://mcp.firecrawl.dev/v2/mcp-oauth --header "x-firecrawl-api-key: fc-你的key"
API key 在 firecrawl.dev 后台创建。
为什么用 API key 而不是 OAuth 登录? 实测 OAuth 方式存下来的凭据只有 accessToken、没有 refreshToken,token 过期后无法自动续期,接口直接 401。API key 不过期,一劳永逸。
2.2 安装 CLI + Skills(可选但推荐)
官方一条命令装齐 CLI 和技能包:
npx -y firecrawl-cli@latest init --all --browser
只想装给 Claude Code、且已有 key 的话,可以限定范围跳过浏览器授权:
npx -y firecrawl-cli@latest init --agent claude-code --skip-auth
装完会有 28 个 skill(12 核心 + 16 工作流)。MCP 和 Skill 是两回事:
| MCP | Skill | |
|---|---|---|
| 本质 | 连接协议,把工具接进会话 | 一份说明书,教模型怎么用工具 |
| 加载 | 每次会话常驻(占上下文) | 相关时才读进来(不占常驻) |
| 能力 | 25 个工具:search / scrape / crawl / monitor / parse 等 | 使用策略 + 成品模板(调研、SEO 审计等) |
2.3 重载机制(关键)
MCP 和 skills 都是会话启动时加载的,装完必须:
托盘退出 Claude → 重开 → 开新会话
只关窗口不行(主进程还活着);旧会话继续用也可能不生效,别赌。
2.4 三个坑
- 别用
&&串命令:claude mcp remove ... && claude mcp add ...里前一条失败(比如本来就 remove 过了),后一条会被静默跳过,看起来像”配置没生效”——分开跑。 - 限流 3 请求/分钟:这是分钟级请求数上限,短时间并发几个调用就会打满,之后所有调用连续 429。MCP 侧只回一句光秃秃的
Request failed with status code 429、不带原因,用 CLI 排查:firecrawl --status # 看剩余配额 firecrawl doctor # 10 项健康检查 - 别用
claude mcp get firecrawl查状态——它会把 API key 明文打印出来进对话记录。查连通性用claude mcp list。
计费:credit 制,搜索 2 credits/次(按官方要求提交反馈可退 1);抓页面按量计费、单页消耗很低。个人使用免费额度基本够。
适合:任何 provider 环境;需要抓全文、整站、定时监控、文档解析这些”深度取网页”的场景。
方案三:自建 MiMo 搜索 MCP
MiMo 有联网搜索能力(OpenAI 端点下的 web_search 工具),但走标准客户端调用不了。解法是自己写一个小 MCP server 直调它的 API。
3.1 服务端代码
~/.claude/mcp-servers/mimo-search/index.js:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import Database from "better-sqlite3";
import { homedir } from "os";
import { join } from "path";
function getMimoApiKey() {
// 优先环境变量
if (process.env.MIMO_API_KEY) return process.env.MIMO_API_KEY;
// 否则从 cc-switch 数据库读(改成本地数据库或其他来源均可)
const dbPath = join(homedir(), ".cc-switch", "cc-switch.db");
const db = new Database(dbPath, { readonly: true });
const row = db
.prepare(
"SELECT settings_config FROM providers WHERE id = '<你的 MiMo provider id>'"
)
.get();
db.close();
if (!row?.settings_config) throw new Error("MiMo provider not found in cc-switch DB");
const cfg = JSON.parse(row.settings_config);
const key = cfg?.env?.ANTHROPIC_AUTH_TOKEN;
if (!key) throw new Error("ANTHROPIC_AUTH_TOKEN not found in MiMo provider config");
return key;
}
const MIMO_API_KEY = getMimoApiKey();
const server = new McpServer({
name: "mimo-search",
version: "1.0.0",
});
server.tool(
"mimo-web-search",
"Search the web using MiMo's web search capability. Returns real-time web results with URLs.",
{
query: z.string().describe("The search query"),
max_results: z.number().optional().default(5).describe("Max number of results (1-10)"),
force_search: z.boolean().optional().default(true).describe("Force search even if model thinks it can answer"),
},
async ({ query, max_results, force_search }) => {
const body = {
model: "mimo-v2.5-pro",
messages: [{ role: "user", content: query }],
tools: [
{
type: "web_search",
max_keyword: 3,
force_search,
limit: max_results,
},
],
max_completion_tokens: 4096,
temperature: 1.0,
top_p: 0.95,
stream: false,
thinking: { type: "disabled" },
};
try {
const resp = await fetch("https://api.xiaomimimo.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": MIMO_API_KEY,
},
body: JSON.stringify(body),
});
if (!resp.ok) {
const errText = await resp.text();
return {
content: [{ type: "text", text: `MiMo API error ${resp.status}: ${errText}` }],
isError: true,
};
}
const data = await resp.json();
const choice = data.choices?.[0];
const text = choice?.message?.content || "(no content)";
const citations = (choice?.message?.annotations || [])
.filter((a) => a.type === "url_citation")
.map((a) => {
const parts = [`- [${a.title || a.url}](${a.url})`];
if (a.publish_time) parts.push(` published: ${a.publish_time.slice(0, 10)}`);
if (a.summary) parts.push(` ${a.summary.trim()}`);
return parts.join("\n");
});
let result = text;
if (citations.length > 0) {
result = `## Summary\n\n${text}\n\n## Sources\n\n${citations.join("\n")}`;
}
return { content: [{ type: "text", text: result }] };
} catch (err) {
return {
content: [{ type: "text", text: `Request failed: ${err.message}` }],
isError: true,
};
}
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
package.json:
{
"name": "mimo-search-mcp",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"better-sqlite3": "^13.0.3"
}
}
3.2 注册与安装
注册到 Claude 桌面版配置(路径:AppData/Roaming/Claude/claude_desktop_config.json):
{
"mcpServers": {
"mimo-search": {
"command": "node",
"args": ["<mimo-search 目录的绝对路径>/index.js"]
}
}
}
然后在目录里 npm install 装依赖。设置 MIMO_API_KEY 环境变量可以跳过数据库那段(代码里第一优先级就是它),比读 cc-switch DB 干净得多。
3.3 实现要点(踩过的坑)
- 搜索结果引用在
choices[0].message.annotations里(type: url_citation,含 url / title / publish_time / summary 字段),不在tool_calls里——最初按 tool_calls 解析导致引用全丢,看起来像”搜不到”。 - 空 annotations = 搜索后端没返回结果,此时模型的散文总结是编的,不要采信。
- 新版 SDK 只发 ESM:如果报”找不到 CJS 导出文件”,把代码写成
import+package.json加"type": "module"即可,不需要降级 SDK。 - key 在服务启动时读一次并缓存,换了 key 要托盘退出重开 App 才生效。
- 支持联网的是
mimo-v2.5-pro和mimo-v2.5,计费约 ¥16/1000 次搜索 + token 费。
3.4 能力边界(实测)
- 能:按主题/关键词搜到页面,返回 AI 整理好的综述 + Sources(标题链接、发布日期、摘要)。查”某商家什么背景””某技术是什么”这类资料性问题很顺手。
- 不能:站点级列表查询(”某站最新 N 篇文章”)、页面实时内容、
site:语法(会被当普通关键词)。 - 注意:索引以中文互联网内容为主,部分境外站点(如维基百科、Reddit)收录不全;价格等时效数据不可直接采信(综述可能混入旧数据),要用官网或 Firecrawl 核实。
适合:想要”一搜就有整理好的答案”、不需要自己翻源的场景。
3.5 懒人版:把提示词丢给 AI
不想一步步手装的话,把下面这段直接发给你的 AI(Claude Code 这类能执行命令的工具都可以),它会按上面的规格把服务建好、依赖装好、配置注册好:
帮我搭一个 MiMo 联网搜索的 MCP server,直接动手,做完告诉我结果:
1. 在 ~/.claude/mcp-servers/mimo-search/ 下创建 Node 项目:
- package.json:type 为 module(ESM),依赖 @modelcontextprotocol/sdk 和 better-sqlite3
- index.js:用 SDK 的 McpServer + StdioServerTransport 起一个 stdio server
2. 工具定义:名称 mimo-web-search,参数 query(必填)、max_results(可选,默认 5)、
force_search(可选,默认 true)
3. 工具实现:
- POST https://api.xiaomimimo.com/v1/chat/completions,header 带 api-key
- body:model 用 mimo-v2.5-pro;messages 放 query;
tools 为 [{ type: "web_search", max_keyword: 3, force_search, limit: max_results }];
thinking 设 disabled;其余参数用合理默认
- 来源引用在 choices[0].message.annotations 数组里(type 为 url_citation,
字段有 url / title / publish_time / summary),不在 tool_calls 里;
输出格式为 "## Summary"(正文)+ "## Sources"(每行 - [标题](url) + published 日期 + 摘要)
- API key 优先读环境变量 MIMO_API_KEY,没有就报清晰的错误提示
4. npm install 装依赖
5. 注册进 Claude Desktop 配置:AppData/Roaming/Claude/claude_desktop_config.json
的 mcpServers 下加一条,command 为 node、args 为 index.js 的绝对路径
6. 做完告诉我:怎么让配置生效(托盘退出重开 + 开新会话)、怎么验证
我的 MiMo API key:<粘贴你的 key;或先设好 MIMO_API_KEY 环境变量,让第 3 步直接读它>
装完记得:托盘退出重开 Claude → 开新会话,然后说一句”用 mimo-web-search 搜一下 XXX”验证。
方案四:项目内 CLI 脚本(不依赖 MCP)
如果你使用的工具支持执行命令但不支持 MCP(或想在脚本流水线里直接调用搜索),可以把同样的逻辑抽成一个 CLI 脚本——两个环境都能用,规则里只写脚本版就不会出现”规则写了但这端跑不了”的问题。
tools/mimo-search.js:
#!/usr/bin/env node
/**
* MiMo 联网搜索 CLI —— 任意能跑 Node 的环境通用。
*
* 用法: node tools/mimo-search.js "查询内容" [max_results]
*
* 返回:AI 整理好的综述(不是原始结果列表)。适合查商家背景/线路/机房这类
* 稳定性信息;**价格等时效数据不可直接采信**(综述可能混入旧价),用 firecrawl 或官网核实。
*
* API key 来源:环境变量 MIMO_API_KEY,否则读 cc-switch DB。
*/
const { execFileSync } = require('child_process');
const path = require('path');
const query = process.argv[2];
const maxResults = Number(process.argv[3]) || 5;
if (!query) {
console.error('用法: node tools/mimo-search.js "查询内容" [max_results]');
process.exit(1);
}
function getMimoApiKey() {
if (process.env.MIMO_API_KEY) return process.env.MIMO_API_KEY;
// 从 cc-switch DB 读(借用 MiMo MCP 目录下的 better-sqlite3;
// 只用环境变量的话这段不会执行,无需该依赖)
const mcpDir = path.join(process.env.USERPROFILE || process.env.HOME, '.claude', 'mcp-servers', 'mimo-search');
const out = execFileSync('node', ['-e', `
const Database = require(${JSON.stringify(path.join(mcpDir, 'node_modules', 'better-sqlite3'))});
const { homedir } = require('os');
const { join } = require('path');
const db = new Database(join(homedir(), '.cc-switch', 'cc-switch.db'), { readonly: true });
const row = db.prepare("SELECT settings_config FROM providers WHERE id = '<你的 MiMo provider id>'").get();
db.close();
if (!row?.settings_config) throw new Error('MiMo provider not found in cc-switch DB');
const cfg = JSON.parse(row.settings_config);
const key = cfg?.env?.ANTHROPIC_AUTH_TOKEN;
if (!key) throw new Error('ANTHROPIC_AUTH_TOKEN not found');
process.stdout.write(key);
`], { encoding: 'utf8' });
return out.trim();
}
(async () => {
const apiKey = getMimoApiKey();
const body = {
model: 'mimo-v2.5-pro',
messages: [{ role: 'user', content: query }],
tools: [{ type: 'web_search', max_keyword: 3, force_search: true, limit: maxResults }],
max_completion_tokens: 4096,
temperature: 1.0,
top_p: 0.95,
stream: false,
thinking: { type: 'disabled' },
};
const resp = await fetch('https://api.xiaomimimo.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'api-key': apiKey },
body: JSON.stringify(body),
});
if (!resp.ok) {
console.error(`MiMo API error ${resp.status}: ${(await resp.text()).slice(0, 300)}`);
process.exit(1);
}
const data = await resp.json();
const choice = data.choices?.[0];
const text = choice?.message?.content || '(no content)';
// 从 annotations 提 url_citation,输出「标题链接 + 发布日期 + 摘要」
const citations = (choice?.message?.annotations || [])
.filter((a) => a.type === 'url_citation')
.map((a) => {
const parts = [`- [${a.title || a.url}](${a.url})`];
if (a.publish_time) parts.push(` published: ${a.publish_time.slice(0, 10)}`);
if (a.summary) parts.push(` ${a.summary.trim()}`);
return parts.join('\n');
});
if (citations.length > 0) {
console.log(`## Summary\n\n${text}\n\n## Sources\n\n${citations.join('\n')}`);
} else {
console.log(text);
}
})().catch((e) => { console.error('[x]', e.message); process.exit(1); });
用法:
node tools/mimo-search.js "商家名 是什么背景" 5
两个说明:
- 最省事的 key 配置是设一个
MIMO_API_KEY环境变量——脚本第一优先级就是它,设了就完全不碰数据库那段。 - 用数据库方案的话,
<你的 MiMo provider id>替换成自己的:在 cc-switch 的 provider 列表里找到 MiMo 那条的 id,或安全地查一下 DB(只查 id/name,别查含 key 的字段):SELECT id, name FROM providers;
优点:不依赖 MCP 支持,任何能跑 Node 的环境(另一个 AI 工具、CI、定时脚本)都能用;也不占会话上下文(MCP 工具定义是每会话常驻的)。
缺点:需要手动调用(或在规则里写明),不像 MCP 那样被模型自动调度。
懒人版:把提示词丢给 AI
同样可以不自己动手——把下面这段发给你的 AI,它会写好脚本并跑一次真实查询给你验证:
帮我加一个 MiMo 联网搜索的 CLI 脚本,直接动手,做完跑一次给我看:
1. 在当前项目建 tools/mimo-search.js(Node,用全局 fetch 即可,无需第三方依赖)
2. 用法:node tools/mimo-search.js "查询内容" [条数],条数默认 5;
没有查询词时打印用法并以退出码 1 结束
3. 实现:
- POST https://api.xiaomimimo.com/v1/chat/completions,header 带 api-key
- body:model 用 mimo-v2.5-pro;messages 放查询词;
tools 为 [{ type: "web_search", max_keyword: 3, force_search: true, limit: 条数 }];
thinking 设 disabled;其余参数用合理默认
- API key 读环境变量 MIMO_API_KEY,没设就报清晰的错误提示
- 来源引用在 choices[0].message.annotations 里(type 为 url_citation,
字段有 url / title / publish_time / summary);
有引用时输出 "## Summary"(正文)+ "## Sources"(每行 - [标题](url) + published 日期 + 摘要),
没有引用时只输出正文
4. 写完用一条真实查询跑通验证(例如 node tools/mimo-search.js "小米 最新消息" 3)
我的 MiMo API key 放在环境变量 MIMO_API_KEY 里(还没设的话告诉我)。
四方案对比
| 方案一 DeepSeek 官方 | 方案二 Firecrawl | 方案三 MiMo MCP | 方案四 CLI 脚本 | |
|---|---|---|---|---|
| 安装成本 | 零(换个 provider) | 装 MCP + 可选 skills | 自己建 MCP server | 存一个 js 文件 |
| 对模型的要求 | 只能用 DeepSeek 模型 | 任意模型 | 任意模型(需支持 MCP) | 任意环境(能跑 Node) |
| 搜索方式 | 模型原生调用 | 工具调用 | 工具调用 | 手动/脚本调用 |
| 返回形态 | 模型总结 + 来源 | 原始结果 / 全文 | AI 综述 + 来源 | AI 综述 + 来源 |
| 费用 | 额外 token 费 | credits(有免费额度) | ~¥16/1000 次 | 同左(同一个 API) |
| 特色能力 | 体验原生 | 抓全文 / 整站 / 监控 / 解析文档 | 中文资料综述 | 可脚本化 / 不占上下文 |
怎么选
- 只用 DeepSeek 模型 → 方案一,什么都不用装,体验最好
- 任意模型 + 需要”深度取网页”(抓全文、爬站、监控、解析文档)→ 方案二,功能最全且与模型解耦
- 想要”一搜就给整理好的答案”、偏资料性查询 → 方案三
- 工具不支持 MCP,或要在脚本/流水线里搜索 → 方案四
- 推荐组合:方案一(日常原生搜索)+ 方案二(深度抓取)互补——一个负责快、一个负责全;MiMo 系(方案三/四)作为中文资料的补充
小结
- 第三方模型下 Web Search 失效的根因:
web_search是服务器端工具,只有自带搜索后端的服务商(如 DeepSeek 官方)能执行;纯中转没有后端,必然失败——要么报错、要么空结果、要么模型编造(没有来源链接的”搜索结果”都是编的) - 四个解法各有取舍:换 provider(零安装但锁模型)、外挂 Firecrawl(最全但按量计费)、自建 MiMo MCP(中文综述顺手)、CLI 脚本(不依赖 MCP、可脚本化)
- 装完 MCP / skill 记得托盘退出重开 + 开新会话才生效;排查限流和健康状态用 CLI,别用会打印 key 的
claude mcp get - 搜索能力只取决于当前生效的 provider——换 provider 后重新测,别从单次失败下结论




