首页 / 资讯中心 / 文章详情

如何用 SearXNG 给本地 Qwen3 搭一个能联网搜索的 MCP?TaoToken 统一 Key 接入实践

如何用 SearXNG 给本地 Qwen3 搭一个能联网搜索的 MCP?TaoToken 统一 Key 接入实践 ★ FEATURED ARTICLE
1. 本地 Qwen3 联网搜索为什么总差一口气你本地跑着 Qwen3-32B-FP8vLLM 起得好好的问它“今天有什么新发布”它一本正经地编。原因不复杂模型权重是训练截止那一刻的快照它没有“现在”这个概念。想让它回答实时问题只有两条路——要么把搜索结果塞进 prompt要么让它自己学会调工具。前者你每次手动复制粘贴后者才是 MCPModel Context Protocol要解决的事。MCP 的本质是给大模型一套标准化的“手和脚”。模型不直接上网它输出一个结构化的工具调用请求你的客户端代码去执行真正的搜索再把结果回填给模型模型基于真实网页内容组织答案。整条链路里SearXNG 负责“搜”MCP Server 负责“把搜索包装成模型能看懂的工具”Qwen3 负责“决定搜什么、怎么总结”。这套方案适合谁适合已经有一台能跑 32B 量化模型的机器、想让本地模型具备实时问答能力的人适合不想把提问内容发给外部搜索 API、希望搜索链路自己掌控的人也适合正在学 MCP 协议、想找一个完整可跑通的端到端案例的开发者。我试过把这套链路跑通之后问“2025 年杭州高考新政策是什么”模型会先输出一段思考然后触发 searxng 工具拿到 9 条真实结果最后分点作答并提示以官方发布为准——这个体验和纯本地模型完全不是一个量级。整条链路有三个组件需要你亲手搭SearXNG 提供 JSON 格式的搜索接口一个 FastMCP/FastAPI 写的 MCP Server 把搜索封装成 SSE 工具一个聊天客户端负责和 Qwen3 对话并处理 tool_calls。下面按顺序来每一步都给可复制的配置和代码。2. TaoToken 统一 Key 接入让模型调用不再到处找地址在搭 MCP 之前先把模型这一端的接入方式理顺。本地 vLLM 当然可以直接用http://127.0.0.1:8700/v1/chat/completions但如果你同时还想用云端更强的模型做对比、或者团队里几个人共用一套 Key、又或者你不想在每台机器上重复配置模型地址那统一入口就很有必要。TaoToken 在这里扮演的角色是“模型调用的统一网关”。你只需要一个 Base URL 和一个 API Key就能在本地 Qwen3、云端模型之间切换客户端代码里的MODEL_API_URL和HEADERS只改一处。对于本篇的 MCP 链路来说这意味着你的聊天客户端可以先用本地 Qwen3 验证工具调用逻辑验证通过后把地址一换就能接别的模型MCP Server 和 SearXNG 完全不用动。具体怎么拿 Key访问https://taotoken.net/api-keys登录后创建一个 API Key复制保存。这个 Key 就是后面所有请求里Authorization: Bearer 你的Key的部分。Base URL 用https://taotoken.net/api注意这个地址不带任何查询参数直接作为 OpenAI 兼容接口的前缀。模型 ID 这块要留意如果你走 TaoToken 调云端模型Model ID 填你实际要用的模型名如果你还是想调本地 vLLM 的 Qwen3-32B-FP8那 Model ID 保持Qwen3-32B-FP8Base URL 换回你本地的http://127.0.0.1:8700/v1。两种方式客户端代码结构完全一样只是配置项不同。提示本地 vLLM 启动时记得加--enable-auto-tool-choice --tool-call-parser hermes否则 Qwen3 不会输出tool_calls字段MCP 链路直接断在第一步。这个参数很多人会漏。如果你更习惯用 Claude Code 这类编码工具来调试 MCP可以在~/.claude/settings.json里配置{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: 你的TaoToken Key, ANTHROPIC_MODEL: Qwen3-32B-FP8 } }这样 Claude Code 的请求也会走统一入口和你的聊天客户端共用同一个 Key。接入文档在https://taotoken.net/doc里面有各客户端的完整配置示例。3. SearXNG 容器启动与 JSON API 配置SearXNG 是一个开源的元搜索引擎它本身不存网页而是把多个搜索源的结果聚合起来返回。我们要用的是它的 JSON 输出能力这样 MCP Server 才能拿到结构化数据。先建目录并拉代码。假设你放在/mnt/program/MCPmkdir -p /mnt/program/MCP cd /mnt/program/MCP git clone https://github.com/searxng/searxng.git cd searxng用 conda 建一个 Python 3.10 环境避免和系统 Python 打架conda create -n MCP_env python3.10 -y conda activate MCP_env pip install -r requirements.txt关键一步是改配置。编辑searxng/settings.yml找到search模块把formats改成包含jsonsearch: formats: - html - json同时把server模块里的secret_key和bind_address改一下server: secret_key: 换成你自己的随机字符串 bind_address: 0.0.0.0 port: 5300端口这里我用了 5300和后面 MCP Server 里的地址保持一致。启动python -m searx.webapp浏览器访问http://你的IP:5300/search?q测试formatjson能看到 JSON 结果就说明 API 通了。如果返回的是 HTML 或者报 403回去检查formats里有没有json以及limiter是不是把请求拦了——本地调试可以在settings.yml里把limiter: false临时关掉。注意SearXNG 默认会限制频繁请求生产环境建议保留 limiter 并配置 Redis。本地自用关掉问题不大但别把这个端口暴露到公网。4. MCP Server 封装把搜索变成模型能调的工具MCP Server 的职责很单一暴露一个 SSE 接口接收搜索关键词调 SearXNG 的 JSON API把结果流式返回。用 FastAPI 写最直接。先装依赖pip install fastapi uvicorn requests fastmcp新建SEMCP.py核心逻辑如下。注意AUTH_TOKEN和SEARXNG_URL要和你实际环境一致from fastapi import FastAPI, Request, HTTPException from fastapi.responses import StreamingResponse import requests, logging, json, asyncio from logging.handlers import RotatingFileHandler AUTH_TOKEN 123456 HOST 0.0.0.0 PORT 5100 SEARXNG_URL http://127.0.0.1:5300/search logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.StreamHandler(), RotatingFileHandler(searxng.log, maxBytes5*1024*1024, backupCount3) ] ) app FastAPI() def validate_token(token: str) - bool: return token fBearer {AUTH_TOKEN} def call_searxng_api(query: str) - dict: url f{SEARXNG_URL}?q{query}formatjson try: response requests.get(url, timeout40) response.raise_for_status() raw_data response.json() if not raw_data.get(results): return {message: 未找到相关结果} return raw_data except requests.exceptions.RequestException as e: logging.error(f请求 SearXNG API 失败: {e}) return {error: str(e)} except json.JSONDecodeError as e: logging.error(fJSON 解析失败: {e}) return {error: 返回的数据不是有效的 JSON} app.get(/sse) async def sse_endpoint(request: Request, id: str): token request.headers.get(Authorization, ) if not validate_token(token): raise HTTPException(status_code403, detailInvalid token) search_results call_searxng_api(id) if error in search_results: raise HTTPException(status_code500, detailsearch_results[error]) async def event_stream(): if message in search_results: yield fdata: {json.dumps(search_results, ensure_asciiFalse)}\n\n else: yield fdata: {json.dumps(search_results, ensure_asciiFalse, indent4)}\n\n yield data: 请求完成\n\n return StreamingResponse(event_stream(), media_typetext/event-stream) if __name__ __main__: import uvicorn logging.info(f启动 MCP 服务监听 {HOST}:{PORT}) uvicorn.run(app, hostHOST, portPORT)启动python SEMCP.py测试一下 SSE 接口是否正常curl -N -H Authorization: Bearer 123456 http://127.0.0.1:5100/sse?id杭州天气能看到data:开头的 JSON 流就说明 MCP Server 通了。这里有个细节SSE 返回的是text/event-stream客户端解析时不能直接resp.json()要按行读、拼data:后面的内容再json.loads。后面客户端代码里会处理。5. Qwen3 函数调用客户端与端到端验证客户端要做三件事把 searxng 定义成 tool、发第一次请求让模型决定是否调用、如果模型返回tool_calls就去调 MCP Server 拿结果、再发第二次请求让模型总结。新建chat_client.pyimport re, requests, json MODEL_API_URL http://127.0.0.1:8700/v1/chat/completions MODEL_NAME Qwen3-32B-FP8 HEADERS {Content-Type: application/json} MCP_SSE_URL http://127.0.0.1:5100/sse MCP_AUTH_TOKEN 123456 tools [{ type: function, function: { name: searxng, description: 通过 MCP 的 SSE 接口做网络搜索并返回详细条目, parameters: { type: object, properties: { id: {type: string, description: 搜索关键词} }, required: [id] } } }] def parse_sse_stream(resp): data_buffer parsed_object None for raw in resp.iter_lines(decode_unicodeTrue): line raw.strip() if not line: if data_buffer: try: parsed_object json.loads(data_buffer) data_buffer except json.JSONDecodeError: pass continue if line.startswith(data:): payload line[len(data:):].strip() if payload 请求完成: if data_buffer and parsed_object is None: try: parsed_object json.loads(data_buffer) except json.JSONDecodeError: pass continue data_buffer payload else: data_buffer line if data_buffer and parsed_object is None: try: parsed_object json.loads(data_buffer) except json.JSONDecodeError: pass return parsed_object def call_searxng(id_value): headers {Authorization: fBearer {MCP_AUTH_TOKEN}} params {id: id_value} try: resp requests.get(MCP_SSE_URL, headersheaders, paramsparams, streamTrue, timeout60) resp.raise_for_status() except requests.exceptions.RequestException as e: print(f[call_searxng] 请求失败: {e}) return [] parsed parse_sse_stream(resp) if not parsed: return [] results parsed.get(results, []) return [e for e in results if isinstance(e, dict) and e.get(title) and e.get(url)] def chat_with_tools(user_prompt): messages [{role: user, content: user_prompt}] payload { model: MODEL_NAME, messages: messages, temperature: 0.6, top_p: 0.95, tools: tools } r1 requests.post(MODEL_API_URL, jsonpayload, headersHEADERS, timeout60) r1.raise_for_status() reply1 r1.json()[choices][0][message] if tool_calls in reply1 and reply1[tool_calls]: call reply1[tool_calls][0][function] name call.get(name) args json.loads(call.get(arguments, {})) if name searxng: search_id args.get(id) results call_searxng(search_id) if search_id else [] tool_content json.dumps(results, ensure_asciiFalse, separators(,, :)) else: tool_content json.dumps({error: fUnknown tool: {name}}, ensure_asciiFalse) if reply1.get(content): messages.append({role: assistant, content: reply1[content]}) messages.append({role: assistant, tool_calls: reply1[tool_calls]}) messages.append({role: tool, name: name, content: tool_content}) payload2 {model: MODEL_NAME, messages: messages} r2 requests.post(MODEL_API_URL, jsonpayload2, headersHEADERS, timeout60) r2.raise_for_status() return r2.json()[choices][0][message].get(content, 模型未返回内容) return reply1.get(content, 模型未返回内容) if __name__ __main__: print(输入问题输入 退出 结束) while True: user_input input(\n你: ) if user_input.lower() in [退出, exit, quit]: break if not user_input.strip(): continue print(\n模型:) print(chat_with_tools(user_input))跑起来之后问“2025 年杭州高考新政策是什么”你会看到模型先输出一段think思考然后触发searxng工具MCP Server 返回 9 条结果模型再基于这些结果分点作答。整个链路里模型没有编造每一条信息都能追溯到搜索结果里的标题和 URL。如果你把MODEL_API_URL换成https://taotoken.net/api/v1/chat/completionsHEADERS里加上Authorization: Bearer 你的TaoToken Key就能用同一套客户端代码调云端模型做对比。MCP Server 和 SearXNG 完全不用改。6. 常见报错排查401、local proxy failed、reading choices链路跑不通的时候报错信息通常指向很具体的位置。下面这几个是我实际踩过的。401 Unauthorized出现在调 MCP Server 或调模型时。先确认Authorization头格式是Bearer token中间有一个空格。MCP Server 里的AUTH_TOKEN和客户端里的MCP_AUTH_TOKEN必须完全一致。如果调模型报 401检查 TaoToken Key 有没有复制完整或者本地 vLLM 有没有设--api-key。local proxy failed / Connection refused客户端连不上 MCP Server 或 SearXNG。按顺序查SearXNG 的 5300 端口通不通curl http://127.0.0.1:5300/search?qtestformatjsonMCP Server 的 5100 端口通不通curl -N -H Authorization: Bearer 123456 http://127.0.0.1:5100/sse?idtest。如果两个都通但客户端还是报错检查客户端里的 IP 是不是写成了127.0.0.1而服务实际在另一台机器上。reading choices 报错 / KeyError: choices模型返回的 JSON 里没有choices字段。常见原因是 vLLM 没加--enable-auto-tool-choice模型把 tool 定义当普通文本处理了返回的是纯 content 而不是 tool_calls。另一个原因是请求体里tools字段格式不对检查type是不是function、function.name和parameters是否完整。OAuth / 403 Invalid tokenMCP Server 的 token 校验没过。除了检查 token 值还要注意 SSE 请求的 header 里Authorization有没有被中间层改写。如果你在客户端和 MCP Server 之间加了反向代理确认代理没有把 Authorization 头吃掉。模型不触发工具调用Qwen3 有时候会直接回答而不调工具。可以在 system prompt 里加一句“涉及实时信息时必须调用 searxng 工具”或者在 tool description 里写得更明确。另外temperature别设太低0.6 左右比较合适。SSE 解析出空结果parse_sse_stream里拼data:后面的内容时如果 JSON 跨多行要确保空行才触发解析。SearXNG 返回的 JSON 如果很大可能会被拆成多个data:行这时候要一直拼到空行再json.loads。排障的时候养成看日志的习惯MCP Server 的searxng.log会记录每次请求的 URL 和响应状态码vLLM 的日志会显示模型实际收到的 prompt 和输出的 tool_calls。两边一对问题基本就定位了。7. 把链路固定下来systemd 与后续调优手动python SEMCP.py只适合调试。要让这套东西长期跑着用 systemd 管起来最省心。两个服务searxng-webapp.service先起semcp.service依赖它。/etc/systemd/system/searxng-webapp.service[Unit] DescriptionSearXNG Web Application Afternetwork.target [Service] Typesimple Userroot Grouproot WorkingDirectory/mnt/program/MCP/searxng ExecStart/bin/bash -c source /root/miniconda/etc/profile.d/conda.sh conda activate MCP_env exec python -m searx.webapp Restartalways RestartSec5 [Install] WantedBymulti-user.target/etc/systemd/system/semcp.service[Unit] DescriptionSearXNG SEMCP Service Afternetwork.target searxng-webapp.service [Service] Typesimple Userroot Grouproot WorkingDirectory/mnt/program/MCP/searxng ExecStart/bin/bash -c source /root/miniconda/etc/profile.d/conda.sh conda activate MCP_env exec python SEMCP.py Restartalways RestartSec5 [Install] WantedBymulti-user.target然后sudo systemctl daemon-reload sudo systemctl enable searxng-webapp.service semcp.service sudo systemctl start semcp.service sudo systemctl status searxng-webapp.service semcp.servicesemcp.service的After里写了searxng-webapp.service启动 semcp 时会自动先拉 searxng。看日志用journalctl -u semcp.service -f。后续调优有几个方向SearXNG 的搜索源可以在settings.yml的engines里按需开关关掉慢的源能明显降低延迟MCP Server 可以加一层缓存相同关键词短时间内不重复请求 SearXNG客户端可以把多轮对话的messages保留下来让模型在追问时能利用上下文。如果你想让模型在回答里带上来源链接在第二次请求的 prompt 里加一句“请在回答中标注信息来源的标题和 URL”Qwen3 会照做。整套链路跑通之后你得到的是一个完全本地可控的联网问答系统搜索走 SearXNG工具调用走 MCP模型走本地 Qwen3 或 TaoToken 统一入口。换模型、换搜索源、换客户端各组件之间解耦改一处不影响其他。
阅读完成 · 觉得有帮助?
咨询建站