1. 从一次 Agent 工具链选型说起:MCP 与 Function Calling 到底差在哪
如果你正在做 AI Agent 的工具链选型,大概率会在某个深夜盯着屏幕纠结:Function Calling 已经能跑通了,为什么还要引入 MCP?MCP 听起来更"标准",但多一层 Server 是不是纯属给自己找麻烦?
我先把结论摆在前面:Function Calling(下称 FC)解决的是"模型能不能调用工具",MCP(Model Context Protocol,模型上下文协议)解决的是"不同模型和不同工具之间怎么用同一套标准对接"。前者是模型的原生能力,后者是模型与工具之间的通用协议层。它们不是替代关系,而是分工关系。
FC 的典型形态是:你在请求里带上一个tools数组,里面是 JSON Schema 描述的函数签名,模型判断需要调用时返回一个结构化的tool_calls,你执行完把结果塞回对话,模型再继续生成。整个过程没有中间层,链路是"用户 → 模型 → 你的代码 → 工具 API"。
MCP 的典型形态是:你把工具按协议封装成一个 MCP Server,通过 stdio 或 Streamable HTTP 暴露出去,任何兼容 MCP 的客户端(Claude Desktop、Cline、Cursor、Claude Code 等)都能发现并调用这些工具。链路变成"用户 → 模型 → MCP Client → MCP Server → 工具 API",多了一层协调层。
这篇文章面向正在选型的开发者,我会用可复制的配置和可复现的验证步骤,把两条路线都跑一遍。你会看到同一个"查询订单状态"的工具,用 FC 怎么写、用 MCP 怎么写,各自的配置文件长什么样,请求发出去之后返回什么,以及踩坑时那些报错到底在说什么。读完你应该能自己判断:下一个项目该用 MCP,还是 FC 已经够了。
需要说明的是,本文所有调用示例都通过统一的 API 入口发起,Base URL 使用https://taotoken.net/api,这样无论你最终选 FC 还是 MCP,模型侧的接入方式是一致的,方便对比。
2. 前置准备:用 TaoToken 统一模型入口,再决定 FC 还是 MCP
在对比两种工具调用方式之前,得先有一个能稳定发起模型请求的入口。否则你会在"模型连不上"和"工具调不通"两个问题之间反复横跳,根本分不清是协议的问题还是网络的问题。
我的做法是先把模型入口固定下来,用 TaoToken 作为统一的 API 网关。它的控制台在https://taotoken.net/console,API Key 在https://taotoken.net/api-keys生成。生成之后你会拿到一个形如sk-xxxxxxxx的密钥,后面 FC 和 MCP 两种方式都会用到它。
为什么强调"统一入口"?因为 FC 和 MCP 的差异在工具层,不在模型层。如果你 FC 用一个厂商的 SDK、MCP 又换另一套鉴权,最后排查问题时变量太多。把 Base URL 固定成https://taotoken.net/api,模型侧的行为就一致了,你观察到的差异就纯粹来自工具调用机制本身。
具体操作上,先在 API Keys 页面创建一个密钥,建议按用途命名,比如fc-demo和mcp-demo各一个,方便后面看调用量时区分。创建后立刻复制保存,页面刷新后就不再完整显示。
然后确认你要用的模型 ID。FC 场景下,模型需要支持tools参数,主流模型基本都支持;MCP 场景下,模型本身不直接感知 MCP,是客户端在中间做转换,所以模型只要支持标准的对话补全即可。这一点很关键:MCP 并不要求模型"懂 MCP 协议",协议是客户端和 Server 之间的事。
如果你打算长期做 Agent 开发,建议顺手看一下 Coding Plan 的说明(https://taotoken.net/coding-plan),它更适合高频编码和 Agent 场景,额度和计费方式跟按次调用不太一样。选型阶段先用按次调用验证逻辑,跑通之后再考虑套餐。
环境变量建议这样设置,后面所有示例都基于它:
export TAOTOKEN_API_KEY="sk-你的密钥" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Python 侧安装依赖:
pip install openai mcpopenai包用来演示 FC,mcp包用来写 MCP Server。两个都装上,方便你在同一台机器上对比。
这里有个容易忽略的点:MCP 的 Python SDK 要求 Python 3.10 以上,如果你本地是 3.8 或 3.9,pip install mcp会报版本不兼容。先用python --version确认一下,不够就升级,别在这个环节卡住。
3. 可复制配置:FC 的 tools 数组与 MCP Server 的完整写法
这一节是全文的核心,我会把两种方式的完整配置都贴出来,你可以直接复制到本地跑。先看 FC。
3.1 Function Calling 的 tools 配置
FC 的核心是把工具描述成 JSON Schema,随请求下发。下面是一个"查询订单状态"的工具定义:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) tools = [ { "type": "function", "function": { "name": "query_order_status", "description": "根据订单号查询订单的当前状态,返回已支付、已发货、已签收等状态", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,形如 ORD20240101001", } }, "required": ["order_id"], }, }, } ] response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "帮我查一下订单 ORD20240101001 的状态"}], tools=tools, tool_choice="auto", ) print(response.choices[0].message.tool_calls)这段代码跑通后,你会看到模型返回一个tool_calls列表,里面包含函数名和参数。注意tool_choice="auto"表示让模型自己决定要不要调用工具;如果你确定这轮必须调用,可以设成{"type": "function", "function": {"name": "query_order_status"}}强制指定。
FC 的配置特点很鲜明:工具定义和请求绑在一起,每次请求都要带上完整的tools数组。工具多了之后,这个数组会变得很长,token 消耗也会上去。这是 FC 的固有成本,后面讲性能时会再提。
3.2 MCP Server 的完整写法
MCP 的思路完全不同:工具定义一次,注册到 Server,之后任何兼容的客户端都能发现它。用 Python SDK 写一个最小 Server:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("order-service") @mcp.tool() def query_order_status(order_id: str) -> str: """根据订单号查询订单的当前状态。 Args: order_id: 订单号,形如 ORD20240101001 """ fake_db = { "ORD20240101001": "已发货", "ORD20240101002": "已签收", } return fake_db.get(order_id, "订单不存在") if __name__ == "__main__": mcp.run(transport="stdio")保存为order_server.py,然后配置客户端。以 Claude Desktop 为例,配置文件在~/Library/Application Support/Claude/claude_desktop_config.json(macOS),内容如下:
{ "mcpServers": { "order-service": { "command": "python", "args": ["/绝对路径/order_server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的密钥" } } } }如果你用的是 Cline 或 Claude Code,配置位置不同但结构一致:都是mcpServers下面挂一个服务名,指定command、args、env。这里必须写绝对路径,相对路径在客户端启动子进程时经常找不到文件,这是新手最容易踩的坑。
对比一下就很清楚了:FC 的工具定义是"每次请求携带",MCP 的工具定义是"一次注册、长期可用"。FC 的配置散落在业务代码里,MCP 的配置集中在客户端的 JSON 文件里。前者灵活但重复,后者规范但需要多一层进程管理。
3.3 三件套对照:Base URL、Key、Model ID
无论走哪条路,模型侧的三件套都要对齐。用表格对照一下:
| 项目 | FC 方式 | MCP 方式 |
|---|---|---|
| Base URL | https://taotoken.net/api | https://taotoken.net/api(由客户端调用模型时使用) |
| API Key | TAOTOKEN_API_KEY环境变量 | 写入 MCP 客户端配置的env |
| Model ID | 请求里显式指定,如gpt-4o-mini | 由客户端配置决定,Server 本身不感知 |
关键区别在于:FC 里模型 ID 是你代码里写死的,MCP 里模型 ID 是客户端配置的,MCP Server 完全不知道背后用的是哪个模型。这正是 MCP 解耦的价值——同一个 Server,换个客户端、换个模型,照样能用。
4. 验证请求:从 tool_calls 到 MCP 工具列表的成功返回
配置写完,得验证它真的能跑。这一节给你两条可复现的验证路径。
4.1 验证 FC:拿到 tool_calls 并回填结果
接着 3.1 的代码,完整跑一轮"调用 → 执行 → 回填":
import json messages = [{"role": "user", "content": "帮我查一下订单 ORD20240101001 的状态"}] response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="auto", ) msg = response.choices[0].message messages.append(msg) if msg.tool_calls: for call in msg.tool_calls: args = json.loads(call.function.arguments) # 这里替换成你真实的业务查询 result = f"订单 {args['order_id']} 当前状态:已发货" messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) final = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools, ) print(final.choices[0].message.content)成功的话,最后会打印类似"订单 ORD20240101001 当前状态为已发货"的自然语言回复。注意tool_call_id必须和模型返回的call.id严格对应,写错了会报 400,提示 tool 消息找不到对应的调用。
4.2 验证 MCP:用 Inspector 看工具列表
MCP 的验证更直观,官方提供了 Inspector 工具:
npx @modelcontextprotocol/inspector python /绝对路径/order_server.py执行后浏览器会打开一个调试界面,左侧能看到order-service这个 Server,点开 Tools 标签,应该能看到query_order_status及其参数 schema。在界面里填入order_id为ORD20240101001,点运行,右侧会返回"已发货"。
这一步能跑通,说明 Server 本身没问题。接下来在 Claude Desktop 或 Cline 里重启客户端,在对话里问"查一下订单 ORD20240101001",客户端会自动发现工具、发起调用、把结果回填给模型。你会在界面上看到工具调用的折叠块,展开能看到入参和返回值。
两条路径的验证重点不同:FC 验证的是"模型有没有正确生成 tool_calls",MCP 验证的是"客户端有没有正确发现并调用 Server 的工具"。前者出问题多半在 schema 描述,后者出问题多半在配置路径或进程启动。
4.3 成功结果的判断标准
FC 成功的标志:response.choices[0].message.tool_calls非空,且function.name等于你定义的工具名,arguments是合法 JSON。
MCP 成功的标志:Inspector 里能看到工具列表,调用返回预期结果;客户端对话里出现工具调用记录,且最终回复引用了工具返回的数据。
如果 FC 返回的tool_calls是None,说明模型认为不需要调用工具,通常是description写得太模糊,或者用户问句和工具能力不匹配。如果 MCP 在客户端里看不到工具,先检查配置文件路径和 Python 解释器路径,再看客户端日志。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节把两条路线最常见的报错集中处理。这些错误我在实际项目里都遇到过,按顺序排查基本能定位。
5.1 401 Unauthorized
FC 场景下出现 401,九成是 API Key 没读到。检查os.environ["TAOTOKEN_API_KEY"]是否真的存在,有时候你在终端export了,但 IDE 里的运行环境没继承。建议在代码里加一行print(os.environ.get("TAOTOKEN_API_KEY", "NOT SET")[:8])确认前几位。
MCP 场景下出现 401,通常是客户端配置的env里没传 Key,或者 Key 写错了。注意 MCP 客户端启动 Server 是独立进程,它不会继承你终端的 shell 环境变量,必须在 JSON 配置的env字段里显式写。
5.2 local proxy failed
这个报错一般出现在客户端尝试连接 MCP Server 时。含义是客户端启动 Server 子进程失败。常见原因有三个:command写的python在客户端的环境里不存在(换成绝对路径,如/usr/bin/python3);args里的脚本路径是相对路径(换成绝对路径);脚本本身有语法错误,启动即崩溃。
排查方法:把配置里的command和args拼成一条命令,在终端里手动执行一遍。如果终端能跑、客户端跑不了,就是环境变量或路径的问题。
5.3 reading 'choices' 报错
TypeError: Cannot read properties of undefined (reading 'choices')这类错误,通常发生在你直接取response.choices但response本身是错误对象的时候。根因往往是请求根本没成功,返回的是错误结构。先打印完整的response看内容,再对照状态码。常见触发是模型 ID 写错,或者tools数组格式不合法导致请求被拒。
5.4 OAuth 相关报错
如果你接的 MCP Server 是远程的、需要 OAuth 鉴权,可能会遇到invalid_grant或token expired。这类问题不在模型侧,而在 Server 的鉴权配置。检查 client_id、client_secret、回调地址是否和 Server 注册的一致。本地 stdio 的 Server 一般不涉及 OAuth,遇到这个报错说明你用的是远程 Server。
5.5 排查顺序建议
遇到问题按这个顺序走:先确认模型请求本身能通(用最简单的对话补全测),再确认工具定义格式正确,最后确认客户端与 Server 的进程通信正常。把"模型问题"和"工具问题"分开,排查效率会高很多。
6. 选型结论与下一步:什么时候用 MCP,什么时候 FC 就够
把前面的内容收拢成一张决策表:
| 判断维度 | 选 FC | 选 MCP |
|---|---|---|
| 工具数量 | 少量、固定 | 多、持续增加 |
| 模型数量 | 单一模型 | 多模型复用 |
| 团队规模 | 个人或小团队 | 多人协作、需要统一标准 |
| 部署形态 | 单机脚本 | 需要独立进程、可被多客户端发现 |
| 性能要求 | 极低延迟、单次短请求 | 可接受一层中转 |
| 安全合规 | 一般场景 | 敏感数据需本地化、需权限管控 |
一句话判断:如果你只是给一个模型加几个工具、跑在单机脚本里,FC 完全够用,别为了"标准"而标准。如果你要让多个客户端、多个模型共用同一批工具,或者工具会持续增加,MCP 的注册式设计会帮你省下大量重复适配。
融合使用也是成熟做法:把稳定的、需要复用的工具封装成 MCP Server,把一次性的、性能敏感的调用留在 FC 里。两者并不冲突,客户端完全可以在同一轮对话里既走 MCP 工具,又走原生 FC。
下一步建议你动手做两件事:一是把本文的order_server.py改成你自己的业务工具,在 Inspector 里验证一遍;二是用 FC 的方式实现同一个工具,对比两者的代码量和维护成本。跑完这两步,选型结论对你来说就不再是纸面分析,而是有体感的判断。
模型入口统一用https://taotoken.net/api,Key 在https://taotoken.net/api-keys生成,接入细节可以对照https://taotoken.net/doc。如果你打算把 Agent 跑成长期任务,Coding Plan 的额度模型值得提前了解;想先直观感受模型对工具调用的理解能力,也可以直接在模型对话里试几轮。工具调用的演进不会停在 FC 或 MCP 任何一边,但把这两条路都亲手跑通的人,在下一轮变化来临时会从容得多。