LLM 网关加联网搜索:new-api 与 LiteLLM 的三种做法
new-api 和 one-api 只转发请求,不会替模型上网。对比客户端、网关和上游厂商三处接入联网搜索的成本,附 LiteLLM websearch_interception 加私有 SearXNG 的配置,以及验证上游搜索开关能否穿过中转的方法。
先说结论:中转网关本身不会联网搜索
new-api 和 one-api 这类中转网关不会替模型联网搜索。它们只把请求转发给 DeepSeek、通义千问或 OpenAI,再把回答交回客户端。想让回答带上最新的网页结果,搜索必须放在客户端、网关或上游厂商中的一处。每一处的费用和限制都不同。
以 QuantumNous/new-api 为例。它的 README(2026-10-17 读取)写明支持 OpenAI Chat Completions、Responses、Anthropic Messages 和 Gemini 四种接口,并能在这几种文本协议之间转换。README 里没有列出任何联网搜索功能。它确实有一套 JavaScript 任务插件,但插件面向的是图片、视频这类任务接口,负责任务状态和结果获取,不会给聊天请求加上搜索。
README 还有一句提醒:协议之间转换时,“protocol-specific tools and fields may not map exactly”。意思是某个协议特有的工具和字段,不一定能原样映射到另一个协议。这句话很重要。它意味着“在请求里打开厂商的搜索开关,再经过 new-api 转发”这条路不能想当然,必须自己测。文章最后一部分给出测试方法。
联网搜索可以放在哪里
- 放在客户端:Open WebUI、Cherry Studio、LobeChat 自己去查一个私有的 SearXNG,把结果写进提示词,再经由 new-api 发给模型。网关看到的只是一次普通的聊天请求。
- 放在网关:在 new-api 前面加一层 LiteLLM,打开
websearch_interception回调,搜索后端用 SearXNG。搜索在服务器端完成,所有走网关的程序都能用。代价是每次搜索至少多一次 LLM 调用。 - 放在上游厂商:打开厂商自带的搜索开关,比如通义千问的
enable_search,或者 OpenAI Responses API 的web_search工具。厂商按调用次数另外收费,而且这些字段要先穿过 new-api 才能到达上游。
SearXNG 是一个开源的元搜索引擎(metasearch engine):它把你的查询转发给多个搜索引擎,再把结果合并后返回。前两种做法都依赖一个你自己控制的 SearXNG。如果你还没部署过,可以先读在 VPS 上自建 SearXNG 搜索引擎。下面只讲接入网关需要的部分。
第一步:部署一个只对内开放的 SearXNG
SearXNG 的 JSON 输出默认是关闭的。LiteLLM 的 SearXNG 文档写明:安装后默认只启用 HTML 格式,要用 API 就必须在 settings.yml 的 search.formats 里加上 json。SearXNG 自己的 Search API 文档说,请求一个没有启用的格式,会得到 403 Forbidden。所以这一步的重点就是打开 json。
先建目录,再写一个最小的 settings.yml:
mkdir -p ~/searxng/config ~/searxng/data
cd ~/searxng
cat > config/settings.yml <<EOF
use_default_settings: true
server:
secret_key: "$(openssl rand -hex 32)"
search:
formats:
- html
- json
EOFuse_default_settings: true 表示这个文件只写你要改的项,其余全部沿用 SearXNG 自带的默认配置。官方示例里的 secret_key 旁边注明了 “change this!”,所以这里用 openssl rand -hex 32 生成一个随机值。
接着建一个 Docker 网络,让 SearXNG、LiteLLM 和 new-api 能用容器名互相访问。然后启动 SearXNG:
docker network create llm
docker run --name searxng -d --restart unless-stopped \
--network llm \
-p 127.0.0.1:8888:8080 \
-v "$(pwd)/config/:/etc/searxng/" \
-v "$(pwd)/data/:/var/cache/searxng/" \
docker.io/searxng/searxng:latest挂载路径和镜像名来自 SearXNG 官方的 Docker 安装文档。官方示例写的是 -p 8888:8080,会把端口开在所有网卡上。这里改成 127.0.0.1:8888:8080,所以只有 VPS 本机能从 8888 端口访问它。同一个 llm 网络里的容器则用 http://searxng:8080 访问,因为容器内部监听的是 8080。
不要在防火墙上放行 8888。网关和客户端都只需要从内部访问它。一个谁都能调用的搜索接口,流量和来源都不受你控制。如果确实要对外开放,先读公开 SearXNG 实例的加固方法,再配置SearXNG 的 limiter.toml 和 Valkey 限流。
检查 JSON 接口是否已经打开:
curl -s "http://127.0.0.1:8888/search?q=test&format=json" | head -c 300正常情况下你会看到一段 JSON,其中有 results 字段。如果看到的是 403 Forbidden,说明 json 没有生效。用 docker exec searxng cat /etc/searxng/settings.yml 确认容器读到的确实是你刚写的文件,然后运行 docker restart searxng。
SearXNG 有两个限制要提前知道
第一,结果条数不能通过 API 控制。LiteLLM 文档说,SearXNG 不支持通过 API 限制条数,并称每页默认约 20 条。但 SearXNG 本身并不保证固定的条数:一页结果来自你启用的各个引擎,条数取决于这些引擎各自返回多少。SearXNG 维护者在 GitHub issue #1761 里解释过,设置了每页条数和没有设置的引擎,“results from engines with and without this setting are mixed in one result list”,也就是会混在同一个结果列表里。第二,同一页 LiteLLM 文档写明 SearXNG 并不直接支持 max_results 参数,但文档自己的示例里仍然传了 max_results=10。不要指望这个参数生效。
这两点合在一起,意味着你没法用参数决定每次搜索带回多少条摘要,条数会随启用的引擎和查询本身变化。在网关做法里,这些摘要会被加进下一次模型请求,按输入 token 计费。启用的引擎越多、搜索越频繁,输入 token 涨得越快。估算成本之前,先看看你的实例实际返回多少条(需要 jq,安装命令见后文):
curl -s "http://127.0.0.1:8888/search?q=test&format=json" | jq '.results | length'做法一:让客户端自己查 SearXNG
这是改动最小的做法。new-api 不用动,搜索由聊天前端完成,结果作为提示词的一部分发给模型。所以 new-api 和上游看到的只是更长的输入,按普通 token 计费,没有按次的搜索费。
Open WebUI。 官方文档给出的环境变量是下面这三个,前提是 Open WebUI 和 SearXNG 在同一个 Docker 网络里:
ENABLE_WEB_SEARCH=true
WEB_SEARCH_ENGINE=searxng
SEARXNG_QUERY_URL=http://searxng:8080/search如果 SearXNG 没有启用 json,Open WebUI 的日志里会出现 403 Client Error: Forbidden。完整步骤见为 Open WebUI 打开 SearXNG 的 JSON API。
Cherry Studio。 在联网搜索设置页选择 SearXNG,填入实例地址。Cherry Studio 文档的说法是:本地运行时填 http://localhost:<端口>,Cherry Studio 跑在 Docker 里时填 http://host.docker.internal:<端口>。Cherry Studio 通常装在你自己的电脑上,而上面的 SearXNG 只监听 VPS 的 127.0.0.1。所以先用 SSH 隧道把端口转到本机:
ssh -N -L 8888:127.0.0.1:8888 user@your-vps隧道保持运行时,在 Cherry Studio 里填 http://localhost:8888。文档同样提醒:JSON 默认没有开启,不开的话验证会失败。
LobeChat(LobeHub)。 它的自部署文档用两个环境变量接入 SearXNG:
SEARCH_PROVIDERS=searxng
SEARXNG_URL=http://searxng:8080文档原话是 SearXNG 必须启用 json 输出,否则调用会报错。
这种做法的短板很明显:每个前端都要单独配置。直接调用 new-api 的脚本、智能体(agent)和命令行工具拿不到任何搜索能力,因为它们不经过这些前端。
做法二:在网关里搜索,用 LiteLLM 的 websearch_interception
LiteLLM 是一个开源的 LLM 网关,可以把多家模型统一成 OpenAI 格式的接口。它有一个名为 websearch_interception 的回调(callback),工作过程如下:
- 客户端的请求里带一个名为
litellm_web_search的函数工具,参数是一个query字符串。 - LiteLLM 把请求发给模型。模型决定需要搜索,于是返回一个对这个工具的调用。
- LiteLLM 拦下这个工具调用,不交回客户端,而是自己去
search_tools里配置的搜索后端查询。 - LiteLLM 把搜索结果加进对话,再请求一次模型,把最终回答交给客户端。
第 4 步就是成本所在。LiteLLM 文档写的是 “Adds one additional LLM call (follow-up request with search results)”,即每次搜索多一次带着搜索结果的后续调用。仓库里 v1.105.1 标签下的 ARCHITECTURE.md 补充了两点。一个请求可能触发多轮搜索,上限由 max_agentic_loops 控制,默认是 3。带 stream=True 的请求会先被转成非流式,等循环结束后,再把最终回答重新组装成流式返回。所以开启拦截后,第一个 token 会晚到很多。
如果你用 Claude Code 这类客户端,它会在 /v1/messages 请求里发送 Anthropic 原生的 web_search_20250305 工具。ARCHITECTURE.md 说这种工具也会被识别,并转成 LiteLLM 的标准搜索工具,不会交给上游去搜。
选哪个 LiteLLM 版本
LiteLLM 文档说搜索 API 从 v1.78.7 开始支持。websearch_interception 的两篇文档都没有写最低版本,所以这里不给最低版本,而是固定一个已经确认过的标签。核对时(2026-10-17),GitHub 上标为 Latest 的稳定版是 v1.105.1。在这个标签下,仓库里同时有 litellm/integrations/websearch_interception、litellm/search 和 litellm/llms/searxng/search 目录,也就是拦截回调和 SearXNG 搜索后端都在。
如果你已经按自建 LiteLLM 网关固定了一个更早的版本,先打开 https://github.com/BerriAI/litellm/tree/<你的标签>/litellm/integrations/websearch_interception,确认这个目录存在再继续。不要用 latest。LiteLLM 的部署文档也建议固定版本标签,而不是会移动的标签。
拓扑:LiteLLM 放在 new-api 前面
最容易验证的接法是让 LiteLLM 站在最前面。客户端请求 LiteLLM 的 4000 端口,LiteLLM 负责搜索,再把模型请求交给 new-api,new-api 照常转发给上游。对 LiteLLM 来说,new-api 只是一个 OpenAI 兼容的上游。
先把现有的 new-api 容器接入 llm 网络。new-api README 里的 docker run 示例把容器命名为 new-api。如果你用的是 Compose,容器名可能不同,用 docker ps 查一下:
docker network connect llm new-api然后在 new-api 后台为 LiteLLM 新建一个令牌,把它和 LiteLLM 的主密钥一起放进环境变量:
export NEWAPI_KEY="sk-在new-api后台新建的令牌"
export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 24)"
echo "$LITELLM_MASTER_KEY"记下打印出来的主密钥,后面的请求都要用它。
编写 config.yaml
mkdir -p ~/litellm
cd ~/litellm
nano config.yaml写入以下内容。YOUR_MODEL 换成 new-api 渠道里配置的模型名:
model_list:
- model_name: newapi-chat
litellm_params:
model: openai/YOUR_MODEL
api_base: http://new-api:3000/v1
api_key: os.environ/NEWAPI_KEY
litellm_settings:
callbacks: ["websearch_interception"]
websearch_interception_params:
enabled_providers: [openai]
search_tool_name: searxng-search
search_tools:
- search_tool_name: searxng-search
litellm_params:
search_provider: searxng
api_base: http://searxng:8080逐项说明:
model: openai/YOUR_MODEL:LiteLLM 文档要求,OpenAI 兼容的接口要在模型名前加openai/前缀,让 LiteLLM 按 OpenAI 提供方处理。api_base末尾带/v1。LiteLLM 文档说,测试时出现Not Found Error,就检查api_base是否带了/v1。enabled_providers: [openai]:拦截只对列出的提供方生效。上面的模型走openai/前缀,所以这里写openai。后面的测试会告诉你这一项对不对。search_tool_name是可选项。不写时,LiteLLM 使用search_tools里的第一个工具。search_provider: searxng加上api_base,就是 LiteLLM 文档里 SearXNG 搜索工具的写法。文档也允许用环境变量SEARXNG_API_BASE指定实例地址,两种方式必须选一种。
启动 LiteLLM
docker run -d --name litellm --restart unless-stopped \
--network llm \
-p 127.0.0.1:4000:4000 \
-v "$(pwd)/config.yaml:/app/config.yaml" \
-e LITELLM_MASTER_KEY \
-e NEWAPI_KEY \
ghcr.io/berriai/litellm:v1.105.1 \
--config /app/config.yaml-e LITELLM_MASTER_KEY 不带等号,表示把当前 shell 里同名变量的值传进容器。端口同样只绑定到 127.0.0.1。这是 LiteLLM 文档里不带数据库的启动方式,所以没有虚拟密钥,也没有费用统计,所有请求都用主密钥。启动后用 docker logs --tail 30 litellm 查看日志,确认容器在运行,配置错误也会出现在这里。
先单独测试搜索
LiteLLM 为每个搜索工具提供一个独立接口。先跳过模型,只测 LiteLLM 到 SearXNG 这一段:
curl -s http://127.0.0.1:4000/v1/search/searxng-search \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "SearXNG"}' | head -c 400正常的返回里有 "object": "search",results 里每一条都有 title、url 和 snippet。这一步失败,问题就在 LiteLLM 到 SearXNG 这一段,跟模型和 new-api 无关。
再测试完整的拦截流程
sudo apt install -y jq
curl -s http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "newapi-chat",
"messages": [{"role": "user", "content": "搜索一下 SearXNG 最近的新闻,并列出来源网址。"}],
"tools": [{
"type": "function",
"function": {
"name": "litellm_web_search",
"description": "Search the web",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
}]
}' | jq '.choices[0].message'健康的结果是 content 里有一段正常回答,并引用了网页内容。如果返回的是 tool_calls,里面是对 litellm_web_search 的调用,说明拦截没有发生。LiteLLM 文档给出的原因是:该提供方不在 enabled_providers 里。
这种接法的代价
LiteLLM 站在前面时,new-api 看到的所有流量都来自同一个令牌。所以 new-api 里按用户分配的额度和倍率,不再能区分到具体的人。如果你依赖 new-api 给每个用户发令牌和限额,可以反过来接:在 new-api 里新建一个指向 LiteLLM 的 OpenAI 兼容渠道,LiteLLM 直接连上游厂商。这样计费仍然留在 new-api。但 tools 字段要先穿过 new-api,所以把上面同一个 curl 请求改发到 new-api 的地址,再测一遍,确认拿到的仍是最终回答。
LiteLLM 还能用 object_permission 按密钥或团队限制谁可以用哪个搜索工具。文档说 search_tools 列表为空或不写时允许所有工具,主密钥不受限制。这需要带数据库的部署。预算上限的思路可以参考在 VPS 上控制 AI 智能体的花费。
做法三:上游厂商的原生搜索开关
OpenAI 和阿里云百炼(Model Studio)都提供自带的联网搜索,按调用次数另外计费。下面是 2026-10-17 从两家官方页面读到的价格:
此图表背后的数据
[
{
"provider": "OpenAI web_search \u5de5\u5177",
"usd_per_1000_calls": "10.00"
},
{
"provider": "\u901a\u4e49\u5343\u95ee agent \u7b56\u7565(\u65b0\u52a0\u5761)",
"usd_per_1000_calls": "10.00"
},
{
"provider": "\u901a\u4e49\u5343\u95ee agent \u7b56\u7565(\u5317\u4eac\u3001\u5f17\u5409\u5c3c\u4e9a\u3001\u9999\u6e2f\u3001\u4e1c\u4eac\u3001\u6cd5\u5170\u514b\u798f)",
"usd_per_1000_calls": 0.573411
}
]OpenAI 的 web search 工具每千次调用 10.00 美元,搜索到的内容还要按模型价格另计 token。阿里云的价格随地域变化:新加坡每千次 10.00 美元,北京、弗吉尼亚、香港、东京和法兰克福是 0.573411 美元。阿里云文档还说明,检索到的内容会加进提示词,按输入 token 计费。内置搜索没有免费调用额度,响应里的 count 字段也不能作为计费或对账依据。
和做法二对比:用私有 SearXNG 不付按次的搜索费,但每次搜索多一次模型调用,外加搜索摘要的输入 token,摘要条数随你启用的引擎而变。哪种更便宜,取决于你所用模型的单价和实例实际返回的条数。用自己的账单算一遍最准。
OpenAI:Responses API 的 web_search 工具
OpenAI 文档的写法是在 Responses API 请求里加 "tools": [{"type": "web_search"}]。Chat Completions 只能通过专门的搜索模型联网,比如 gpt-5-search-api,这类模型每次都会先搜索再回答。
通义千问:enable_search
百炼的 OpenAI 兼容 Chat Completions 接口用 "enable_search": true 打开搜索,策略写在 search_options 里。enable_search 不是 OpenAI 的标准参数,所以用 Python SDK 时要放进 extra_body。文档还有两点要注意。第一,OpenAI 兼容协议不会在响应里返回搜索来源。第二,Qwen3.8 系列在 Chat Completions 上不支持 agent 策略,要改用 Responses API 的 web_search 工具。支持搜索的模型列表按地域不同,先在文档里确认你的地域和模型。
怎么判断搜索开关穿过了 new-api
new-api 的 README 已经提醒,协议特有的工具和字段可能映射不准。所以不要从“请求成功”推断“搜索成功”,要看响应本身。
OpenAI 这条路由,看响应里有没有搜索调用的记录:
curl -s http://127.0.0.1:3000/v1/responses \
-H "Authorization: Bearer $NEWAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_MODEL",
"tools": [{"type": "web_search"}],
"input": "今天有哪些关于 SearXNG 的新闻?请给出来源网址。"
}' > resp.json
jq '[.output[].type]' resp.json
jq '.output[] | select(.type=="message") | .content[0].annotations' resp.json按 OpenAI 文档,搜索真的发生时,output 里会有一个 web_search_call 项和一个 message 项。message 的 content[0].annotations 里是 url_citation 对象,包含网址和标题。如果只有 message 而没有 web_search_call,说明搜索没有发生:工具可能在转发途中丢了,也可能上游根本没收到。
通义千问这条路由更难判断,因为 OpenAI 兼容协议不返回搜索来源:
curl -s http://127.0.0.1:3000/v1/chat/completions \
-H "Authorization: Bearer $NEWAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-plus",
"messages": [{"role": "user", "content": "今天上海的天气怎么样?"}],
"enable_search": true,
"search_options": {"search_strategy": "agent"}
}' | jq '.choices[0].message.content, .usage'qwen-plus 是阿里云文档示例里的模型名,换成你在 new-api 里配置的名字。可以用两种方法检查。一是问一个答案每天都在变的问题,把 "enable_search" 分别设为 true 和 false 各请求一次,对比回答内容和 usage 里的输入 token。二是在阿里云控制台的账单里,查看有没有联网搜索的计费项。这两种方法都是间接证据,不是官方给出的判断标准。测完之前,不要假设开关已经生效。
三种做法怎么选
如果只有一两个聊天前端,用户也都通过它们使用模型,选做法一。它不改网关,也没有额外的模型调用。
如果需要让脚本、智能体和所有客户端都能搜索,选做法二。搜索后端在你自己手里,调整搜索引擎只需改 SearXNG。代价是更高的延迟和多出来的模型调用。想把不同任务分给不同模型,可以看编程智能体的多模型路由。
如果已经在用 OpenAI 或通义千问,愿意按次付费,也想要厂商自己的检索结果,选做法三。但它能否穿过 new-api,只能靠上一节的测试来回答。
常见故障和你会看到的报错
403 Forbidden 或 403 Client Error: Forbidden。 SearXNG 没有启用 json 格式。检查 settings.yml 的 search.formats,确认文件确实挂载进了容器,然后重启 SearXNG。
No search tool configured。 LiteLLM 的 search_tools 里没有任何搜索工具。LiteLLM 文档给出的修复是至少添加一个。检查 search_tools 是否写在配置文件的顶层,和 model_list 同级。
模型返回 tool_calls,没有最终回答。 该提供方不在 enabled_providers 里,所以 LiteLLM 把工具调用原样交回了客户端。把对应的提供方加进列表,然后运行 docker restart litellm。
Not Found Error。 LiteLLM 请求 new-api 时路径不对。检查 api_base 末尾是否有 /v1。
搜索接口超时或连接被拒绝。 LiteLLM 和 SearXNG 不在同一个 Docker 网络里,searxng 这个名字解析不到。运行 docker network inspect llm,确认 searxng、litellm 和 new-api 三个容器都在列表里。
搜索有返回,但结果很少。 常见原因是 SearXNG 背后的部分搜索引擎报错或被验证码拦住。用 docker logs searxng 查看引擎报错,再参考修复 SearXNG 引擎的验证码错误。
流式输出一下子全部出来。 这不是故障。ARCHITECTURE.md 说明,拦截会把流式请求先转成非流式,搜索循环结束后再重新组装成流。
Chat Completions 请求在多轮搜索后报错。 按 ARCHITECTURE.md,/v1/chat/completions 达到 max_agentic_loops 上限时会抛出错误,而不是结束这一轮。/v1/messages 在上限时会结束这一轮,返回的文字可能为空。上限可以在 websearch_interception_params 里调整。客户端在请求体里传这个值是无效的。
常见问题(FAQ)
new-api 或 one-api 能直接开启联网搜索吗?
不能。new-api 的 README(2026-10-17 读取)只列出了 Chat Completions、Responses、Anthropic Messages 和 Gemini 四种接口的转发与转换,以及面向图片和视频任务的 JavaScript 插件,没有联网搜索功能。要让回答带上网页结果,可以让客户端自己查 SearXNG,可以在网关前加一层启用 websearch_interception 的 LiteLLM,也可以打开上游厂商的搜索开关,但要亲自测试它能否穿过 new-api。
为什么 LiteLLM 返回了 tool_calls,而不是搜索后的回答?
因为该模型的提供方不在 websearch_interception_params.enabled_providers 里。LiteLLM 只拦截列表中提供方的工具调用,其他的会原样交回客户端。用 openai/ 前缀接入 new-api 的模型,在配置里写 openai。改好后重启 LiteLLM,再发一次带 litellm_web_search 工具的请求,content 里应出现正常回答。
SearXNG 返回 403 Forbidden 是什么原因?
SearXNG 默认只启用 HTML 输出,请求一个没有启用的格式就会得到 403 Forbidden。在 settings.yml 的 search.formats 下同时写上 html 和 json,重启容器,再用 curl -s "http://127.0.0.1:8888/search?q=test&format=json" 确认能拿到 JSON。Open WebUI、Cherry Studio 和 LobeChat 遇到这个问题时报错文字不同,但原因相同。
为什么 max_results 设成 10,SearXNG 还是返回约 20 条结果?
LiteLLM 的 SearXNG 文档说明,SearXNG 不支持通过 API 限制条数,也不直接支持 max_results 参数,所以这个参数不会生效。文档里的约 20 条只是默认情况下的大致数量,不是保证。SearXNG 把各个启用引擎的结果合并成一个列表,实际条数取决于启用了哪些引擎、它们各自返回多少,可能多于也可能少于 20 条。在网关拦截做法里,这些结果都会进入下一次模型请求,按输入 token 计费。
在网关里做联网搜索,每次请求会多花多少钱?
LiteLLM 文档说,每次搜索会多一次带搜索结果的模型调用。一个请求最多触发 max_agentic_loops 轮搜索,默认 3 轮。用私有 SearXNG 没有按次的搜索费,花费来自额外的模型调用和多出来的输入 token。相比之下,OpenAI 的 web search 工具和通义千问的联网搜索都按每千次调用另外收费,具体价格见上文图表。