1. uv 建环境的八步照抄,分水岭在 .env
原文章最讨巧的地方,是用 uv 替代 conda 来搭 MCP 客户端环境,新手少了一层"conda 装不上、频道找不到"的折腾。uv init、uv venv、activate、uv add openai python-dotenv httpx、uv pip install setuptools wheel 这几步,本身和"用哪家模型的 API"完全无关,所以这次接入配置的改写,前八步一个字都不用换。真正要动的是第九步和第十步:原文让你去 DeepSeek 的 API 平台注册、创建接口、充 1 块钱,然后把BASE_URL指向https://api.deepseek.com。这一次把这两步换成先在 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册、创建 API Key,再把.env里的BASE_URL改成统一通道地址。client_new.py 和 server.py 里的 Function Calling 链路一行不改,天气查询照样能跑通。
这篇文章落点在"接入配置",所以我会按原文的十四步顺序走,前面几步快速带过,重点放在 .env、client_new.py 里真正用到 BASE_URL 的那一行、以及uv run client_new.py server.py跑起来之后怎么判断 TaoToken 的 Key 是不是真的接通。
1.1 为什么原文用 uv 而不是 conda
conda 的问题是它不只是一个 Python 下载器,它还管频道、管系统级依赖,遇到公司网络或者国内镜像没配好就容易卡在 Solving environment。uv 的定位更窄,它就是个比 pip 快的 Python 与包管理工具,uv venv建出来的虚拟环境结构和 venv 一样,.\.venv\Scripts\activate之后你看到的提示符也没有区别。
对新手的实际意义是:项目级依赖放在pyproject.toml,环境隔离干净,卸载重装成本低。MCP 的客户端脚本本身依赖很少,就 openai、python-dotenv、httpx 三个主包,用 uv 一次uv add就都装好了,不必为了一个天气 demo 去折腾 conda 的 channel 优先级。
所以这一步沿用原文,不做替换。后面凡是涉及"要连哪家模型"的地方,才切到 TaoToken。
1.2 前八步命令回放
在项目根目录的终端里依次执行:
pip install uv uv init uv venv .\.venv\Scripts\activate uv add openai python-dotenv httpx uv pip install setuptools wheel注意第五条是uv add、第六条是uv pip install,这两个不要混:uv add会把包写进项目的依赖清单,uv pip install是临时往环境里塞包,用来解决解释器基础组件缺失的问题。原文把这一点点出来了,是踩过的经验,不改。
这八步做完,.venv目录就有了,后续所有 python 命令都在激活状态下执行。环境就绪,接下来才是本篇文章真正改写的部分。
2. 第九步改写:DeepSeek 平台注册换成 TaoToken 创建 Key
原文第九步是"登录 DeepSeek 的 API 平台,注册、创建接口、充 1 块钱"。这一步在很多新手眼里是最容易卡住的:注册、实名、充值、找到"接口密钥"页面,走完一圈才拿到一把sk-开头的 Key。这一篇的改写方案,是把这一步整体替换成去 TaoToken 官网创建 Key。
2.1 原来的做法为什么会让新手卡住
不是因为 DeepSeek 平台不好用,而是因为你会开始纠结:我这把 Key 只能用 DeepSeek 一家的模型吗?回头想换成别的模型,是不是又得在这家平台再注册一遍、再充一次?MCP 客户端里模型名和 BASE_URL 都写死在.env,换一次模型就要改一遍 base_url。
另一种常见情况是已经有多把 Key 散落在不同项目里,用哪个、去哪看用量,慢慢就乱了。所以接入方式的选择,其实解决的是"同一套 MCP 客户端能不能反复换模型而不重写配置"这个问题,跟模型本身能力强不强是两回事。
2.2 打开官网拿一把 Key
先打开 TaoToken 注册并登录,进控制台创建一把 API Key。Key 请存到本地安全位置,本文里一律用占位符YOUR_API_KEY指代。
同一页面也能看到模型广场当时的模型列表,deepseek-chat这个模型 ID 以模型广场当时列表里的写法为准,不要凭印象自己加日期后缀。原文.env里MODEL=deepseek-chat保持不变,这一步只是把 Key 的来源从 DeepSeek 官方换成了 TaoToken。后面确实想换模型,也是回到这个页面看列表,改.env里一行MODEL就行,客户端脚本不用动。
到这一步为止,.env需要的三件材料已经齐了:一把 Key(YOUR_API_KEY)、一个通道地址(下一节写)、一个模型 ID(deepseek-chat)。
3. 第十步改写:.env 里的 BASE_URL 指向 TaoToken 统一通道
原文第十步是在项目根目录新建.env,填三行:BASE_URL=https://api.deepseek.com、MODEL=deepseek-chat、OPENAI_API_KEY="sk-xxxxxxxxx"。这一篇只改第一行,其余两行的键名和写法不动。
3.1 .env 最终长这样
BASE_URL=https://taotoken.net/api MODEL=deepseek-chat OPENAI_API_KEY="YOUR_API_KEY"三行,键名分别是BASE_URL、MODEL、OPENAI_API_KEY,和 client_new.py 里os.getenv读的三个变量名一一对应。YOUR_API_KEY换成你从 TaoToken 控制台 创建的那把 Key,引号可以加可以不加,加了更保险,避免开头结尾多个空格。
注意BASE_URL里填的是https://taotoken.net/api,这是给客户端脚本用的接口根地址,不是给人点的网页。网页注册、看模型、看用量走的是https://taotoken.net/?utm_source=taotoken_aicg_blog_end,两者不要混:.env里千万不要把带?utm_source=的那串网址填进去,写在脚本里跑起来会报错。
3.2 为什么末尾不能加 /v1
OpenAI 官方 SDK 的惯例是 base_url 到主机名就停,版本路径/v1由 SDK 内部的chat.completions.create拼接,实际请求的路径是{base_url}/chat/completions。如果你的 base_url 末尾又写了/v1,最终请求会变成.../v1/chat/completions,而 TaoToken 的兼容入口已经处理了这一段,多出来的/v1会得到 404。
所以.env里就写https://taotoken.net/api,结尾不带斜杠、不带/v1、不带任何查询参数。这是本篇文章最容易一脚踩空的细节。
3.3 .env 加载的时机
client_new.py 里第一行代码就是:
from dotenv import load_dotenv load_dotenv()load_dotenv()在模块导入阶段就执行,把.env里的三个值塞进环境变量。MCPClient.__init__里再os.getenv读出来。所以.env必须放在你执行uv run时的当前工作目录下。如果你在别的子目录里执行,load_dotenv()找不到文件,self.openai_api_key就是None,脚本会在这里抛未找到 OpenAI API Key。
稳妥的做法:每次uv run client_new.py server.py之前,先cd到项目根目录。
4. server.py 与 client_new.py:MCP 的 Function Calling 链路没动
原文第十二、十三步生成的两个脚本,是本篇技术主体,比配 Key 的篇幅长得多。它们的职责分离得很清楚:server.py 晒出query_weather这个工具,client_new.py 负责让模型自己决定要不要调它。
4.1 server.py 只干天气这一件事
server.py 用FastMCP("WeatherServer")起一个 stdio 服务,里面两个函数:
fetch_weather(city):拿httpx.AsyncClient向 OpenWeather 的https://api.openweathermap.org/data/2.5/weather发请求,参数是q=city、appid=OPENWEATHER_API_KEY、units=metric、lang=zh_cn,返回原始 JSON。format_weather(data):把 JSON 里的城市、温度、湿度、风速、天气描述抽出来,拼成几行可读文本。
真正对外暴露的工具是@mcp.tool()装饰的query_weather(city: str) -> str,它调用上面两个函数,返回拼好的字符串。这里的city要求传英文名,比如Beijing、Tokyo,因为 OpenWeather 的城市检索参数是按英文索引的。最后mcp.run(transport='stdio')让 server 以标准输入输出的方式和客户端通信。
server.py 里要替换的 Key 只有一处:API_KEY = "xxx"换成你自己从 OpenWeather 官网申请到的 Key。这一步和 TaoToken 无关,原文第十一步就是去 openweathermap.org 注册、拿 Key,照旧。
4.2 client_new.py 里真正用到 BASE_URL 的是哪一行
client_new.py 里和 TaoToken 直接相关的其实就一行:
self.client = OpenAI(api_key=self.openai_api_key, base_url=self.base_url)self.openai_api_key来自.env的OPENAI_API_KEY,self.base_url来自.env的BASE_URL。也就是说,MCP 客户端调用大模型的所有请求,都从这个OpenAI实例发出。把.env里的BASE_URL指向https://taotoken.net/api,就等于把 client_new.py 的模型调用切到 TaoToken 的兼容通道上,其他代码一行不动。
再往下看process_query,逻辑分四步:
- 用
self.session.list_tools()问 server 要工具清单,把每个工具转成 OpenAI 的tools参数格式(type: function加上function.name、description、input_schema)。 - 调
self.client.chat.completions.create(model=..., messages=..., tools=...),把用户问题和工具清单一并交给模型。 - 检查
content.finish_reason,如果是tool_calls,就解析出tool_name和tool_args,通过self.session.call_tool让 server 真正执行。 - 把模型的工具调用和工具执行结果都追加进
messages,再请求一次模型,生成最终的自然语言回答。
第二步和第四步用的都是同一个self.client,所以只要.env里 base_url 是对的,整条链路都跟着走 TaoToken,不需要分段改。
4.3 MCP 客户端里加一句日志有好处
原文的process_query里插了好几个print(11111)、print(2222)、print(33333)。这种打印在生产代码里不合适,但在调试 MCP 时非常有用,因为它能让你看清卡在哪一步:是工具清单没拿到、还是模型没返回 tool_calls、还是工具执行失败。第一次跑通之前,留着这些调试打印没坏处。
5. uv run client_new.py server.py 之后怎么判断 TaoToken 真接通了
原文第十四步就是uv run client_new.py server.py。这一句跑起来,进程会以 stdio 方式拉起 server.py,同时启动聊天循环。判断 TaoToken 的 Key 和 Base URL 是不是真的把 client_new.py 里的模型调用配通,看下面三个信号。
5.1 三个信号说明链路走通了
第一个信号是启动后打印出工具清单:
已连接到服务器,支持以下工具: ['query_weather']这说明 server.py 起来了、stdio 通道通了、工具被客户端注册到了,这一步和 TaoToken 无关,但它是后面 Function Calling 的前提。
第二个信号是你输入Beijing之后,控制台出现类似[Calling tool query_weather with args {'city': 'Beijing'}]这样一行。这表示模型在第一次chat.completions.create时返回了finish_reason == "tool_calls",也就是说模型那边确实收到了我们传的工具清单,并且决定调用query_weather——这已经意味着 TaoToken 这条通道完成了第一次模型往返。
第三个信号是最后输出的天气文本,包含城市名、温度、湿度、风速和天气描述。这一行是模型拿到工具执行结果之后,第二次请求生成的最终回答。能走到这里,就说明 TaoToken 的 Key、Base URL、模型 ID 三件东西都对上了,MCP 客户端里的模型调用被配通了。
5.2 结果对不上时的常规自查
如果第一个信号就没出现,问题在 server.py 或 uv 环境,不在 TaoToken;先确认uv run的当前目录里有server.py,以及 OpenWeather 的 Key 填了。
如果出现第二个信号但没有第三个信号,通常是第二次chat.completions.create出错了,把异常打印出来看,多半是.env里 base_url 写错或模型 ID 写错。
如果两个信号都没有,直接在输入城市名之后看到 API 报错,那基本就是.env里的OPENAI_API_KEY或BASE_URL的问题,对照下一节的排障段。
6. 排障:401、模型名、工具没触发
跑 MCP + Function Calling 最容易撞的三类错,和本篇配置强相关,逐个对一下。
6.1 401 Unauthorized
第一种可能:OPENAI_API_KEY没取到。检查.env是不是放在执行uv run的目录下,可以用一段最小测试确认:
from dotenv import load_dotenv import os load_dotenv() print(os.getenv("OPENAI_API_KEY")) print(os.getenv("BASE_URL"))第二种可能:Key 复制时带了空格或换行。.env里用双引号包住 Key 能避免尾部空格被截。
第三种可能:Key 本身已经失效或者未激活。回到 TaoToken 控制台 重新生成一把,替换.env里那一行,重新执行脚本即可。
6.2 404 或者 "model not found"
常见原因是BASE_URL末尾多写了/v1。改成https://taotoken.net/api就好,别加斜杠。
另一个原因是MODEL写成了一个当前模型广场列表里不存在的 ID,比如自己凭印象加了日期后缀。回到网站看模型广场当时的写法,把.env里MODEL一字不差地抄过来。
6.3 模型没有调用 query_weather
表现为:输入Beijing,模型直接用自己的知识回答天气,而没有出现[Calling tool query_weather ...]。这不是通道问题,而是工具描述没让模型产生调用意愿。可以从两个方向调:
第一,server.py里query_weather的 docstring 要写清楚"输入指定城市的英文名称,返回今日天气",如果原来的文档不足以让模型判断何时调用,可以再补一句"当用户询问某个城市的天气时使用此工具"。
第二,输入时把意图说清楚,比如直接输入Beijing而不是一句含糊的话,模型更容易识别成天气查询。跑通一次之后再尝试更自然的表达,看模型的判定边界。
7. 跑通之后回控制台对一下这次调用
前面六节走完,.env里三行配置指向 TaoToken,client_new.py里的模型调用走兼容通道,server.py里的query_weather通过 MCP 的 Function Calling 触发,整条天气查询链路就跑起来了。跑通之后建议回控制台看一眼这次调用有没有被记上,这一步能帮你确认计费口径,后面换模型或调整并发时心里有数。
具体入口按你的下一步动作挑:
- 想先用同一把 Key 发一条文本消息,验证模型 ID 和通道地址没填错,可以去 TaoToken 模型对话,这里能直接看到模型的返回。
- 准备把这个 MCP 客户端当成日常写代码的助手,去 Coding Plan 看看套餐是否够用。
- 需要再建一把 Key 分给别的项目,控制台 API Keys 里创建,Key 一律用
YOUR_API_KEY这种占位符管理,不要写进任何提交到仓库的文件。 - 如果你顺手想把这套 Function Calling 的思路挪到 Claude Code 之类的执行工具上,参考 Claude Code 接入文档 里的环境变量写法,把
ANTHROPIC_BASE_URL也指向同一个通道。
一个提醒:MCP 客户端里的query_weather只能查天气,不要因为看到call_tool就顺手把本地目录、数据库连接之类的东西挂进去。工具描述的边界,就是模型能做事的边界。