资讯动态

大模型工具调用实战:从Function Calling原理到代码实现

发布时间:2026/10/18 5:40:03 来源:尧图企业网站定制
1. 为什么大模型需要“外挂工具”从一个让人抓狂的场景说起先讲一个我实际遇到的场景。刚接触大模型应用开发那会儿我用某个开源模型写了个内部小助手准备让它接管公司周报里的数据整理。模型本身很能聊你问它“上周销售数据怎么样”它能说得头头是道。但真让它算一笔账“A组本周新增客户35个流失8个净增多少”它就露馅了——要么给你编一个“27”要么说“基于我的分析大约是30左右”。问它今天几号它给你一个“2025年1月1日”这种鬼话。为什么因为大模型本质上是一个“文本接龙机器”它的所有知识都来自训练数据它没有感知当前时间的能力也不会真的去执行计算。你让它做算术它是在“猜一个看起来像算术答案的文本”而不是“算”。这就是LLM绑定Tool工具调用要解决的核心问题——让大模型从“会说话”变成“会干活”。所谓Tool就是你暴露给模型的一组函数接口比如“查数据库”“调用API”“执行Python代码”“发邮件”。模型在理解用户意图之后决定调用哪个工具、传什么参数然后把工具返回的真实结果纳回对话里最终生成让用户满意的答案。这套机制在行业里一般叫Function Calling也有一些框架叫Tool Calling本质是同一件事。零基础学AI大模型很多人第一步会玩对话、写提示词但真正让大模型进入生产环境发挥价值的就是这一层“绑定工具”的能力。我曾经带的A同学第一次听到这个概念时问我为什么不直接提示词里写“请你计算35减去8”就行了这个问题的答案恰恰是理解Tool设计的入口——模型的数学能力并不稳定你无法保证它每次的输出都是可验证的。但如果你给模型挂一个计算器工具让它把“35, 8, 减法”传给计算器拿回来的结果就是确定的。工具的意义不只是扩展能力更是把不可控的模型输出变成可控的系统行为。这篇文章适合几类人刚入门大模型想搞懂Tool机制的新手正在做Agent或RAG项目、需要让模型操作外部系统的人还有那些已经跑通对话API、但总觉得模型“不够聪明”的开发者。我会用自己的一个完整实战例子从原理到代码到踩坑一步步演示LLM怎么绑定、调用工具把可复用的套路掰开揉碎给你看。整个过程不依赖特定平台用通用的API风格和示例代码做演示理解了思路之后换哪个模型都能搬过去。2. 搞懂Tool机制的本质模型不执行任何操作它只是“发指令”2.1 工具调用的完整链路人类世界的一次“代购”要理解LLM绑Tool最容易的类比是“代购”。你用户想吃一道菜但你不会做也不会买菜。大模型像一个聪明的管家它也不会做菜但它知道附近有菜市场、知道哪家店卖什么、知道怎么下订单。当你说“今晚想吃红烧肉”时管家不会自己去烧而是列出一张购物清单五花肉2斤、冰糖50克、生抽100毫升、八角两颗然后把清单交给跑腿小哥。跑腿小哥按清单采购把买回来的菜放到管家面前。最后管家看着这些食材给你汇报“菜都买齐了五花肉多少钱冰糖多少钱一共花了多少。”在这个故事里管家就是大模型它负责理解你的意图、生成工具调用指令购物清单就是模型输出的结构化指令tool_calls包含工具名称和参数跑腿小哥就是你的代码你负责真正去执行外部函数买回来的菜就是函数的返回结果需要重新“喂”给模型管家最后的汇报就是模型结合工具结果生成的最终回复。记住这个链路所有Tool调用都逃不出这个模式。很多初学者最大的误区在于以为给模型注册了函数模型就会自己去执行代码。不是的模型只是输出一段结构化的“调用意图”真正执行函数的是你写的代码。如果你不写执行逻辑模型只会停留在“我建议你调用get_weather(city北京)”这一步不会有任何实际效果。2.2 ChatCompletion轮到你了一次完整的REST请求目前市面上绝大多数大模型的API都兼容OpenAI风格的接口别管你用的是哪家模型只要它支持函数调用请求结构基本都长这样{ model: some-model-name, messages: [ {role: user, content: 北京今天适合出门吗} ], tools: [ { type: function, function: { name: get_weather, description: 获取指定城市的当前天气信息, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京 } }, required: [city] } } } ], tool_choice: auto }这个请求里新增了两个核心字段tools你向模型描述的工具列表。每个工具必须有函数名、描述、参数结构。这里的参数结构用JSON Schema描述模型会根据这个结构来决定“该传什么参数”。tool_choice控制模型是否必须调用工具。auto表示让模型自己判断该不该调用none表示禁止调用required或指定某个函数名表示强制调用。如果模型判断确实需要查天气它返回的消息里会多出tool_calls字段大致长这样{ role: assistant, content: null, tool_calls: [ { id: call_abc123, type: function, function: { name: get_weather, arguments: {\city\: \北京\} } } ] }看到没有模型并没有去查天气它只是告诉你“请帮我调用get_weather参数是北京”。你拿到这个结果之后自己在代码里解析函数名和参数调用真正的get_weather(city北京)函数拿到数据之后再以tool角色的消息发回给API然后模型才会基于这个结果输出最终文字。整个协议一共就这么几个关键角色system/user/assistant/tool。不少初学者就是死在“不知道把工具结果用什么角色传回去”看到返回不了结果就懵了。2.3 API参数里的关键细节id、content、role都不能马虎这里有一个很容易被忽略的细节当模型返回tool_calls时那条assistant消息的content通常是nulltool_calls里每条记录有自己的id。你在把工具结果回传给API时消息格式必须是{ role: tool, tool_call_id: call_abc123, content: {\temperature\: 22, \condition\: \晴\} }tool_call_id必须和assistant返回的那条id一一对应这是把“工具结果关联到哪一次调用”的关键。如果你同时调用了多个工具模型会返回多条tool_calls你需要逐条执行、逐条用对应的id回传。顺序方面建议按模型返回的顺序执行但严格来说只要tool_call_id匹配顺序乱一点也能跑不过稳定起见别乱。很多人还会忽略的一点工具结果content虽然是字符串格式但你可以放任意内容。放JSON字符串最通用因为模型的描述能力很强能给出一段人话。3. 跑通第一个实战Demo让模型学会调用两个工具3.1 环境准备选什么模型、装什么库在动手写代码之前先说说环境。你需要一个支持Function Calling的模型API。现在国内几个主流的大模型平台基本都支持了包括很多开源模型也能通过兼容接口暴露出来。我这边的做法是用一套OpenAI风格API的服务地址配合openai这个Python库来访问。你不需要自己去部署模型申请一个API Key就行很多平台都提供免费额度足够跑通这个Demo。在终端里执行pip install openai然后用环境变量保存API地址和Key方便代码里读取。这里有一个建议不要把API Key硬编码到代码里哪怕只是个人练习也养成用环境变量的习惯。等你想把自己的代码发布到GitHub或者给同事看的时候就会感谢这个习惯。3.2 定义第一个工具加法和天气查询为了演示我选用两个有代表性的工具加法计算器展示确定性计算能力对应那些模型容易算错、但代码一秒能算对的场景天气查询展示外部数据获取能力对应需要实时信息的场景。先设计工具的JSON Schema。用字典存起来方便后续复用def get_weather(city: str) - str: 模拟查询天气的工具函数真实项目中可以换成调用天气API # 这里用一个固定数据模拟真实场景应请求外部接口 weather_data { 北京: {temperature: 22, condition: 晴, wind: 东北风3级}, 上海: {temperature: 25, condition: 多云, wind: 东南风2级}, 广州: {temperature: 28, condition: 小雨, wind: 南风4级}, } if city in weather_data: import json return json.dumps(weather_data[city], ensure_asciiFalse) return json.dumps({error: 暂无该城市数据}, ensure_asciiFalse) def add_numbers(a: float, b: float) - float: 加法计算器 return a b3.3 构建tools注册列表格式不对全盘皆输这是最容易出错的地方。工具注册列表是严格的JSON结构你的函数必须转换成标准格式tools [ { type: function, function: { name: get_weather, description: 查询指定城市当前的天气情况包括温度、天气状况和风力, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 } }, required: [city] } } }, { type: function, function: { name: add_numbers, description: 计算两个数字相加的结果, parameters: { type: object, properties: { a: {type: number, description: 第一个加数}, b: {type: number, description: 第二个加数} }, required: [a, b] } } } ]这里有几个容易踩的坑description要写得具体模型是靠描述来判断“什么时候该用这个工具”的。比如get_weather的描述里如果没有“天气”两个字模型可能理解偏了。参数类型要写清楚。type: number和type: integer在JSON Schema中是不同概念模型遵循得也参差不齐。如果你是整数运算直接写integer更精确。required数组里列的参数是模型必须传的。如果你漏了模型可能返回一个缺参数的调用。3.4 完成一次完整调用从用户提问到最终回答现在让我们把整个流程串起来。写一个主函数它负责做四件事构造第一轮请求、判断模型是否需要调用工具、执行真正的函数、把结果回传给模型。import json import os from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) def run_conversation(user_input: str): messages [{role: user, content: user_input}] response client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools, tool_choiceauto, ) assistant_message response.choices[0].message # 如果模型没有请求调用工具直接返回文字回答 if not assistant_message.tool_calls: return assistant_message.content # 如果模型请求调用工具先把assistant的消息加入上下文 messages.append(assistant_message) # 遍历所有工具调用请求 for tool_call in assistant_message.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) # 执行真实函数 if fn_name get_weather: result get_weather(cityfn_args.get(city)) elif fn_name add_numbers: result str(add_numbers(afn_args.get(a), bfn_args.get(b))) else: result json.dumps({error: fUnknown tool {fn_name}}, ensure_asciiFalse) # 把工具结果加入上下文用tool角色 messages.append({ role: tool, tool_call_id: tool_call.id, content: result, }) # 第二次请求让模型基于工具结果生成最终答案 second_response client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools, tool_choiceauto, ) return second_response.choices[0].message.content if __name__ __main__: print(run_conversation(北京现在天气怎么样)) print(run_conversation(请计算123456789 987654321 等于多少))看上去代码不长但链路里的细节值得逐行拆解assistant_message.tool_calls是列表是的模型一次可以请求调用多个工具。所以要用for遍历不能只处理第一个。把assistant_message原样加入messages是为了保留上下文让模型知道“自己刚才请求了工具”。如果你漏了这一步第二次请求时模型会失去刚才发出指令的“记忆”可能答非所问。工具函数返回的结果一律先转成字符串。模型接收的content字段就是字符串。你可以返回22.0也可以返回JSON字符串甚至返回一长段文本。如果你用了国内某些模型平台API风格基本兼容但有个别平台对tool_call_id字段的名称略有差异有的叫tool_call_id有的叫id建议先读一下对应API文档。以openai库为准、底层适配通常问题不大。3.5 一个容易被忽略的点要不要把tools放进第二次请求答案是要。第二次请求时我还是把tools和tool_choiceauto传进去了。为什么因为模型可能看完第一个工具结果还不够还想再调用第二个工具。比如用户问“北京和上海哪个更暖和温差几度”模型可能先查北京、再查上海这两次请求就会交叉发生。如果你第二次请求不传tools模型就丧失了继续调用工具的能力只能基于已有的部分信息硬答。那会不会导致无限循环会所以实际项目里都会加一个max_iterations限制比如最多允许模型调用5轮工具。下面我专门用一个章节说这个坑。4. 调试中最容易翻车的几个地方我的真实踩坑记录这一节说几个我实际调试中被折腾到头皮发麻的问题都是初学者几乎一定会碰到的。4.1 模型返回的参数格式别全信我在某次调试中让模型给add_numbers传两个参数a和b结果模型返回{a: 一百二十三, b: 456}第一个参数居然是中文数字字符串。我json.loads成功了但把一百二十三往函数里传直接报错。这个问题的根源是模型在生成arguments时并不会“真的读”你的JSON Schema去校验它只是“尽量按格式生成”。所以你的执行层必须做兜底校验甚至要做类型转换。我个人现在的做法是给工具写一个通用的解析函数def safe_get_arg(args_dict, key, defaultNone, cast_typestr): 安全获取参数并做类型转换 value args_dict.get(key, default) if value is None: return default try: return cast_type(value) except (ValueError, TypeError): return default注意一百二十三这种中文数字用float()也转不了真要处理还得写个中文数字解析器但那属于极少数情况。实际部署时你可以选择让模型把参数用number类型描述得更严格或者在执行层捕获异常并返回错误信息让模型自己纠正。把错误信息以普通字符串形式返回给模型它会尝试修正参数重新调用——这是Function Calling里一个非常有用的小技巧我们后面细说。4.2 模型返回了不存在的工具名模型是生成式的它有可能编造一个你没注册过的工具名。我见过的一次我只注册了get_weather和add_numbers模型却返回了一个calculate_discount。为什么因为它的训练数据里可能见过类似命名的工具它觉得“这里该调用一个折扣计算函数”。如果我在执行层直接fn_name ...对比那这个调用就会掉进else分支返回Unknown tool错误。这种问题的处理策略是执行层遇到未知函数名时返回一个明确的报错字符串同时鼓励模型改用其他工具。比如result json.dumps({error: f未知工具: {fn_name}可用工具只有 get_weather 和 add_numbers})然后模型读了这条错误信息往往会自己纠正重新选择正确的工具。这个反馈闭环是整个Function Calling稳定工作的关键。4.3 循环工具调用把你的token烧穿有一次我写的Agent卡死在一个循环里用户问“上海天气怎么样”模型调get_weather天气函数返回{temperature: 25, condition: 多云}但模型看了结果又调了一次get_weather因为描述里写着“只查北京上海广州”它可能觉得自己刚才传错了城市又试了一次。几次反复之后我的API账单在十分钟内翻了一倍。解决方案很简单就是加循环上限MAX_ITERATIONS 5 for _ in range(MAX_ITERATIONS): # ... 请求、判断、执行工具 ... if not assistant_message.tool_calls: break加上这个上限之后即便模型想反复调用超过5轮就直接返回当前的内容也不会无限烧token。实际项目中MAX_ITERATIONS根据任务复杂度调节一般是3到8。如果超过了这个数还没完成说明要么是任务描述不够清晰要么是工具本身设计有问题需要回到Schema层面去修正。4.4 多工具并行调用自己管理执行顺序前面提到模型一次可能返回多个tool_calls。初学者容易以为模型会“并行执行”它们——不是的模型只是返回了一个列表执行是你的代码做的。你对列表可以用顺序执行也可以用线程池并发执行。如果这些工具之间没有依赖并发能大幅缩短总耗时如果有依赖比如“先查A城市再对比B城市”那你就要自己理清逻辑。我在一个项目里写了并发执行版本from concurrent.futures import ThreadPoolExecutor def execute_tool(tool_call): fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) # 根据fn_name分发执行 ... with ThreadPoolExecutor(max_workerslen(tool_calls)) as executor: results list(executor.map(execute_tool, tool_calls))注意用并发时tool_call_id和结果的对应关系千万别搞错。我建议把执行函数写成返回(tool_call.id, result)的元组再统一组装消息避免错位。5. 从Demo到产品工具设计的边界、成本与控制5.1 工具粒度怎么拆粗了模型不会用细了模型调不过来工具设计是整个Function Calling应用最耗心思的地方。我见过有人把“查天气”“查湿度”“查风力”“查空气质量”拆成四个工具结果模型经常不知道用户问“适合跑步吗”该调用哪个也见过有人把所有数据库查询合并成一个query_database工具靠一个SQL字符串参数走天下结果SQL生成的质量惨不忍睹。经验法则是以“用户意图”为粒度拆工具。比如对于天气场景一个get_weather(city, date)就比拆成四个小工具合适。对于数据库场景如果你只关心查询一个query_database(sql)就够了如果还要支持写入、更新、删除应该拆成独立工具并且每个工具的描述里写清楚“危险操作需要二次确认”。工具数量也不是越多越好控制在5~10个之内模型的选择准确率会显著提升。超过15个很多模型的工具选择就开始飘了你需要在描述上做更严格的约束或者引入多级Agent来分摊。5.2 工具执行出错了正确姿势是“把错误喂回去”工具执行不可能每次都顺利。比如查天气的外部API超时了返回了一个网络异常查数据库时SQL写错了抛了异常。很多初学者遇到这种情况会直接让整个程序崩溃这是不对的。正确的思路是把错误结果转成文本以tool角色的消息返回给模型让模型理解发生了什么并尝试用更合适的参数重新调用。这里有个关键纪律错误信息不能裸抛Python异常。模型的上下文不是代码执行环境它看不懂KeyError: city。你应该先捕获异常转成描述性的文字比如“天气服务暂时不可用请尝试查询其他城市”或“数据库中不存在表user_orders_2025可用表有users,orders”。模型看到这类信息才能做出有意义的下一步决策。我实际项目里的做法是给工具套一层统一的包装器def tool_wrapper(func): functools.wraps(func) def wrapped(*args, **kwargs): try: return json.dumps({status: success, data: func(*args, **kwargs)}, ensure_asciiFalse) except Exception as e: return json.dumps({status: error, message: str(e)}, ensure_asciiFalse) return wrapped统一格式的好处是模型看到success或error的标记一眼就能读懂执行结果。5.3 成本控制token消耗远比想象中快每多一轮工具调用都要把历史消息重新发给模型这意味着工具调用越多的对话token消耗呈线性增长。如果你在系统提示词里塞了一大段背景说明每一轮都要重复发送成本会更高。这里有几个我自己常用的省成本策略精简系统提示词不要把长篇的业务规则放在system里能放进工具描述里的就放工具描述里。适当截断历史对于长对话只需要保留最近几轮的messages更早的内容该丢就丢。缓存工具结果如果用户反复查同一个城市的天气第二次直接命中缓存不需要再调API。只在需要时传tools如果当前任务完全不涉及工具调用第二次请求时可以不传tools能省一部分输入token的解析开销——虽然不算多但积少成多。5.4 别迷信“自动判断”tool_choice该强制时就强制tool_choiceauto是默认选项模型自己判断是否调用工具。但在某些场景下你应该关闭自动判断强制固定工具调用。比如你的Agent就是一个“意图分类器”期望它每次调用classify_intent(text)这个工具那么tool_choice可以直接指定为tool_choice: {type: function, function: {name: classify_intent}}这样模型就会“必须”调用该工具不会自作主张输出文字。对于工作流固定的场景这个技巧能显著提升稳定性。反过来如果某个环节不想要模型调用工具你可以设为tool_choice: none强制纯对话输出。动手实验时建议两套都试试感受一下模型输出的差异。6. 深入一步从工具集合到Agent雏形这套机制还能怎么玩6.1 多工具协作的经典案例查询订单状态一旦你理解了Tool机制就会发现它其实是所有Agent应用的基石。这里再拆一个更贴近业务的例子一个“查订单”的Agent。它需要三个工具get_user_info(user_name)根据用户名查到用户IDget_orders(user_id, statusNone)根据用户ID查订单列表get_order_detail(order_id)查订单的详细信息。当用户问“帮我看看我最近买了什么还没发货”时模型的调用链路可能是先调get_user_info拿到用户ID发现有查过用户再调get_orders如果订单列表里有未发货的再继续调get_order_detail。此时你会发现多轮调用是串联的而且每轮都要把你拿到的中间结果回传给模型模型才能决定下一步。这其实就是最朴素的Agent雏形——模型作为决策大脑工具作为手脚每一轮工具结果为下一轮决策提供依据。6.2 一个提升稳定性的模式让工具返回“人能读懂的文本”有些开发者在工具里返回的是高度结构化的原始数据比如一个包含几十个键的JSON。模型确实能解析JSON但并非所有模型都能把嵌套很深的JSON理解准确。我发现一个高阶技巧许多优秀的Agent系统工具返回的是经过加工的、语义化的文本。比如查询订单你的工具函数可以直接返回订单202405220012用户张三下单日期2024-05-20包含商品A2件、商品B1件当前状态为“已发货”预计送达2024-05-25。而不是返回一个原始字典让模型去理解。这样虽然牺牲了一点通用性但模型生成最终答案时会准确得多。对于复杂JSON可以考虑用一个“摘要工具”先做一层加工再返回给模型。6.3 工具调用的安全边界不该放开的能力绝不放开最后也是最重要的一点当你把工具的调用权交给模型时一定要想清楚这个工具可能被模型“误闯”到什么边界。比如你注册了一个send_email工具模型可能因为用户一句“帮我给所有人发一封道歉邮件”就把邮件群发了。这不是模型有心干坏事而是它没有足够的安全意识。给高风险工具加确认机制是很成熟的工业界做法。比如在执行send_email之前先调用ask_user_confirmation工具让模型先向用户复述“即将给XX发送邮件内容为XXX是否确认”等用户明确同意后再真正执行。严格来说工具的调用顺序控制也可以写进工具描述里比如在send_email的description里写明“此工具会真实发送邮件使用前必须先调用ask_user_confirmation获得用户确认”。模型对这类约束的遵循程度越来越高了但保险起见你的执行层仍需要加一道硬校验。另外一个容易被忽略的安全点是工具调用过程中传入了用户的原始隐私数据。如果你把用户输入的完整文本一股脑塞给某个外部工具会产生数据外泄风险。所以API Key、密码、身份证号这类数据绝不能设计成模型的工具参数。7. 写在最后的实操心得API文档翻烂了不如亲手跑一遍循环很多人学LLM工具绑定喜欢先翻几十页文档再找各种Agent框架去跑现成Demo。我的建议相反先用最少的代码把一次完整的“用户提问-模型选工具-代码执行-结果回传-模型总结”跑通再去看框架。因为这一步跑通之后你会对消息协议、tool_calls格式、tool_call_id配对这些核心概念建立肌肉记忆后面无论换哪个框架、哪个模型都能一眼看懂它的封装在干什么。实际上各家对这种机制有各自叫法有的叫Function Calling有的叫Tool Use有的把它并入结构化输出能力。但底层逻辑一致模型是决策者不是执行者你能控制的是工具列表和执行代码模型负责的是“选择哪个工具、传什么参数、怎么组织最终回答”。搞清楚这个边界往后你看到任何复杂的Agent流水线都不会发怵——它们本质上都是这篇文章里那个循环的延伸多几层工具、多几个条件判断、多几个状态管理而已。最后再分享一个小技巧。调试工具调用时把你的messages列表打印出来看。我调试时经常做的一件事是把第二轮请求发出的完整JSON保存到文件然后人肉读一遍。你会发现很多“模型为什么这么答”的疑问在看了完整消息上下文后瞬间就有答案了。我见过不少回传工具结果时把role: tool写错成role: assistant导致一直重试报错的同学如果早点打日志看一眼几十秒钟就能定位。调AI项目日志就是你最好的老师。

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

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

免费获取报价 →
↑