Function Calling 深度解析:让大模型第一次伸出手 title: Function Calling 深度解析让大模型第一次伸出手 slug: function-calling-deep-dive tags: [人工智能, 大模型, Function Calling, Agent, LLM] summary: 很多人以为 Function Calling 是模型在执行函数其实模型只输出结构化调用意图真正执行的是你的应用。本文讲透 8 步完整时序、工具定义写法、description 关键作用、多轮循环与四大常见坑附可运行代码。这是「近五年 AI 技术演进」系列深挖的第三篇。第一篇《从 ChatGPT 到 Agent Harness》铺开全景第二篇拆了知识层的 RAG这一篇轮到模型变成 Agent的第一个转折点——Function Calling函数调用。一句话定位模型原生输出结构化的工具调用意图。它出现之前模型只会说话它出现之后模型第一次能伸手去碰外部世界。后来的 Tool 层、MCP 协议全部是从这一个能力里长出来的。全文约 5500 字建议收藏后按章节阅读。一、先纠正一个流传极广的误解开始之前必须把一个误解按死在原地因为它在教程、博客、甚至不少付费课程里反复出现模型自己不执行任何代码。它从头到尾没有运行过你定义的函数一次都没有。Function Calling 的全部输出是模型在合适的时候返回一段结构化 JSON内容是我想调用这个函数、参数是这些——仅此而已。这是一份意图声明不是执行结果。真正拿到这段 JSON、去解析、去调用真实的天气 API、去查数据库、把结果回传给模型的是你的应用代码。模型负责决策应用负责执行这条分界线贯穿全文也是理解整个 Agent 技术栈的第一性原理。误解从哪来三个来源。一是名字本身Function Calling函数调用听起来就像模型调用了函数但准确的说法应该是函数调用请求二是大量教程只演示到模型返回了一个 JSON就草草收尾中间那段应用真正去执行的环节被整段略过三是演示效果太逼真——你问天气它就答天气看起来像是模型自己出门看了天。实际上模型只是演了一场它输出的 JSON 被你的代码接走你的代码去调 API结果再喂回去它才把数字组织成人话。一个类比模型像一个只收发工单的总调度员。他看了车间里有哪些设备工具清单接到客户需求后填一张标准格式的工单结构化 JSON交给跑腿的你的应用代码去真正操作设备拿到产出后他再把产出整理成客户能看懂的答复。调度员自己从头到尾没碰过任何一台设备——他只会填工单工单填得又快又准这就是 Function Calling 的全部。想通这一点后面所有东西都是水到渠成为什么要回传结果模型不知道执行结果得有人告诉它、为什么会循环模型可能连续填好几张工单、为什么安全问题要出在应用侧真正干活的是你。二、为什么需要它正则解析的黑暗时代要看懂 Function Calling 的价值得先看看它出现之前开发者是怎么过的。2023 年 6 月之前OpenAI 的 API 只能返回纯文本。你想让模型调用一个天气查询唯一的办法是在 Prompt 里写上规矩如果你需要查天气请按{tool: get_weather, city: ...}的格式输出不要输出其他内容。然后写正则去抓模型输出里的这段字符串。这条路能跑但每一步都在漏风格式漂移。模型是按概率生成文本的它不保证守约。你要求 JSON它可能给你加一句好的以下是查询结果再接 JSON可能用 markdown 代码块包起来可能在括号里多打一个空格。你的正则要为每一种漂移打补丁补丁越打越多最后变成一坨谁都不敢动的祖传代码。参数结构脆弱。参数一复杂——嵌套对象、数组、可选字段——让模型在自由文本里输出合法的嵌套 JSON再写正则去解析失败率会肉眼可见地上升。转义字符、缺右括号、字符串里混进引号任何一样都能让你的解析器当场去世。无法声明约束。Prompt 里说city 是字符串、必填模型记不记全凭概率。没有类型系统没有必填校验没有枚举约束一切正确性都靠事后祈祷。两类方案放在一起看差距一目了然维度传统 Prompt 解析Function Calling输出格式自由文本靠 Prompt 约定结构化 JSONAPI 层保证格式解析方式正则 / 字符串匹配随输出漂移补丁越打越多SDK 直接返回对象零解析成本参数约束靠口头描述模型记不记看运气JSON Schema 声明类型 / 必填 / 枚举API 强校验可靠性格式漂移、解析失败是常态格式由接口契约保证只剩语义正确性问题维护成本每个工具一套解析代码所有工具一套统一协议本质区别一句话传统方案是求模型配合Function Calling 是模型经过专门训练来配合。2023 年 6 月 OpenAI 在 API 中正式推出 Function Calling输出的结构化格式由 API 直接保证——Prompt 解析时代正式结束。此后各家模型Claude、Gemini、通义、DeepSeek 等陆续跟进到 2024 年它已经是所有主流模型的标配能力。三、完整时序一次 Function Calling 到底发生了什么把流程完整走一遍。假设用户问北京今天天气怎么样帮我查下适合穿什么。① 应用组装工具定义。你的代码把本应用可用的工具整理成 JSON Schema 格式的清单具体写法见第五节。这个清单来自你的应用——模型出厂时并不知道你有哪些工具。② 工具清单连同用户问题发给模型。一次普通的 chat 请求只是多了个tools参数。③ 模型判断需要调用工具返回 tool_calls。模型读完用户问题和工具清单判断查天气这件事需要用get_weather于是不直接回答而是返回一条带tool_calls的消息函数名 参数 JSON。注意——到这里为止模型的任务已经完成了它只输出意图。④ 应用解析 tool_calls。你的代码从响应里取出函数名和参数。⑤ 应用执行真正的函数。你的代码调用真实的天气 API。真正的执行发生在这一步发生在你的服务器上——这是整条分界线的另一侧。⑥ 结果作为 tool 角色消息回传。把执行结果包成role: tool的消息追加进对话历史再次请求模型。⑦ 模型生成最终回答。模型看到晴18~26℃的结果结合用户适合穿什么的问题输出北京今天晴18~26℃建议穿轻薄外套。⑧ 结束或循环。如果模型觉得信息还不够——比如用户接着问那上海呢——它会再次返回tool_calls流程从第 ③ 步重新开始直到模型认为任务完成返回不带tool_calls的纯文本回答。再强调一次那条分界线③ 之前和 ⑤ 之后是模型的地盘④⑤ 是你的地盘。模型只输出意图应用负责执行。看懂这张图Function Calling 你就懂了 80%。四、最小可用代码一个能跑的完整循环下面是一段可以直接运行的完整代码包含工具定义、发起请求、处理tool_calls、回传结果的最小循环。依赖只有一个 OpenAI SDKpip install openai国内各家兼容 OpenAI 协议的模型服务也都能直接对接from openai import OpenAI import json client OpenAI(api_key你的KEY) # 也可填国内兼容 OpenAI 协议的服务地址 # 工具定义JSON Schema 格式随请求发给模型 tools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气包括温度和天气状况, parameters: { type: object, properties: { city: {type: string, description: 城市名如北京}, }, required: [city], }, }, }, { type: function, function: { name: query_database, description: 在业务数据库中执行只读 SQL 查询返回查询结果列表, parameters: { type: object, properties: { sql: {type: string, description: 完整的 SELECT 查询语句}, }, required: [sql], }, }, }, ] # 工具的真实实现执行权在你手里 def get_weather(city: str) - str: return json.dumps({city: city, weather: 晴, temp: 18~26℃}) def query_database(sql: str) - str: return json.dumps({rows: [{用户数: 1200}]}) # 实际项目里执行只读 SQL dispatch {get_weather: get_weather, query_database: query_database} # 核心循环处理 tool_calls 直到模型不再要求调用工具 messages [{role: user, content: 北京今天天气怎么样适合穿什么}] while True: resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, # 每一轮都要带上工具清单 ) msg resp.choices[0].message if not msg.tool_calls: # 模型不再要求调工具 → 任务完成 print(msg.content) # 北京今天晴18~26℃建议穿轻薄外套。 break messages.append(msg) # 把模型的 tool_calls 消息记入对话历史 for call in msg.tool_calls: fn dispatch[call.function.name] # 按函数名分发 args json.loads(call.function.arguments) # 参数是 JSON 字符串 result fn(**args) # ← 应用真正执行 messages.append({ # 把结果回传给模型 role: tool, tool_call_id: call.id, content: result, })对照第三节的时序看这段代码while循环就是⑦ 结束或循环的落地——每轮先问模型模型还要工具就执行并回传不要了就收工。全代码没有一行是模型替你执行的fn(**args)这一行永远跑在你的进程里。五、工具定义怎么写逐字段拆解工具定义是模型与你的应用之间的接口契约四个字段每个都有讲究{ type: function, function: { name: get_weather, description: 查询指定城市的实时天气包括温度范围和天气状况。当用户询问任何城市的天气、温度、是否下雨时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名如北京。支持中文不要带行政区划后缀 }, date: { type: string, enum: [today, tomorrow, week], description: 查询目标日期默认 today } }, required: [city] } } }name函数名工具的唯一标识模型输出tool_calls时就用它指名道姓。要求一眼能看懂——用get_weather而不是gw或func_1。命名用动词 名词的蛇形风格get_xxx、query_xxx、send_xxx模型对这类命名方式的工具语义最熟悉。注意 name 只能让模型知道这是个什么活它描述不了什么时候该用这个活——那是 description 的事。description工具描述写给模型看的使用说明书告诉模型这个工具干什么、什么时候该用、什么时候不该用。这是四个字段里对调用准确率影响最大的一个单独展开到下一节。parameters参数定义标准的 JSON Schema。type: object固定properties里逐个声明参数type管类型string / number / boolean / array / objectdescription管语义enum管枚举。数据库查询例子里的sql参数就是典型——模型会根据 description 里的只读 SELECT约束自己不去生成DELETE。参数名同样要让模型看得懂city优于cstart_date优于sd。required必填参数声明哪些参数不能缺。模型会据此决定是否追问用户、是否用默认值。原则是能少则少——必填项越多模型凑参数的压力越大幻觉出参数值的概率越高。非必填的参数如上面的date在 description 里写清默认行为即可。把天气查询和数据库查询两个工具放进同一个tools数组发给模型模型会自己根据用户问题挑选——这正是 Function Calling 的另一层含义模型不只是会调用还会选。六、description写给模型看的使用说明书先想清楚一件事模型是怎么决定该不该用这个工具的它看的就是工具清单里的 description。用户问北京多少度模型在脑内拿这句话和每个工具的 description 做语义匹配匹配上get_weather的描述就决定调用它。description 写得含糊匹配就靠猜写得清楚模型就是一台精准的路由器。一个真实的对比。差的写法{name: search_orders, description: 查询订单}用户问帮我看看昨天下单的怎么还没发货模型会犹豫订单查询物流查询还是售后查询犹豫的结果要么是该调不调要么是不该调乱调。好的写法{name: search_orders, description: 按时间范围、订单号或用户 ID 查询订单列表返回订单状态、金额和物流信息。当用户询问订单进度、发货状态、退款进度时使用但查询快递实时位置应使用 track_logistics不要用本工具。}不仅说清了干什么还说清了什么时候用和什么时候不该用、该用谁。三条实战写法第一写触发条件不止写功能。查询订单是功能描述当用户询问订单进度、发货状态时使用是触发条件。模型做的是场景匹配触发条件越具体路由越准。第二边界要写。有多个相近工具时互相在 description 里点名分工查实时物流用 X不要用本工具能显著降低误调用率。工具越多这一条越重要。第三把坑写进去。参数格式有讲究的如日期格式 YYYY-MM-DD、有安全边界的如仅支持只读查询都写进 description。它是你能影响模型行为的最后一道文档。七、多轮循环为什么复杂任务需要反复调用第三节的例子一轮就结束了但真实任务往往不是。看这个对话用户帮我看看这个月销量前十的产品再查一下这些产品剩下的库存够不够。模型的第一反应不是一次调完而是分步走先query_database查销量前十的产品——因为第二步要查库存得先知道查哪些产品参数依赖第一步的结果。拿到产品列表后模型第二次返回tool_calls用产品列表作为参数再调一次query_database查库存结果回来后它才综合两份数据生成最终回答。这就是多轮循环的本质后一步的参数依赖前一步的结果模型必须在拿到结果 → 决定下一步之间反复横跳。复杂任务里这个循环可能转上十几圈——先查表结构、再拼 SQL、再执行、再核对、再补查——每一圈都是完整的模型出意图 → 应用执行 → 结果回传。循环什么时候停只有一种情况模型返回的消息里不再有tool_calls说明它认为信息够了、任务完成了。第四节的while循环判断的正是这个条件。这也解释了为什么第四节代码里tools参数每一轮都要带上——模型每一轮都在重新决策继续调工具还是收工工具清单得一直在场。生产环境里还要给循环加一个最大轮数上限比如 25 轮防止模型陷入调工具 → 结果不满意 → 再调的死循环烧掉你的 token。八、Function Calling 与 Tool一个考驾照一个是车这两个词经常混用但指的是两个层面的东西Function Calling 是模型能力经过专门训练后模型能在合适时机输出结构化的调用意图。它是会表达想干什么——一种语言输出能力模型侧的事情Tool 是外部能力真正干活的东西——一个函数、一个 HTTP API、一次数据库查询、一个搜索引擎。它不依赖模型存在没有模型它也照样运行应用侧的事情。类比Tool 是车Function Calling 是考驾照。车早就在停车场里停着你的 API 一直都在模型考过驾照之后才第一次有能力指着车说我要开这辆去这个地址。这个区分也解释了两个常见的实际现象第一同一个 Tool 接给不同模型效果参差不齐——车是同一辆车但司机驾照水平不同这就是各家模型 Function Calling 能力差异的直接体现第二模型名册里有某个工具但你的应用没实现它就会出问题详见下一节的幻觉工具——意图和执行是两套系统契约对不上就会翻车。顺着这条线往下看Function Calling 催生了 Tool 层——开发者开始批量造工具、管理工具清单而工具越造越多、模型越来越多N 个模型 × M 个工具的适配问题又催生了 2024 年底的 MCP 协议把工具怎么接入标准化。回头看Function Calling 是模型 → Agent的第一个转折点模型第一次能伸手后面的一切Tool 层、MCP、Agent Harness都是这只手越伸越远的产物。九、四个常见的坑Function Calling 用起来不难但生产环境里有四个高频翻车点坑一参数类型错误。模型输出18字符串而你的函数要18数字、日期格式给了下周三而不是2026-10-20。解法在参数的description里把类型和格式写死整数不要带引号、日期格式 YYYY-MM-DD应用侧再做一层参数校验和类型转换——永远不要假设模型给的参数直接可用。坑二幻觉出不存在的工具。模型返回了一个tool_calls函数名却是get_weather_history——你的工具清单里根本没有这个工具。原因通常是 description 写得太泛让模型以为应该还有更多工具。解法应用侧按dispatch表查不到函数名时不要硬调把工具 X 不存在可用工具有哪些作为 tool 消息回传让模型纠正重选同时收紧 description 的表达范围。坑三并行调用处理不当。模型可能一次返回多个tool_calls查北京和上海的天气→ 两个调用一次返回且这两个调用之间没有依赖。如果你的代码只处理第一个第二个的结果就永远回不去下一轮模型会重复请求。解法第四节代码里的for call in msg.tool_calls就是为此而生——遍历处理每一个每条结果带上自己的tool_call_id回传。无依赖的调用还可以并发执行能明显降低总耗时。坑四超时处理。工具是真实世界的调用数据库会慢、外部 API 会挂。工具一超时整个循环就卡死在那里。解法给每次工具执行设独立超时一般 10~30 秒超时后不要静默吞掉把查询超时作为结果回传给模型——模型会自己决定重试、换工具还是告诉用户暂时查不到。把失败也当输入喂给模型这是 Function Calling 架构下最优雅的容错方式模型是循环里最好的异常处理器。十、写在最后回到那句话模型只输出意图应用负责执行。Function Calling 的全部技术含量都在这个意图与执行分离的架构上。模型擅长的是理解场景、挑选工具、组织参数——这些是概率和语义的活应用擅长的是真正调 API、查数据库、处理超时和权限——这些是确定性的活。两边的分工边界画清楚各干各擅长的事系统才既灵活又可控。理解了这个你就理解了 Agent 时代一半的安全设计为什么工具要有权限控制因为执行在应用侧权限是你的责任、为什么要做参数校验因为模型输出只是建议、为什么 MCP 要标准化协议因为意图的表达方式需要统一。这只在 2023 年 6 月第一次伸出去的手此后越伸越远——下一站工具接入的标准化MCP。