从 Brave Search 到 Tavily:一次 Open WebUI Web Search 稳定性取舍

内网部署了自有推理服务器和Open WebUI 前端界面。最近小伙伴在使用 Open WebUI 的 Web Search 功能时,遇到了一个很典型、但一开始并不直观的问题:
搜索经常失败,错误来自搜索服务端,而不是模型本身。

这篇文章简单记录一下:我遇到了什么问题、尝试过哪些解决手段、如何做技术评估,以及最终为什么换成了 Tavily。


问题是什么

最初使用的是 Brave Search API 作为 Open WebUI 的 Web Search 后端。

在日常使用中,多人使用的时候频繁遇到:

  • 搜索偶发失败
  • 返回 too many connections / rate limited
  • 同样的问题在“低负载”下也会出现

直觉上很奇怪:
我并没有显式做高并发搜索。(但我确实需要,且希望并发进行搜索,这样出来结果快一些)


真正的原因

后来发现问题尽管单个用户的并发进行了控制,但多个用户使用的时候却没有有效控制并发,所以问题并不在“用户并发”,而在 Open WebUI + LLM + Tool 的调用模型

  • 一次用户提问,可能触发 多次搜索
  • 多个用户同时请求
  • 推理链、重试、搜索改写都会放大请求数
  • Brave Search 本身对并发和连接数限制非常严格

结果就是:
LLM 无意中变成了并发制造机,而 Brave Search 并不适合这个调用模式。


我尝试过什么

在不修改 Open WebUI 关于单词搜索的并发设置的情况下,主要尝试了三类手段:

  1. 限制搜索并发
  • 在 Brave Search API 前加反向代理
  • 只对搜索请求做并发闸门
  • 可以缓解问题,但工程复杂度上升
  1. 降低 WebUI 搜索触发频率
  • 调低 tool 使用倾向
  • 减少一次回答里的搜索次数
  • 本质是止痛药,不是根治
  1. 评估不同搜索后端的“AI 友好度”
  • Brave
  • SerpAPI
  • Bing
  • Tavily

简单评估结论

Open WebUI 的使用场景 出发,而不是单纯 API 能力:

  • Brave Search
  • 成本低
  • 但并发和连接限制过紧
  • 不加代理就不稳定
  • SerpAPI
  • 规则清晰、工程友好
  • 成本高
  • 更像“传统搜索 API”
  • Bing Search API
  • 产品生命周期存在不确定性
  • 不值得继续投入
  • Tavily
  • 明确为 AI / RAG / Agent 场景设计
  • 对 LLM 扇出式调用更友好
  • 行为可预期,整体最省心

最终选择

我最终换成了 Tavily。

原因很简单:

我希望尽可能快一点,支持合理的并发,但又不想为一个“搜索工具”
去额外维护限流代理、并发闸门和异常兜底。而 Tavily 的并发支持比 Brave 更慷慨,且总体更面向 AI Agents。

在 Open WebUI 这种 “模型主导、工具被动调用” 的架构下,
搜索后端是否理解 AI 的调用模式,比价格和传统性能指标更重要。


一点总结

这次经历最大的收获不是“换了哪个搜索 API”,而是一个更普遍的判断:

不是所有 Web Search API 都适合直接挂在 LLM 后面用。

如果一个搜索服务是为“人类 + 页面浏览”设计的,
那在 LLM 场景下,你迟早会为并发、限流和不可预期的失败买单。

Tavily 至少在这个阶段,帮我省掉了这些精力。

OpenWebUI 訪問後端 LLM 報錯:Unexpected token ‘d’, “data: {“id”… is not valid JSON

這幾天在部署企業內部 AI, Open WebUI 用 Docker 部署在一台 Web Server 上,因為上面有多個服務,所以用了 nginx 反向代理,真正運行的端口在 3000,反向代理到 HTTPS。當我用 Open Webui 訪問 Inference Server上部署的後端 LLM 的時候,不管後端用什麼引擎,Open Webui都報錯:

Unexpected token 'd', "data: {"id"... is not valid JSON

一開始以為是後端 LLM 模型的設置問題,後來又以為是 Open WebUI 的設置問題。

折騰了挺久才知道其實是 web socket 設置的問題。


這個錯誤訊息的關鍵是:回來的串流內容長得像 SSE(每行前面有 data: …),但 Open WebUI 這邊把它當成「一次性 JSON」去 JSON.parse(),所以第一個字符是 d 就直接報錯了。

最後解決方法分兩部分,涉及 nginx 設置,也涉及 Open WebUI 的 docker compose 設置。

Docker Compose 文件裡面,需要確保如下環境變量:

environment:
  - WEBUI_URL=https://chat.example.com
  - CORS_ALLOW_ORIGIN=https://chat.example.com
  - WEBUI_SESSION_COOKIE_SECURE=true
  - WEBUI_COOKIE_SECURE=True

    Nginx 的設置裡面:

    upstream open-webui {
        keepalive 32;
        server localhost:3000;
    }
    server {
        server_name   chat.augwit.com;
    
        access_log /var/log/nginx/chat.example.com/access.log;
        error_log /var/log/nginx/chat.example.com/error.log;
    
    
        location /ws/ {
          proxy_pass http://open-webui;
          proxy_http_version 1.1;
    
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection $connection_upgrade;
    
          proxy_set_header Host $host;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Real-IP $remote_addr;
    
          proxy_buffering off;
          proxy_read_timeout 3600;
          proxy_send_timeout 3600;
        }
    
        location / {
            proxy_pass   http://open-webui;
    
            proxy_http_version 1.1;
    
            # WebSocket 必需
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
    
            proxy_set_header Host $host;
            proxy_set_header X-NginX-Proxy true;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Port $server_port;
            proxy_set_header X-Forwarded-Uri $request_uri;
    
            # 串流 / SSE 常需要關掉 buffering,不然容易卡或解析怪
            proxy_buffering off;
    
            # 依需求調大超時(串流聊天很容易超過預設)
            proxy_read_timeout 3600;
            proxy_send_timeout 3600;
    
            client_max_body_size  1024m;
        }
    
        listen 443 ssl; # managed by Certbot
        ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem; # managed by Certbot
        ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem; # managed by Certbot
        include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    
    }

    最近因為在不同語言環境下工作,所以這篇正好是中文繁體。

    MacOS下 CPU 高占用的 crashpad_handler 进程

    这几天电脑有时候卡,作为新买的 MBP,配置不低,肯定不是硬件问题。打开 Activity Monitor 一看,好家伙,有个 crashpad_handler 进程占用我 CPU 快 100%。这个进程看名字像是一个搜集应用程序崩溃的信息并上报的程序,这类程序按道理应该是很轻量级的,怎么会这么诡异?

    没敢直接杀进程,先网上搜了一遍。先说结论:杀掉进程可以,没啥顾虑。

    下面是我在 Reddit 上看到一个贴文,内容大致是这样的:

    “我在 Mac 的「活动监视器」(Activity Monitor)里,发现一个名为 chrome_crashpad_handler 的进程,不论我有没有装 Chrome,都在吃大量 CPU 资源,导致风扇狂转、电池急速耗电。我根本没在用 Chrome,可这个程序却莫名其妙地存在,并持续占用系统资源。”

    好家伙,跟我的情况基本差不多。看样子不止我一个人碰到这种奇怪问题了。

    下面是社区里其他人的分析与建议(我把它们当成对我问题的可能解释和处置选项来看):


    社区的看法与分析

    • 有人说 crashpad handler 的本质是一个用于「捕捉异常 / 崩溃报告」的辅助程序。它的工作是监控程序发生崩溃时收集数据、写报告、上传报错信息给服务端。
    • 有时候,即便我没有安装 Chrome 或其他 Google 软件,这个进程也会出现。换句话说,它很可能是被某些基于 Electron 的应用(例如 VS Code)间接调用或触发的。
    • 在某些情况下,这个进程会不断崩溃 — 也就是说它自身也出现问题,导致系统日志里记录很多 crash。
    • 有用户指出,这种异常行为在较早版本的 Chromium / Chrome 中就被投诉过:crashpad_handler 会占用极高 CPU,重启或杀掉后偶尔会恢复正常,但问题可能复发。

    给出的一些操作建议 / 可尝试的方式

    • 强制退出 / 杀掉这个进程:可以在活动监视器里尝试“强制退出”这个程序,看看系统负载是否降下来。
    • 删除可疑文件 / 资源:在 Finder 里搜 “chrome_crashpad_handler” 这样的关键词,尝试找出这个程序的可执行文件或相关目录,把它移出垃圾桶或删除(当然要小心备份)。
    • 重启 Mac:重启可能使它暂时不再启动,或清除残留状态。
    • 检查那些可能调用它的软件:特别是 Electron 应用,比如 VS Code、某些插件或扩展。有可能是这些应用在运行时触发了 crashpad handler 进程。
    • 更新 / 卸载 / 重新安装软件:尝试更新相关应用或者卸载后再装,有可能修正触发机制。
    • 监测日志 / 崩溃报告:检查系统日志或崩溃报告,看这个进程什么时候触发,是什么程序作为其父进程。

    Reddit 这篇帖子原链接:https://www.reddit.com/r/macbook/comments/nac3z1/in_activity_monitor_i_see_chrome_crashpad_handler/


    使用 react-native-quick-crypto 的项目遇到 “global.base64ToArrayBuffer is not a function” 问题

    我们有个项目里面用 react-native-quick-crypto 代替 crypto-js 来进行 AES 加解密以及 pbkdf2 运算。最开始是仅仅做 pbkdf2 运算,用于将客户密码生成一个 encryption key,以此来加密读写数据库。运行良好没有报错,效率提升显著,原来的 crypto-js 需要二三十秒的时间,现在一两秒就完成了。

    后来又添加了进行 AES 加解密的功能,结果涉及 base64转码就报错了。报错信息如下:

    TypeError: global.base64ToArrayBuffer is not a function (it is undefined)

    当时在用 AI 编程,gemini 帮我在 index.js 中引入各种 polyfill 均没有解决。后来自己研究了一下,想明白了,这个 base64ToArrayBuffer 函数在我们代码中并没有直接使用,是 react-native-quick-crypto 间接使用的,而其依赖的该函数应该是在 react-native-quick-base64 包中。

    查找 package-lock.json,却发现 react-native-quick-base64 已经安装了。正在无计可施之际,进一步在网上搜索答案,找到了这个 issue,里面有人提到他通过安装 react-native-quick-base64 解决。于是抱着试试看的态度,运行:

    npm install react-native-quick-base64

    问题果然得到解决。由于不是很了解背后机制,又质问了一下 gemini,这回这货好像反应过来了,给了我如下的解释:

    这是一个非常好的问题,也是 React Native 和 Node.js 生态系统中一个常见的、令人困惑的现象。
    
      您观察得非常准确:react-native-quick-base64 确实是 react-native-quick-crypto 的一个传递性依赖 (transitive dependency),这意味着当您安装 react-native-quick-crypto 时,npm 或 yarn 也会自动安装 react-native-quick-base64 到 node_modules 文件夹中,并且 package-lock.json 会记录下来。
    
      为什么直接安装它就解决了问题?
    
      这通常是由于以下一个或多个原因造成的:
    
       1. 模块解析顺序和全局 Polyfill 的时机:
           * react-native-quick-base64 库很可能在被导入时,会执行一些全局的 polyfill 操作(例如,它可能提供了 global.atob 或 global.base64ToArrayBuffer)。
           * 当它只是一个传递性依赖时,打包工具(如 Metro)在解析模块时,可能不会保证 react-native-quick-base64 的初始化代码在 react-native-quick-crypto 尝试使用它之前运行。
           * 当您将其作为直接依赖安装时,它在 node_modules 中的位置(通常是顶层)以及打包工具的处理方式可能会发生变化,从而确保它的 polyfill 代码在更早、更可靠的时机被执行,使得 global.base64ToArrayBuffer 在 react-native-quick-crypto 需要它时已经可用。
    
       2. 原生模块的链接问题 (如果它有原生部分):
           * 虽然 react-native-quick-base64 听起来像是一个纯 JavaScript 库,但如果它内部包含任何原生代码(即使是很小的部分),那么当它只是一个传递性依赖时,原生构建系统(如 iOS 的 CocoaPods 或 Android 的 Gradle)可能不会正确地发现并链接它。
           * 当您将其添加到 package.json 作为直接依赖时,它会被明确地包含在原生构建过程中,确保所有必要的原生组件都被正确链接。
    
       3. 打包工具的优化行为:
           * 打包工具(如 Metro)会进行各种优化,例如“hoisting”(提升)依赖项。有时,这些优化可能会导致某些模块的初始化顺序或作用域出现意想不到的问题。直接声明为依赖可以避免这些不确定性。
    
      总结
    
      这并不是 react-native-quick-crypto 官方文档的疏漏,而更像是JavaScript 模块系统、打包工具和原生模块链接之间复杂交互的一个副作用。
    
      最佳实践:
      既然您已经发现直接安装 react-native-quick-base64 解决了问题,那么请务必将其保留在 `package.json` 中作为直接依赖。这能确保您的项目在未来构建时,这个关键的 polyfill 能够稳定可靠地被加载。

    补充版本信息:

    react-native 0.79.6
    react-native-quick-crypto 0.7.17

    MacOS 下 Gemini Cli 无法通过浏览器授权的解决办法

    最近在用 Claude Code 的同时,也在用 Gemini Cli 做一些事。今天换回刚返修回来的 MacBook Pro,在机器里面安装了 Gemini Cli 之后,发现使用 Chrome 浏览器登录会卡在点击完同意授权按钮之后,页面没有刷新,而命令行过一会儿后则报错,报错如下:

    Failed to login. Message: request to https://oauth2.googleapis.com/token failed, reason: connect ETIMEDOUT xx.xxx.xxx.xx:443

    一开始以为是缓存等问题,清理删除了 ~/.gemini目录,重新安装了 Gemini Cli,但还是不行。后来想到也许是网络问题,因为最近是在国内,所以是在必要的时候用了科学上网的,也许配置不到位。后来一番查找资料,果然通过网络设置解决了。

    我是用 Clash,配了一个自己搭的 https 转发服务。平时只是用系统代理,没有带开 TUN 模式。当打开 TUN 模式之后,授权就可以了。

    应邀出席校长怀旧联谊会感怀

    架桥中学校庆发起人刘双林聚集当时小学校长(各村校的负责人)叙谈旧情诚邀余参加(当时余为车堰小学校长)

    难忘别梦三十秋,

    如烟往事萦心头。

    吾辈历尽教书苦,

    新秀哪知育人忧。

    教育业绩均在目,

    同仁洪福早退休。

    今日刘郎邀盛会,

    汝曹欢乐余独羞。

    备注:1,师愧于生;2,业绩平平

    苦读

    孙敬头悬梁,苏秦锥刺股,

    匡胤囊萤学,孙康映雪读。

    四人虽家贫,有志自勤苦,

    终亦成大业,美名传千古。