MCP 协议如何让大模型完成工具调用

你在 MCP 客户端里已经能看到 tools/list 返回了四个工具,但把同一份 JSON Schema 发给大模型后,choices0.message.tool_calls 却是 None。别急着换模型,问题多半不在模型本身,而在 tools 参数和提示词的衔接方式。这篇文章会用一个最小 Python 脚本跑通从工具 schema 到 tool_call 返回的完整路径,再逐步加上执行回传和排错方法。

一、场景与前置条件

这篇教程解决一个很具体的问题:在 MCP 协议接入链路里,大模型明明拿到了工具定义,却不发起工具调用;或者发起了调用,客户端却不知道如何把执行结果回传。你手上需要有的东西不多:Python 3.10 以上、openai 包 1.x、一个能访问 OpenAI 兼容端点的 API Key,以及一个 MCP 客户端或你手工维护的 tools 列表。

版本限制要说清楚,这里写的不是一份可以套用到任何平台的通用 demo。示例代码全部从环境变量读取密钥,因为硬编码的 Key 很容易被提交进 Git 历史,一旦泄露只能删掉重发。大模型调用名使用 minimax-m2.5,这是模型广场里的稳定 alias,不要根据展示名自行拼接。后面的代码如果没有特殊说明,都在本地终端执行。

二、环境准备

先安装依赖。openai 包会使用我们传入的 base_url 和 api_key,不需要额外封装。

bash 复制代码
pip install openai python-dotenv

密钥放环境变量的具体做法是:在项目根目录建 .env,写入密钥,变量名以脚本里的 os.environ 读取项为准。这样 .env 可以加进 .gitignore,即使仓库公开也不会漏 Key。另一个容易填错的是 Base URL,只有一个正确写法;下面脚本里已经写死,复制时不要把域名和 /api/v1 路径拆开改。填错的表现很直接:连最基础的 /chat/completions 都会 404 或连接超时。

运行脚本前先确认终端能读到这个变量。用 print(os.environ.get(...)) 看一眼不是必须的,但排错时比反复重跑更省时间。如果你是 Windows 终端,export 要换成 set;用 .env 文件就可以绕开这个差异。

三、最小可跑示例

下面这个脚本不启动 MCP server,而是把 MCP 客户端里 tools/list 返回的 tools 字段手工搬进 tools 列表。跑通后你就能确认:大模型在拿到工具描述后会不会返回 tool_calls。

python 复制代码
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.environ['SILVAMUX_API_KEY'],
    base_url='https://www.silvamux.com/api/v1',
)

tools = [
    {
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': '查询指定城市的当前天气',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string', 'description': '城市名称'}
                },
                'required': ['city'],
            },
        },
    }
]

resp = client.chat.completions.create(
    model='minimax-m2.5',
    messages=[
        {'role': 'system', 'content': '你只在需要实时数据时发起工具调用。'},
        {'role': 'user', 'content': '北京现在天气怎么样?'},
    ],
    tools=tools,
    tool_choice='auto',
)

msg = resp.choices[0].message
print('content:', msg.content)
print('tool_calls:', msg.tool_calls)
if msg.tool_calls:
    print('function name:', msg.tool_calls[0].function.name)
    print('arguments:', msg.tool_calls[0].function.arguments)

这段脚本的关键在 tools 参数。MCP 协议负责把工具、资源、提示组织成统一描述,但大模型这端要的是 OpenAI 兼容的 tools 数组格式。你在 MCP 客户端看到的是带包裹层的 JSON,不能整段塞进 messages。需要把工具名、描述和 parameters 映射过来,像上面这样以 type 为 function 的字典传给 create。

另一个容易填错的参数是 tool_choice。写成 tool_choice='auto' 表示由模型根据 system 和 user 消息判断是否需要工具。省略它不一定报错,但有些模型会直接回答而不是发起工具调用。如果你的业务逻辑要求必须调用,可以把 tool_choice 设成具体的函数名,不过示例里先保持 auto 更容易观察模型行为。

四、逐步扩展

最小示例拿到了 tool_call,但没有执行工具,也没有把结果告诉模型。下一步加一个本地函数 get_weather,并把它返回的对象作为 tool 角色消息回传。这个改动前后的差别很明显:没有回传时,对话停在调用动作;回传后,模型才会根据真实结果组织最终回答。

python 复制代码
import json
import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.environ['SILVAMUX_API_KEY'],
    base_url='https://www.silvamux.com/api/v1',
)

def get_weather(city: str):
    if not city:
        return {'error': 'city is required'}
    return {'temperature': -2, 'condition': '晴', 'city': city}

tools = [
    {
        'type': 'function',
        'function': {
            'name': 'get_weather',
            'description': '查询指定城市的当前天气',
            'parameters': {
                'type': 'object',
                'properties': {
                    'city': {'type': 'string', 'description': '城市名称'}
                },
                'required': ['city'],
            },
        },
    }
]

messages = [
    {'role': 'system', 'content': '你只在需要实时数据时发起工具调用。'},
    {'role': 'user', 'content': '北京现在天气怎么样?'},
]

first = client.chat.completions.create(
    model='minimax-m2.5',
    messages=messages,
    tools=tools,
    tool_choice='auto',
)

if not first.choices[0].message.tool_calls:
    raise SystemExit('模型没有发起工具调用,先检查 tools 和 tool_choice')

tool_call = first.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments or '{}')
result = get_weather(args.get('city', ''))

messages.append(first.choices[0].message)
messages.append({
    'role': 'tool',
    'tool_call_id': tool_call.id,
    'content': json.dumps(result, ensure_ascii=False),
})

second = client.chat.completions.create(
    model='minimax-m2.5',
    messages=messages,
    tools=tools,
)
print(second.choices[0].message.content)

关键在 messages.append(first.choices0.message) 这一行。它把首轮助手消息里的 tool_calls 原样保留;后面跟着的 tool 消息必须带同一个 tool_call_id。如果 id 写错,服务端会返回 400,提示 tool_call_id 不匹配。

执行本地函数前用 json.loads 解析 arguments,因为模型返回的是 JSON 字符串,不是 Python 字典。arguments 有时会是空串,直接 json.loads('') 会抛 JSONDecodeError,所以示例里写了 or '{}' 做兜底。这个兜底在真实业务里很重要,因为用户输入可能没有提供必填参数。

五、排错

这里列四个在 MCP 协议与大模型工具调用联调时最容易出现的报错。每个都跟参数形态有关,不是网络抖动。

1. tool_calls 永远是 None

报错文本:content 为 None,tool_calls 为 None,finish_reason 是 stop。

直接原因:模型判断不需要调用工具,通常是提示词没有给出实时数据需求,或者 tools 的 description 太模糊。

修法:把 system 消息改成"你只在需要实时数据时发起工具调用",并在工具 description 里写清楚触发条件。不要只写"查询天气"四个字。

2. JSONDecodeError

报错文本:

text 复制代码
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

直接原因:tool_call.function.arguments 是空字符串,直接交给 json.loads 会解析失败。

修法:使用 json.loads(tool_call.function.arguments or '{}'),或者先判断空值再走默认参数。

3. tool_call_id 缺失

报错文本:

text 复制代码
openai.BadRequestError: Error code: 400 - Missing required parameter: 'messages.3.tool_call_id'.

直接原因:tool 角色消息没有携带与上一轮 tool_call 对应的 id。

修法:从第一轮响应里的 tool_call.id 原样复制。即使只有一个工具调用,也不能省略这个字段。

4. 401 Invalid API Key

报错文本:

text 复制代码
openai.AuthenticationError: Error code: 401 - Invalid API Key

直接原因:环境变量没有加载成功,或者 .env 里的 Key 前多了空格、换行。

修法:确认脚本顶部有 load_dotenv(),确认变量名与 os.environ 读取的名称完全一致。把 Key 重新粘贴成一行,避免尾随空格。

常见问题

问:MCP 协议和 OpenAI 的 tool_calls 是不是一回事?

答:不是。MCP 协议解决的是客户端怎么发现工具、资源和提示;OpenAI tool_calls 是大模型根据工具描述选择调用哪个函数并生成 JSON 参数。集成时通常要先把 MCP server 的 tools/list 结果映射成 OpenAI tools 数组,再把用户消息和这个数组一起发给大模型。

问:工具调用为什么总是不触发?

答:最常见是 tools 参数没传,或者描述过于笼统。也可能是 tool_choice 缺失时模型选择直接回答。先打印 finish_reason,如果是 stop 而不是 tool_calls,就把 system prompt 的限制条件收紧,并给工具 description 写清"何时应使用"。

问:MCP 客户端已经拿到工具列表,能不能直接塞给大模型?

答:不能。MCP 客户端返回的是协议层的数据结构,和 OpenAI 兼容端点期望的 tools 数组字段层级不同。需要去壳,只保留 name、description、parameters。映射完成后,再把用户需求作为普通 message 发出,不然会看到 400。

以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 minimax-m2.5。参数变更或调用名以 开发者文档上的模型广场为准,读者可用该地址查询模型调用名和错误处理说明。

相关推荐
Mars-xq1 小时前
家庭膳食营养速查表
大数据·人工智能
guo_wen_qiang1 小时前
云服务器安装MongoDB-资源包安装
运维·服务器·mongodb
Earth explosion1 小时前
企业级Agent:MCP正在成为新的控制平面
大数据·平面
benchmark_cc1 小时前
实时行情系统如何避免无意义的重复请求?从请求去重到缓存设计
大数据·开发语言·python·pandas·量化交易·股票数据·quantdash
Allen-Steven1 小时前
SQL查询专项:从完整书写到综合判断
运维·服务器·数据库
guo_wen_qiang1 小时前
云服务器安装MongoDB
运维·服务器
Data analyse4562 小时前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
anew___2 小时前
《从零手写操作系统 (46):设备模型与devtmpfs——硬件资源的文件系统化》
linux·运维·服务器
蝶依斓(湖南)全案整装2 小时前
整装企业多平台内容差异化表达方法:形态适配与事实一致
大数据