前面两篇分别聊了LLM这个"超级毕业生"本身,以及怎么用Prompt跟他沟通。今天我们说点更实在的------怎么让他帮你干活。
LLM知识渊博,但他没有亲身经历,也不能接触实时世界。他不知道现在外面下没下雨,不会帮你查订单,更没法帮你发一封邮件。**Tools(工具)**就是补上这块短板的。
你可以把Tools想象成给这个毕业生配上的手机和工具箱。他不会做饭,但你给他手机里装一个订餐App,他就能帮你叫外卖了。他不会查天气,但你给他一个天气查询接口,你问他"北京今天天气怎么样",他不再说"抱歉,我无法获取实时信息",而是告诉你"今天北京晴,25度"。
这就是Tools的价值------让LLM拥有与现实世界交互和获取实时信息的能力 。但要注意,装上App不等于他会自动点开。谁来点、怎么点、点错了怎么办,这就是Tool Calling要解决的事。
Tool Calling:不是魔法,是协作流程
Tool Calling (工具调用),在OpenAI等API文档里常叫Function Calling(函数调用),是同一类机制,只是叫法不同。它的核心逻辑是:模型在回答时不瞎编,而是输出结构化的"我要调用哪个工具、参数是什么",由你的程序真正执行后,再把结果交回给模型,模型基于结果继续推理和回复。
整个链条必须能在你的脑子里(和代码里)跑通:
AI判断要调工具 → 输出工具名 + 参数 → 你的代码执行函数 → 结果返回 → AI继续推理
这个过程对用户来说是无感的,他只知道问了问题,得到了准确答案。但对开发者来说,每一步都需要处理。
AI到底是怎么"调用函数"的?
先说一句大实话:LLM本身不会在你的服务器上执行代码,它只会生成文本。之所以能"调用函数",是因为厂商在训练和对齐模型时,教会了它一件事------除了普通回复,还可以输出一种约定格式的结构化内容,也就是"工具名 + JSON参数"。
真正执行函数的是你的应用,不是模型。可以这样理解:
- 模型 = 毕业生在填一张《工具申请单》
- 你的程序 = 行政部按单子去办事,然后把回执贴回他桌上
- 他再根据回执用自然语言向你汇报
所以"AI会调用函数"这个说法,准确讲应该是:AI会决定并请求调用;执行权始终在你手里。这个设计既安全(你可以做鉴权、限流),又灵活(工具里可以连数据库、调外部API,而模型根本碰不到密钥)。
Function Calling的底层流程
Function Calling在API层面的实现,其实就是在普通的对话流程里加了一个分支:
-
注册 :你把每个工具的
name、description、parameters(一般是JSON Schema)发给模型。这相当于告诉毕业生,你有哪些工具可用,各自怎么用。 -
决策 :模型生成回复时,内部判断是直接说话,还是发起工具调用。这个判断对用户不可见,体现在API返回里就是有没有
tool_calls字段。 -
结构化输出 :如果决定调用,模型会返回类似这样的东西:
json{ "name": "get_weather", "arguments": { "city": "北京" } } -
执行与回灌 :你的代码根据
name找到对应函数并执行,把返回结果作为一条tool角色的消息追加到对话里,再请求模型生成最终回答。
模型从头到尾都没运行过get_weather这个函数,它只是预测出了"应该填这张单子"。这和预测下一个汉字是同一套能力,只不过输出格式被约束成了JSON。
这里要区分两个叫法:Tool Calling 是更通用的说法(MCP、Agent框架里常用);Function Calling 则多指OpenAI等文档里的tools/functions字段。在Mastra这类框架里,你用的是createTool,底层还是同一套循环,只是帮你封装好了。
Tool Schema:写一份好用的说明书
Tool Schema 就是描述工具的说明书,告诉模型这个工具叫什么、干什么、需要哪些参数、每个参数什么类型。通常用JSON Schema表达,挂在工具的parameters或inputSchema字段上。
一份清晰的Schema至少包含三样:
- name :唯一标识,比如
query_order,英文、蛇形命名是常见约定。 - description:用自然语言说明什么时候该用、不要用错场景。模型主要靠这段描述来选工具,所以写得越具体越好。比如"查询指定城市的实时天气。用户问气温、下雨、穿衣建议时使用",比光写"查天气"要好一百倍。
- parameters :一个
type: object,里面定义properties(每个字段的类型和描述)和required(必填字段列表)。
举个例子,一个天气查询工具的Schema大致长这样:
json
{
"name": "get_weather",
"description": "查询指定城市的实时天气。用户问气温、下雨、穿衣建议时使用。",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名,如 北京、上海" }
},
"required": ["city"]
}
}
写Schema是个工程活,不是写一两次就完美。我自己的经验是,description要多花心思,把什么场景用、什么场景别用写明白,能减少很多误调用。参数的类型和描述也要准确,不然模型可能传个"北京今天"这种带多余文字的字符串过来,你后面处理就得费劲。
参数验证:模型填的单子不能全信
模型填好的参数不一定合法。可能漏字段、类型错了、枚举值瞎写,甚至被注入恶意字符串。所以参数验证必须在你的程序侧做,不能全信模型。
常见做法是:
- 用Zod 、JSON Schema校验库等,在
execute之前校验arguments。 - 校验失败时,不要直接抛异常把对话卡死,而是把错误信息作为工具结果返回给模型,让它重试或改口告诉用户"这个参数格式不对"。
- 对
orderId、userId这类字段做格式白名单(正则、长度、类型);敏感操作还得再查用户权限。绝对不要因为模型说"可以删"就真的删。
还是用那个比喻:申请单上的金额可以写错,财务部必须复核后才付款。在Mastra的createTool里,inputSchema既给模型看,也给运行时校验用,两处合一,你少写一套重复逻辑。
一次完整的Tool Calling:把链路串起来
下面用一个实际的例子,把整个Tool Calling流程串起来。这也是后面要讲的Agent Loop里的关键一步。
假设用户问:"我订单88392到哪了?"
- 组装请求:你的应用把System Prompt、用户消息,以及可用工具的Schema列表一起发给LLM。
- 模型决策 :LLM分析后,决定调用
query_order工具,返回tool_calls,内容是query_order({ "order_id": "88392" })。 - 解析与验证 :你的代码拿到这个JSON参数,先校验
order_id是不是合法格式。没问题,进下一步;如果有问题,把错误信息回给模型让它修正。 - 执行工具 :代码查数据库,拿到这条订单的真实状态,返回
{ "status": "已发货", "eta": "明天" }。 - 结果回灌 :把这条结果作为
tool角色消息追加到对话里,再请求模型继续推理。 - 最终回复:模型根据回执,用自然语言告诉用户:"您的订单已发货,预计明天送达。"
整个过程对用户来说只有一问一答,但背后走了好几个来回。这就是Tool Calling的魅力:它让LLM从只会说话,变成了能动手的助手。
实际开发中,你还会碰到模型一次返回多个tool_calls(并行调用)、模型调用工具后觉得信息不够再次调用、或者调用失败需要重试等等情况。但万变不离其宗,核心循环就是上面这几步。
Tool Calling是Agent能"动手"的核心机制。没有它,Tools只是一堆放在毕业生面前的App图标,没人点开。有了它,LLM才真正具备了与现实世界交互的能力。下一篇我们会继续深入聊Context和Memory------模型怎么记东西、怎么在有限的窗口里管理海量信息。