联网搜索
Spark Work 内置联网搜索 spark_search:web_search / fetch_url 工具、免密默认链(Bing / DuckDuckGo / 百度)、keyed 后端(博查 / Tavily / Serper)、配置项与降级策略。
SDK 自带的WebSearch/WebFetch是 Anthropic 第一方服务端工具, 一旦会话走第三方 OpenAI 兼容供应商就会被剥离。Spark Work 因此内置了独立的 spark_searchMCP server:它在本地 Node 子进程里自己发 HTTP,与模型供应商完全解耦, 对普通会话与团队成员默认挂载,零配置即可用。
1. 工具与返回结构
命名空间是mcp__spark_search__,只有两个工具(SDK 允许清单里的名字就是下面这两个):
| 工具 | 参数 | 说明 |
|---|---|---|
web_search | query(必填)·count(1~20,默认 8)·time_range(day/week/month/year/ all,默认all)·site(限定域名) | 联网搜索,返回排序结果,命中解析后自动去重 |
fetch_url | url(必填,仅 http/https)·max_chars(默认 8000,最大 50000) | 抓取网页并清洗成可读正文,替代失效的 WebFetch |
time_range只对 keyed 后端(bocha / tavily / serper)精确生效;免密 HTML 引擎会忽略它。site是拼进查询串的(site:example.com之类写法同样可用)。
web_search的返回对象:
{
provider: "bocha" | "tavily" | "serper" | "bing" | "duckduckgo" | "baidu",
query: "...",
results: [{ title, url, snippet, source?, date?, score? }],
warnings?: ["tavily: HTTP 429 ...", ...], // 降级过程说明
answer?: "..." // tavily / serper 的摘要(如果渠道给了)
}
fetch_url的返回对象:
{
url: "<跟随重定向后的最终地址>",
title?: "<HTML 的 <title>,非 HTML 时没有>",
contentType: "text/html; charset=utf-8",
truncated: true | false,
chars: 12000,
text: "<正文;被截断时末尾会附加 [truncated, N chars total]>"
}
内容类型处理:JSON 原样返回;HTML/XML(或内容看起来像 HTML)会剥标签、解 HTML 实体、压缩空白后转成 文本;其它类型原样返回。正文超过max_chars 会截断,参数被夹在 500~50000 之间。
2. 后端与降级链
免密默认链(零 key、零配置,顺序固定):
bing→ https://www.bing.com/searchduckduckgo→ https://html.duckduckgo.com/html/baidu→ https://www.baidu.com/s(末级回退)
链式规则:某个引擎抛错或返回 0 条结果,就往下走,并把原因累积到warnings里返回。 三个都失败才抛错,错误信息形如 All keyless engines failed. bing: GET https://www.bing.com/search timed out [timeout] | ...。 请求会带桌面 Chrome 的 User-Agent 与Accept-Language: zh-CN,zh;q=0.9,en;q=0.8。
keyed 后端(配了 key 自动优先,质量更高):
| provider | 默认地址 | 请求要点 |
|---|---|---|
bocha(博查,auto时的默认 keyed 选择) | https://api.bochaai.com(`/v1/web-search`) | POST,Authorization: Bearer;time_range映射成 freshness:oneDay / oneWeek / oneMonth / oneYear / noLimit |
tavily | https://api.tavily.com(`/search`) | POST,key 在 body 的api_key;search_depth: basic、include_answer: true;day/week/month 会切topic: news+ days,year 不支持 |
serper(Google) | https://google.serper.dev(`/search`) | POST,X-API-KEY;time_range映射成 tbs:qdr:d / qdr:w / qdr:m / qdr:y |
免密引擎也可被单独指定:provider填bing/baidu/ duckduckgo时只用那一个,不再链式降级。
keyed 后端出错或 0 结果时不会直接失败,而是回落免密链,并在 warnings里保留原因,所以「配置了 key 但结果看起来是白牌的」通常意味着 keyed 调用失败了。
3. 配置 keyed 后端
配置读的是app_settings表里分类为webSearch的三个键, 每次解析 MCP server 时实时读取:
| 键 | 取值 | 说明 |
|---|---|---|
provider | auto(默认)/bocha/tavily/ serper/bing/baidu/duckduckgo | auto= 有apiKey就走 keyed(默认 bocha),否则走免密链 |
apiKey | string | 仅 bocha / tavily / serper 需要;留空则不会走 keyed |
baseUrl | string | 可选,覆盖 keyed 后端的 base url(自建代理、镜像时用) |
当前版本没有对应的设置页面,最快的写法是让 Agent 调 mcp__spark_platform__settings_set:
settings_set({ category: "webSearch", key: "provider", value: "bocha" })
settings_set({ category: "webSearch", key: "apiKey", value: "<你的 key>" })
settings_set({ category: "webSearch", key: "baseUrl", value: "https://api.bochaai.com" }) # 可选
读取入口在子进程侧,对应这三个环境变量(key 只存在于该子进程内存):
SPARK_SEARCH_PROVIDER auto | bocha | tavily | serper | bing | baidu | duckduckgo
SPARK_SEARCH_API_KEY keyed 后端的 key
SPARK_SEARCH_BASE_URL keyed 后端 base url 覆盖
SPARK_SEARCH_TIMEOUT_MS 单次请求超时,默认 15000(1000~60000)
SPARK_SEARCH_TOTAL_TIMEOUT_MS 单次搜索总时限,默认 20000(1000~90000)
SPARK_SEARCH_MAX_RETRIES 可重试错误的最大重试次数,默认 1(0~3)
SPARK_SEARCH_RETRY_BACKOFF_MS 退避基数,默认 250(1~2000)
SPARK_SEARCH_FETCH_MAX_CHARS fetch_url 默认正文上限,默认 8000(500~50000)
SPARK_SEARCH_BING_URL / SPARK_SEARCH_BAIDU_URL / SPARK_SEARCH_DUCKDUCKGO_URL 覆盖免密引擎地址
其中前三个变量由应用按webSearch设置实时注入;其余是部署级逃生口,只有你自己 在启动环境里设置时才生效。
4. 超时、重试与限额
| 项 | 默认 | 说明 |
|---|---|---|
| 单次请求超时 | 15 s | 可配 1~60 s |
| 单次搜索总时限 | 20 s | 可配 1~90 s;HTML 正文读取也共享这个 deadline |
| 可重试错误重试次数 | 1 | 可配 0 |
| 结果条数 | 8 | 工具参数 1~20;keyed 渠道请求里也会夹到 20 |
| fetch_url 正文上限 | 8000 字符 | 参数 500~50000 |
哪些错误算「可重试」:timeout、network、rate_limited(429)、server_error(5xx)。会读取响应里的Retry-After(秒数或 HTTP 日期), 但退避上限 2000 ms。4xx(除 429)与invalid_response(返回体不是 JSON)不重试。
错误信息里带方括号分类标签,便于快速判断:
GET https://www.bing.com/search timed out [timeout]
GET https://api.tavily.com/search failed: fetch failed [network]
POST https://google.serper.dev/search returned HTTP 429 [rate_limited]
POST https://api.bochaai.com/v1/web-search returned HTTP 500 [server_error]
POST https://api.bochaai.com/v1/web-search returned HTTP 401 [http_4xx]
Non-JSON response from https://... [invalid_response]
GET https://... exceeded request budget [budget_exhausted]
5. 在会话里怎么用
不需要任何配置,直接派活:
帮我搜一下最近一周关于 React Server Components 的最佳实践,整理成 5 条要点并给出链接。
Agent 会先web_search拿候选,再对关键页面fetch_url 读全文,最后给出带 出处的结论。注入的 system prompt(WEB_SEARCH_SYSTEM_PROMPT)明确要求:时效性问题要 主动核实而不是靠模型记忆、优先一手来源、结论附近给 URL、不要把「没搜到」当成「不存在」。
挂载范围:
- 宿主会话默认挂载,工具名会合并进 SDK 的允许清单。
- 团队成员默认也挂载,与 Host 走同一套解析。
- 例外:显式标记为只读的「原子」成员从空能力集开始,不会拿到
spark_search。 - 脚本文件缺失时(
web-search-mcp-server.mjs找不到或缺少依赖)会记 warn 并跳过, 此时工具列表里不会出现这两个工具。
配套技能是内置技能builtin:multi-search-engine,它的requiredTools 就是这两个工具,内容是多角度检索、交叉验证、给出处的操作规范。
6. 实现位置与排查
- MCP server:
packages/agent-runtime/src/tools/web-search-mcp-server.mjs - HTTP/重试/deadline 工具:
packages/agent-runtime/src/tools/web-search-http.mjs - 接线:
SessionService.resolveWebSearchMcpServer()(读webSearch设置并注入 环境变量)+ 各轮次的mcpServers.spark_search/allowedTools合并 - 工具允许清单:
SEARCH_TOOL_NAMES(session-mcp-tooling-helpers.ts) - 提示词:
WEB_SEARCH_SYSTEM_PROMPT(同文件) - 伴随技能:
apps/desktop/resources/skills/multi-search-engine/
| 现象 | 排查 |
|---|---|
| Agent 说不能联网 | 先确认工具列表里有没有mcp__spark_search__web_search;没有就是 MCP 脚本没解析成功(脚本缺失或依赖不全),看主进程日志里的 warn。 |
| 结果都是白牌 / 质量低 | 说明回落到了免密链。检查app_settings.webSearch的provider与 apiKey;返回结果里的warnings会写明细。 |
报All keyless engines failed | 三个免密引擎都被限流或网络不通。换 keyed 后端,或确认所在网络能直连这几个域名。 |
time_range没生效 | 免密引擎不支持时间过滤;只有 bocha / tavily / serper 会翻译该参数(tavily 不支持 year)。 |
| 抓到的正文太短 | 页面可能是 JS 渲染的空壳。fetch_url不做渲染,这种页面请改用浏览器自动化。 |
速查表
| 项目 | 内容 |
|---|---|
| MCP 与工具 | spark_search:mcp__spark_search__web_search / mcp__spark_search__fetch_url |
| 免密默认链 | www.bing.com → html.duckduckgo.com → www.baidu.com(失败/0 结果自动降级并写入 warnings) |
| keyed 后端 | bocha(api.bochaai.com/v1/web-search)· tavily(api.tavily.com/search)· serper(google.serper.dev/search) |
| 配置位置 | app_settings.webSearch.{provider, apiKey, baseUrl},provider 默认 auto |
| 超时 | 单次 15s(1 |
| fetch_url 正文上限 | 默认 8000 字符,范围 500~50000 |
| 条件变量 | SPARK_SEARCH_PROVIDER / _API_KEY / _BASE_URL / _TIMEOUT_MS / _TOTAL_TIMEOUT_MS / _MAX_RETRIES / _RETRY_BACKOFF_MS / _FETCH_MAX_CHARS |
上手清单:让 Agent 完成一次联网调研
用 spark_search 调研一个最新技术话题
- 打开新会话,无需任何额外配置
- 给 Agent 发指令:「调研 XXX 最近一周的最佳实践,给出链接」
- Agent 调用 mcp__spark_search__web_search 拿候选(需要精确时间过滤时先配 keyed 后端)
- 对关键页面调 mcp__spark_search__fetch_url 读全文
- 汇总成带出处的要点或 Markdown 报告;结果里的 warnings 可判断是否发生了降级
常见问题
为什么走第三方供应商后 WebSearch 失效?
Anthropic 自带的 WebSearch / WebFetch 是服务端工具,第三方 OpenAI 兼容网关不会提供,所以会被剥离。spark_search 在本地子进程发 HTTP,与供应商解耦,任意供应商下都可用。
需要 key 吗?
不需要。免密链 Bing → DuckDuckGo → 百度 零配置可用。填 key 只是获得更高质量的结果与精确的 time_range 过滤。
key 会泄露给模型吗?
不会。key 只作为环境变量注入搜索子进程内存,不写进提示词,也不随工具结果返回。
团队成员也能用吗?
默认可以,成员与 Host 走同一套解析。但显式标记为只读的原子成员从空能力集开始,不会挂载 spark_search。
在哪儿填 key?
配置存在 app_settings 的 webSearch 分类(provider / apiKey / baseUrl)。当前版本没有对应的设置页面,最方便的方式是让 Agent 调 mcp__spark_platform__settings_set 写入。
搜索会返回几条?
count 参数 1~20,默认 8;超过 20 会被夹到 20,keyed 渠道请求里也按 20 上限发送。