大模型工具调用function call原理及实现
气死我了,终于有人把这事儿说明白了
你知道2023年6月那会儿,我有多崩溃吗?
用户轻飘飘一句“北京今天天气怎么样”,我得先写个正则表达式,跟做手术似的把“北京”两个字从一堆汉字里精准剥离出来。然后调天气API,拿到数据再塞回给模型,祈祷它别整幺蛾子。
结果你猜怎么着?
我明明在Prompt里写了“请输出JSON”——它非要回我一句:“好的,我这就帮你查~☀️”
那个笑脸符号给我看怒了。
又笨重,又危险,动不动就炸。
我那时候在上刷到一个老哥的吐槽,到现在还记得——“我把function描述写得比我的简历还长,模型还是瞎调。”
扎不扎心?
那时我们怎么活下来的?靠玄学
最早的方案,说白了就是“玄学调优”。
你在系统Prompt里反复念叨:“如果用户问的是实时信息,请输出JSON,key是function,value是参数列表。”
然后呢?
然后双手合十,祈求老天爷保佑模型别把JSON放进Markdown代码块里,别给参数加多余的引号,别突然蹦出一句“我也不是很确定,但我觉得应该调用这个函数……”
说到这儿,有个事儿我至今想来后背还发凉。
有一次我测试一个“查询订单”的工具,用户只是随口问了一句:“你们公司靠谱吗?”
结果你猜模型干了什么?
它直接把我内部的 query_order(status="cancelled") 给输出出来了!
隐私?全没了!
那个阶段所谓的“工具调用”,就是看脸。脸好的时候,输出格式对了;脸不好的时候,直接崩给你看。
2023年6月,炸弹来了
就在那段时间,OpenAI在GPT-4的API里悄悄加了个 functions 参数。
说实话,我第一反应是——“又他妈画饼。”
但我还是手贱试了一把。
然后我整个人傻了。
我注册了一个 get_weather 函数,描述了 location 和 unit 两个参数,然后问:“波士顿现在气温多少?”
它返回的不是废话文本,而是一个干净的结构化JSON:
{
"name": "get_weather",
"arguments": "{\"location\": \"Boston\", \"unit\": \"fahrenheit\"}"
}你看,它没有自己去查天气。它只是告诉你:“我要调用这个函数,参数是这个。”
我拿到这个JSON,解析,调API,拿到结果再喂回去,模型才给出自然语言的回答。
整个过程一句话就能说清楚:模型只负责决策,不负责执行。
就这一点,把所有混乱都消除了。
因为这时的格式是API强制约束的,不是靠Prompt忽悠出来的。模型经过了专门的微调,知道什么时候该输出调用指令、什么时候不该输出。
有同行问我:“那它怎么知道该不该调?”
这个问题问到点子上了。
你看过OpenAI的官方博客就会知道,Function Calling的模型经历了SFT和RLHF的训练,这里面也包括了基于AI反馈的强化学习——就是用一个AI当裁判,给候选响应打分,而不是全靠人工。
成本低、速度快,还能批量训练。
模型在大量反馈中学会了边界感:用户问“你叫什么名字”,直接回答就行,没必要调工具;用户问“帮我查个实时数据”,这时候再出手。
这个边界感,SFT给不了。必须靠反馈信号反复捶打才能形成。
踩过的坑,每一个都血淋淋的
我刚开始写Function Calling,犯的第一个错是——函数描述写得太抽象。
比如我写 get_weather 的描述:“获取天气信息。”
结果呢?用户问“明天适不适合跑步”,模型死活不调天气工具——因为“适不适合跑步”和“获取天气信息”在语义上没半毛钱关系。
后来我把描述改成了:“获取指定地点和日期的天气实况或预报,用于回答与天气相关的问题(如是否下雨、温度高低、是否适合户外活动)。”
模型一下子就懂了。
你能想到吗?参数描述也是个巨坑。
有个参数叫 time,我写的是“时间”。结果模型把用户说的“后天”直接传了个字符串“后天”过来,完全不是规范的日期格式。
后来我加了详细描述:“日期,格式为YYYY-MM-DD,支持相对日期如‘后天’自动转换”,并且设置了format为date。
模型才学会输出规范格式。
还有一个让我血压飙升的坑:模型会自己脑补不存在的结果。
有次我测试一个 search_web 工具,还没等我执行函数呢,模型直接开始回答问题了。
后来发现是什么问题?是我在调用时没传入 function_call="auto",直接就留空了。你说它不瞎猜才怪。
设置 function_call="auto" 之后,模型才学会了在需要的时候输出调用指令,不需要的时候正常回答。这个特性到GPT-3.5-turbo-1106之后才真正稳定下来。
别人怎么跟进的?
Function Calling这东西不是OpenAI的专利。
Anthropic在Claude 2.1里也加了,格式大差不差,只是参数叫 tools 而不是 functions。Google的Gemini系列也支持,消息结构里多了一层 tool_call 字段。
开源这边,Llama 3.1和Qwen 2.5都通过微调实现了类似的能力。
我当时还特意去看了ChatGLM2和ChatGLM3的源码差异。改动其实不大——加几个特殊token(<|assistant|>之类的),再加一些处理function schema的逻辑。
用法跟OpenAI几乎镜像,schema结构一模一样。
但有意思的是,各家模型的“调用意愿”差别超级大。
有的模型,你问个“2+2等于几”它都要调一下 calculator 工具——明显是训练过度偏向了调用。有的模型正好相反,明明该调工具了,它死扛着用自己的训练知识回答,然后给你一个幻觉答案。
这背后还是训练数据的平衡问题,不是改几句描述能解决的。
一次调用不够?那就循环调用
单个Function Calling,解决不了复杂场景。
比如用户说:“帮我查一下明天上海天气,如果超过30度就给我发个邮件提醒。”
怎么搞?得先调天气,根据结果决定要不要调邮件。一个完整的Agent流程,得把Function Calling放在循环里——模型输出调用指令,代码执行,结果喂回,模型再判断,可能再调用,直到最后回答用户。
我在这件事上也踩过坑。
有一次,模型发现天气超过30度,调了邮件工具,邮件发成功了。结果你猜怎么着?它又调了一次邮件,说“我要再确认一遍。”
用户收到两封重复邮件。
解决方案其实很简单:在系统提示里加一句“每个工具只能调用一次,不要重复调用”,代码层再做好幂等性检查——记录已调用的函数和结果,再次调用时直接返回缓存。
这才是Agent的朴素实现:不是一次决策,而是一个决策-执行-反馈的闭环。
Function Calling就是这个循环的基础砖块。
现在和未来
到今天,我手上几乎所有涉及大模型的生产代码,都用上了Function Calling。
行业正在把它标准化:Anthropic的MCP和Google的A2A都在尝试定义一套协议,让工具的注册、发现、调用更统一。
我个人的看法是,MCP的潜力更大。因为它把工具描述从代码级变成了可插拔的配置——你只需要在一个JSON文件里声明一个 weather_api 的endpoint,模型就能自动理解怎么用,不需要每次在代码里写死schema。
但也别高兴太早。
标准化的代价是灵活性下降,而且模型理解协议本身还需要额外的推理成本。目前实测,MCP协议的调用成功率比硬编码的Function Calling低了大概5个百分点。
短期来看,自己管理schema还是更靠谱。
关于训练层面,已经有公司在尝试用强化学习直接训练模型的“调用路径”,而不只是单次调用。前文提到的AI反馈在工具调用训练中起了很大作用,但我个人觉得这还不够。
未来的模型应该具备“调用规划能力”——第一次输出时就能不止输出一次调用,而是一个调用序列。
OpenAI的o1系列已经有些苗头了。它在推理过程中会自己生成计划,然后把计划分解成小步骤,每步对应一个工具调用。虽然o1没开放工具调用的接口,但我猜这个方向是明确的。
最后说两句掏心窝子的话
Function Calling不是什么黑魔法。
它本质上就是一件事:让模型输出一个结构化的调用指令,然后你替它干活。
过去三年,我见过太多AI创业团队在“工具调用”上过度DIY——自己写一堆正则、用一些小模型做意图分类、硬配API。其实直接用模型自带的能力,把schema写清楚,就能省掉80%的脏活。
但最容易被忽视的,还是描述的质量。
我见过一个团队,写了30个工具的schema,结果模型死活不调用他们最核心的 search_products——就因为他们描述写得太模糊。
后来我帮他们把描述从“搜索商品”改成了“根据用户输入的关键词、品类、价格区间,从商品数据库中检索匹配的商品列表,用于回答‘找一下……’、‘有没有……’、‘推荐……’等需求”。
调用率从22%直接飙到79%。
这个细节,说难听点,决定了一个Agent能不能跑起来。
模型本身是聪明的,但你要告诉它工具怎么用、什么时候用、参数是什么——而且要用模型能理解的语言,而不是人类语言。
说白了,你在写一份给AI看的说明书,而不是给自己看的。
这就是我踩坑三年,最大的心得。
读者评论 4