深夜十一点,你走进一家还没打烊的餐厅。你对着服务员说:“来一杯热拿铁,少冰。”服务员并没有亲自跑去咖啡豆仓库拉货,也没有自己动手磨豆子——他熟练地写下一张小票:“拿铁,热,少冰”,贴在传菜口,后厨收到后照单制作,几分钟后端出咖啡,服务员再把咖啡端到你面前。整个过程里,服务员只做两件事:听懂你的话,写对单子;至于咖啡怎么萃取,那是后厨的事。 大模型(LLM)就是这位服务员。它博学多才,能写诗、能翻译,但它出生时并不自带“实时天气”“你的订单数据库”“某个公司的股票行情”这些后厨能力。Function Calling(函数调用)就是递给它的那张传菜小票:当对话需要真实数据或实际动作时,模型不是自己编一个答案,而是生成一张结构化的小票,交给程序里的“后厨函数”去执行,再把结果端回来。这一来,大模型就从“只会背菜谱的机器人”变成了“会帮你向世界下单的服务员”。
掌握 Function Calling,你就能让大模型真正“动手办事”——查询数据、下单、发通知。这是从聊天机器人进阶到 AI 应用开发者的关键一步,也是当前 AI 工程师岗位最基础的硬技能之一。
目录
可视化演示
\u5c55\u793aSeq2Seq\u6a21\u578b\u4e2d\u6ce8\u610f\u529b\u6743\u91cd\u77e9\u9635\u7684\u5206\u5e03\uff0c\u901a\u8fc7\u6e90\u5e8f\u5217\u4e0e\u76ee\u6807\u5e8f\u5217\u7684\u5bf9\u9f50\u5173\u7cfb\u76f4\u89c2\u5448\u73b0\u6ce8\u610f\u529b\u5206\u914d\u673a\u5236\u3002
本节配有交互动效(进入视口后自动加载,约 3KB)。
\u6ce8\u610f\u529b\u6743\u91cd\u77e9\u9635 \u7ebf\u6761\u7c97\u7ec6\u8868\u793a\u6ce8\u610f\u529b\u6743\u91cd\u5927\u5c0f \u7ea2\u8272\u4e3a\u6700\u5927\u6743\u91cd\u8fde\u63a5 \u03b1 = softmax(Q\u00b7K^T/\u221ad) \u70b9\u51fb\u76ee\u6807\u8bcd\u67e5\u770b\u6ce8\u610f\u529b\u5206\u5e03
函数调用概览——服务员为什么不自己炒菜
想象一位新来的服务员。顾客问“今天北京多少度?”,他不能凭空编一个温度,因为后厨(外部世界)才有温度计。没有 Function Calling 时,模型只能凭训练时的记忆“猜一个答案”,就像服务员凭印象瞎报菜价——看着像那么回事,其实根本不新鲜。而有了 Function Calling,模型会在发现自己“没有这道菜”时,主动写一张小票交给后厨:“请查询北京今天的天气”。后厨查完,把结果端回来,服务员再转述给顾客。
从技术上说,Function Calling 是让大模型在生成自然语言之外,额外生成一段结构化的函数调用指令。这段指令通常包含两个关键信息:调用哪个函数(函数名),以及传入什么参数。它并不是让模型真的去执行代码——模型只是一个“写字的人”,真正动手的是你程序里写好的函数。这就像服务员只负责写单和传菜,绝不亲自进后厨炒菜。
为什么需要这种设计?因为大模型的核心是“预测下一个词”,它不掌握实时数据,也不具备执行动作的能力。如果让它在对话里直接算出订单金额、直接扣款,那无异于让服务员凭记忆报出全城所有菜品的最新价格——风险极高。Function Calling 把“理解意图”和“执行动作”干净地切开:模型负责理解,代码负责执行,双方通过一张 JSON 小票协作。
典型场景俯拾皆是:智能客服查订单、订餐助手下单、数据分析师用自然语言查数据库、IoT 助手控制设备。在这些场景里,模型不再“编答案”,而是“找答案”。就像餐厅服务员可以不会做满汉全席,但只要他懂得把每道菜准确地传到后厨,这桌饭就能稳稳当当地上齐。
函数 Schema——写好菜单上的菜品卡
餐厅里每道菜都会有一张菜品卡:菜名、简介、需要什么食材、哪些配料可加可不加。后厨照卡备菜,服务员照卡下单,顾客照卡点菜。在 Function Calling 中,这张“菜品卡”就叫 函数 Schema(结构定义)。你必须在调用模型之前,把你准备的每个函数都写成一张这样的卡,告诉模型:世界上存在一个叫 get_weather 的函数,它的作用是查询天气,它需要一个叫 city 的参数,而且这个参数是必须的。
用术语说,Schema 是一个 JSON 对象,常见的结构如下:type 固定声明这是一个“函数”工具;name 是函数名;description 用自然语言描述这个函数做什么、什么情况下该用它;parameters 是一个 JSON Schema,定义了每个参数的名称、类型和含义;required 列出哪些参数是必填的。这些字段共同构成模型“决定是否调用、调用时填什么参数”的依据。
1. name(函数名):函数的唯一标识。模型输出调用指令时,会把这个名字原样写在 JSON 里,程序才能找到对应函数执行。可以把它理解成菜单上的“宫保鸡丁”四个字,顾客报出菜名,后厨立刻知道做哪道菜。
2. description(功能描述):用一句人话说明函数是干什么的。这是模型判断“该不该调用”的核心依据。描述写得越清楚,模型越知道什么时候该掏小票,什么时候不该。
3. parameters(参数定义):声明这个函数需要哪些输入,每个输入是什么类型、什么含义。模型会从这里学习如何从用户的话里抽取参数,就像菜品卡写明“需要鸡胸肉 200 克、花生米 50 克”。
Schema 写得有多好,模型就有多靠谱。如果你的 description 写的是“处理一些事情”,模型很可能在需要查询天气时不敢调用,或者乱调用。好的 Schema 应该像一位老练的厨师写菜单:每道菜的名字清晰、用料明确、口味可选。参数描述也别吝啬,例如 city 参数可以写“城市名,如北京、上海”,模型就能从“北京天气怎么样”里准确抽出“北京”。这顿“函数服务”好不好吃,从你写菜单那一刻就决定了。
提示词设计——培训服务员何时下单
有了菜品卡,还得培训服务员:什么情况下该下单?顾客说“你们有炒饭吗”是询问,不需要下单;顾客说“我要一份扬州炒饭”才需要下单。大模型同样需要一套“培训手册”,告诉它调用函数的时机和参数抽取规则。这部分工作就写在 System Prompt 里,我们称之为提示词设计。
首先,要明确调用时机。一个好的提示词会这样写:“当用户询问天气、且能确定城市时,调用 get_weather;如果城市不明确,先向用户询问城市再调用。”这相当于教服务员:顾客说“帮我看看外边冷不冷”时,先确认地点,再决定是否后厨查天气,而不是直接瞎报。
其次,是参数抽取。用户说话往往随意:“北京明天穿什么?”你希望模型抽取出 city="北京"、date="明天" 并映射成函数可用的日期格式。提示词里可以补充说明:“日期需转换为 YYYY-MM-DD 格式”。模型就像服务员听顾客说“老样子”——他能根据培训经验,把“老样子”翻译成具体菜品。参数抽取质量直接决定函数执行的成功率。
最后,考虑默认行为与边界情况。如果函数有默认值,比如 get_weather 不传日期就默认今天,就可以在 Schema 的 description 里写“默认当天”;如果用户要求的功能不在任何函数覆盖范围内,提示词要告诉模型“不要虚构,直接说明暂无此服务”。边界情况就像顾客点了一道菜单上没有的菜——服务员应当礼貌告知,而不是随便写个菜名让后厨为难。提示词 + Schema 是一对搭档:Schema 告诉模型“有哪些菜”,提示词告诉模型“什么时候点、怎么点”。
调用执行流程——从下单到上菜的四步
当顾客说完“帮我查一下北京的天气”,真正的 Function Calling 流程就开始了。这顿饭一共分四步上桌。第一步,模型输出:模型带着你的消息和 Schema 一起推理,发现“该查天气了”,于是在回复中输出一个特殊的 tool_calls 字段,里面写着函数名和参数。第二步,解析调用:你的程序收到这个字段,把它从 JSON 里读出来,确认要调用哪个函数、参数是什么。第三步,执行函数:程序在你自己的代码里调用那个真实函数,比如向天气 API 发请求,拿到真实数据。第四步,结果回填:把函数返回结果作为一条特殊消息送回给模型,模型再根据结果组织成一句自然语言回复用户。
用公式看,前两步发生在模型侧,后两步发生在你的程序侧:
1. messages(对话消息):当前对话的全部上下文,包括用户输入和历史消息。服务员要记住顾客前面说过的话,模型同样需要这些信息来判断当下该不该调用工具。
2. LLM 输出(模型推理):模型读完消息和工具声明后,决定生成普通文本回复,还是生成 tool_calls 调用指令。它像一个训练有素的服务员,能判断“这桌该下单了”。
3. f(arguments)(函数执行):你的程序真正调用函数,传入从模型输出中解析出的参数。这是热锅热油的真实步骤,不是模型在脑内模拟的“想象做菜”。
四步流程中有一个关键认知必须刻在脑子里:模型永远不会替你去执行函数。get_weather 的 API 请求是你发出的,数据库查询是你写的 SQL 做的事,扣款操作是你调用的支付接口完成的。模型只是那位把菜名写在小票上的服务员,而后厨的火,始终燃烧在你自己家的灶台上。这张传菜小票虽然只是薄薄一层 JSON,却把“意图”和“执行”牢牢焊接在了一起。
返回结果处理——后厨出品的最后质检
后厨端出一盘菜,服务员不能直接往桌上一扔——他得先看一眼菜是否完整、是否符合备注,再决定怎么跟顾客说。函数返回结果也是如此:函数执行完,返回的往往是一串字符串或 JSON,你(或程序)需要对它做解析、校验、错误处理,最后把干净的结果交回给模型,让模型生成对用户友好的回复。
先看结果解析。假设 get_weather 返回的是一串 JSON:{"city":"北京","weather":"晴 22°C"}。你的程序应该用 json.loads 把它解析成字典,而不是直接把这串原始文本塞回给模型——就像服务员看到后厨递来的原料,要先装盘,不能直接把血淋淋的鸡肉端给顾客。解析后的结构化数据,模型才能高效阅读。
再看错误处理。如果城市名不存在,函数可能返回 {"error":"city not found"}。这时你传给模型的结果应该明确带着错误信息,并引导模型向用户道歉或追问:“抱歉,我没有查到这座城市,您说的是‘北京’吗?”如果不做错误处理,模型可能基于残缺结果瞎编一个天气,酿成“服务员把菜烧糊了还给顾客上桌”的事故。永远把错误信息当作合法结果的一部分来传递,是 Function Calling 应用成熟的标志。
格式化回复也非常重要。模型接回函数结果后,不再是“朗读 JSON”,而是把结果转成自然的回答:“北京今天晴,气温 22 摄氏度。”这一步通常在第二轮回调中完成:你以 tool 角色把函数结果加入对话,然后让模型再次生成最终回复。最后别忘了校验防御:在使用模型吐出的 arguments 之前,要在你的代码里检查参数类型、范围,防止模型生成不符合预期的参数。就像服务员下单前要确认顾客没有少给钱——信任,但要核对。
多轮调用编排——一餐饭的多次加菜
真实的对话很少一次就结束。顾客先问北京天气,又问“那上海呢?”,接着说“帮我订两张明天去上海的机票”。每一次追问都可能触发新的函数调用。这就是多轮调用编排:让模型在连续几轮对话中,依据对话历史反复调用不同函数,完成一个复杂的任务。
关键前提是保留完整对话历史。比如第一轮模型调用了 get_weather,返回结果已经作为 tool 消息写进了消息列表;第二轮用户问“上海呢?”,如果只把新问题发给模型,模型根本不知道“上海”指的是“查上海天气”还是“订上海的机票”。正确做法是把之前所有轮次的 user、assistant、tool 消息都保留下来,追加式地发给模型,让模型像服务员一样完整记得整桌客人的所有需求。
1. history(历史状态):已有的对话消息数组,保存了此前所有轮次的交流记录。它是模型理解当前语境的记忆载体,相当于服务员脑中“这桌客人之前说过什么”的印象。
2. assistant_tool_call(模型的调用记录):模型生成的那条 tool_calls 消息,必须原样追加进历史。它记录了“我当时决定调用哪个函数、传了什么参数”,是后续对话的逻辑锚点。
3. tool_result(函数结果):把函数执行结果也追加进去。模型在下一轮生成时能看到“函数实际返回了什么”,才能接着用户的话往下说,比如“上海是小雨,建议带伞”。
当一次请求需要调用多个函数时,还可能出现连续调用:模型先调用 query_weather 得知“明天下雨”,再调用 recommend_destination 推荐室内景点。某些 API 还支持并行调用:模型一次输出多个 tool_calls,你的程序可以并行执行它们,再一起回填结果,就像服务员一次性把三张单子夹在一起送到后厨,提高效率。但别忘了设置终止条件:当模型不再输出 tool_calls 时,说明任务完成了,此时把最终回复交给用户;如果给模型设一个最大循环次数(比如 5 次),还能防止它陷入“反复调工具却永不结账”的死循环。一餐饭总有吃完的时候,聪明的编排者会记得替顾客买单。
AI 应用场景
智能客服订餐助手
阶段:Schema 定义 + 多轮调用编排
解决的问题:传统客服机器人只能根据话术模板回答固定问题,无法对接真实库存、菜单和订单系统。用户说“来一杯拿铁和一份提拉米苏”,机器人根本不知道这两个商品是否还有货、能不能加购物车。用 Function Calling 后,机器人成为“传菜服务员”,把每个需求转成真实的系统调用。
具体案例:用户说“我要一杯热拿铁,少冰,再来一块提拉米苏”。模型第一轮调用 check_stock 确认提拉米苏有货,第二轮调用 add_to_cart 将两件商品加入购物车,第三轮调用 create_order 生成订单并返回订单号,最后模型用自然语言告知用户“您的订单 1024 已确认,预计 20 分钟后送达”。全程用户感觉是在和一个真人服务员对话,实际每一单都触发了真实系统操作。
企业数据问答系统
阶段:函数 Schema 定义 + 返回结果处理
解决的问题:老板问“上季度华东区销售额是多少?”,传统做法是让数据分析师手写 SQL 查询,周期长且门槛高。Function Calling 让模型自动把自然语言问题转成结构化查询参数,交给后端查询函数执行,再把查询结果格式化为图表和文字摘要。
具体案例:用户在对话框中输入“对比华东和华南上季度各月销售额”。模型调用 query_sales,参数为 region: ["华东","华南"]、time_range: "上一季度",后端执行 SQL 返回一个 JSON 数组。函数返回后,模型把原始数据整理成“华东 3 月销售额环比上升 12%,华南持平”的报告。即便查询失败,模型也能根据错误信息提示用户调整问题。
Python 演示
# -*- coding: utf-8 -*-"""Function Calling 模拟演示:大模型连接外部天气服务"""import json def get_weather(city, date="今天"): """模拟真实后端的天气查询函数""" weather_map = {"北京": "晴 22°C", "上海": "小雨 18°C", "广州": "多云 26°C"} return json.dumps({"city": city, "date": date, "weather": weather_map.get(city, "未知")}, ensure_ascii=False) FUNCTIONS = {"get_weather": get_weather} def mock_llm(messages, tools): """模拟支持 Function Calling 的 LLM,真实环境替换为 client.chat.completions.create""" last = messages[-1]["content"] if messages else "" if "天气" in last and not any(m["role"] == "tool" for m in messages): args = {"city": last.split("查")[-1].strip("的天气 ") or "北京", "date": "今天"} return { "tool_calls": [{ "id": "call_001", "type": "function", "function": {"name": "get_weather", "arguments": json.dumps(args, ensure_ascii=False)} }] } return {"content": "已为你查到最新天气信息,请注意查看。"} def run_function_call(user_input): """执行完整的多轮函数调用流程""" tools = [{ "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当天的天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名,如北京"}, "date": {"type": "string", "description": "查询日期,默认今天"} }, "required": ["city"] } } }] history = [{"role": "user", "content": user_input}] first = mock_llm(history, tools) if "tool_calls" not in first: print("模型回复:", first.get("content")) return calls = first["tool_calls"] history.append({"role": "assistant", "content": None, "tool_calls": calls}) for call in calls: fn_name = call["function"]["name"] fn_args = json.loads(call["function"]["arguments"]) fn_result = FUNCTIONS[fn_name](**fn_args) history.append({"role": "tool", "tool_call_id": call["id"], "content": fn_result}) second = mock_llm(history, tools) print("用户:", user_input) print("函数结果:", fn_result) print("模型最终回复:", second.get("content")) if __name__ == "__main__": run_function_call("帮我查一下北京的天气")
tools = [{...}]定义函数 Schema,相当于给服务员看的菜品卡。参数必须用 JSON Schema 格式声明类型、描述和必填项。first = mock_llm(history, tools)模拟真实环境中的client.chat.completions.create(...)调用,传入消息和工具声明,模型返回 tool_calls。history.append({"role": "assistant", "content": None, "tool_calls": calls})把模型的调用指令追加进历史,这是多轮对话不丢状态的命脉。fn_result = FUNCTIONS[fn_name](**fn_args)程序真正执行函数。注意:执行发生在你的代码里,模型只负责生成参数。history.append({"role": "tool", "tool_call_id": call["id"], "content": fn_result})将函数返回结果以tool角色回填,模型下一轮才能据此生成自然语言回复。
常见误区
误区很多初学者以为 Function Calling 就是让大模型“自己算出来”——比如用户问“3.14 的平方是多少”,就直接让模型在对话里计算出结果,而不去调用计算函数。他们觉得模型既然这么聪明,何必还要写一个 `calculate` 函数。
正解模型只负责生成调用指令(写小票),真正执行 calculate 函数的是你程序里的 Python 代码。就像服务员只写单,绝不亲自炒菜。哪怕模型能“凭感觉”说出一个近似结果,它也无法保证准确性和实时性,严格来说那只是它在玩文字接龙。
为何常见:因为大模型生成的文本太自然了,让人误以为它真的“动手做了”。实际上它连加法都可能算错,更别提访问外部系统。
误区有人以为只要在 System Prompt 里写“你可以调用 get_weather 函数”,模型就会真的去调用。他们不定义 tools 参数,只靠文字描述让模型‘假装’调用。
正解Function Calling 的前提是把函数以结构化的 tools 形式传给模型。只有看到 Schema(函数名、参数定义、必填项),模型才会产出规整的 tool_calls JSON。提示词里口头的“调用”,模型只会当成聊天内容,输出一段文字,你的程序无从解析执行。
为何常见:传统 Prompt 工程中,大家习惯了“用话术引导模型”,容易忽视 Function Calling 是 API 层面的结构化能力,而非纯文本技巧。
误区初学者写 Schema 时只写 `name` 和 `parameters`,觉得 `description` 可有可无。结果模型要么不调用函数,要么把用户说的“北京”抽成 `city` 参数时抽错了。
正解description 是模型理解函数功能和学习参数抽取规则的主要来源。写清楚“city 是城市名,如北京、上海”,模型才能从“北京明天天气如何”里准确抽出 city="北京"。菜单上只有菜名没有简介,服务员也容易上错菜。
为何常见:因为 Schema 看起来像“格式定义”,人们容易只关注字段结构,而忽略自然语言描述对模型行为的关键引导作用。
误区有些开发者在每轮对话中只把最新的用户消息发给模型,导致第一次调用天气的结果在第二轮就“失忆”。用户追问“那上海呢?”模型完全不知道这是在问天气。
正解正确的做法是把整个消息列表(含 user、assistant 的 tool_calls、tool 的执行结果)都保留下来并发给模型。历史是模型的记性,丢掉历史等于让服务员忘了整桌客人说过什么。推荐在循环中控制最大调用次数,既保留状态又防止死循环。
为何常见:因为普通聊天接口只需要传最近消息,大家习惯性延续了这种“无状态”思维,没意识到工具调用本身会改变对话状态。
面试题
Q1. Function Calling 的核心作用是什么?
- A.让模型自动写代码
- B.输出结构化函数调用← 正确答案
- C.替代全部后端接口
- D.在本地直接执行程序
答案:B。Function Calling 的核心是让模型在对话中输出结构化的 tool_calls 指令,包含函数名和参数,而不是让模型自己执行代码。它像服务员写传菜小票,真正炒菜的是后厨(你的程序)。
Q2. 函数 Schema 中哪个字段描述“函数是做什么的”?
- A.函数名称字段
- B.参数类型字段
- C.函数描述字段← 正确答案
- D.必填数组字段
答案:C。description 字段用于说明函数的功能和适用场景,是模型决定是否调用以及如何抽取参数的重要依据。name 是函数名,parameters 定义参数,required 声明必填项,它们各有分工。
Q3. 下面哪项是典型的函数调用场景?
- A.图像风格迁移
- B.语音识别转换
- C.实时天气查询← 正确答案
- D.文本情感分类
答案:C。Function Calling 适合模型需要获取实时数据或执行动作的场景,如天气查询。图像风格迁移、语音识别通常是模型多模态能力或独立模型完成的任务,不依赖工具调用来弥补信息缺失。
Q4. 模型返回的 tool_calls 中,调用参数存放在哪个属性?
- A.role 属性
- B.type 属性
- C.name 属性
- D.arguments 属性← 正确答案
答案:D。tool_calls 里的每个调用包含 id、type 和 function,而 function 对象内有 name(函数名)和 arguments(参数 JSON 字符串)。解析时先取 function.arguments 再 json.loads,就像服务员从单子上读出“热拿铁、少冰”。
Q5. 函数执行完成后,如何把结果交还给模型继续对话?
- A.以 user 角色追加消息
- B.以 tool 角色追加消息← 正确答案
- C.以 assistant 角色追加消息
- D.替换掉整段历史对话
答案:B。OpenAI 协议要求函数结果以 role="tool" 且带 tool_call_id 的消息追加进历史,与对应的 assistant tool_calls 消息配对。这样模型才能“看到”后厨端出的真实菜品,而不是顾客自说自话。
Q6. 编写函数 Schema 时,下列哪些做法正确?
- A.参数写清含义← 正确答案
- B.尽量省略参数
- C.明确必填字段← 正确答案
- D.写清函数功能← 正确答案
答案:ACD。Schema 的 description 要写清函数功能,每个参数的描述要说明含义和示例,required 要列出必填字段,这些都能提升模型抽参的准确性。刻意省略参数会让模型缺少输入依据,就像菜品卡不写食材,后厨无法备菜。
Q7. 多轮调用时,最容易导致模型“失忆”的做法是?
- A.保留全部历史消息
- B.追加 tool 结果消息
- C.只传最新用户消息← 正确答案
- D.设置最大循环次数
答案:C。只把最新用户消息发给模型,模型就丢失了此前“调用过什么函数、返回了什么结果”的上下文。比如用户先问北京天气再问“那上海呢”,模型不知道“那”指的是天气查询。历史消息是模型的服务员记忆。
Q8. 当一次回复需要调用多个独立函数时,通常采用什么模式?
- A.并行返回多次调用← 正确答案
- B.强制只调用一个
- C.把多次调用写成文本
- D.让用户人工拼接结果
答案:A。OpenAI 等平台支持模型一次返回多个 tool_calls,程序可并行执行这些独立函数,再统一回填结果。这就像服务员把多张单子同时夹到传菜口,后厨可以同时开火,比串行逐个调用效率更高。
Q9. 解析模型返回的 arguments 时,正确做法是?
- A.直接 eval 执行
- B.用 JSON 解析并校验← 正确答案
- C.按字符串位置截取
- D.忽略参数直接调用
答案:B。arguments 是 JSON 字符串,需要用 json.loads 解析成对象,并在执行前校验字段类型和范围。直接 eval 有代码注入风险,字符串截取脆弱且容易出错,忽略参数则函数无法正常运行。
Q10. 关于多轮调用编排,以下说法正确的有?
- A.保留完整调用历史← 正确答案
- B.丢弃函数结果消息
- C.追加 tool 结果回填← 正确答案
- D.设置终止条件防死循环← 正确答案
答案:ACD。多轮调用要求保留并追加历史,尤其是 assistant 的 tool_calls 与 tool 的返回结果必须成对出现,保证模型知道函数实际执行了什么。同时应设置最大循环次数,防止模型反复输出 tool_calls 却不结束对话,导致无限“加菜”。