1. 从一次“工具调用失败”说起:Prompt、AI Agent、MCP、function calling 到底谁管谁
刚接触 AI 应用开发时,最容易混淆的就是这四个词。我第一次写 Agent 的时候,把工具描述硬塞进 system prompt,结果模型返回了一段“我建议你调用 list_files 函数”的自然语言,程序解析直接崩掉。后来才明白:Prompt 负责“说清楚要干什么”,function calling 负责“用固定格式说”,AI Agent 负责“循环执行直到完成”,MCP 负责“把工具变成可复用的服务”。四者不是替代关系,而是分层协作。
这篇文章面向刚入门的工程师,目标很明确:给你可复制的 Prompt 模板、Agent 循环伪代码、MCP 工具声明示例、function calling 参数结构,最后用 TaoToken 统一 Key 跑一次端到端调用,让你亲眼看到模型返回的 tool_calls 长什么样。你不需要先买一堆 API Key,也不需要把每个厂商的 SDK 都装一遍。
先给一个最简分层认知,后面所有代码都围绕这张图展开:
| 层 | 角色 | 解决的问题 | 典型产物 |
|---|---|---|---|
| Prompt 层 | 人设 + 任务描述 | 让模型知道“你是谁、要干嘛” | system prompt / user prompt |
| function calling 层 | 结构化调用协议 | 让模型用 JSON 说“我要调哪个函数、参数是什么” | tools 数组 + tool_calls 返回 |
| AI Agent 层 | 循环控制器 | 把模型输出变成真实函数执行,再把结果喂回去 | while 循环 + 工具路由 |
| MCP 层 | 工具服务化协议 | 让工具跨 Agent 复用,不用每个项目拷贝代码 | MCP Server / MCP Client |
很多人把 function calling 和 MCP 混为一谈。简单说:function calling 是模型和 Agent 之间的“语言规范”,MCP 是 Agent 和工具服务之间的“通信规范”。一个管上行,一个管下行。你完全可以在没有 MCP 的情况下用 function calling 写一个本地 Agent,也完全可以在 MCP Server 里暴露工具、由 Agent 通过 function calling 触发。两者配合,才是完整的自动化链路。
我试过最笨的办法:把工具说明写进 system prompt,让模型“自觉”返回 JSON。结果十次里有三次格式跑偏,重试逻辑写得比业务代码还长。后来换成标准 function calling,模型返回的 tool_calls 直接是结构化对象,解析成本几乎为零。这个坑值得你提前避开。
2. TaoToken 前置准备:统一 Key 与 API 通道,省掉多厂商适配
在写 Agent 之前,先解决“模型从哪来”的问题。如果你同时用几家模型,每家的 Base URL、鉴权头、参数命名都不一样,Agent 里会塞满 if-else。TaoToken 的思路是提供一个统一的 OpenAI 兼容通道,你只维护一个 Key 和一个 Base URL,切换模型只改 model 字段。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。API 地址是 https://taotoken.net/api ,注意这个地址后面不加任何查询参数,直接作为 base_url 使用。
你需要准备三样东西,我把它叫做“接入三件套”:
- Base URL:https://taotoken.net/api
- API Key:在控制台创建,形如 sk-xxxx
- Model ID:比如 claude-sonnet-4-20250514、gpt-4o 等,按你实际可用的填
创建 Key 的入口在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。如果你还没决定用哪个模型,可以先到模型对话页试一下:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。长期写代码或跑 Agent 的话,Coding Plan 会更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
环境变量建议这样设,后面所有代码都读这两个变量:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用 Python,装一个 openai 官方 SDK 就够了,因为 TaoToken 兼容 OpenAI 的接口格式:
pip install openai这里有个细节:base_url 结尾不要带/v1,SDK 会自己拼。如果你手动用 curl,完整路径是https://taotoken.net/api/v1/chat/completions。我踩过的坑是有人把 base_url 写成https://taotoken.net/api/v1,结果 SDK 又拼了一次/v1,变成/v1/v1/chat/completions,直接 404。记住:SDK 里填https://taotoken.net/api,curl 里用完整路径。
Key 的权限建议最小化:只开 chat 和 models 权限,不要开管理类权限。如果你在团队里共享,给每个人单独建 Key,方便排查是谁的调用出了问题。控制台里可以看每个 Key 的调用量和余额,排障时非常有用。
3. 可复制配置:Prompt 模板、function calling 参数结构、MCP 工具声明
这一节是全文最“硬”的部分,给你三份可以直接抄的配置。先看 Prompt 模板。system prompt 负责定人设和约束,user prompt 负责传任务。我常用的模板长这样:
你是一个严谨的运维助手。你可以调用工具来查询服务器状态。 规则: 1. 需要实时数据时必须调用工具,不要凭记忆回答。 2. 调用工具前先说明你要查什么。 3. 工具返回后,用一句话总结结果。user prompt 就一句话:“帮我看看 web-01 的磁盘使用率。” 这样模型知道该调工具,而不是瞎编。
接下来是 function calling 的参数结构。这是 OpenAI 兼容格式,TaoToken 同样支持:
{ "model": "claude-sonnet-4-20250514", "messages": [ {"role": "system", "content": "你是一个严谨的运维助手。"}, {"role": "user", "content": "帮我看看 web-01 的磁盘使用率。"} ], "tools": [ { "type": "function", "function": { "name": "get_disk_usage", "description": "查询指定主机的磁盘使用率", "parameters": { "type": "object", "properties": { "hostname": { "type": "string", "description": "主机名,例如 web-01" } }, "required": ["hostname"] } } } ], "tool_choice": "auto" }注意parameters是标准 JSON Schema,required里写必填字段。模型返回的tool_calls会带上function.name和function.arguments,arguments 是字符串形式的 JSON,需要你自己json.loads一次。
再看 MCP 工具声明示例。MCP Server 通常用一个 JSON 描述自己暴露的工具,结构类似:
{ "name": "ops-tools", "version": "1.0.0", "tools": [ { "name": "get_disk_usage", "description": "查询指定主机的磁盘使用率", "inputSchema": { "type": "object", "properties": { "hostname": {"type": "string"} }, "required": ["hostname"] } } ] }你会发现 MCP 的inputSchema和 function calling 的parameters几乎一样。这不是巧合:MCP 在设计时就考虑了和 function calling 的映射。Agent 从 MCP Server 拉到工具列表后,可以直接转成 function calling 的 tools 数组发给模型。这样你只需要在 MCP Server 里维护一份工具定义,所有 Agent 都能复用。
如果你用 Claude Code 或 Cline 这类工具,它们的 MCP 配置通常是一个 JSON 文件,路径类似~/.config/claude/claude_desktop_config.json或项目下的.cline/mcp.json。配置里同样要写全三件套:Base URL、Key、Model ID。比如:
{ "mcpServers": { "ops-tools": { "command": "python", "args": ["-m", "ops_tools.server"], "env": { "TAOTOKEN_API_KEY": "sk-你的key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }这里 Model ID 不一定写在 MCP 配置里,而是写在 Agent 的模型配置里。但只要你用 TaoToken,Base URL 和 Key 就是统一的那一套,不用为每个工具单独配。
4. 端到端验证:用 TaoToken 跑一次 function calling 并检查结果
现在把前面的东西串起来。下面这段 Python 代码可以直接复制运行,它会完成一次完整的 function calling 调用,并打印模型返回的 tool_calls。
import os import json 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": "get_disk_usage", "description": "查询指定主机的磁盘使用率", "parameters": { "type": "object", "properties": { "hostname": {"type": "string", "description": "主机名"} }, "required": ["hostname"], }, }, } ] resp = client.chat.completions.create( model="claude-sonnet-4-20250514", messages=[ {"role": "system", "content": "你是一个严谨的运维助手。"}, {"role": "user", "content": "帮我看看 web-01 的磁盘使用率。"}, ], tools=tools, tool_choice="auto", ) msg = resp.choices[0].message print("finish_reason:", resp.choices[0].finish_reason) print("content:", msg.content) print("tool_calls:", msg.tool_calls) if msg.tool_calls: call = msg.tool_calls[0] args = json.loads(call.function.arguments) print("函数名:", call.function.name) print("参数:", args)运行后你会看到类似输出:
finish_reason: tool_calls content: None tool_calls: [ChatCompletionMessageToolCall(id='call_xxx', function=Function(name='get_disk_usage', arguments='{"hostname":"web-01"}'), type='function')] 函数名: get_disk_usage 参数: {'hostname': 'web-01'}看到finish_reason: tool_calls就说明模型正确触发了工具调用。接下来你要做的是:执行真实函数,把结果作为role: tool的消息追加回去,再请求一次模型,让它生成最终回答。这就是 Agent 循环的核心。
def fake_get_disk_usage(hostname): return f"{hostname} 磁盘使用率 78%" tool_result = fake_get_disk_usage(args["hostname"]) messages = [ {"role": "system", "content": "你是一个严谨的运维助手。"}, {"role": "user", "content": "帮我看看 web-01 的磁盘使用率。"}, msg, { "role": "tool", "tool_call_id": call.id, "content": tool_result, }, ] final = client.chat.completions.create( model="claude-sonnet-4-20250514", messages=messages, tools=tools, ) print(final.choices[0].message.content)最终输出类似:“web-01 的磁盘使用率为 78%,建议关注。” 到这里,一次完整的 function calling 闭环就跑通了。你验证了模型能正确选择工具、生成参数、接收结果并总结。整个过程只用了 TaoToken 一个 Key 和一个 Base URL。
如果你想验证模型本身是否正常,可以到模型对话页手动发一句“你好”看看返回:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。如果那边正常而代码报错,问题多半在参数格式或网络配置上。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
排障部分我按真实报错来写,你遇到哪个直接对号入座。
401 Unauthorized:最常见的原因是 Key 没读到。先确认echo $TAOTOKEN_API_KEY有输出,且以sk-开头。如果 Key 正确,检查 base_url 是否写成了https://taotoken.net/api/v1,SDK 会重复拼/v1导致鉴权失败。正确写法是https://taotoken.net/api。还有一种情况是 Key 被禁用或余额不足,去控制台看一下 Key 状态。
local proxy failed / connection refused:这类报错通常出现在你本地配了代理,但代理没启动。先检查环境变量HTTP_PROXY、HTTPS_PROXY是否指向了一个不存在的端口。如果你不需要代理,直接unset掉。另外确认你的网络能正常访问https://taotoken.net/api,可以用curl -I https://taotoken.net/api看返回码。
reading choices 报错 / KeyError: 'choices':这通常说明返回体不是标准的 chat completion 结构。先打印完整resp看看。常见原因是 model 字段填了一个不存在的模型名,服务端返回了错误对象,而你的代码直接取resp.choices就崩了。解决办法是先判断resp里有没有error字段,再取 choices。另外确认你用的 SDK 版本和接口兼容,老版本 openai SDK 对某些字段解析不同。
OAuth 相关报错:如果你在 Claude Code 或 Cline 里看到 OAuth 失败,通常是因为工具默认走了官方登录流程,而你想用 API Key。这时候要在配置里显式指定 API Key 模式,把 Base URL 指向https://taotoken.net/api,Key 填你的 TaoToken Key。有些工具需要你在设置里关掉“使用官方账号登录”,改成“自定义 API 端点”。配置完重启工具再试。
tool_calls 为空:模型没有触发工具调用,先检查tool_choice是不是auto,再检查 tools 数组的 JSON Schema 是否合法。如果parameters里写了required但字段名拼错,模型可能无法正确生成参数。另外,有些模型对工具描述很敏感,description 写得太模糊会导致模型不调用。把 description 写具体,比如“查询指定主机的磁盘使用率”就比“查磁盘”好。
参数解析失败:json.loads(call.function.arguments)报错,说明模型返回的 arguments 不是合法 JSON。这在弱模型上偶发。解决办法是加一层 try-except,解析失败时把原始字符串打日志,并让模型重试一次。标准 function calling 下这种情况比手写 prompt 少很多,但并非为零。
排障时建议打开 SDK 的日志,或者用 curl 直接发一次请求,排除 SDK 封装的干扰。curl 示例:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":"hi"}]}'如果 curl 通而代码不通,问题就在代码的 base_url 或 SDK 配置上。
6. 把四者串成一条可维护的链路:从 Prompt 到 MCP 的工程化建议
跑通一次调用只是开始,真正要落地的是可维护性。我的建议是:Prompt 单独放一个文件,用模板变量注入,不要硬编码在代码里。function calling 的 tools 定义从 MCP Server 动态拉取,而不是手写。Agent 循环写成独立函数,方便加日志和重试。MCP Server 按领域拆分,比如 ops-tools、db-tools、web-tools,每个 Server 只暴露相关工具。
这样分层之后,你要换模型,只改 model 字段;要加工具,只改 MCP Server;要调人设,只改 Prompt 文件。三者互不影响。TaoToken 在这里的角色是统一出口:不管底层换哪个模型,Agent 侧的 Base URL 和 Key 都不变。你可以在控制台按 Key 维度看调用量,排查是哪个 Agent 在疯狂重试。
如果你准备长期跑 Agent,建议用 Coding Plan,额度更稳:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入文档在这里,遇到参数细节可以查:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。Key 管理在控制台:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
最后给一个 Agent 循环的伪代码,你可以直接照着实现:
def run_agent(user_input, tools, max_turns=5): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input}, ] for _ in range(max_turns): resp = client.chat.completions.create( model=MODEL_ID, messages=messages, tools=tools, ) msg = resp.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for call in msg.tool_calls: args = json.loads(call.function.arguments) result = dispatch(call.function.name, args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result), }) return "达到最大轮次,任务未完成"这段伪代码就是 AI Agent 的心脏。Prompt 给它目标,function calling 给它格式,MCP 给它工具,TaoToken 给它模型通道。四者各司其职,你只需要把每一层配置对,剩下的就是业务逻辑了。