1. 从“只会聊天”到“真的能干活”:工具调用链路到底卡在哪
大模型默认情况下只能基于已有知识做推理和回答,遇到“帮我查一下今天某只股票的价格”“把这段 Python 跑一遍看报错”这类需求,它要么编,要么拒。Function Call 和 MCP 就是给它装上手脚的两套机制:前者是模型原生的结构化输出能力,后者是把外部工具标准化接进来的协议层。听起来很美,但真正动手时,多数人卡在三个地方:一是每个模型厂商的 tool_calls 格式不一样,OpenAI 一套、通义一套、Claude 又一套,适配层写到手软;二是 Key 管理混乱,测试阶段手里攥着五六个平台的 Key,环境变量改来改去;三是 MCP Server 的配置散落在各个客户端的 settings.json、config.toml 里,换个工具就要重配一遍。
这篇就围绕这条链路,用 TaoToken 的统一 Key 和 API 通道把 Function Call 的 JSON 参数构造、MCP 服务接入、以及一次可复现的调用验证串起来。适合已经在写 Agent、想跑通工具调用闭环但被多平台适配拖住的开发者。读完之后你应该能拿到一套可以直接抄的配置骨架,以及一个能跑出真实工具返回结果的验证脚本。
2. TaoToken 前置:统一 Key 与 API 通道准备
TaoToken 在这里扮演的角色是统一入口:你不需要为每个模型单独申请 Key、单独记 Base URL,而是用一套 Key 走同一个 API 通道,模型侧由它做路由。对工具调用场景来说,这点很关键——Function Call 的请求体里要带 tools 数组,MCP 的 Client 初始化也要填 base_url,如果每个模型换一个地址,配置会碎成渣。
先拿 Key。打开 https://taotoken.net/api-keys ,登录后创建一个新 Key,复制出来存到环境变量里,别硬编码进代码:
export TAOTOKEN_API_KEY="sk-你的key"API 通道的基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 base_url 用。模型对话的调试入口在 https://taotoken.net/model-chat ,接入文档在 https://taotoken.net/doc ,遇到 401 或 404 先翻文档确认路径拼写。
如果你后面要长期跑编码类 Agent,比如让模型反复调 code_runner、file_reader 这类工具,可以顺带看下 Coding Plan: https://taotoken.net/coding-plan ,它针对高频工具调用场景做了配额优化,比按次计费划算。
注意:Key 只创建一次就够,不要每个工具单独建。统一 Key 的意义就在于所有工具调用走同一条通道,排障时只需要看一个日志。
3. 可复制配置:settings.json 与 config.toml 骨架
MCP 的配置分两种客户端形态:一种是 JSON 配置(Claude Desktop、部分 IDE 插件用 settings.json),一种是 TOML 配置(部分 CLI 工具用 config.toml)。下面两份骨架都基于 TaoToken 的统一通道,把 base_url 和 api_key 抽出来,工具定义单独放。
先看 settings.json,这是给支持 MCP 的桌面客户端用的:
{ "mcpServers": { "taotoken-tools": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-everything"], "env": { "TAOTOKEN_API_KEY": "sk-你的key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } }, "llm": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key": "sk-你的key", "model": "claude-3-5-sonnet", "tools": [ { "type": "function", "function": { "name": "calculator", "description": "执行加减乘除、开方、幂运算", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "标准数学表达式,如 100+200" } }, "required": ["expression"] } } } ] } }再看 config.toml,这是给 CLI 类工具用的:
[llm] provider = "taotoken" base_url = "https://taotoken.net/api" api_key = "sk-你的key" model = "claude-3-5-sonnet" [[llm.tools]] name = "web_search" description = "联网检索实时信息" [llm.tools.parameters] type = "object" [llm.tools.parameters.properties.query] type = "string" description = "精简搜索关键词" [llm.tools.parameters.required] query = true [mcp_servers.taotoken_tools] command = "npx" args = ["-y", "@modelcontextprotocol/server-everything"] [mcp_servers.taotoken_tools.env] TAOTOKEN_API_KEY = "sk-你的key" TAOTOKEN_BASE_URL = "https://taotoken.net/api"两份配置的核心逻辑一致:LLM 侧声明 tools 数组,MCP 侧声明 server 启动命令和环境变量。区别只是语法。把 api_key 换成你自己的,base_url 保持 https://taotoken.net/api 不变。
4. 验证请求:一次可复现的 Function Call 闭环
配置写完不验证等于没写。下面用 curl 发一次带 tools 的请求,看模型是否返回结构化的 tool_calls,再手动执行工具、把结果回传,完成一轮闭环。
第一步,发请求:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "messages": [ {"role": "user", "content": "帮我计算 100 加 200"} ], "tools": [ { "type": "function", "function": { "name": "calculator", "description": "执行数学计算", "parameters": { "type": "object", "properties": { "expression": {"type": "string"} }, "required": ["expression"] } } } ], "tool_choice": "auto" }'预期返回里会有一个 tool_calls 字段,结构大致是:
{ "choices": [{ "message": { "role": "assistant", "tool_calls": [{ "id": "call_abc123", "type": "function", "function": { "name": "calculator", "arguments": "{\"expression\":\"100+200\"}" } }] } }] }第二步,本地执行工具。这里用 Python 模拟 calculator:
import json def calculator(expression: str) -> str: return str(eval(expression)) tool_call = { "id": "call_abc123", "function": {"name": "calculator", "arguments": "{\"expression\":\"100+200\"}"} } args = json.loads(tool_call["function"]["arguments"]) result = calculator(args["expression"]) print(result) # 300第三步,把工具结果回传给模型,让它整理成自然语言:
curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "messages": [ {"role": "user", "content": "帮我计算 100 加 200"}, {"role": "assistant", "tool_calls": [{"id": "call_abc123", "type": "function", "function": {"name": "calculator", "arguments": "{\"expression\":\"100+200\"}"}}]}, {"role": "tool", "tool_call_id": "call_abc123", "content": "300"} ] }'这次返回的 message.content 应该是“100 加 200 等于 300”之类的自然语言。到这一步,Function Call 的完整闭环就跑通了:模型输出调用指令 → 本地执行 → 结果回传 → 模型整理回答。
MCP 侧的验证类似,只是工具发现和执行由 MCP Client 自动完成。启动配置里的 server-everything 后,Client 会通过 JSON-RPC 拉取工具列表,你只需要在对话里提问,剩下的路由由协议层处理。
5. 本篇常见错排查
报错一:401 Unauthorized。九成是 Key 没读到环境变量。检查echo $TAOTOKEN_API_KEY是否有输出,以及 curl 里的$TAOTOKEN_API_KEY有没有被单引号包住导致不展开。单引号内的变量不会解析,这是最常见的坑。
报错二:tool_calls 返回空数组。模型没触发工具调用,通常是 tool_choice 设成了 none,或者 tools 数组里的 description 写得太模糊。把 description 写具体,比如“执行加减乘除、开方、幂运算”比“计算工具”更容易触发。
报错三:arguments 解析失败。模型返回的 arguments 是字符串,不是对象,必须json.loads一次。如果模型输出了带 markdown 代码块的 JSON,说明系统提示里没加“仅输出标准 JSON”的约束,回到第 3 节的配置里补上。
报错四:MCP Server 启动失败。看 settings.json 里的 command 路径是否正确,npx 是否在 PATH 里。env 里的 TAOTOKEN_BASE_URL 必须是 https://taotoken.net/api ,多一个斜杠或少一个 v1 都会导致 404。
报错五:多轮调用时上下文丢失。每轮回传工具结果时,messages 数组要带上完整的 assistant tool_calls 和对应的 tool 消息,缺一个模型就不知道结果对应哪次调用。tool_call_id 必须严格匹配。
6. 把链路固定下来,比换模型更重要
工具调用这条链路,真正难的不是模型选哪个,而是配置和排障的确定性。统一 Key 和统一 base_url 的价值在于,当你从 Claude 换到别的模型时,settings.json 里只需要改 model 字段,tools 数组和 MCP 配置原封不动。我试过在三个不同客户端之间迁移同一套工具定义,只要 base_url 指向 https://taotoken.net/api ,迁移成本基本就是复制粘贴。
如果你还在调试阶段,建议先用模型对话页面手动发几次带 tools 的请求,观察 tool_calls 的返回结构,再去写代码。接入文档里对参数格式有完整说明,遇到路径问题优先翻文档而不是猜。长期跑编码类 Agent 的话,Coding Plan 的配额模型比按次调用更适合高频工具场景。把上面那份 settings.json 存下来,下次换工具只需要改 tools 数组,剩下的交给统一通道。