MCP 与 Function Calling 同时跑,模型没给 tool_calls?TaoToken 这样改 base_url 把execute_tool换成execute_tool_with_mcp之后MCP 与 Function Calling 明明已经在同一条链路里第一次模型返回却还是没有tool_callsprocess_user_query直接走进elsesearch工具安静地没执行。这个问题不一定出在 MCP Server也不一定是函数定义错了很多时候是base_url和 Key 还停在 OpenRouter 那一套。打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一把 TaoToken Key把OPENROUTER_API_KEY换掉再把base_url指到 https://taotoken.net/api然后重新跑一次“纽约明天天气怎么样”看第一次模型返回里有没有tool_calls。下面按排障顺序走一遍先确认触发点再改配置再验证 MCP 与 Function Calling 是否真的在同一条链路里。1. 换了 execute_tool_with_mcp 后tool_calls 为什么还是没出现1.1 execute_tool_with_mcp 只换了执行端触发端仍是 Function Calling很多人把execute_tool改成execute_tool_with_mcp之后会下意识觉得“MCP 已经接进去了模型应该会调用工具”。但这条链路里MCP 负责的是服务器与函数之间的发现、调用协议Function Calling 负责的是模型从tools列表里挑函数并在返回里给出tool_calls。两者是互补关系不是替代关系。也就是说execute_tool_with_mcp改的是“工具怎么被执行”而process_user_query里的if tool_calls in first_model_message and first_model_message[tool_calls]判断的是“模型有没有决定调用工具”。如果第一次模型返回压根没有tool_calls后面的 MCP Client 连启动机会都没有search自然不会执行。1.2 process_user_query 走进 else 分支时发生了什么原始代码的逻辑很直接先把用户问题塞进history调用一次模型拿到first_model_message然后检查有没有tool_calls。有的话提取tool_name、tool_args执行工具把工具结果以role: tool塞回history再调用第二次模型总结答案。没有的话直接返回first_model_message[content]。所以当第一次返回没有tool_calls时你会看到类似这样的结果final_response里是模型自己编的一段天气回答tool_executed是Falsetool_result为空。MCP Server 的日志也不会出现因为execute_tool_with_mcp_async根本没被调用。排障第一步不是去查 MCP Server 有没有启动而是去看第一次模型请求和第一次模型返回。1.3 第一次模型返回长什么样才算带上了 tool_calls一次正常的 Function Calling 返回finish_reason通常是tool_callsmessage.content可能是空字符串message.tool_calls里包含工具名和参数。你可以在call_model里把返回日志打出来重点看这几个字段{ choices: [ { message: { role: assistant, content: , tool_calls: [ { id: call_xxx, type: function, function: { name: search, arguments: {\query\:\纽约 明天 天气预报\} } } ] }, finish_reason: tool_calls } ] }如果finish_reason是stopcontent里直接是一段天气描述tool_calls字段不存在或者为空那就说明模型没有选择search。接下来要检查的是模型 ID、tools数组、base_url和 Key 是否被换成了可用的统一通道。2. 把 OPENROUTER_API_KEY 和 base_url 换成 TaoToken 通道2.1 打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 YOUR_API_KEY原文代码里写的是self.api_key OPENROUTER_API_KEYbase_url指向https://openrouter.ai/api/v1/chat/completions。如果你现在要走 TaoToken 的统一接入先去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册并创建 API Key。Key 创建出来后代码里不要继续引用OPENROUTER_API_KEY改用占位符YOUR_API_KEY实际运行时再替换成你自己的 Key。这里注意一点官网落地页和接口地址不是一回事。注册、创建 Key、看模型广场、看用量在落地页完成填进代码或工具里的 Base URL 用https://taotoken.net/api末尾不要加/v1更不要把 UTM 参数加到接口地址上。2.2 在 LLMProcessor 里把 base_url 指到 https://taotoken.net/api如果你继续用requests直接发请求需要把完整端点处理干净更稳妥的做法是改用 OpenAI 兼容客户端把base_url填成https://taotoken.net/api让 SDK 去拼聊天补全路径。下面这段可以替换原来的__init__import os from openai import OpenAI class LLMProcessor: def __init__(self): self.api_key YOUR_API_KEY self.base_url https://taotoken.net/api self.client OpenAI( api_keyself.api_key, base_urlself.base_url, ) self.history [] self.model_name YOUR_MODEL_ID原来的self.headers和requests.post可以不再保留。这样改完请求会走 TaoToken 的兼容通道而不是继续打 OpenRouter 的地址。Key 从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建模型 ID 不要凭记忆写去模型广场看当前列表里哪个支持工具调用。2.3 模型 ID 以模型广场当时列表为准原文示例用的是openai/gpt-4o-mini但在 TaoToken 里不要直接假设模型 ID 一定相同。模型广场里会列出当前可用的模型和对应 ID把那里看到的 ID 复制到self.model_name或MODEL_NAME里。如果某个模型不支持 Function Calling即使tools写得再标准第一次返回也可能没有tool_calls。所以排障时先确认模型本身支持工具调用再去看代码。还有一个容易忽略的点Base URL 填https://taotoken.net/api后不要再在末尾补/v1。配置成https://taotoken.net/api/v1是常见错误可能导致路径拼接后出现 404 或者返回非预期内容。接口地址和官网落地页分开记落地页是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end接口 Base URL 是https://taotoken.net/api。3. tools 数组里的 search schema 要原样保留3.1 第一次请求体里的 tools 必须是 Function Calling 格式Function Calling 协议里tools是传给模型 API 的可用工具列表结构是 OpenAI 风格的TOOLS [ { type: function, function: { name: search, description: 搜索网络, parameters: { type: object, properties: { query: { type: string, description: 要搜索的内容 } }, required: [query] } } } ]第一次请求时messages里通常只有用户问题tools里带上searchstream设为False。模型会根据description和parameters判断是否需要调用search。如果tools为空或者 schema 被改成了 MCP 的 tool 描述模型就可能不返回tool_calls。3.2 MCP Server 的 mcp.tool() 不是直接塞给模型 API 的格式MCP Server 里通常这样写from mcp.server.fastmcp import FastMCP mcp FastMCP(search_mcp_server, log_levelERROR) mcp.tool() async def search(query: str) - str: 搜索网络 Args: query: 搜索内容 return 来自 MCP Server 的答案纽约市今天的天气是晴天明天的天气是多云。 if __name__ __main__: mcp.run(transportstdio)这个mcp.tool()是给 MCP Client 发现和调用的描述不是直接替换TOOLS的 JSON。正确做法是TOOLS保持 Function Calling 格式execute_tool_with_mcp根据模型返回的tool_name去调 MCP Server 里同名工具。两边名字可以都叫search但格式不能混。3.3 改配置时最容易弄丢的是 parameters 里的 required排障时把第一次请求体完整打出来确认tools里有name、description、parameters并且parameters里有properties.query和required: [query]。有些人在改base_url和 Key 的时候顺手重构了代码把TOOLS挪成了别的结构结果模型收到的工具列表不合法。只要tools数组里的searchschema 原样保留模型才有机会根据“纽约明天天气怎么样”返回tool_calls。4. 用“纽约明天天气怎么样”复现整条链路4.1 第一次模型请求应该带哪些字段把process_user_query的第一次调用单独打日志期望看到类似结构first_request_body { model: YOUR_MODEL_ID, messages: [ {role: user, content: 纽约明天天气怎么样} ], tools: TOOLS, stream: False, }如果这里tools是空列表或者model填了一个不支持工具调用的 ID第一次返回大概率不会出现tool_calls。确认这两个字段之后再看第一次返回里的finish_reason和message.tool_calls。4.2 第二次请求要把 tool 结果塞回 messages当第一次返回带tool_calls时提取tool_call.function.name和tool_call.function.arguments执行execute_tool_with_mcp然后把结果按role: tool追加到historytool_call first_model_message[tool_calls][0] tool_name tool_call[function][name] tool_args json.loads(tool_call[function][arguments]) result self.execute_tool_with_mcp(tool_name, tool_args) self.history.append({ role: tool, tool_call_id: tool_call[id], name: tool_name, content: result })第二次请求再把更新后的history和tools一起发给模型模型会基于工具结果生成最终回答。如果第一次没有tool_calls这段代码不会执行history里也不会出现role: tool的消息。4.3 MCP Server 返回的标记结果用来确认链路真的走到了mcp_server.py里的search返回内容带一句“来自 MCP Server 的答案”这是用来验证 MCP 是否真的被调用的标记。如果你在最终回答里看到模型总结了这句话说明第一次返回有tool_callsexecute_tool_with_mcp启动成功MCP Client 调到了 MCP Tool。如果最终回答只是模型自己编的天气没有 MCP 标记就回到第一次返回去查tool_calls。5. 还是没有 tool_calls 时按这几处排查5.1 模型 ID 不支持 Function Calling 或与工具列表不匹配先去模型广场看当前模型是否支持工具调用把YOUR_MODEL_ID换成明确支持 Function Calling 的模型。有些模型虽然能聊天但不会处理tools参数。换模型之后重新跑“纽约明天天气怎么样”看第一次返回是否出现tool_calls。如果换了支持工具调用的模型仍然没有再查tools数组。5.2 tools 字段为空、schema 不合法或 description 太模糊检查第一次请求体里tools是否存在search的parameters是否符合 JSON Schemarequired是否包含query。如果description写成“搜索”两个字模型可能不容易判断什么时候该调用。可以保留原文的“搜索网络”并让query的描述清楚一点。修改后重新请求重点看模型返回里有没有tool_calls。5.3 base_url 末尾多了 /v1 或 Key 没有生效接口 Base URL 应该填https://taotoken.net/api不要写成https://taotoken.net/api/v1。Key 用YOUR_API_KEY实际运行时替换成从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建的那把。如果返回 401先查 Key 是否复制完整如果返回 404查 Base URL 和请求路径是否被改坏。注意接口地址后面不要加 UTM 参数。5.4 MCP Client 启动失败或 tool_name 对不上execute_tool_with_mcp_async里会计算mcp_server.py的绝对路径并用MCPClient(uv, [run, mcp_server_path])启动。如果uv不在环境变量里或者mcp_server.py路径不对MCP 调用会失败。另外模型返回的tool_name必须和 MCP Server 里mcp.tool()的函数名一致比如两边都叫search。名字对不上时client.call_tool会找不到对应工具。6. 跑通之后去控制台对一下这次调用6.1 用同一把 Key 在模型对话里发一条测试消息配置保存后先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认模型 ID 和 Base URL 没填错。这一步能快速区分是 Key/模型问题还是tools与process_user_query的问题。如果模型对话正常但 Python 脚本没有tool_calls重点回到TOOLS和第一次请求体。6.2 Key 在控制台创建长期跑看 Coding PlanKey 在 控制台 API Keys 创建。如果这条 MCP 与 Function Calling 链路要长期跑可以打开 Coding Plan 看套餐是否够用。注意接口 Base URL 始终是https://taotoken.net/api创建 Key 和看用量才去带 UTM 的官网落地页。6.3 看这次调用有没有记上账最后回到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的控制台看这次“纽约明天天气怎么样”的调用有没有记上账。如果用量里能看到请求说明 Key 和 Base URL 已经生效如果看不到再检查代码里是不是还在用旧的OPENROUTER_API_KEY。把第一次模型返回里的tool_calls、MCP Server 返回的标记结果、控制台用量三处对齐MCP 与 Function Calling 同时跑的链路就算真正打通了。如果第一次返回仍然没有tool_calls先把模型 ID 换成模型广场里明确支持工具调用的那个再确认tools数组里的searchschema 没被改坏最后看base_url是不是还带着 OpenRouter 的尾巴。把这几处对齐MCP 与 Function Calling 才能在同一条链路里继续跑。