1. 从一次工具调用失败说起:大模型上下文工程到底难在哪
大模型上下文工程(Context Engineering)这两年从提示词工程里独立出来,核心原因就一个:模型本身再强,它也不知道你公司数据库里有什么、今天天气几度、订单状态到哪一步了。工具使用(Tool Use)和 MCP(Model Context Protocol)就是给模型接上外部能力的两种主流方式。
我见过太多人卡在同一个地方:本地写好了工具函数,Schema 也定义了,但一到真实请求就报错——要么是tools字段格式不对,要么是模型返回了tool_calls但自己不知道怎么把结果塞回去,要么是 MCP Server 起来了但客户端连不上。这些问题不是模型能力问题,是上下文工程没做对。
这篇面向需要为模型接入外部能力的开发者,把 Function Calling 和 MCP 两条链路串起来讲。你会看到可复制的 MCP 服务配置片段、工具 Schema 定义示例,以及用 TaoToken 统一 Key/API 通道完成一次完整 Function Calling 请求的验证步骤。跑通之后,从工具注册到模型调用的整条链路你就清楚了。
适合谁看:已经会调大模型 API、想给模型加外部能力的后端或全栈开发者;正在评估 MCP 要不要接入自己系统的技术负责人;以及被各家模型工具调用格式差异折磨过的同学。
先说结论:Function Calling 是模型厂商各自实现的工具调用协议,MCP 是 Anthropic 推动的开放标准,两者本质都是"把工具描述塞进上下文,让模型决定调不调、调哪个、传什么参数"。区别在于 MCP 把工具的注册、发现、执行做成了独立服务,客户端通过统一协议通信,不用为每个模型写一套适配。
2. TaoToken 前置准备:统一 Key 与 API 通道
在动手写工具调用之前,先把 API 通道理顺。我试过同时对接多家模型做工具调用,最烦的就是每家 Base URL、鉴权头、请求体格式都不一样,调试成本极高。TaoToken 的价值在于提供一个统一的 API 入口,Key 和 Base URL 一套配置,模型 ID 切换即可,工具调用的请求结构保持一致。
你需要先拿到 API Key。访问 https://taotoken.net/api-keys 创建,注意这个页面是控制台里的密钥管理入口。创建后复制保存,后面所有请求都用它。
Base URL 统一用https://taotoken.net/api,注意这个地址不带任何查询参数。模型 ID 按你实际要用的填,比如gpt-4o、claude-sonnet-4-20250514这类,具体以控制台模型列表为准。
如果你用的是 Claude Code 这类编码 Agent,配置方式略有不同。Claude Code 通过环境变量读取 Base URL 和 Key,你可以在 shell 配置里设置:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="你的TaoToken Key"然后启动 Claude Code 即可。它内部走的是 Anthropic 的 Messages API 格式,TaoToken 做了协议兼容,工具调用(Tool Use)的tools字段和tool_use/tool_result消息块都能正常透传。
对于 Cline、Cursor 这类支持 MCP 的编辑器,配置在各自的 settings 里。以 Cline 为例,它的 MCP 配置走cline_mcp_settings.json,模型 API 配置走单独的 Provider 设置。这里要区分清楚:MCP 配置管的是工具服务怎么连,模型 API 配置管的是模型怎么调,两者是独立的。
一个常见的坑是把 MCP Server 的地址和模型 API 的 Base URL 搞混。MCP Server 是你自己起的本地或远程服务,模型 API 是 TaoToken 的地址,别填反了。
配置完成后,建议先用一个最简单的对话请求验证通道是否通:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "ping"}] }'返回里有choices[0].message.content就说明通道正常。这一步别跳过,后面工具调用报错时,你能快速判断是通道问题还是 Schema 问题。
3. 可复制配置:MCP 服务与工具 Schema 定义
这一节给可直接复制的配置。先看 MCP Server 的配置片段,以 Claude Code 的.mcp.json为例,这个文件放在项目根目录:
{ "mcpServers": { "demo-stdio": { "command": "python", "args": ["/Users/yourname/projects/mcp-demo/server.py"], "env": { "DEMO_API_KEY": "stdio-key-12345", "DEMO_ENV": "development" } }, "demo-http": { "type": "http", "url": "http://127.0.0.1:8002/mcp", "headers": { "X-API-Key": "http-key-abcde", "X-Environment": "production" } } } }这里有两个 Server:demo-stdio通过命令拉起,走标准输入输出通信;demo-http走 Streamable HTTP,适合远程或容器化部署。注意type字段,stdio 类型不需要写,http 和 sse 类型必须显式声明。
对应的 MCP Server 代码用 FastMCP 写,工具定义如下:
from fastmcp import FastMCP mcp = FastMCP("Demo Server") @mcp.tool() def get_weather(city: str) -> dict: """查询指定城市的当前天气。 Args: city: 城市名称,例如 "Beijing" Returns: 包含温度和天气状况的字典 """ return {"city": city, "temp": 22, "condition": "sunny"} @mcp.tool() def add_numbers(a: float, b: float) -> float: """计算两个数字之和。 Args: a: 第一个数字 b: 第二个数字 Returns: 两数之和 """ return a + b if __name__ == "__main__": mcp.run(transport="http", host="127.0.0.1", port=8002, path="/mcp")启动后,MCP Client 会通过tools/list拿到工具列表,格式是 JSON-RPC。你可以用 curl 手动验证:
curl --location 'http://127.0.0.1:8002/mcp' \ --header 'Accept: application/json, text/event-stream' \ --header 'Content-Type: application/json' \ --data '{ "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0"} }, "jsonrpc": "2.0", "id": 0 }' -i响应头里会有mcp-session-id,后续请求都要带上它。这一步是 MCP 握手,少了 session id 后面全报错。
再看 Function Calling 的工具 Schema,这是直接塞进模型请求tools字段的:
{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如 Beijing" } }, "required": ["city"], "additionalProperties": false } } }注意additionalProperties: false和required这两个字段,它们能显著降低模型传错参数的概率。MCP 的工具定义和这个结构高度对应,只是 MCP 走的是inputSchema字段名,Function Calling 走的是parameters,这是最容易混淆的地方。
4. 验证请求:用 TaoToken 跑通一次 Function Calling
现在把工具 Schema 和 TaoToken 通道结合起来,发一次完整的 Function Calling 请求。请求体如下:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "北京今天天气怎么样?"} ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"], "additionalProperties": false } } } ], "tool_choice": "auto" }'成功返回的关键标志是choices[0].message.tool_calls数组非空,里面包含function.name和function.arguments。arguments 是 JSON 字符串,需要你自己解析。比如返回:
{ "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Beijing\"}" } } ] }拿到之后,你在本地执行get_weather("Beijing"),得到结果{"city": "Beijing", "temp": 22, "condition": "sunny"}。然后把这个结果作为tool角色的消息追加到上下文,再发一次请求:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [ {"role": "user", "content": "北京今天天气怎么样?"}, { "role": "assistant", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"Beijing\"}" } } ] }, { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"city\": \"Beijing\", \"temp\": 22, \"condition\": \"sunny\"}" } ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称"} }, "required": ["city"], "additionalProperties": false } } } ] }'这次返回的choices[0].message.content就是模型根据工具结果生成的最终回答,类似"北京今天晴,气温 22 度"。到这里,从工具注册到模型调用的完整链路就跑通了。
如果你用 Python SDK,逻辑一样,只是用client.chat.completions.create封装。关键点在于tool_call_id必须和上一轮 assistant 消息里的 id 严格对应,否则模型会报上下文不一致。
5. 常见报错排查:401、local proxy failed、reading choices
这一节对照真实报错,给排查路径。
401 Unauthorized:最常见。先检查Authorization头是不是Bearer开头,Key 有没有多余空格。如果 Key 是从控制台复制的,注意别把换行符带进去。还有一种情况是 Key 权限不足,去 https://taotoken.net/api-keys 确认 Key 状态是启用。
local proxy failed / connection refused:这个报错通常出现在 MCP Client 连本地 Server 时。检查 MCP Server 是否真的起来了,端口有没有被占用。stdio 类型的 Server 如果启动命令路径写错,Client 会直接报拉起失败。http 类型的话,用curl http://127.0.0.1:8002/mcp确认端口通不通。另外注意,MCP Server 的地址和模型 API 的 Base URL 是两个东西,别把https://taotoken.net/api填到 MCP 配置里。
reading choices 相关报错:一般是响应体解析失败。可能原因有三个:一是模型返回了tool_calls但你的代码还在读content,导致content为 null 时报错;二是流式响应没处理完整,choices数组为空;三是请求体里tools字段格式不对,模型直接返回了错误信息而不是正常结构。建议先关掉流式,用非流式请求确认结构,再开流式。
OAuth / 鉴权失败:如果你用的是 Claude Code 或 Codex 这类工具,它们可能走 OAuth 流程。检查环境变量ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY是否都设置了,且 Base URL 没有多余路径。Codex 的auth.json里如果残留了旧的 token,也会导致鉴权冲突,清掉重新登录。
工具调用返回空 arguments:模型知道要调工具,但没传参数。检查 Schema 里required是否声明了必填字段,description是否足够清晰。描述太模糊时,模型可能选择不传参。
MCP session id 丢失:Streamable HTTP 模式下,initialize之后的所有请求都要带Mcp-Session-Id头。漏了会返回 400 或直接断开。建议在客户端封装里统一管理 session id。
排查顺序建议:先确认模型 API 通道通(用最简单的对话请求),再确认 MCP Server 单独能通(用 curl 打 tools/list),最后再串起来。分层排查能省很多时间。
6. 继续深入:把工具调用接进你的系统
跑通单次 Function Calling 只是起点。真实系统里,你需要考虑工具的数量管理、执行结果的上下文卸载、以及多模型切换时的兼容性。
工具数量一多,上下文会被工具定义撑爆。这时候要么做工具分组按需加载,要么用 MCP 的 Server 聚合能力,把多个工具服务绑到一个入口下。执行结果太长时,别原样塞回上下文,做摘要或只保留关键字段,这是上下文工程里"结果卸载"的核心动作。
多模型场景下,MCP 的价值更明显:工具定义一次,Claude、GPT、Gemini 都能通过各自的 MCP Client 调用,不用为每家写一套 Schema 适配。Function Calling 则更适合单模型快速集成、对延迟敏感的场景。
如果你要长期做编码 Agent 或自动化流程,建议把模型通道固定下来,用 TaoToken 的 Coding Plan 统一管理额度和模型切换,省得每个项目单独配 Key。接入文档在 https://taotoken.net/doc 有完整的参数说明和示例。想先验证模型对工具调用的支持情况,可以直接在模型对话页面试:https://taotoken.net/chat 。
最后给一个实用技巧:调试工具调用时,把tool_choice设成required强制模型必须调工具,能快速验证 Schema 是否正确。等 Schema 稳定了再改回auto。这个开关在排查"模型不调工具"的问题时特别有用。