1. 通义千问 + FastMCP 天气查询机器人:为什么要把 API Key 挪到 TaoToken
通义千问 + FastMCP 天气查询机器人,本质上是让大语言模型通过 MCP(Model 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录。登录后进入控制台,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。在控制台里你能看到账户概览、用量统计,以及最关键的 API Keys 管理入口。
接着去 API Keys 页面创建密钥,地址 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。点新建,给它起个能认出来的名字,比如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_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。在里面选一个模型发一句话,确认能返回内容,同时记下页面上显示的模型标识。这个标识就是后面客户端model字段要填的值。天气机器人对模型能力要求不高,选一个响应快、支持 function calling 的即可。
准备工作清单可以对照下面这张表,逐项确认:
| 项目 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 不带 UTM,不带 /v1 |
| API Key | 控制台创建 | 只显示一次,妥善保存 |
| Model ID | 模型对话页确认 | 需支持工具调用 |
| 接入文档 | https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite | 参数细节以文档为准 |
这里有个容易踩的坑:有人把官网首页地址当成 API 地址填进base_url,结果请求打到网页上返回 HTML,SDK 解析 JSON 直接抛异常。记住 API 入口是https://taotoken.net/api,和官网首页是两个不同的东西。
另外,如果你打算长期跑编码类或 Agent 类任务,可以了解下 Coding Plan,地址 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。天气机器人本身用不上,但同一套 Key 管理思路可以复用到更重的场景。前置准备到这就够了,接下来进入代码改造。
3. 可复制配置:FastMCP 服务端与通义千问客户端改造
这一节是全文的核心,给出能直接粘贴运行的配置片段。改造分两块:FastMCP 服务端基本不动,只确认工具定义;通义千问客户端把api_key和base_url换成 TaoToken 的值。
先看服务端。FastMCP 的服务端代码和用哪家模型无关,它只负责把天气查询函数暴露成 MCP 工具。核心结构如下,文件路径weather/weather.py:
from 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/geo+json"} async with httpx.AsyncClient() as client: try: resp = await client.get(url, headers=headers, timeout=30.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 到 90,longitude 范围 -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(transport="stdio")服务端不需要任何 Key,它只调用公开的天气 API。真正需要改的是客户端。下面是通义千问客户端的初始化部分,文件路径mcp-client/client-qwen.py,重点看OpenAI(...)那几行:
import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) MODEL_ID = os.getenv("TAOTOKEN_MODEL_ID", "qwen-plus")对应的.env文件改成这样,把原来分散的QWEN_API_KEY、QWEN_BASE_URL替换掉:
TAOTOKEN_API_KEY=sk-你的TaoToken密钥 TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL_ID=qwen-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( model=MODEL_ID, messages=messages, tools=tools, tool_choice="auto", )这样改完之后,想换模型只需要改.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_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,用同一个模型 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(indent=2))如果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_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,需要的话对照着看。
工具调用参数类型不匹配
报错:
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( command=sys.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_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,同一套 Key 管理思路能直接复用。想手动验证模型效果的,去模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。接入过程中遇到参数细节问题,查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。需要新建或轮换密钥,去 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
最后留一个实用技巧:把TAOTOKEN_MODEL_ID设成环境变量而不是写死在.env里,这样在 CI 里可以用矩阵策略同时跑多个模型,一份代码覆盖多组对比。天气机器人本身不复杂,但把这套配置管理方式固化下来,后面接任何 MCP 工具都能少走弯路。