一句话:当模型请求被路由到不支持「服务端工具」的中转时,内置联网搜索会静默失效——它不报错,只是把垃圾塞进上下文。替代方案必须同时解决检索、内容、质量、网络四层,才谈得上"能用"。
验证环境: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: 的查询,有些引擎会直接忽略这个操作符。所以不是"按顺序重试",而是按查询特征选择降级顺序。
| 顺序 | 引擎 | 适用 | 说明 |
|---|---|---|---|
| 1 | Brave Search | 中英文、site: | 质量最好,返回带日期摘要;需走本地代理 |
| 2 | 360 搜索 | 中文 | 直连可用,支持 site:,自动翻 3 页补足条数 |
| 3 | Seznam | 英文、site: | 服务端渲染,英文技术查询和 site: 命中准确 |
| 4 | Marginalia | 英文技术 | 独立小索引,适合小众技术主题 |
| 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 node | 1.00 | high,全部是官方文档页 |
| 英文技术查询(asyncio TaskGroup) | 0.88 | high,官方文档 + 社区讨论 |
| 中文长尾(崂山道教 太清宫 历史沿革) | 0.48 | high,百科 + 行业协会 |
| 必应对同一长尾查返回知乎杂答 | 0.00 | low,被拦下换引擎 |
| 带 site: 但引擎忽略该操作符 | — | off-site,整批丢弃 |
判定为 low 时,输出里会带一行明确告警:"没有引擎给出与本次查询匹配的结果,请改写查询"。让模型知道"这次没搜到",比让它误以为搜到了重要得多。
4.4 网络层:代理与出口 IP 轮换
境外引擎在国内网络下必须走代理。这里踩到一个很隐蔽的坑,值得单独说。
同一个出口 IP、同一时刻,curl 能通,Python 的 urllib 被拒。一开始以为是 User-Agent 或 Cookie 的问题,反复对比后发现真正原因是 HTTP 版本:
| 客户端 | HTTP 版本 | 结果 |
|---|---|---|
curl | HTTP/2 | 200,返回 20 条结果 |
| Python urllib | HTTP/1.1 | 429 Too Many Requests |
curl --http1.1 | HTTP/1.1 | 429 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. 怎么接进去:三件事缺一不可
替代品做好了,还要让助手真的去用它。这一步比写脚本更容易被忽略。
- 把工具注册成标准接口。用 MCP(模型上下文协议)暴露成一个工具,参数对齐内置搜索:
query/count/fetch_pages/fetch_count/allowed_domains/blocked_domains。工具描述里写明"可替代内置搜索",模型看到工具列表就知道用途。 - 禁掉旧工具。在权限配置里拒绝内置搜索。否则模型仍会调用它,继续拿回垃圾。
- 把新工具加进允许列表,并写进项目规则。不加允许列表,每搜一次弹一次确认;不写进规则,模型在长任务里可能想不起来。
命令行也能直接用,方便脚本和自动化:
# 默认:结果 + 摘要 + 前 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. 实测数据
| 查询 | 引擎 | 相关性 | 耗时 | 结果 |
|---|---|---|---|---|
崂山道教 太清宫 历史沿革 | Brave | high | 8.5s | 百科 + 省道教协会,正文含 2160 年沿革全文 |
python asyncio TaskGroup cancel scope | Brave | high | 7.3s | 官方文档 + 社区讨论,正文含示例代码 |
site:docs.comfy.org custom node | Brave | high | 4.0s | 全部官方文档页,正文含分步教程 |
site:github.com anthropic-sdk-python tool use | Brave | high | 2.5s | 目标仓库 README 正文 |
乱码对照 zxqv kklmno 9931 | Seznam | low | 3.1s | 输出告警,明确告知不可信 |
耗时说明:带 site: 的查询 2.5–4 秒,普通查询带正文 5–8.5 秒(含被反爬站的超时),--no-fetch 约 1 秒。
8. 适用边界
这套方案不完美,以下情况要提前知道:
- 依赖本地代理。换到没有代理的环境,境外引擎全部不可用,只剩国内引擎。脚本会自动尝试拉起代理,但代理本身需要账号。
- 前端渲染页抓不到正文。论坛类(Discourse)、单页应用这类内容由 JS 在浏览器里拼出来的站点,静态抓取拿不到,会静默省略正文,只保留标题和链接。
- 自动换节点会改动当前线路。这是刻意行为,但对正在用同一条线路做别的事情会有打扰,可关闭。
- 第三方引擎的稳定性不由我们控制。页面结构一变,解析就会失效,需要定期回归测试三条典型查询。
9. 小结
这次排查真正有价值的部分,不是"换了个搜索工具",而是三件事:
- 先确认工具由谁执行。服务端工具、客户端工具、中转层模拟的工具,行为完全不同。排查时第一件事是看请求日志里实际执行的模型是谁,而不是界面显示的名字。
- 替代品要自检质量。任何"格式正确但内容不可信"的返回,都比失败更危险。加一层相关性打分,让"没搜到"变得可见。
- 协议细节会伪装成封禁。HTTP/1.1 被限流看起来像 IP 被封,实际是协议版本问题。遇到 429,先对比不同客户端,再怀疑 IP。
如果你的团队也在用中转服务跑 AI 编程助手,建议先做一件事:随便搜一条技术长尾词,然后检查返回结果里有没有你查询里的关键词。如果没有,说明内置搜索早就失效了,只是没人发现。