资讯动态

Function Calling 实战:从原理到工程化落地

发布时间:2026/10/13 20:55:10 来源:尧图企业网站定制
1. 从“只会聊天”到“动手干活”Function Calling 到底解决了什么问题很多人第一次接触 Function Calling 这个概念时脑子里冒出来的疑问都差不多大模型不是已经能回答问题了吗为什么还要让它去“调用工具”这个疑问其实特别关键因为它直接指向了大模型最本质的一个短板——它只会“说”不会“做”。你问它“今天天气怎么样”它可以凭训练数据里的统计规律编出一段听起来很合理的话但它并不知道此时此刻你所在城市的真实温度。你让它“帮我查一下订单状态”它可能会一本正经地给你一个格式完美的假订单号。这不是它故意骗你而是它的工作机制决定的它是一个基于概率生成文本的模型不是一个能实时访问外部系统的执行引擎。Function Calling 要解决的就是这个断层。它的核心思路是让大模型在对话过程中不只是输出一段自然语言而是输出一个结构化的“调用意图”——告诉外部程序“我需要调用哪个函数、传什么参数”。真正执行这个函数的不是模型本身而是你写的代码。模型负责理解和决策代码负责执行和返回结果两者配合完成一件完整的事。打个比方大模型像一个特别聪明但被关在房间里的人他读过很多书、懂很多道理但他看不到外面的世界也碰不到任何东西。Function Calling 就像给他装了一部电话他可以拨号请求外面的人帮他查资料、下单、发消息外面的人做完之后再把结果告诉他。他本身还是没有走出房间但他的能力边界被极大地扩展了。这套机制的价值在于三个层面。第一突破知识截止和实时性限制。模型训练数据有截止日期但通过调用搜索接口、数据库查询接口它可以获取最新信息。第二突破能力边界。模型不能直接发邮件、不能直接操作数据库、不能直接控制硬件但通过函数调用这些都能间接实现。第三提高可靠性。与其让模型自由发挥生成一段可能出错的文本不如让它输出结构化参数由确定性代码来执行出错概率大幅降低。适合读这篇内容的人我大致分三类一是正在做 AI 应用开发、想把大模型接入自己业务系统的工程师二是对大模型原理感兴趣、想搞清楚“它到底怎么跟外部世界交互”的技术爱好者三是已经在用一些 AI 工具平台、想理解底层机制以便更好设计工作流的产品或运营同学。不管你是哪一类接下来的内容都会从原理到实操把 Function Calling 这件事讲透。2. 拆开黑盒一次完整的 Function Calling 到底经历了什么2.1 模型看到的不是“函数”而是一份说明书很多人以为 Function Calling 是模型真的去“执行”了一个函数这个理解是偏的。模型做的事情本质上只有一件根据对话上下文和一份函数描述清单判断是否需要调用某个函数并生成符合格式的参数。这份函数描述清单通常以 JSON Schema 的形式随请求一起发给模型。它长这样{ name: get_weather, description: 查询指定城市的实时天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位 } }, required: [city] } }模型收到这段描述后并不会去“理解”天气查询的业务逻辑它只是把这段结构化信息和用户的问题做匹配。用户说“北京今天热不热”模型判断这跟get_weather这个函数的描述高度相关于是生成一个调用请求参数是{city: 北京, unit: celsius}。这里有一个特别容易踩的坑函数描述的质量直接决定调用准确率。我见过太多人把 description 写得含糊不清比如只写“查询数据”结果模型根本分不清该调这个还是调另一个。描述要写得像给一个新同事交代任务一样具体把什么场景用、参数什么含义、有没有默认值都写清楚。2.2 调用请求的返回格式与解析逻辑当模型决定调用函数时它返回的响应里会包含一个结构化的调用信息而不是普通的文本内容。以主流接口的返回结构为例大致是这样的{ role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: get_weather, arguments: {\city\: \北京\, \unit\: \celsius\} } } ] }注意几个细节。content可能是空的因为模型这一轮没有生成自然语言它只生成了调用意图。arguments是一个 JSON 字符串不是对象你需要自己解析。id是这个调用请求的唯一标识后面把执行结果传回给模型时需要带上这个 id 做对应。你的程序拿到这个返回后要做的事情很明确解析出函数名和参数在自己的代码里找到对应的函数执行它拿到结果。比如你本地有一个get_weather函数就去调真实的气象 API拿到“北京当前 28 摄氏度晴”这样的结果。2.3 把执行结果喂回模型第二轮对话的关键函数执行完之后你拿到的是原始结果。这个结果不能直接展示给用户因为用户问的是“北京今天热不热”你直接甩一个 API 原始 JSON 过去体验很差。你需要把执行结果作为一条新的消息追加到对话历史里再发给模型一次。这条消息的角色通常是tool或function并且要带上之前那个id{ role: tool, tool_call_id: call_abc123, content: {\city\: \北京\, \temperature\: 28, \condition\: \晴\, \unit\: \celsius\} }模型收到这个结果后会结合之前的用户问题和函数返回数据生成一段自然语言回复“北京现在 28 度天气晴朗挺热的出门注意防晒。”这时候用户看到的才是最终答案。整个流程走下来其实是两轮模型调用加一次本地函数执行。第一轮模型决定调什么本地执行第二轮模型根据结果组织语言。理解这个“两轮”结构特别重要因为很多初学者会误以为一次调用就能搞定结果在代码里怎么都拿不到最终回复。2.4 为什么不能让模型直接输出最终答案有人可能会想既然模型已经知道要查天气了为什么不直接让它编一个温度出来反正用户也看不出来。这个想法很危险。模型编造的温度可能完全错误而用户如果据此做决策——比如决定穿什么衣服、要不要带伞——就会产生实际影响。Function Calling 的设计哲学是把“决策”和“执行”分离。模型擅长的是理解意图、匹配工具、提取参数这些是语言理解任务。而获取真实数据、执行副作用操作交给确定性代码。两者各司其职系统的可靠性和可维护性都更高。这也是为什么在工程实践中我们总是强调能通过函数调用拿到的数据绝不让模型自己生成。3. 手把手搭一个最小可运行示例从零跑通天气查询3.1 环境准备与依赖选择要跑通一个 Function Calling 的最小示例你需要的环境其实很简单一个能调用大模型接口的 SDK一个 HTTP 请求库用来调真实 API再加上基础的 JSON 处理能力。Python 环境下我通常用openai风格的 SDK 加上requests就够了。pip install openai requests这里不绑定具体某家厂商因为 Function Calling 的协议设计在各家接口里高度相似你理解了其中一套迁移到另一套的成本很低。关键是要搞清楚你用的接口在“函数描述字段名”“调用返回结构”“结果回传格式”这三个地方的具体写法不同厂商可能有细微差异。提示如果你用的是兼容某类接口协议的国内模型服务字段名基本一致但建议先拿官方文档里的最小示例跑一遍确认返回结构再改自己的代码。3.2 定义函数描述与本地执行函数先定义一个真实的天气查询函数。这里用一个公开的免费天气接口做演示实际项目中你换成自己的业务接口即可。import requests import json def get_weather(city: str, unit: str celsius) - dict: # 实际项目中替换为你的真实天气 API url fhttps://api.example.com/weather?city{city}unit{unit} resp requests.get(url, timeout5) data resp.json() return { city: city, temperature: data[temp], condition: data[condition], unit: unit }然后定义给模型看的函数描述tools [ { type: function, function: { name: get_weather, description: 查询指定城市的实时天气情况包括温度和天气状况, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认摄氏度 } }, required: [city] } } } ]注意description的写法。我特意写了“包括温度和天气状况”这样模型知道这个函数能返回什么在组织最终回复时会更准确。required里只放了city因为unit有默认值不是必须的。3.3 第一轮调用让模型决定是否调用工具from openai import OpenAI client OpenAI(api_keyyour-api-key, base_urlyour-base-url) messages [ {role: user, content: 北京今天天气怎么样} ] response client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools, tool_choiceauto ) msg response.choices[0].message print(msg.tool_calls)如果一切正常你会看到msg.tool_calls里有一个调用请求函数名是get_weather参数里city是“北京”。如果模型没有返回tool_calls而是直接给了文本回复那说明它判断不需要调用工具或者你的函数描述跟问题匹配度不够。3.4 执行函数并把结果送回模型if msg.tool_calls: tool_call msg.tool_calls[0] func_name tool_call.function.name func_args json.loads(tool_call.function.arguments) # 执行本地函数 if func_name get_weather: result get_weather(**func_args) # 把模型这轮的回复加入历史 messages.append(msg) # 把函数执行结果加入历史 messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 第二轮调用让模型组织最终回复 final_response client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools ) print(final_response.choices[0].message.content)跑完这段你应该能看到类似“北京今天 28 度天气晴朗”这样的自然语言回复。整个链路就通了。3.5 实测中最容易卡住的三个地方第一个卡点是参数解析。tool_call.function.arguments是字符串不是字典必须json.loads一下。我见过有人直接当字典用结果报TypeError排查半天。第二个卡点是消息顺序。第一轮模型返回的 assistant 消息必须原样追加到历史里不能只追加 tool 结果。因为模型需要看到自己之前发出的调用请求才能把 tool 结果和请求对应起来。少了这一步接口会报错说 tool 消息没有对应的调用。第三个卡点是多工具场景下的选择。当你定义了多个函数模型可能一次返回多个tool_calls你需要遍历处理每个都执行并回传结果。不要假设只有一个。4. 从能跑到好用参数设计、多工具编排与错误处理4.1 参数 schema 的设计细节决定调用成功率函数描述里的parameters不是随便写写就行的。我总结了几个实操中特别影响成功率的点。枚举值要写全。比如unit字段如果你只写type: string模型可能传“摄氏度”“C”“摄氏”各种变体你的函数处理起来很麻烦。加上enum: [celsius, fahrenheit]之后模型只会在两个值里选确定性大幅提升。必填和选填要分清。required数组里只放真正必须的参数。如果把可选参数也放进去模型每次都得编一个值出来反而容易出错。有默认值的参数就让它选填模型不传的时候你的函数用默认值兜底。描述里给例子。对于城市名这种参数描述里写“例如北京、上海、广州”模型提取时会更准确。对于日期格式直接写“格式为 YYYY-MM-DD”比写“日期字符串”有用得多。嵌套结构要谨慎。有些场景参数本身是个对象或数组schema 会变得复杂。我的经验是如果嵌套超过两层考虑拆成多个函数或者把复杂参数序列化成字符串让模型生成。模型对深层嵌套结构的生成准确率会明显下降。4.2 多函数场景下的路由与编排实际项目里你不可能只有一个函数。当你有五六个甚至十几个函数时模型的路由准确率会面临挑战。这时候有几个策略可以用。函数命名要有区分度。get_user_info和get_user_profile这种命名模型很容易搞混。改成get_user_basic_info和get_user_preference就清楚多了。命名本身就是给模型的路由信号。描述里写清楚适用场景。不要只描述函数做什么还要写“什么时候用这个函数”。比如“当用户询问订单物流状态时使用此函数”这种场景化的描述对路由帮助很大。函数数量多时考虑分组。如果确实有几十个函数可以考虑先让模型做一个粗粒度的分类决策再在子集里做细粒度选择。这相当于给模型加了一层导航降低单次决策的复杂度。实测数据在我做过的一个项目里函数数量从 3 个增加到 12 个时如果不优化描述路由准确率从 95% 掉到了 70% 左右。重新梳理命名和描述后回升到 90% 以上。这个投入是值得的。4.3 函数执行失败时怎么让模型“圆回来”函数执行不可能永远成功。网络超时、参数不合法、下游服务报错这些都会发生。关键问题是执行失败了怎么让模型给用户一个合理的回复而不是直接崩掉。我的做法是把错误信息也作为 tool 结果回传但格式要友好try: result get_weather(**func_args) tool_content json.dumps(result, ensure_asciiFalse) except Exception as e: tool_content json.dumps({ error: True, message: f查询失败{str(e)}请稍后重试 }, ensure_asciiFalse)模型收到这个错误信息后会生成类似“抱歉暂时查不到北京的天气可能是服务繁忙你可以稍后再试”的回复。用户体验比直接报错好得多。但要注意不要让模型自己编造失败原因。错误信息里写什么模型就基于什么回复。如果你传的是“城市不存在”模型就会说城市不存在如果你传的是“网络超时”模型就会说网络问题。所以错误信息的措辞要准确。4.4 安全边界哪些操作绝对不能交给模型直接触发这是我最想强调的一点。Function Calling 让模型有了“动手”的能力但有些操作一旦被模型误触发后果很严重。涉及资金的操作必须加人工确认。模型判断要调用支付函数不能直接执行要先返回给用户确认。涉及数据删除的操作同理删除是不可逆的必须二次确认。涉及权限变更的操作也要谨慎模型可能被诱导做出越权调用。我的建议是给函数加一个risk_level标记高风险函数在执行前强制走确认流程。这不是技术问题是产品设计问题但必须在架构阶段就考虑进去。注意永远不要假设模型不会被恶意输入诱导。用户可以通过精心构造的 prompt 让模型调用本不该调用的函数。你的安全防线应该在代码层而不是指望模型“自觉”。5. 工程化落地时绕不开的几个现实问题5.1 延迟与成本两轮调用带来的额外开销Function Calling 的代价是显而易见的原本一次模型调用能完成的事现在变成了两次。第一次调用模型生成调用意图第二次调用模型组织最终回复。每次调用都有网络延迟和 token 消耗。在实测中一次完整的 Function Calling 流程端到端延迟通常在 2 到 5 秒之间具体取决于模型响应速度和函数执行时间。如果函数本身还要调外部 API延迟会进一步增加。优化方向有几个。并行执行多个函数调用如果模型一次返回了多个 tool_calls用并发方式执行而不是串行。缓存高频查询结果比如天气数据缓存几分钟避免重复调用。选择更快的模型做第一轮决策有些场景下第一轮只需要判断调什么不需要很强的生成能力可以用小模型。成本方面两轮调用的 token 消耗大约是单轮的 1.5 到 2 倍。如果函数返回结果很长第二轮输入的 token 会更多。控制函数返回结果的精简度只传模型需要的信息不要一股脑把原始数据全塞回去。5.2 多轮对话中函数上下文的维护在连续对话里函数调用的历史会不断累积。用户问了天气你调了天气函数然后用户又问“那明天呢”这时候对话历史里已经有了之前的函数调用记录。模型需要理解“那明天呢”是在延续天气查询的上下文。这里有个细节函数执行结果是否要一直保留在对话历史里。如果保留token 会越来越多如果不保留模型可能丢失上下文。我的做法是保留最近几轮的完整记录更早的做摘要压缩。具体保留几轮取决于你的 token 预算和业务对上下文连贯性的要求。另一个问题是函数调用失败后的重试。如果第一次调用因为参数问题失败了模型在后续轮次里可能会调整参数重试。你要确保你的代码能处理这种“同一个函数被多次调用”的情况不要假设每个函数只调一次。5.3 流式输出与函数调用的兼容处理很多应用为了体验好会用流式输出让用户看到文字逐字出现。但 Function Calling 和流式输出结合时有个矛盾模型在决定调用函数的那一轮返回的不是文本而是结构化的调用信息流式解析会变得复杂。实际处理时通常的做法是第一轮不用流式等模型返回完整的 tool_calls 后执行函数第二轮组织最终回复时再用流式。这样用户感知到的等待主要在第一轮但第一轮通常很快因为模型只需要输出调用意图不需要生成大段文本。如果你的场景对首字延迟特别敏感可以考虑在等待函数执行时给用户一个“正在查询...”的提示缓解等待焦虑。这个体验细节在真实产品里很重要。5.4 测试与监控怎么知道函数调用靠不靠谱上线之后你需要持续监控几个指标。调用触发率用户问了该调函数的问题模型有没有正确触发。参数准确率触发了之后参数提取对不对。执行成功率函数本身执行成功没有。最终回复质量用户看到的回复是否合理。我习惯在日志里记录每次完整的调用链路用户输入、模型第一轮返回、函数执行结果、模型第二轮返回。这样出问题时可以完整复现。同时定期抽样人工评估因为有些错误是自动指标看不出来的比如模型参数提取对了但语义理解偏了。测试用例的设计要覆盖边界情况参数缺失、参数格式错误、函数执行超时、多个函数竞争、用户意图模糊。这些场景在真实使用中都会遇到提前测过心里有底。6. 我对 Function Calling 的一些个人判断Function Calling 这个机制表面上看是让模型“调用工具”但更深层的意义是它定义了一种模型与外部系统协作的标准协议。模型负责它擅长的语言理解和决策代码负责它擅长的确定性执行两者通过结构化的接口通信。这个分工模式我觉得会是未来 AI 应用架构的基础范式之一。在实际项目里踩过几轮坑之后我最大的体会是不要指望模型一次就完美。函数描述要迭代参数 schema 要打磨错误处理要兜底安全边界要设防。这些东西没有捷径都是在真实场景里一点点调出来的。但一旦调顺了整个系统的能力边界会比纯对话式应用大一个量级。如果你正准备在自己的项目里接入 Function Calling我的建议是从一个最简单的单函数场景开始把完整链路跑通理解每一轮的数据流转然后再逐步增加函数数量、优化描述、加错误处理。不要一上来就设计一个十几个函数的复杂系统那样出了问题你根本不知道是哪一环。另外函数描述文件建议纳入版本管理。每次修改 description 或 parameters都记录一下改动原因和效果变化。这个习惯在函数数量多起来之后会救你的命因为你会忘记当初为什么把某个描述写成那样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑