本系列前两篇讲了 MCP 的接入(SDK 十几行跑起 Server)和协议层(60 行手写 JSON-RPC)。但留了一个最关键的问题没拆:客户端把 20 个工具清单递给大模型之后,模型是怎么决定"这次该调 echo、还是直接回答、还是先调两个工具再回答"的?
这篇文章就拆这一环——function calling 的决策机制。搞懂它,你才能真正解释那些日常玄学:为什么工具描述改一个词,模型就不调了;为什么工具挂多了,回答反而变笨;为什么模型的参数偶尔是坏的 JSON。
很多人以为"模型调用了工具",好像模型伸手去执行了什么。真相是:模型只做一件事——生成文本,只不过是一段特殊格式的文本。
完整的决策循环分五步,执行方是应用,不是模型:
所以工具调用本质是一场应用与模型之间的结构化对白。模型是大脑,应用是手脚。理解了这一点,后面所有机制都顺理成章。
第二个常见误解:以为工具定义放在某个"工具库"里,模型按需查询。实际是——每一次请求,所有工具定义都会序列化成文本,塞进模型的上下文窗口,和用户消息一起算输入 token。
这意味着三件事:
这也是官方生态在推"渐进式工具发现"(按需加载工具清单)的原因——工具一多,全量注入的模式顶不住。
模型逐 token 生成。走到"该输出内容"的位置时,它面对的候选里既有普通文本的 token,也有"开始一段工具调用"的特殊 token。选哪个,是由上下文概率决定的——用户问题与哪个工具描述更匹配,那个方向的概率就越高。
所以工具描述不是文档,是决策特征。描述里写着"获取天气预报,输入城市名",用户问天气时这段文字被激活的概率就高;描述写得含糊,概率就流向"直接回答"。
主流 API 都提供工具选择开关,常用四档:
工程上这很有用:确定性流程用强制指定,开放对话用 auto。
复杂问题模型可以一次生成多个工具调用(并行调用),也可以在一轮里"调工具 → 看结果 → 再调另一个工具"地走多步。整个循环由应用驱动:只要有工具调用输出,执行完把结果塞回去再请求,直到模型输出纯文本为止。
看一段真实的消息流(OpenAI 风格),注意第二段的 tool_calls:
[
{ "role": "user", "content": "上海明天热吗" },
{
"role": "assistant",
"tool_calls": [
{
"id": "call_001",
"type": "function",
"function": {
"name": "get_forecast",
"arguments": "{\"city\": \"上海\"}"
}
}
]
},
{ "role": "tool", "tool_call_id": "call_001", "content": "{\"temp\": 31, \"cond\": \"多云\"}" },
{ "role": "assistant", "content": "上海明天 31 度,多云,稍微有点热。" }
]注意看 arguments 那行:它是一个JSON 字符串,不是 JSON 对象。因为模型的输出本质是文本流,参数是逐 token 拼出来的字符串,API 原样交付。所以应用侧必须自己 JSON.parse,而且必须 try-catch——模型偶尔会产出带尾逗号、单引号甚至截断的坏 JSON。这是 function calling 工程化的第一道防线。
把机制翻译成可操作的清单:
现在把三篇文章串成一条线:
tools/list,拿到工具清单(第一篇的 registerTool 注册的那些);tools/call 发给你的 Server(第二篇手写的那个方法);MCP 负责运输,function calling 负责决策,应用负责执行——三层各司其职,这就是"AI 应用的手和眼"的全部解剖图。
function calling 没有魔法:工具定义是塞进上下文的文本,"调用"是模型生成的一段特殊文本,选哪个工具由描述与问题的匹配概率决定。记住三个工程锚点——描述即决策��征、arguments 必须防坏 JSON、工具越多越钝——你写出来的工具生态,模型用起来会准得多。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。