大语言模型实战(十一)——通义千问 + FastMCP 天气查询机器人:把 API Key 改到 TaoToken 统一管理
1. 通义千问 FastMCP 天气查询机器人为什么要把 API Key 挪到 TaoToken通义千问 FastMCP 天气查询机器人本质上是让大语言模型通过 MCPModel Context Protocol协议去调用一个真实的天气工具而不是靠模型自己“编”天气。FastMCP 负责把 Python 函数暴露成标准工具通义千问负责理解用户意图并决定调用哪个工具两者通过 stdio 通道完成 JSON-RPC 通信。这套组合适合谁适合已经跑通过单模型 Demo、手里攒了三五个 API Key、每次换模型都要翻代码改base_url的开发者。我最早做这个天气机器人时.env里躺着通义千问的QWEN_API_KEY、QWEN_BASE_URL后来想换成别的模型对比效果又加了一组DEEPSEEK_API_KEY、DEEPSEEK_BASE_URL。再后来接 Claude 做工具调用测试.env直接膨胀成六行。问题不在于行数多而在于每次切换都要改客户端初始化代码改完还要重新确认model字段和base_url是否匹配稍不留神就是 401 或者model not found。真正的痛点是 Key 分散带来的三个连锁反应。第一多环境同步困难本地.env、测试机环境变量、CI 里的 secrets 三份内容经常不一致排查半天发现是某台机器少了一个变量。第二切换成本高想从qwen-plus换到qwen-max或者换个兼容 OpenAI 协议的模型得动代码而不是动配置。第三密钥轮换麻烦一旦某个 Key 需要更新所有引用它的脚本都要重新部署。把 API Key 和 Base URL 统一收到 TaoToken 管理解决的正是这三件事。客户端代码里只保留一个base_url指向 TaoToken 的兼容入口api_key用 TaoToken 签发的 Key具体背后路由到哪个模型由配置决定。这样 FastMCP 服务端完全不用动天气工具照常暴露通义千问客户端只改初始化那两行模型切换变成改一个字符串。下面我会给出可直接复制的服务端与客户端配置片段并附一次完整的天气查询链路验证包括预期返回长什么样。需要先说明一点TaoToken 在这里扮演的是统一接入与密钥管理入口不是让你绕过任何合规流程。你仍然需要按各模型厂商的要求正常申请和使用只是把分散的凭证收敛到一处方便工程化管理。这一点在多人协作或者多项目复用时尤其明显。2. TaoToken 前置准备拿到统一 Base URL 与 API Key在动手改代码之前先把 TaoToken 这边的准备工作做完。这一步不复杂但顺序别搞反否则后面客户端初始化会一直报鉴权错误。首先访问官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录后进入控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。在控制台里你能看到账户概览、用量统计以及最关键的 API Keys 管理入口。接着去 API Keys 页面创建密钥地址 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。点新建给它起个能认出来的名字比如weather-mcp-dev方便以后按项目区分。创建完成后密钥只显示一次复制下来存到安全的地方别直接贴进代码提交。然后确认统一接入的 Base URL。TaoToken 的 API 入口是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 OpenAI 兼容客户端的base_url使用。很多 OpenAI SDK 会自动在base_url后面拼/chat/completions所以填的时候不要自己再加/v1之类的后缀除非文档明确要求。我实测下来直接填https://taotoken.net/api就能正常走通对话补全。如果你不确定该用哪个模型 ID可以先去模型对话页面手动试一次地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。在里面选一个模型发一句话确认能返回内容同时记下页面上显示的模型标识。这个标识就是后面客户端model字段要填的值。天气机器人对模型能力要求不高选一个响应快、支持 function calling 的即可。准备工作清单可以对照下面这张表逐项确认项目值说明Base URLhttps://taotoken.net/api不带 UTM不带 /v1API Key控制台创建只显示一次妥善保存Model ID模型对话页确认需支持工具调用接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite参数细节以文档为准这里有个容易踩的坑有人把官网首页地址当成 API 地址填进base_url结果请求打到网页上返回 HTMLSDK 解析 JSON 直接抛异常。记住 API 入口是https://taotoken.net/api和官网首页是两个不同的东西。另外如果你打算长期跑编码类或 Agent 类任务可以了解下 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。天气机器人本身用不上但同一套 Key 管理思路可以复用到更重的场景。前置准备到这就够了接下来进入代码改造。3. 可复制配置FastMCP 服务端与通义千问客户端改造这一节是全文的核心给出能直接粘贴运行的配置片段。改造分两块FastMCP 服务端基本不动只确认工具定义通义千问客户端把api_key和base_url换成 TaoToken 的值。先看服务端。FastMCP 的服务端代码和用哪家模型无关它只负责把天气查询函数暴露成 MCP 工具。核心结构如下文件路径weather/weather.pyfrom mcp.server.fastmcp import FastMCP import httpx mcp FastMCP(weather) NWS_API_BASE https://api.weather.gov USER_AGENT weather-app/1.0 async def make_nws_request(url: str): headers {User-Agent: USER_AGENT, Accept: application/geojson} async with httpx.AsyncClient() as client: try: resp await client.get(url, headersheaders, timeout30.0) resp.raise_for_status() return resp.json() except Exception: return None mcp.tool() async def get_alerts(state: str) - str: 获取指定州的活跃天气警报state 为两个字母的州代码如 CA、NY。 url f{NWS_API_BASE}/alerts/active/area/{state} data await make_nws_request(url) if not data or features not in data: return 无法获取警报数据。 if not data[features]: return 该州当前没有活跃警报。 return \n---\n.join(str(f) for f in data[features]) mcp.tool() async def get_forecast(latitude: float, longitude: float) - str: 获取指定经纬度的天气预报latitude 范围 -90 到 90longitude 范围 -180 到 180。 points_url f{NWS_API_BASE}/points/{latitude},{longitude} points_data await make_nws_request(points_url) if not points_data: return 无法获取该位置的预报数据。 forecast_url points_data[properties][forecast] forecast_data await make_nws_request(forecast_url) if not forecast_data: return 无法获取详细预报。 periods forecast_data[properties][periods] return \n---\n.join( f{p[name]}: {p[temperature]}°{p[temperatureUnit]}, f风 {p[windSpeed]} {p[windDirection]}, {p[detailedForecast]} for p in periods[:5] ) if __name__ __main__: mcp.run(transportstdio)服务端不需要任何 Key它只调用公开的天气 API。真正需要改的是客户端。下面是通义千问客户端的初始化部分文件路径mcp-client/client-qwen.py重点看OpenAI(...)那几行import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), ) MODEL_ID os.getenv(TAOTOKEN_MODEL_ID, qwen-plus)对应的.env文件改成这样把原来分散的QWEN_API_KEY、QWEN_BASE_URL替换掉TAOTOKEN_API_KEYsk-你的TaoToken密钥 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODEL_IDqwen-plus如果你更习惯用 TOML 管理配置比如放在config.toml里可以写成[taotoken] api_key sk-你的TaoToken密钥 base_url https://taotoken.net/api model_id qwen-plus然后在客户端里用tomllib读取。两种方式都行关键是 Base URL、Key、Model ID 三件套齐全且 Base URL 指向https://taotoken.net/api。调用对话补全的地方也要跟着改把硬编码的模型名换成变量response client.chat.completions.create( modelMODEL_ID, messagesmessages, toolstools, tool_choiceauto, )这样改完之后想换模型只需要改.env里的TAOTOKEN_MODEL_ID代码一行不动。我试过从qwen-plus切到另一个兼容模型重启客户端就生效整个过程不到十秒。这就是把 Key 和 Base URL 统一管理带来的直接收益。有一点要提醒base_url末尾不要加斜杠也不要加/v1。OpenAI SDK 会自己拼接路径多写反而会 404。如果你在别的项目里见过https://xxx/v1的写法那是那家服务的要求TaoToken 这边按https://taotoken.net/api填即可具体以接入文档为准。4. 验证请求一次完整的天气查询链路与预期返回配置改完必须跑一次完整链路确认没问题。验证分两步先确认客户端能连上 MCP Server 并列出工具再发一条真实天气查询看返回。启动客户端命令是python client-qwen.py ../weather/weather.py预期输出里应该能看到工具列表 正在连接到 Server: ../weather/weather.py 连接成功可用工具: [get_alerts, get_forecast]如果这一步就报错先别往下走去看第 5 节的排查。工具列表能正常打印说明 MCP 通道通了接下来测模型调用。输入一条查询请输入查询: 加州有没有天气警报预期会看到工具被调用然后模型基于工具返回生成自然语言回答正在调用通义千问... 调用工具: get_alerts参数: {state: CA} 工具返回结果 正在获取最终回答... 回答 ------------------------------------------------------------ 根据最新数据加州目前没有活跃的天气警报天气状况较为稳定。 ------------------------------------------------------------再测一条需要经纬度的预报查询请输入查询: 纽约市的天气预报预期返回 调用工具: get_forecast参数: {latitude: 40.7128, longitude: -74.006} 工具返回结果 回答 ------------------------------------------------------------ 纽约市未来几天预报如下 今晚温度 45°F西风 10-15 mph天气晴朗 明天白天高温 52°F西风 12-18 mph ... ------------------------------------------------------------这里的关键验证点是模型没有自己编天气而是先触发了tool_calls客户端拿到参数后通过 MCP 调用服务端工具再把结果回传给模型生成最终回答。整个链路里TaoToken 只负责模型这一段的鉴权和路由天气数据仍然来自公开 API。如果你想更直观地确认模型侧配置生效可以打开模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 用同一个模型 ID 发一句“你好”确认能正常返回。两边都能通说明 Key 和 Base URL 没问题。验证通过后建议把这次成功的请求参数记下来包括模型 ID、Base URL、工具名。后面换模型或者排查问题时这份记录能帮你快速定位是配置问题还是代码问题。实测下来只要三件套填对第一次就能跑通不需要反复试。5. 本篇常见错误排查401、local proxy failed、reading choices 等这一节按真实报错来对照每条给出原因和改法。这些错误我在不同阶段都遇到过按顺序排查基本能覆盖九成问题。401 Unauthorized / invalid api key报错长这样openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}原因通常是三种Key 复制时带了空格或换行.env没被正确加载Key 本身已失效或被删。先确认.env在客户端运行目录或父目录然后打印一下确认加载成功python -c from dotenv import load_dotenv import os load_dotenv() print(KEY:, os.getenv(TAOTOKEN_API_KEY, NOT FOUND)[:8]) print(URL:, os.getenv(TAOTOKEN_BASE_URL, NOT FOUND)) 如果 Key 显示NOT FOUND说明load_dotenv()没找到文件检查路径。如果 Key 前八位对但请求仍 401去控制台确认这个 Key 是否被禁用。local proxy failed / connection error报错类似openai.APIConnectionError: Connection error. httpx.ConnectError: [Errno 111] Connection refused这类多半是base_url写错或者本机网络环境有额外代理设置干扰。先确认base_url是https://taotoken.net/api没有多余后缀。然后检查环境变量里有没有残留的HTTP_PROXY、HTTPS_PROXY指向了不可用的地址env | grep -i proxy如果有临时清掉再试unset HTTP_PROXY HTTPS_PROXY注意这里说的是清理本机残留的代理环境变量不是让你去配置任何网络工具。企业内网环境可能需要按 IT 要求设置按你们自己的规范来。reading choices / list index out of range报错IndexError: list index out of range或者AttributeError: NoneType object has no attribute choices这通常发生在解析响应时response.choices[0]取不到值。原因可能是模型返回了错误结构或者请求被拦截返回了非预期内容。先打印完整响应看看print(response.model_dump_json(indent2))如果choices为空检查model字段填的模型 ID 是否在 TaoToken 支持列表里。填了一个不存在的模型 ID有的服务会返回空 choices 而不是明确报错。OAuth / token expired如果你在别的工具里见过 OAuth 相关报错比如OAuth token expired, please re-authenticate那是另一套鉴权体系和本篇的 API Key 方式不同。本篇客户端用的是静态 Key不存在 OAuth 刷新问题。如果你同时装了 Claude Code 之类的工具注意区分它们的配置文件别把 OAuth 凭证和 API Key 混在一起。Claude Code 的接入配置在文档里有单独说明地址 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 需要的话对照着看。工具调用参数类型不匹配报错Tool call failed: latitude must be a float, not str这是模型把经纬度当字符串传了。解决办法是在工具函数的 docstring 里把类型和范围写清楚FastMCP 会根据类型注解生成 JSON Schema模型看到number类型就不会传字符串。确保函数签名是latitude: float, longitude: float别写成str。ModuleNotFoundError: No module named mcp客户端能跑但启动 Server 时报这个。原因是客户端用的 Python 环境和 Server 用的不是同一个。在StdioServerParameters里显式指定解释器路径import sys server_params StdioServerParameters( commandsys.executable, args[server_script_path], )用sys.executable能保证 Server 和客户端跑在同一个环境里省去手动找路径的麻烦。排查顺序建议先看 401再看连接错误最后看解析错误。大部分问题集中在第一步和第二步把 Key 和 Base URL 确认对后面基本顺畅。6. 把配置收拢之后长期维护与 CTA天气机器人跑通只是起点真正省心的是后续维护。以前每加一个模型就要动代码现在只需要在.env里改TAOTOKEN_MODEL_ID。团队协作时把.env模板提交到仓库真实 Key 放在各自的本地环境或 CI secrets 里新人拉下来填一个 Key 就能跑不用挨个申请。如果你打算把这个模式扩展到更多工具比如数据库查询、内部 API 调用FastMCP 的mcp.tool()装饰器可以继续加服务端结构不变。客户端那边因为统一了 Base URL换模型做效果对比的成本几乎为零。我试过同一套天气工具分别用两个模型跑只改一个环境变量对比结果很直观。需要长期跑编码类或 Agent 类任务的可以看看 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 同一套 Key 管理思路能直接复用。想手动验证模型效果的去模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。接入过程中遇到参数细节问题查接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。需要新建或轮换密钥去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后留一个实用技巧把TAOTOKEN_MODEL_ID设成环境变量而不是写死在.env里这样在 CI 里可以用矩阵策略同时跑多个模型一份代码覆盖多组对比。天气机器人本身不复杂但把这套配置管理方式固化下来后面接任何 MCP 工具都能少走弯路。