Function Calling 的真相,大模型其实一个工具都不会调
你想做一个能查天气的智能助手,或者一个能查公司数据库的智能客服,再或者,让 AI 在你不在工位的时候,替你回邮件、订会议室。
这些需求凑到一起,都会撞上同一堵墙。
模型不知道北京今天多少度,它进不去你的数据库,更没有权限碰你的邮箱。
想翻过这堵墙,就绕不开一个技术,Function Calling,中文一般叫函数调用,或者工具调用。
这篇文章就把它的机制完整拆一遍。主要包括五个部分,首先我们看一个反直觉的真相,它跟大多数人想象中的完全不一样。然后我们把一次完整的函数调用拆成五步,一步步走。接下来讲工具说明书,整个机制里最值得你花时间的地方。再往下,聊聊实际用起来最常见的几个坑。收尾我们看看,循环起来的函数调用是怎么变成 Agent 的。
好,先从那个真相开始。
玻璃房
Function Calling 给人的直觉印象是,模型学会了使用工具,它自己伸手去查天气、查数据库、发邮件。
实际发生的事情完全不是这样。
大模型从头到尾,只会做一件事,输出文本。
你把问题发进去,它吐一段文本出来。你把资料发进去,它吐一段文本出来。它这辈子没干过别的。
你可以把它想象成一位住在玻璃房里的顾问。需求可以从门缝递进去,他能给出判断,能写下指示,但他自己的手够不到外面的任何东西。递不出一张纸,按不下任何一个按钮。
拿我们的场景来说,用户问了一句,北京今天多少度。
模型收到这句话,它想查天气吗?它可能「想」。但它查不了。
它连一次 HTTP 请求都发不出去。
不是权限不够,也不是没装插件。是它的运行机制里根本不存在「发请求」这个动作。模型的工作方式,是根据收到的全部文本,一个字一个字地预测下一个字。预测,输出,结束。它能做的所有动作就是吐字。
再补一刀。就算模型脑子里「记得」某些天气信息,那些也是训练数据里的旧闻。训练是有截止日期的,今天的温度,它不可能记得。所以对「北京今天多少度」这种问题,模型只有两条路,要么承认不知道,要么编一个像样的数字。
顺便说一句,这个设计不是缺陷,恰恰是分寸。模型不能擅自对外发起任何动作,所有的钥匙就都攥在你的程序手里。调谁的 API、用什么权限、一天限多少次,全是你说了算。模型从玻璃房里递出来的永远只是一张请求单,批不批,是你的事。
看这张图,左边进的是文本,右边出的也是文本。天气 API、数据库、邮件服务,全都挂在你程序的手上,而不是模型的手上。
那天气到底是谁查的?
你的程序。
所以 Function Calling 是一场双人舞。模型负责说,说清楚想调哪个工具,参数是什么。程序负责做,真的去调 API、查数据库、发邮件。两边靠文本传话,一来一回。
这个分工先记住,下面我们把整场舞完整走一遍。
一次往返
先搭一个最小的场景。
用户问了这句话,北京今天多少度。
我们提前在自己的程序里注册了一个工具,名字叫 get_weather。它背后接了一个真实的天气 API,传进去一个城市名,返回这个城市的实时天气。
整篇文章我们就用这一个例子,把它从头用到尾。
一次完整的函数调用,就是围绕这句话和这个工具的一次往返。先看地图,一共五步。
下面我们一步一步走。
第一步,说明书和问题一起出门
模型住在玻璃房里,它不知道你有一个 get_weather。
你要是不告诉它,它连编都没法编。
所以第一步,你的程序把工具的说明书和用户的问题,打包在同一次请求里,一起发给模型。
说明书长什么样呢?大概是这么三样东西。
名称 get_weather
描述 查询指定城市的实时天气
参数 city 字符串 要查询的城市名
名称,是工具叫什么。描述,是这个工具是干什么的、什么时候该用它。参数,是调用它需要填哪些字段,每个字段是什么类型。
真实世界里这份说明书是一段 JSON 格式的 schema,结构稍微复杂一点,但内容就是这三样,名称、描述、参数的定义。参数那一栏还能写得更细,每个字段是字符串还是数字,是必填还是可选,取值是不是只能从一个名单里挑,都写得进去。写得越细,模型填单子的时候就越有据可依。
还有一个容易忽略的点。模型没有持久的记忆,每一次请求对它来说都是全新的一场对话。所以你发过去的不只是这一句问题和说明书,还有之前的对话历史。模型是看着这一整包内容来做判断的。
好,第一步就到这里。你的程序把话递进了玻璃房,接下来轮到模型表演。
第二步,模型开口要工具
模型看完这包内容,开始生成。
注意,它没有去查天气。它也查不了,我们在玻璃房那节说过了,它只会吐字。
它做的是,输出一段结构非常规整的文本,大意是,我要调用 get_weather,参数 city 的值是北京。
要调用的工具 get_weather
参数 city 是 北京
就这么多。
还有一个同样重要的情况得说一下。调工具不是模型的必选项。
如果用户问的不是天气,而是天气是什么意思这种概念问题,模型看完说明书会觉得,这个我用不上,于是它跳过工具,直接生成一段普通的回答。走不走工具,是模型每次都要做的判断,说明书就是它做这个判断的依据。这一点我们讲说明书的时候还会回来。
这里要留意一个措辞上的细节。模型说的是「我要调这个工具」,而不是「我调了」。它写下的是一张单子,一个请求,一个意图。动作本身,一下都没发生。
你可能会有点不服气。因为在真实的 API 里,你拿到的往往已经是解析好的结构体,工具名和参数都摆在整整齐齐的字段里,看起来一点都不像「一段文本」。
没错,这确实是 API 帮你做的一层加工。模型那一端真实发生的事情,仍然是吐出一串 token。只不过模型被专门训练过,让它想在需要工具时输出一种非常规整的格式,API 再把这串格式稳稳地解析成对象递给你。
也就是说,你手里那个结构体是果,模型输出的文本是因。
而且 API 会给你一个标记,告诉你这次模型是想要调用工具,还是想把话说完。你的程序看这个标记,就知道下一步该干嘛。
回扣一下主线。到这一步,模型的工作已经完成了它的前半段,它交了张单子,说要调 get_weather,city 是北京。然后它就停在玻璃房里,等。
那谁来调?
第三步,程序真的去调
你的程序接住模型的输出,一看标记,是要调工具。再看内容,get_weather,city 是北京。
程序里真的有一段代码叫 get_weather。这段代码是你写的,它拿着北京,去请求真实的天气 API,带上鉴权,处理超时。API 返回了结果,晴,26 度。
这一步是整个机制里唯一的真动作。
HTTP 请求是你发的,密钥是你配的,报错了也是你兜底。模型在这件事里的角色,到第二步交完单子就停了。
这个感觉有点像餐厅点菜。服务员把单子递进后厨,菜不是服务员炒的,但他得把单子递对,把菜端回来。模型就是那个服务员,你的程序是后厨。
好,菜出锅了。北京,晴,26 度。下一步怎么办?
第四步,把结果拼回对话
程序拿到 26 度,不能就这么直接甩给用户吗?
坦率的讲,有时候可以。如果你的产品只是想显示一个温度数字,程序确实可以跳过模型,直接展示。
但多数时候不行。因为用户要的是一句话,而且模型还不知道结果。它的上下文里,目前只有用户的问题和它自己那张单子。它不知道天气查出来是多少。
所以我们把结果拼回对话。
具体的做法是,程序把工具的返回结果,作为一条新消息,追加到对话历史的末尾,然后把这份更新过的对话,再次发给模型。
这里有个细节值得单独说。拼回去的不一定只有成功的结果。
假如天气 API 挂了,或者城市名没匹配上,工具失败了,这个失败也要如实拼回对话,告诉模型,工具调用出错了,原因是这个。模型看到失败信息,可以选择换个参数重试,可以再调一次,也可以直接跟用户说,抱歉刚才没查到。你把失败藏起来,模型就永远以为查到了,后面的回答全是空中楼阁。
另外,有些工具的返回特别长,比如查数据库回来一大段记录。一股脑全塞回去既费钱又容易把关键信息淹没,实际工程里往往要先裁剪再拼。怎么裁是另一门学问,这里先不展开。
这一次模型看到的内容是完整的。用户问了北京今天多少度,我自己说要调 get_weather,工具返回了晴 26 度。三段全在。
也就是说,第四步干的事情是搬运。把玻璃房外面的东西,写成文字,从门缝塞回去。
第五步,模型说话
模型基于这份完整的上下文,生成自然语言的回答。
北京今天晴,最高 26 度,适合出门。
程序把这句话展示给用户。一次函数调用的往返,到这里就走完了。是不是很简单。
我们把整场舞放到一张时序图里,再整体看一遍。
看这张图的时候,盯住一个东西,模型和天气 API 之间,从头到尾没有任何一条线。
所有的箭头都要经过你的程序。
再回头数一数那五步,真正干活的只有第三步,是你写的代码在调 API。其余四步,全部是在组织和搬运文本。
这就是 Function Calling 的全部秘密。它不是模型获得了什么新能力,而是你的程序和模型之间,学会了一套配合的协议。模型出嘴,程序出手。
好,一次往返拆完了。但你可能会隐隐觉得哪里不对,模型怎么知道该调 get_weather 的?它怎么知道 city 要填北京?
问得好。这就得聊聊那份说明书了。
说明书
第二步里模型做的判断,调不调、调哪个、参数填什么,依据从哪来?
不是来自你的代码。模型从来没有见过你的代码,它看不见 get_weather 函数体里写了什么,也看不见你接的是哪家的天气 API。
它的全部依据,就是第一步发进去的那份说明书。
说明书就是模型对你工具的全部认知。这份纸上写的每一个字,都会直接变成模型的行为。
那说明书写得好不好,差别有多大?
我们走一遍反面。假设描述这一栏写得特别省事,就写了两个字,查天气。
用户问北京今天多少度,模型看着这两个字琢磨。什么算天气?湿度算吗?问明天下雨算吗?city 该填北京、北京市还是 beijing?说明书没说。
模型只能靠猜。猜得好是运气,猜得差就是事故。
更糟的情况是,模型觉得这天气问题自己好像也能答,说明书又没写清楚什么时候该调,于是它压根不调工具,直接编了一个温度出来。用户看到的 22 度,是模型一本正经编的。
那好的说明书长什么样?
描述写具体,查询指定城市的实时天气,当用户询问某个城市当前的温度或天气状况时调用。参数写清楚,city,字符串,传城市的中文全名。
你可以把它想象成远程指挥朋友帮你弄家里的路由器。他看不见你的路由器,你发的每条指令他只能照字面理解。重启一下那个东西,和按住背面小孔里的重置键十秒,是完全两种下场。
所以写说明书的时候有个心态上的转变。你不是在给机器填配置表,你是在给一个看不见现场、只能靠文字行动的帮工写指令。他不知道的,就是你没写的。
实际写的时候还有两个好用的手段。一个是给参数加约束,比如 city 可以限定枚举值,模型就没法填一个不在名单里的城市。另一个是把不该调用的场景也写进描述里,比如只支持实时天气,不预报未来几天,模型的判断会稳很多。
不过话说回来,说明书写得再好,模型也不是机器。它是个概率模型,照着说明书办事,绝大多数时候靠谱,偶尔也会翻车。
翻车长什么样,我们来看看最常见的几种。
坑
第一种坑,模型幻觉出不存在的参数。
说明书里 city 是唯一的参数,模型却给你输出一个 unit,值是摄氏度。它不是故意捣乱,是它见过太多天气相关的代码,觉得这里「应该」有个温度单位。参数不在 schema 里,你的程序一解析就报错。
应对的思路是两手抓。一手是程序侧做好校验,不认识的参数直接拒绝或者忽略,别让它带崩流程。另一手是说明书侧把参数写全写细,模型可猜的空间越小,编参数的概率越低。
第二种坑,该调的时候不调。
就是我们说明书那节走过的反面,用户问北京今天多少度,模型觉得自己知道,直接编了个数字回答。这类坑最隐蔽,因为流程不报错,回答看起来也像模像样,错误藏得很深。
解法主要在说明书,把「什么情况必须调用」写明白,模型就有了明确的行为依据。
第三种坑,不该调的时候乱调。
用户只是随口感慨了一句今天天气不错,模型噌地调了一次 get_weather。流程没坏,但白白多了一次请求,多花一份钱,用户还得多等一秒。
这类坑同样靠描述去收,写清楚工具服务于什么意图,闲聊和感慨不属于。
三种坑放在一起看,你会发现它们的病根是同一个,模型对工具的理解完全来自说明书,而模型执行说明书的方式是概率性的,不是机械的。
写好说明书,就是把概率往靠谱那边推。做好校验,就是给概率留了兜底。
再给一个实操建议。排查这类问题的时候,别只看最终回答,把每次请求发出去的完整内容和模型返回的原始输出都打出来看。九成的坑,对着原始输入输出看两眼就能定位,是说明书没写清楚,还是参数校验没兜住。
好,坑就聊到这里。到这里为止,我们讲的都是一次往返,模型说一次,程序做一次,一问一答就结束。
但如果任务没那么简单呢?
循环
我们把开头的例子升个级。
用户这次问的是,北京和上海今天哪个更热?
一次 get_weather 只能查一个城市。这个任务需要两次工具调用,还要一步比较。
流程会变成这样。程序把问题和说明书发给模型,模型输出单子,要调 get_weather,一次要北京,一次要上海,有些模型支持在一次回复里同时请求多个工具调用。程序分别执行,拿到两个结果,晴 26 度,多云 32 度,把两个结果都拼回对话,再发给模型。
模型这回看到的上下文里,两个城市的数据都齐了,它生成回答,上海更热,32 度,比北京的 26 度高了 6 度。
注意这个过程里,先查哪个城市、要不要查第二个、什么时候数据够了可以回答,这些决定都是模型自己做的。你的程序只负责执行和搬运。
把这套东西套上循环,就是 Agent。
看这张图,中间那个循环就是 Agent 的心脏。模型看上下文,决定下一步,程序执行,结果拼回去,模型再看,再决定。一圈一圈转下去,直到模型判断不需要再调工具了,输出最终回答,循环结束。
循环虽好,也得给它上个保险。
模型有可能判断失灵,一圈接一圈地转下去停不下来,比如反复用同一个参数调同一个工具,每次都觉得结果不对。所以实际写 Agent 的时候,几乎都要设一个最大轮数的兜底,转够了圈数就强制停下,别让一次失灵烧掉一整晚的请求。
回到我们开头画的那些需求。你想要一个替你订会议室的助手,拆开看,就是查空闲会议室的工具、发邀请的工具、写通知邮件的工具,每一样都是一个 get_weather 式的往返,套上这个循环,让模型自己串起来。没什么新魔法,只是往返的次数变多了。
所以 Agent 不是什么神秘的新物种。给它一套工具说明书,再套上这个循环,让它自己决定下一步做什么、什么时候停,这就是 Agent。函数调用是那条腿,循环是让它走起来的方式。
当然,循环一多,新问题就来了。工具越接越多,天气一家、地图一家、数据库一家、邮件又一家,每家的接法还各不相同。为了这个乱局,才有了 MCP 这样的标准协议,专门统一工具的接入方式。这一层我们之前那篇《MCP 到底解决了个什么问题?》已经拆过,这里就不展开了。
到这里,我们把整条链路重走一遍。
首先用户问了一句,北京今天多少度。
然后我们的程序把这句话,连同 get_weather 的说明书,还有对话历史,一起发给模型。
模型没有去查天气,它输出一段结构化的文本,说要调用 get_weather,参数 city 的值是北京。
接着程序解析这段文本,真的去请求天气 API,拿到结果,晴,26 度。
然后程序把这个结果作为一条新消息,拼回对话历史,再次发给模型。
模型基于这份完整的上下文,生成自然语言回答,北京今天晴,最高 26 度。
程序把答案交给用户,一次往返结束。
如果给这个往返套上循环,让模型自己决定下一步调什么、什么时候停,它就升级成了 Agent。
这就是整场双人舞的完整走位。
到这里,一次函数调用的完整往返就算是拆完了。如果你想系统地学习这些内容,从大模型原理一路讲到 Agent、RAG 和模型部署,可以看看这门0基础Agent开发课,220 个课时,从零基础一路讲到生产部署。
评论区
暂无评论,快来抢沙发吧