1. 为什么 Function Call 写多了会崩:一个咖啡机场景
如果你用 Python 接过三个以上外部 API,大概率经历过这种崩溃:每个接口一套参数命名,美团叫cup_size,饿了么叫size_code,星巴克叫size;接口一升级,所有调用点全红。Function Call 本身没问题,它解决的是"让模型知道有哪些函数可调",但它不解决"函数背后怎么统一管理、怎么复用、怎么发现"。
我把它类比成办公室咖啡机。Function Call 像是你手把手教每个新同事:美式按哪个键、拿铁先按哪个再按哪个、糖度怎么调。人一多、机器一换,教学成本爆炸。MCP(Model Context Protocol)想做的事,是给办公室装一台"智能咖啡机"——你只需要说"来杯美式",它自己去匹配豆子、水温、杯型,甚至自动选最便宜的那家供应商。
这篇面向 Python 开发者,把 Function Call 和 MCP 的协作方式讲清楚:Function Call 负责"模型决定调什么",MCP 负责"这个调用怎么标准化地落到真实服务上"。我会给一套可复制的 MCP 服务端骨架、Function Call 调试验证动作,以及通过 TaoToken 统一 Key/API 通道接入的方式。适合已经写过 Function Call、但被多接口维护折磨过的同学。
2. 前置准备:TaoToken 统一 Key 与 API 通道
在动手写 MCP 服务端之前,先把"模型侧"的通道理顺。MCP 服务端负责工具执行,但工具调用往往需要模型先产出结构化参数,这就需要一个稳定的模型 API 入口。TaoToken 在这里的角色是统一 Key 和 API 通道,省去你在多个模型供应商之间来回切换配置。
官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=
API 基址(不带 UTM):https://taotoken.net/api
你需要先拿到 API Key,再去配置模型调用。拿 Key 的路径在控制台里,具体入口:
- 控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
- API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
拿到 Key 后,建议先做一次最小验证,确认通道可用,再往下写 MCP。验证模型是否正常,可以直接用模型对话页面:
- 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite
如果你后续要做长期编码或 Agent 类项目,Coding Plan 会更省心:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
接入文档在这里,配置参数以文档为准:
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
注意:API Key 只放在环境变量里,不要硬编码进 MCP 服务端源码,更不要提交到 Git。这是后面排障章节里最常见的翻车点之一。
3. 可复制配置:MCP 服务端骨架 + Function Call 调试
3.1 环境搭建
用 uv 管理虚拟环境,比裸 pip 快且干净。Windows 和 macOS 通用:
# 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh # 创建并激活虚拟环境 uv venv mcp-demo source mcp-demo/bin/activate # Windows 用 mcp-demo\Scripts\activate # 安装依赖 uv pip install "mcp[cli]" httpx pydantic loguru openai这里openai库是用来走 TaoToken 的兼容接口,mcp[cli]是 MCP 官方 SDK,pydantic做参数校验,loguru做日志。
3.2 模型通道配置
把 TaoToken 的 Key 和基址写进环境变量:
export TAOTOKEN_API_KEY="你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"然后在 Python 里初始化客户端:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) resp = client.chat.completions.create( model="gpt-4o-mini", # 具体模型名以接入文档为准 messages=[{"role": "user", "content": "只回复 ok"}], ) print(resp.choices[0].message.content)这一步能打印出ok,说明模型通道通了。如果报 401,先回去检查 Key 是否复制完整;如果报连接错误,检查 base_url 是否写成了带路径的地址。
3.3 MCP 服务端骨架
下面是一个"咖啡采购"MCP 服务端骨架,核心是把不同平台的参数差异收敛到一个工具里。注意参数用 Pydantic 模型校验,避免非法输入。
from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from pydantic import BaseModel, Field import httpx from loguru import logger class BuyCoffeeParams(BaseModel): coffee_type: str = Field(..., description="咖啡类型,如 美式/拿铁") size: str = Field(..., description="杯型,如 中杯/大杯") app = Server("coffee-service") PLATFORM_PARAMS = { "meituan": lambda p: {"product_type": p.coffee_type, "cup_size": p.size}, "eleme": lambda p: {"coffee_type": p.coffee_type.upper(), "size_code": f"SIZE_{p.size}"}, } async def select_platform() -> str: # 真实场景里做比价/比时效,这里简化 return "meituan" async def unified_order(params: BuyCoffeeParams) -> str: platform = await select_platform() payload = PLATFORM_PARAMS[platform](params) logger.info(f"路由到 {platform},参数 {payload}") async with httpx.AsyncClient(timeout=10) as c: # 这里换成真实下单地址 resp = await c.post("https://example.com/order", json=payload) return resp.text @app.list_tools() async def list_tools(): return [ Tool( name="buy_coffee", description="统一咖啡采购,自动适配不同平台参数", inputSchema=BuyCoffeeParams.model_json_schema(), ) ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name != "buy_coffee": raise ValueError(f"未知工具: {name}") params = BuyCoffeeParams(**arguments) result = await unified_order(params) return [TextContent(type="text", text=result)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ == "__main__": import asyncio asyncio.run(main())关键点:inputSchema直接由 Pydantic 模型生成,模型侧看到的参数定义和服务端校验用的是同一份,不会出现"模型传了 size,服务端要 cup_size"的错位。
3.4 Function Call 调试验证
MCP 服务端跑起来后,用 Function Call 做一次端到端验证。把工具定义喂给模型,看它能否产出正确参数:
tools = [{ "type": "function", "function": { "name": "buy_coffee", "description": "统一咖啡采购", "parameters": BuyCoffeeParams.model_json_schema(), }, }] resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "帮我买一杯大杯美式"}], tools=tools, tool_choice="auto", ) msg = resp.choices[0].message if msg.tool_calls: call = msg.tool_calls[0] print("模型决定调用:", call.function.name) print("参数:", call.function.arguments) else: print("模型没有触发工具调用,检查 description 是否清晰")预期输出类似:
模型决定调用: buy_coffee 参数: {"coffee_type": "美式", "size": "大杯"}拿到参数后,再把它交给 MCP 服务端的call_tool执行,整条链路就闭环了。这一步的意义是:模型只负责"决定调什么、传什么参数",MCP 负责"怎么执行、怎么适配平台"。
4. 验证请求与成功结果
把上面的流程串成一个可运行的验证脚本,确认三件事:模型通道通、工具定义被识别、参数能落到服务端。
import json from mcp.server import Server # 1. 模型通道验证 assert client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "ping"}], ).choices[0].message.content # 2. 工具定义验证 schema = BuyCoffeeParams.model_json_schema() assert "coffee_type" in schema["properties"] assert "size" in schema["properties"] # 3. 参数解析验证 params = BuyCoffeeParams(**json.loads('{"coffee_type":"美式","size":"大杯"}')) assert params.size == "大杯" print("三项验证通过,链路可用")成功结果表现为:模型返回tool_calls,参数 JSON 能被 Pydantic 正常解析,服务端日志打印出路由平台和转换后的参数。如果模型不触发工具调用,优先检查description是否写得太模糊——这是 Function Call 最常见的"沉默失败"。
5. 本篇常见错排查
5.1 模型不触发工具调用
现象:msg.tool_calls为空。原因通常是工具description太笼统,或者用户问句和工具能力不匹配。解决:把 description 写成"什么时候该用我",比如"当用户要买咖啡、下单饮品时调用",而不是只写"咖啡采购"。
5.2 参数名对不上
现象:服务端报ValidationError,提示缺少字段。原因:模型侧看到的 schema 和服务端 Pydantic 模型不是同一份。解决:统一用model_json_schema()生成,不要手写两套参数定义。
5.3 401 / 连接错误
现象:模型调用直接报鉴权失败或连接超时。原因:Key 没放进环境变量、base_url 写错、或者把带路径的地址当成了基址。解决:确认TAOTOKEN_BASE_URL是https://taotoken.net/api,Key 从 API Keys 页面重新复制。接入细节以接入文档为准。
5.4 同步客户端拖垮并发
现象:并发一上来服务端就卡死。原因:用了同步httpx.Client或requests。解决:MCP 服务端统一用httpx.AsyncClient,所有工具函数写成async def。
5.5 服务发现没有心跳
现象:某个下游平台挂了,请求一直超时。原因:没有健康检查,故障节点还在被路由。解决:在select_platform里加一层心跳检测,把连续失败的平台临时剔除。
6. 下一步:把通道和工具都管起来
Function Call 和 MCP 不是替代关系。Function Call 让模型能"点单",MCP 让"点单"标准化地落到真实服务。你要做的,是把模型通道和工具执行分开管理:模型通道用 TaoToken 统一 Key,工具执行用 MCP 服务端收敛参数差异。
如果你还在排障阶段,先把 API Keys 和接入文档过一遍:
- API Keys:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
- 接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
如果你要验证模型是否正常产出工具参数,用模型对话页面快速试:
- 模型对话:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite
如果你打算长期做编码或 Agent 类项目,直接上 Coding Plan,省去每次手动配通道的麻烦:
- Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
最后留一个我踩过的坑:MCP 服务端的日志一定要打全,尤其是路由到哪个平台、转换后的参数长什么样。Function Call 出问题时,模型侧往往只告诉你"调用了工具",真正的原因藏在服务端日志里。把日志打清楚,排障时间能砍掉一半。