跳至内容

让 AI 编程助手重新学会搜索:WebSearch 失效定位与四层替代方案

从服务端工具的执行边界,到多引擎降级、正文抓取、相关性闸门与出口 IP 轮换的完整实践
2026年11月2日 由
让 AI 编程助手重新学会搜索:WebSearch 失效定位与四层替代方案
赵博

一句话:当模型请求被路由到不支持「服务端工具」的中转时,内置联网搜索会静默失效——它不报错,只是把垃圾塞进上下文。替代方案必须同时解决检索、内容、质量、网络四层,才谈得上"能用"。

验证环境:macOS(Darwin 25.6)+ AI 编程助手桌面版 + 本地网络代理。文中所有结论均为 2026-11-02 实机复现,不是推测。

做 AIGC 调研时踩到一个很典型的坑:助手表现得很正常,调用搜索工具也不报错,但返回的东西完全不能用。排查下来发现,问题不在网络,也不在权限,而在"谁来执行这个工具"——这是所有依赖第三方中转的团队都会遇到的一类问题。

本文记录完整的定位路径和最终方案。如果你也在用中转服务跑 AI 编程助手,可以直接跳到第 4 节看架构。

1. 故障长什么样

两种形态,交替出现:

  • 形态一:伪造的工具调用。返回内容里只有一段泄漏出来的调用标记,形如 invoke name="Bash",没有任何真实结果。这是模型自带的工具调用特殊 token 被原样流出——调用根本没有被执行。
  • 形态二:无关的导航站。无论查询是中文还是英文、是长尾还是带 site: 限定,结果几乎相同,都是同一批 AI 工具导航站。这是中转层用一套低质量搜索兜底。

两种形态的共同点:没有任何错误提示。工具返回成功,内容却是假的。这比直接失败危险得多——如果不抽查,错误的"事实"会一路写进文档。

2. 定位:三分钟排除法

先按最省事的顺序排除,三步就能定位到根因。

第一步:确认权限没问题

查本地配置里搜索工具是否被拦截。我们的配置里已经显式放行 WebSearch,所以不是权限问题。

第二步:确认网络没问题

curl -s -o /dev/null -w "%{http_code}\n" https://api.github.com
curl -s -o /dev/null -w "%{http_code}\n" https://raw.githubusercontent.com

都是 200。网络通,能直连境外站点,也不是断网。

第三步:看请求到底发给了谁

这一步是关键。查本地中转工具的配置库,看当前生效的 provider 和模型路由:

# 中转工具的配置库里通常有 provider 列表和请求日志
sqlite3 ~/.cc-switch/cc-switch.db \
  "select datetime(created_at,'unixepoch','localtime'), request_model, model, status_code
   from proxy_request_logs order by created_at desc limit 10;"

结果一目了然:界面里显示的模型名是 claude-*,但 model 字段记录的真实模型是另一家的对话模型,HTTP 状态码全是 200。也就是说:请求确实成功了,只是执行它的不是你以为的那个模型。

3. 根因:服务端工具的执行边界

联网搜索不是普通工具。普通工具(读文件、跑命令)由本地客户端执行,模型只负责发指令;而联网搜索是服务端工具(server-side tool),由 API 服务商在自己的服务器上执行,模型拿到的直接是搜索结果。

第三方中转通常只实现"对话补全"这一个接口,不会去实现服务端的工具执行。于是就出现两种退化:

  • 模型按训练时的习惯输出一段工具调用文本 → 中转层不认识,原样透传 → 形态一。
  • 中转层自己做了一层"搜索兜底" → 质量极差但格式正确 → 形态二。

结论:这个问题在客户端修不好。它不是配置错误,而是架构决定的能力缺失。要恢复内置搜索,只有两条路:换成官方服务商,或换成明确实现了服务端工具的转中。两条路都涉及账号和计费,不是技术层面能绕过的。

既然修不了,就换掉它。下面是我们落地的替代方案。

4. 四层替代架构

很多人做替代时只做一个"调 API 拿结果"的脚本,然后发现:通了,但还是不好用。因为一个能顶替内置搜索的东西,必须同时解决四件事。

  • 检索层:拿到真实、相关的候选结果。单引擎一定不够。
  • 内容层:把结果正文取回来,让模型能直接引用,而不是再发一次抓取请求。
  • 质量层:自己判断这批结果可不可信,不可信就换引擎或明确告警。
  • 网络层:代理、超时、限流。境外引擎在国内网络下必然要面对这些。

4.1 检索层:按查询类型分流的降级链

不同引擎的强项完全不同。中文长尾查询,国际引擎经常给不出本地答案;带 site: 的查询,有些引擎会直接忽略这个操作符。所以不是"按顺序重试",而是按查询特征选择降级顺序。

顺序引擎适用说明
1Brave Search中英文、site:质量最好,返回带日期摘要;需走本地代理
2360 搜索中文直连可用,支持 site:,自动翻 3 页补足条数
3Seznam英文、site:服务端渲染,英文技术查询和 site: 命中准确
4Marginalia英文技术独立小索引,适合小众技术主题
5必应 RSS兜底国内环境下相关性差,输出里会标注降级

分流规则:

  • 查询含中日韩字符 → 中文引擎优先(360 → Seznam → 必应)。
  • 查询含 site: 或指定了域名白名单 → 引擎必须支持该操作符(Brave → Seznam)。
  • 其余 → Brave → Seznam → Marginalia。
  • 首个引擎结果少于 5 条时,再取一个引擎按 URL 去重合并,补召回。

4.2 内容层:把正文抓回来

只有标题和摘要,模型还得再抓一次页面,一来一回多两秒。所以我们默认抓取前 3 条结果的正文,单页上限 1800 字符,并发执行。

正文抽取是这里最容易做糙的部分。最初的实现直接对整页剥标签,结果 docs.python.org 抓到的是侧边导航,论坛页抓到的是 rel="stylesheet" 这种残缺属性。后来改成按块抽取:

# 只取真正的正文块,丢弃短导航项
for tag, raw in re.findall(r"<(p|h[1-6]|pre|blockquote|li|td)[^>]*>(.*?)\1>", fragment):
    text = strip_tags(raw)
    if tag in ("li", "td") and len(text) < 60:
        continue          # 列表项和表格单元格大多是菜单
    parts.append(text)

再选"包含最多段落文字的容器"(<article> / <main> / <body> 三者里选段落总字数最大的),而不是简单取整页。

还有一道兜底判断:如果抽出来的文本不足 150 字,或者出现 ="、rel=、data- 这类标签残留超过 3 次,直接丢弃这段正文。宁可没有正文,也不能把残缺 HTML 喂给模型——那会污染上下文,比没抓到更糟。

另外两类站点要特殊处理:百度百科、知乎、抖音、视频站这类几乎都是 JS 壳或反爬墙,抓取会吃满超时却一无所获。所以抓取队列会把它们排到最后,优先抓文档站和博客。这一条把常见查询的耗时从 14 秒压到 5–8 秒。

4.3 质量层:相关性闸门

这是整个方案里最容易被忽略、但最该有的一层。内置搜索不做自检,所以遇到上面说的"形态二",你得到的只是一批格式正确的垃圾。我们要自己判一次。

打分方法很朴素,不需要模型参与:

  • 把查询拆成检索词:英文按词拆(去停用词),中文按相邻二字切片。
  • 每条结果用「标题 + 摘要 + 域名」去匹配这些词,算命中率。
  • 标题命中过半额外加权——标题里出现的词比摘要里提到一次更有意义。
def assess(query, results):
    info = tokenize(query)          # 英文切词 + 中文二字切片
    scores = [score(r, info) for r in results[:5]]
    mean = sum(scores) / len(scores)

    if info.sites:                  # 带 site: 时额外校验域名命中率
        ratio = site_hit_ratio(results[:5], info.sites)
        if ratio < 0.5:
            return "off-site"       # 引擎忽略了 site:,整批丢弃

    if mean >= 0.45: return "high"
    if mean >= 0.20: return "medium"
    return "low"                    # 继续试下一个引擎

阈值是实测定出来的,效果对照:

场景得分判定
site:docs.comfy.org custom node1.00high,全部是官方文档页
英文技术查询(asyncio TaskGroup)0.88high,官方文档 + 社区讨论
中文长尾(崂山道教 太清宫 历史沿革)0.48high,百科 + 行业协会
必应对同一长尾查返回知乎杂答0.00low,被拦下换引擎
带 site: 但引擎忽略该操作符—off-site,整批丢弃

判定为 low 时,输出里会带一行明确告警:"没有引擎给出与本次查询匹配的结果,请改写查询"。让模型知道"这次没搜到",比让它误以为搜到了重要得多。

4.4 网络层:代理与出口 IP 轮换

境外引擎在国内网络下必须走代理。这里踩到一个很隐蔽的坑,值得单独说。

同一个出口 IP、同一时刻,curl 能通,Python 的 urllib 被拒。一开始以为是 User-Agent 或 Cookie 的问题,反复对比后发现真正原因是 HTTP 版本:

客户端HTTP 版本结果
curlHTTP/2200,返回 20 条结果
Python urllibHTTP/1.1429 Too Many Requests
curl --http1.1HTTP/1.1429 Too Many Requests

也就是说,搜索引擎把 HTTP/1.1 的请求一律当成爬虫限流了。Python 标准库说不出 HTTP/2,所以这一路只能改为调用系统 curl 子进程。这是让方案能工作的前提,不是可选优化——如果照着"用 requests 发请求"的常规写法,会一直以为是 IP 被封。

第二个坑:即使走 HTTP/2,数据中心出口 IP 还是会被限流。解决办法是遇到 429 就换出口节点:通过代理工具的本地控制接口切换线路(优先日本 → 新加坡 → 香港 → 美国),换完清掉冷却状态重试一次,仍失败才降级到其他引擎。

# 走本地控制接口切换节点,换出口 IP
curl -X PUT "http://127.0.0.1:9090/proxies/<分组名>" \
  -H 'Content-Type: application/json' \
  -d '{"name":"<节点名>"}'

这一步要开代理工具默认关闭的 HTTP 控制端口(只绑回环地址)。顺带还修了另一个问题:代理工具启动应用时默认不会自动拉起核心,导致"应用开着但代理端口没起来",脚本等 12 秒后放弃。把启动时自动运行核心打开即可。

5. 怎么接进去:三件事缺一不可

替代品做好了,还要让助手真的去用它。这一步比写脚本更容易被忽略。

  1. 把工具注册成标准接口。用 MCP(模型上下文协议)暴露成一个工具,参数对齐内置搜索:query / count / fetch_pages / fetch_count / allowed_domains / blocked_domains。工具描述里写明"可替代内置搜索",模型看到工具列表就知道用途。
  2. 禁掉旧工具。在权限配置里拒绝内置搜索。否则模型仍会调用它,继续拿回垃圾。
  3. 把新工具加进允许列表,并写进项目规则。不加允许列表,每搜一次弹一次确认;不写进规则,模型在长任务里可能想不起来。

命令行也能直接用,方便脚本和自动化:

# 默认:结果 + 摘要 + 前 3 条正文
web-search "崂山道教 太清宫 历史沿革" 6

# 只要链接和摘要,最快(约 1 秒)
web-search "kotlin coroutine flow" 5 --no-fetch

# 限定域名,等价于 site:
web-search "custom node walkthrough" 5 --domains docs.comfy.org

# 结构化输出,给脚本或子任务用
web-search "site:docs.comfy.org custom node" 5 --json

6. 与内置搜索的能力对照

能力内置(服务端)本方案
标题 / 链接 / 摘要✓✓
页面正文供引用✓✓(前 3 条,单页 1800 字)
域名白 / 黑名单、site:✓✓
中文检索质量一般✓ 自动切换中文引擎
相关性自检✗✓ 三档评分 + 域名命中校验
被限流时换出口 IP✗✓ 自动切换节点
抓取被客户端域名策略拦截的站点✗✓ 脚本自行抓取
结果缓存无✓ 同查询 10 分钟
多引擎合并补召回✗✓
结构化 JSON 输出✗✓
依赖服务端直连需本机代理,稳定性略低

7. 实测数据

查询引擎相关性耗时结果
崂山道教 太清宫 历史沿革Bravehigh8.5s百科 + 省道教协会,正文含 2160 年沿革全文
python asyncio TaskGroup cancel scopeBravehigh7.3s官方文档 + 社区讨论,正文含示例代码
site:docs.comfy.org custom nodeBravehigh4.0s全部官方文档页,正文含分步教程
site:github.com anthropic-sdk-python tool useBravehigh2.5s目标仓库 README 正文
乱码对照 zxqv kklmno 9931Seznamlow3.1s输出告警,明确告知不可信

耗时说明:带 site: 的查询 2.5–4 秒,普通查询带正文 5–8.5 秒(含被反爬站的超时),--no-fetch 约 1 秒。

8. 适用边界

这套方案不完美,以下情况要提前知道:

  • 依赖本地代理。换到没有代理的环境,境外引擎全部不可用,只剩国内引擎。脚本会自动尝试拉起代理,但代理本身需要账号。
  • 前端渲染页抓不到正文。论坛类(Discourse)、单页应用这类内容由 JS 在浏览器里拼出来的站点,静态抓取拿不到,会静默省略正文,只保留标题和链接。
  • 自动换节点会改动当前线路。这是刻意行为,但对正在用同一条线路做别的事情会有打扰,可关闭。
  • 第三方引擎的稳定性不由我们控制。页面结构一变,解析就会失效,需要定期回归测试三条典型查询。

9. 小结

这次排查真正有价值的部分,不是"换了个搜索工具",而是三件事:

  1. 先确认工具由谁执行。服务端工具、客户端工具、中转层模拟的工具,行为完全不同。排查时第一件事是看请求日志里实际执行的模型是谁,而不是界面显示的名字。
  2. 替代品要自检质量。任何"格式正确但内容不可信"的返回,都比失败更危险。加一层相关性打分,让"没搜到"变得可见。
  3. 协议细节会伪装成封禁。HTTP/1.1 被限流看起来像 IP 被封,实际是协议版本问题。遇到 429,先对比不同客户端,再怀疑 IP。

如果你的团队也在用中转服务跑 AI 编程助手,建议先做一件事:随便搜一条技术长尾词,然后检查返回结果里有没有你查询里的关键词。如果没有,说明内置搜索早就失效了,只是没人发现。

Anatomy:让 AI Agent 先造一台机器,再解释一个概念
GitHub 1.48 万 Star 的 Agent Skill,把抽象系统变成可运行、可检查的交互式机械图