Function-Calling让大模型从会说到会干
引言
小明上周搞定了多轮对话,聊天机器人跑得有模有样。这周一上班,老板又丢来一个需求:
“咱们的 AI 客服能不能查订单状态?用户问’我的订单到哪了’,AI 直接回答,别让用户自己去系统里翻。”
小明心想这还不简单?给 AI 加条 system 消息:”你可以查询订单状态”。然后测试——
用户问:”我的订单 88899 到哪了?”
AI 回答:”您的订单 88899 已发货,预计明天送达,当前在成都转运中心。”
小明愣了。订单系统里根本没有这个订单号。AI 一本正经地编了一个答案。
他去找老张:”AI 又不老实了,我让它查订单,它直接编了个假的。”
老张说:”这叫幻觉。大模型的训练数据里没有你的订单系统,它不知道真实数据,就只能编。”
“那怎么办?让它连我的数据库?”
“大模型连不了你的数据库。但有个办法——Function Calling。让 AI 知道’有个查订单的函数可以调’,它需要查的时候就告诉你’调这个函数,参数是订单号 88899’,然后你的代码去真正查,把结果喂给它,它再基于真实数据回答用户。”
小明问:”所以 AI 自己不查,是让我去查?”
“对。AI 只是点菜,你的代码才是厨房。 今天就带你搞懂这个。”
核心认知:模型不执行函数,它只是”点菜”
老张在白板上画了个餐厅的流程:
“用户是顾客,大模型是服务员,你的代码是厨房。”老张说,”服务员不会炒菜,但他知道厨房有什么菜、每道菜需要什么食材。他负责把用户的需求翻译成’厨房听得懂的指令’,厨房做完菜再端给服务员,服务员最后给用户上菜。”
小明问:”很多人以为大模型能直接运行我的 Python 函数?”
“这是最常见的误解。”老张摇头,”大模型从头到尾不执行任何代码。它只做一件事——输出一个结构化的 JSON,告诉你’应该调哪个函数、参数是什么’。真正执行函数的是你的 Python 程序。”
分工是这样的:
| 角色 | 做什么 |
|---|---|
| 开发者(你) | 定义工具列表(菜单)、实现真正的函数、编排对话循环 |
| 大模型 | 分析用户意图,决定调用哪个工具,输出结构化参数 |
| 用户 | 用自然语言提问 |
“明白了。”小明说,”大模型是个’翻译官’,把用户的自然语言翻译成函数调用指令。真正的活还是我干。”
“说到点子上了。”
两轮对话:Function Calling 的完整流程
老张说:”普通对话是一轮——你问,AI 答。Function Calling 至少两轮——第一轮 AI 决定用什么工具,第二轮 AI 基于工具结果生成回答。”
他在白板上画了张完整的流程图:
“看到了没?”老张说,”第一次调用模型不回答问题,而是告诉你’我需要调 get_weather 这个函数,参数是北京’。你执行完函数,把结果塞回 messages 列表,第二次调用模型才基于真实数据生成最终回答。”
小明问:”所以 messages 列表在这两轮里一直在变?”
“对。messages 是累积的——第一次调用后,你要把模型的 tool_calls 请求追加进去;函数执行完,再把 tool 结果追加进去。第二次调用时,messages 列表比第一次多了两条消息。”
“这就是上次讲多轮对话时说的’消息列表是累积的’——Function Calling 只是多了一种 tool 角色的消息而已。”
工具定义:给模型一份”菜单”
老张说:”模型要知道有哪些工具可用,你得提前告诉它。这就是 tools 定义——用 JSON Schema 格式描述每个函数的名字、用途和参数。”
老张逐字段解释:
-
type: "function"— 固定写法,目前只有这一种类型 -
name— 函数名,必须和你 Python 代码里的函数名完全一致 -
description— 最关键字段。模型靠这段话判断”什么时候该调这个函数” -
parameters— JSON Schema 格式,描述参数的类型、枚举值、是否必填 -
required— 哪些参数不能省略
“重点说这个 description。”老张敲了敲白板,”很多人写 description 就一句’获取天气’——太模糊了。模型不知道什么场景该调。应该写清楚做什么 + 什么时候用。”
| 写法 | 效果 |
|---|---|
"获取天气" |
太模糊,模型可能在该调时不调,不该调时乱调 |
"获取指定城市当前的天气情况" |
清晰,模型知道用户问天气时该调 |
"查询天气信息,支持中国城市,返回实时数据" |
更好,连适用范围都说了 |
“还有参数的 description 也要写好。location 写’城市名称’不如写’城市名称,例如:北京’——给个示例值,模型提取参数的准确率会高很多。”
小明问:”那如果我有多个工具呢?”
“往 tools 列表里加就行。模型会根据用户的问题,自己判断该调哪个——或者一个都不调。”
“就像菜单上有多道菜,服务员根据顾客的需求决定传哪张单子到厨房。”
核心代码:两轮调用的完整实现
老张说:”概念讲完了,上代码。这是最小可运行版本,去掉注释不到 40 行。”
老张说:”看着长,拆开就三步。逐段讲。”
逐段拆解
第一步:定义工具函数
“这就是’厨房’——真正干活的函数。函数名、参数名必须和 tools 定义里完全一致。参数名不一致,模型传过来的值你接不住。”
“这里用假数据模拟。实际项目里,这个函数内部会调真实的天气 API、查数据库、或者读文件。但对外接口不变——接收参数,返回 dict。”
第二步:定义工具列表和映射表
“tools 是给模型看的’菜单’,available_functions 是给你的代码用的’路由表’——模型说’调 get_current_weather’,你通过这张表找到真正的 Python 函数并执行。”
“为什么不直接用函数名调?因为模型返回的是字符串 'get_current_weather',不是函数对象。你需要一个映射把字符串转成可调用的函数。”
第三步:第一轮调用——模型决定用什么工具
“注意这里比普通调用多了两个参数:tools 和 tool_choice。tools 是工具列表,tool_choice 控制模型的行为——auto 是让模型自己判断要不要调。”
“如果模型认为不需要工具,tool_calls 就是 None,response_message.content 直接就是最终回答。如果模型认为需要工具,content 是 None,tool_calls 里有调用请求。”
“这行 messages.append(response_message) 很关键。 模型的 tool_calls 请求也是一条 assistant 消息,必须存入历史。不然第二轮调用时,模型不知道自己之前要求调什么函数。”
第四步:执行函数,把结果喂回去
老张指着这段说:”这是 Function Calling 最核心的四行。”
-
func_name— 模型告诉你调哪个函数 -
func_args— 模型给你的参数,是 JSON 字符串,json.loads()转成 dict -
func_result— 通过映射表找到函数,用**func_args解包参数执行 -
messages.append(...)— 把执行结果包成tool消息塞回历史
“重点说这个 tool 消息。”老张画了个框:
“tool_call_id 是必须的——它把模型的调用请求和你的执行结果关联起来。模型可能同时调多个函数,每个函数的结果要用对应的 id 标记,模型才知道哪个结果对应哪个请求。”
“content 必须是字符串。函数返回的是 dict,json.dumps() 转成 JSON 字符串。ensure_ascii=False 保证中文不被转成 \uXXXX。”
第五步:第二轮调用——模型基于真实数据生成回答
“第二次调用不需要传 tools——因为不需要模型再决定调不调函数了。只需要把完整的 messages 历史(包含工具结果)传过去,让模型基于真实数据生成自然语言回答。”
“这轮的 messages 列表长这样:”
“模型看到这条 tool 消息,就知道’北京天气是晴,25°C’,然后生成最终回答。这就是从’编造’到’基于真实数据’的关键跨越。“
跑起来看看
小明运行了程序:
小明又试了几个问题:
小明注意到:”第三个问题它没调函数?”
“对。”老张说,”因为 tool_choice='auto',模型自己判断——用户问天气就调函数,用户打招呼就不调,直接回复。这就是’智能’的地方。”
“如果你问’成都和北京哪个热’呢?”
“模型会同时调两次函数——一次查成都,一次查北京。tool_calls 里有两条,你的 for 循环分别执行,两个结果都追加到 messages,第二轮调用模型综合两个结果回答。”
小明瞪大眼睛:”一个 for 循环就搞定了并行调用?”
“对。tool_calls 是个数组,里面有几条就调几次。你循环执行就行。模型自己决定调几次、调哪个。”
tool_choice:控制模型的行为
老张说:”tool_choice 这个参数控制模型’要不要调工具’。四个选项:”
| 值 | 含义 | 使用场景 |
|---|---|---|
"auto" |
模型自行决定(默认) | 通用对话,让模型自己判断 |
"none" |
禁止调用任何工具 | 纯聊天模式,模型只回答不调函数 |
"required" |
强制必须调用工具 | 业务流程必须走工具,不允许直接回答 |
{"type":"function","function":{"name":"xxx"}} |
强制调用指定函数 | 明确知道要用哪个工具 |
小明问:”auto 和 required 有什么区别?”
“auto 时模型可能不调——用户说’你好’,模型直接回复,不调函数。required 时模型必须调——不管用户问什么,都要调一个函数。”
“什么时候用 required?”
“比如你的业务流程必须走工具——用户问任何问题都必须先查知识库再回答。或者你在调试阶段,想强制模型调某个函数看看参数对不对。”
“入门阶段用 auto 就够了。等你对模型行为有更高要求时再调。”
四个必踩的坑
坑一:函数名不匹配
tools 定义里写 "name": "get_weather",Python 函数写 def get_current_weather()——名字不一样。
模型返回 'get_weather',你的 available_functions 里没有这个键,直接炸了。
解法:tools 里的 name 必须和 available_functions 的键完全一致。建议用常量管理,避免手写拼错:
坑二:参数 JSON 解析失败
模型返回的 arguments 是 JSON 字符串,99% 的情况没问题。但偶尔模型会返回不合法的 JSON——少了引号、多了逗号、中文没转义。
解法:加 try-except,解析失败时给个默认值或提示用户换种问法:
坑三:忘记把 tool_calls 追加到 messages
“这个坑最常见。”老张说,”第一轮调用拿到 tool_calls 后,直接去执行函数了,忘了把 response_message 追加到 messages。结果第二轮调用时,messages 里只有 user 消息和 tool 消息——缺了中间那条 assistant 消息。“
API 直接报错——tool 消息前面必须有对应的 assistant 消息(带 tool_calls 的那条)。
解法:记住顺序——先追加 assistant 消息,再追加 tool 消息:
坑四:tool_call_id 不匹配
“模型同时调两个函数时,每个 tool_call 有不同的 id。你执行完函数,返回结果时 tool_call_id 必须对应——”
“如果你把北京的结果标记了成都的 id,模型就以为’北京的结果是成都的’,回答就全乱了。”
解法:for 循环里用当前 tool_call.id,别用别的:
消息列表在 Function Calling 中的变化
老张说:”上次讲多轮对话时,messages 列表只有 system/user/assistant 三种角色。Function Calling 引入了第四种——tool。整个消息流转是这样的:”
“注意第一次的 assistant 消息,content 是 None,但多了 tool_calls 字段。第二次的 assistant 消息,content 才是真正的文字回复。”
“如果你在这个基础上继续多轮对话,后面的消息追加逻辑跟上次讲的一模一样——user 追加问题,assistant 追加回复。Function Calling 只是在中间插了一组 assistant(tool_calls) + tool(结果) 消息。”
小明恍然大悟:”所以 Function Calling 并没有改变消息列表的底层逻辑,只是多了一种消息类型。”
“完全正确。messages 列表是整个大模型开发的地基,Function Calling 只是在上面加了一层楼。“
这篇文章的核心
老张最后在白板上写了一句话:
Function Calling = 模型点菜 + 代码上菜 + 模型转述。
小明看着白板,把它拍了下来。
| 概念 | 一句话 |
|---|---|
| 本质 | 模型不执行函数,只输出结构化调用指令,你的代码真正执行 |
| 两轮对话 | 第1次决定用什么工具,第2次基于工具结果生成回答 |
| tools 定义 | name + description + parameters(JSON Schema) |
| description | 最关键字段,决定模型何时调用,写清用途+示例值 |
| tool_calls | 模型返回的调用请求,含函数名和参数(JSON 字符串) |
| tool 消息 | role: "tool" + tool_call_id + content(字符串) |
| tool_call_id | 关联模型的调用请求和你的执行结果,不能搞混 |
| tool_choice | auto(自动)/ none(禁止)/ required(必须)/ 指定函数 |
| available_functions | 函数名→实际函数的映射表,模型给字符串你给执行 |
| messages 变化 | 多了 assistant(tool_calls) + tool(结果) 两种消息 |
“大模型从’会说’变成’会干’,靠的不是自己变聪明了,而是你给它配了一双手。Function Calling 就是这双手的接口协议。”
下一步
-
多个工具组合 — 让模型在一个对话里调多个不同函数 -
嵌套调用 — 函数 A 的结果作为函数 B 的参数,模型自己编排 -
并行调用 — 模型同时调多个函数,你的 for 循环处理 -
错误处理 — 函数执行失败时怎么告诉模型,让它换个策略 -
Agent 工作流 — Function Calling 是 Agent 的基础,Agent 就是在这个循环上加上任务规划和自主决策
“每一步都是在 tools 列表和 messages 列表上做加法。两轮调用的骨架从来没变过。”
总结
上篇你学会了让 AI 记住对话历史。这篇你学会了让 AI 调用外部工具——核心就是两轮 API 调用:第一轮模型决定用什么工具,你的代码执行函数,第二轮模型基于真实结果生成回答。
大模型本身不会查天气、不会查订单、不会发邮件。但通过 Function Calling,它能”意识到”自己需要调一个函数,告诉你调哪个、参数是什么。你执行完把结果喂回去,它就能基于真实数据回答用户。
这就是从”聊天机器人”到”AI 助手”的分水岭。聊天机器人只会说,AI 助手会干。
小明把 Function Calling 跑通后,给老板演示了 AI 客服查订单。老板问:”它是怎么知道订单系统的?”
小明说:”它不知道。它只是告诉我’调 query_order_status 函数,参数是订单号’,真正查数据库的是我的代码。它只是个翻译官。”
老板愣了一下,然后笑了:”那这个翻译官还挺靠谱。”
老张听说后跟小明说:”记住,Function Calling 的本质不是让 AI 变强了,是让 AI 变诚实了——它不再编造,而是承认’我需要查一下’,然后让你去查。“
转载请注明来源:Function-Calling让大模型从会说到会干























