尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
大模型Function Calling实战:从JSON解析困境到稳定工具调用
做LLM应用开发的人大概率都经历过这一段尴尬辛辛苦苦把用户指令塞给大模型让它输出一段JSON用于触发业务动作结果模型生成一个带Markdown注释的、缺字段的、甚至多出幻想字段的“假JSON”程序解析直接报错。我当初在一套问答系统里为了解析模型输出的工具参数前后写了七个补丁函数。直到系统性地把Function Calling的文档啃完我才搞明白一件事那些解析问题并不是模型笨而是我用错了方式。Function Calling不是给你一个“输出JSON的更好办法”而是把“调用外部工具”这件事从文本层面提到了协议层面。这篇内容就是我从概念、机制、实操到排坑的完整记录适合正在做大模型Agent、自然语言查询、工作流自动化的开发者参考。1. Function Calling到底解决了什么问题从“只能聊天”到“可以动手”1.1 一次自然语言请求如何变成一次系统操作先说一个最常见的场景。用户在对话框里输入“帮我查一下某市明天的天气顺便算一下满100打八折后多少钱”。程序要做的事情有两件调天气接口、做价格计算。如果模型没有工具调用的能力它只能干一件事给你返回一段看起来像模像样的文字告诉你“好的我来帮你查”或者干脆自己编一个天气数据出来。这显然不可接受。Function Calling的做法是在发起请求时把你能执行的函数清单一起作为“工具”塞给模型。模型读完用户输入之后如果判断需要外部数据或动作就不再硬编自然语言而是按照清单里某个函数的定义生成一段结构化的调用指令。指令内容包括函数名和参数列表。你的程序拿到这段指令执行真实的函数再把执行结果作为消息回传给模型模型基于真实结果生成最终回答。也就是说整个链路的职责分得很清楚模型负责判断是否需要调用工具、选择哪个工具、生成符合要求的参数程序负责真正执行函数、做权限校验、处理异常、把结果格式化回传模型再负责基于函数执行结果组织成对用户友好的自然语言回复这个分工看起来理所当然但和“让模型输出JSON再解析”完全是两类东西。后者把模型的输出当作纯文本来处理前者把“工具选择”提升到了协议层模型在训练阶段就已经见过大量类似样例知道返回的字段应该是什么语义、什么结构。1.2 传统JSON输出和Function Calling的关键差异为了把这件事讲透我拿自己踩过的对比列表放在一起看方式典型做法常见翻车现场提示词要求输出JSON“请以JSON格式返回结果包含name和price字段”模型输出带json代码块、字段缺失、值随意编造后处理提示词约束在提示词里给一个JSON示例再用解析器强行提取模型理解了格式但业务语义理解错参数值填错Function Calling通过tools参数声明函数由模型生成结构化tool_calls相对受控但仍需要程序端校验和错误兜底表格里的前两种方式本质上都在“赌”模型今天心情好愿意老老实实按格式输出。可一旦上下文长了、用户表述绕了、模型换了版本输出就很容易出问题。Function Calling最大的进步在于工具格式和调用逻辑被“显式建模”进了模型的训练和推理过程中。模型不是在“生成一段包含函数名的文本”而是在“选择动作并填充参数”这两件事的概率分布空间完全不一样。1.3 什么项目适合用它基于我自己的落地经验适合引入Function Calling的场景有几类垂直行业Agent客服助手、订单查询、售后处理用户输入自然语言系统执行真实业务动作NL2SQL把“上个月华东区销售总额是多少”转换成数据库查询动作工作流自动化邮件发送、日程创建、审批流触发、提醒设置数据分析助手让模型调用统计函数、图表生成函数再把结果汇总给用户不太适合的场景也很明确纯闲聊、文本润色、翻译、内容生成这些场景没有“动作”需要执行强行挂一串工具反而浪费token、增加延迟。判断标准就一条用户意图里是否存在外部数据获取或者系统副作用操作。有就用Function Calling没有别硬上。2. 拆解函数调用的底层交互机制模型只是决策者不是执行者2.1 模型眼里的“函数”长什么样初学者最容易误会的一点是以为Function Calling是把Python函数直接丢给模型。实际上模型能看到的只是函数的“描述文本”通常包含四件事函数名机器可读的唯一标识比如query_weather函数说明description告诉模型这个函数是干什么的、什么时候用参数定义parameters使用JSON Schema描述每个参数的类型、含义、约束必填项与枚举值明确哪些参数必须有哪些值才合法这段话才是模型做决策的全部依据。它看不到你的函数体更不会替你执行代码。你在函数体里藏的任何逻辑模型一概不知。所以函数描述写得清不清楚直接决定了模型选得准不准。我见过很多团队把工具描述写得三行以内、参数名用缩写结果模型在多个相似函数之间反复横跳就是这个原因。2.2 一次完整的函数调用循环我用文字描述一个标准的调用流程走完一遍你就不会再把“模型直接返回JSON”当作工具调用了用户输入进入消息列表例如“某市明天天气怎么样”请求携带tools参数里面包含query_weather等函数声明模型处理输入在函数清单里判断需要调用query_weather返回tool_calls字段内容包含函数名和参数比如city某市你的程序拦截到tool_calls暂停“让模型继续回答”的动作转而去执行真实的天气接口天气接口返回结果程序把它封装成一条roletool的消息消息里带上对应的调用ID这条工具结果被追加进消息历史再次发回给模型模型看到查询结果后组织成一句自然语言“某市明天晴18度”这里最关键的一步是第5步。工具结果必须以结构化的消息形式回传而不是直接拼进用户原始输入。主流的兼容协议里工具结果的role固定为tool并且必须和上一步的tool_call_id一一对应。这个对应关系一旦错乱模型就会把某次调用的结果安到另一个函数头上产生的幻觉非常隐蔽。2.3 模型的三种返回形态在实际调用中模型不会只有“直接返回工具调用”一种表现我归纳了三种纯自然语言回答没有tool_calls字段。常见于问候、澄清、权限不足等场景纯工具调用content为空tool_calls里带完整函数调用指令边回答边调用content里有“好的我来查询一下”之类的过渡文字同时带tool_calls第三种形态最容易让人踩坑。有的同学写解析逻辑时只判断content发现返回里有文字就直接把工具调用丢了有的同学则相反看到tool_calls就直接忽略content导致聊天记录缺失了一段上下文。我的建议是工具处理逻辑只认tool_calls的存不存在不认content是否为空但在把消息写回历史时content和tool_calls都要保留这样下一轮模型才能保持一致性。2.4 和ReAct模式的区别很多文章把Function Calling和ReAct放在一起讲我简单梳理一下。ReAct是一种通用的推理范式核心是让模型在上下文中写出thinking步骤然后调用工具再观察工具返回结果循环往复。它是一种“思想框架”不限制具体实现方式。Function Calling则把这个思想变成了工程可落地的协议模型不需要写自由文本式的行动描述而是直接输出结构化字段。你可以理解成ReAct是大方向Function Calling是其中一个更可控、更省心的具体实现方式。3. 完整实现一个函数调用流程注册、分发、回传、再推理3.1 环境准备实操之前先说环境。现在主流的大模型服务基本都对齐了一套通用的tools协议你只需要确认你用的模型支持工具调用能力就行。主流的商用闭源模型和一批中大规模开源模型都有不错的支持。客户端方面常规的Python SDK就可以完成任务不需要额外安装复杂框架。这里提醒一点如果你用的是自部署的开源模型先确认版本是否覆盖tools参数的兼容实现。有些旧版本聊天模型表面上能接收tools参数实际是把它当成普通文本吞进去了返回的还是自然语言。3.2 定义你的工具清单我以两个最简单的业务函数为例一个查天气一个计算折扣价。工具定义的JSON结构大致长这样[ { type: function, function: { name: query_weather, description: 查询指定城市在指定日期的天气情况适合用户在询问天气、出行建议、是否需要带伞时使用, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如某市 }, date: { type: string, enum: [today, tomorrow, day_after_tomorrow] } }, required: [city] } } }, { type: function, function: { name: calculate_discount, description: 根据原价和折扣率计算最终价格适用于满减、打折等价格计算场景, parameters: { type: object, properties: { original_price: { type: number, description: 商品原价单位为元 }, discount_rate: { type: number, description: 折扣率例如打八折填0.8 } }, required: [original_price, discount_rate] } } } ]写这个JSON的时候注意几件事。第一description不要写太抽象“查询天气”四个字远远不够要写清楚适用场景。第二日期这种有枚举约束的字段尽量用enum把合法值列出来不然模型会给你填“明天”还是“2025-06-01”完全不可预测。第三required只放真正必须的参数别把所有可选参数都塞进去否则模型为了满足必填项反而会编个不存在的值。3.3 核心代码流程下面是一段简化的Python骨架展示了从首次请求到工具结果回传的完整闭环import json def execute_tool_call(tool_call): 根据模型返回的调用指令分发到具体函数返回执行结果字符串 func_name tool_call.function.name arguments json.loads(tool_call.function.arguments) if func_name query_weather: return query_weather(arguments[city]) elif func_name calculate_discount: return calculate_discount( arguments[original_price], arguments[discount_rate] ) else: return 未知函数请重新选择 def run_conversation(user_input, tools, max_iter6): messages [{role: user, content: user_input}] for _ in range(max_iter): response client.chat.completions.create( modelMODEL_NAME, messagesmessages, toolstools, tool_choiceauto ) msg response.choices[0].message if not msg.tool_calls: # 模型不再调用工具返回最终回答 return msg.content # 把模型的回复追加进历史保留content和tool_calls messages.append({ role: assistant, content: msg.content, tool_calls: [tc.model_dump() for tc in msg.tool_calls] }) # 逐个执行函数并回传结果 for tc in msg.tool_calls: result execute_tool_call(tc) messages.append({ role: tool, tool_call_id: tc.id, name: tc.function.name, content: result }) return 已达最大轮次对话终止这段代码有几个细节必须强调。第一msg.tool_calls的判断才是核心不是msg.content。第二assistant消息要把tool_calls原样存回消息历史否则下一轮模型不知道你刚才调用了哪个函数。第三tool消息的tool_call_id必须和tc.id对应上这是协议层面的关联键。第四循环要有上限我建议单个对话里最大调6到10轮超过就强制终止防止模型陷入反复调同一个函数的死循环。3.4 函数失败不该直接崩溃实际的函数调用不会每次都成功。天气接口超时、数据库查询报错、参数非法这些情况都很常见。新手容易犯的错是函数一旦抛异常整个对话流程就崩了。但更合理的做法是把错误信息作为工具结果回传让模型自己决定下一步。比如你是查订单接口函数返回“用户不存在”模型收到这个结果后可以改成让用户确认账号再接一次。这种容错设计能让整套系统比以前做规则判断灵活得多。4. 实战中反复踩到的坑从工具描述到循环退出条件4.1 别用正则去抠函数调用字段我见过有人在代码里直接对模型返回的文本做正则匹配试图从content里抠出函数名和参数。这属于典型的用处理自然语言的方式处理结构化协议字段。正确的姿势是直接读message对象上的tool_calls属性。只有一种情况需要你动字符串有些网关的兼容层把function.arguments以字符串形式返回而不是字典。这时你需要json.loads一次再拿里面的字段。另外要留意少部分模型服务返回的arguments是带转义符的字符串解析前可以先strip掉多余空白。4.2 函数描述不清楚模型就会选错有一个我印象很深的案例。一个项目里有两个函数一个叫“查询订单状态”一个叫“查询退款进度”。前者的description写的是“查询用户订单”后者写的是“查询用户退款”。用户问“我之前退货的钱到哪了”模型直接调了“查询订单状态”结果自然是查不到任何信息。后期把两个description改写得更具体、加入触发场景准确率才明显上来。描述优化有几个通用技巧写清楚“什么时候用”比写“是什么”更重要存在相似函数时明确指出和另一个函数的区别比如“仅用于查询正常订单不含售后退款单”参数说明里给一个形象示例比如日期字段写“格式为YYYY-MM-DD例如2025-01-15”4.3 参数幻觉防不胜防即使有JSON Schema约束模型仍然可能填入不存在的枚举值或者把数字格式搞错。比如schema要求temperature是number模型传了个字符串“cold”。这类问题的核心原因是模型的本质是概率生成它理解schema语义但遇到模糊上下文时仍会“自由发挥”。我的建议是程序端做二次校验不能信任任何来自模型的参数值。轻量做法是写校验函数检查必填项是否齐全、枚举值是否在预期范围内、数字是否可解析。更重的做法是引入校验引擎或者让工具自己处理。尤其是涉及金额、城市编码、订单号这类关键参数宁可多一层防御也不要因为省几行代码把一个错误请求发出去了。4.4 工具列表膨胀带来的选择灾难项目早期只有五个工具模型选得又快又准。等工具增加到二十个以上情况就变了token消耗明显上升、响应变慢、模型开始在相似函数之间犹豫。这时候就要做工具管理了。我实践下来比较有效的方法是分层路由。第一步先用一个轻量分类器可以是小模型或规则判断用户请求属于哪个业务域比如订单域、客服域、财务域第二步把对应域的工具子集作为tools参数传给模型。这样做的好处是既控制token成本又减少模型的选择空间准确率提升非常明显。当然跨域请求会麻烦一点但实际业务里绝大多数请求都能归属到单一领域。4.5 循环退出的边界条件工具调用的循环也有翻车点。最基础的退出条件是“模型不再返回tool_calls”但我建议再加一层轮次上限。原因很简单模型并非总能理性收敛。比如某个查询工具返回结果为空模型可能认为是查询条件不对就换个参数反复查同一个工具形成事实上的死循环。轮次上限建议调成6到10之间。太少不够完成贝联操作比如先查用户再查订单太多会让单次对话迟迟不结束。另一个容易忽略的问题是消息历史越滚越大。每轮函数调用至少增加一条assistant消息和一条tool消息几轮下来上下文体积膨胀得很快。对于上下文窗口较小的模型你需要考虑压缩策略把早期工具调用的长结果截断、只保留关键结论或者只保留最近几轮的完整消息。4.6 并行执行导致的结果错配一次返回多个tool_calls时程序端通常会并行执行。这时候最怕的就是结果顺序错乱。协议要求每个工具结果绑定自己的tool_call_id所以并行场景下建议把每个调用ID和其对应的执行结果放进同一个结构里最后按ID回传。如果某个子任务超时或异常也要返回与该ID对应的错误信息。千万别把“所有调用都成功”当默认假设。同时涉及改数据或副作用的工具最好做幂等设计防止模型在一次对话里因为用户重复表述而重复触发同一个动作。5. 进阶用法并行、级联、流式与状态保存5.1 并行调用一次决策执行多个动作当单个用户请求里包含多个独立意图时模型可以在一条回复里返回多个tool_calls。比如用户说“某市天气怎么样顺便看一下我账户余额”模型可能同时返回query_weather和query_balance两个调用。程序端拿到多个调用后可以并发执行再把各自的返回结果按序回传。这样比串行执行节省了一大段等待时间而且用户只需要一条消息就能触发所有动作。不过并行调用也有适用边界。如果第二个函数依赖第一个函数的输出比如先拿到用户ID才能查订单那就不适合并行需要走级联。所以写工具定义的时候可以把不存在依赖关系的函数放一起存在明确依赖关系的尽量在一个函数内部封装避免模型把执行顺序搞乱。5.2 级联调用用上一次结果驱动下一次动作级联是更常见的Agent形态。经典流程是用户问“我上个月买了多少钱的东西”程序先调用get_user_id拿到用户ID后再调用get_monthly_orders最后调用calculate_total。这种场景下每轮函数结果都要回传并追加进消息历史模型在下一轮看到结果后才知道下一步该选哪个函数。级联调用在实现上有一个技巧工具结果不要太长。如果第一个函数返回了一大段完整订单明细动辄几千字模型不仅处理慢还容易在后续决策时分心。建议在函数内部做一次数据裁剪只返回和下一步决策相关的关键字段。比如订单线条数、总金额、状态类型而不是把每条明细的备注全部塞回上下文。5.3 流式输出下的工具调用处理带tools请求也可以开启流式输出但坑在于流式模式下tool_calls不是一次性完整返回的而是像自然语言一样分片到来。有的实现里函数名、参数、ID分别在多个chunk里才补全。处理策略是先把所有delta累积起来等整个流结束后再拼接出完整的arguments字符串然后统一执行工具。在用户界面层面我的经验是不要试图在“函数执行前”给用户播打字机效果因为函数执行期间模型不会产字。更好的体验是检测到模型即将调用工具时前端显示一个“正在查询...”的卡片状态等工具结果回来后再展示模型最终生成的文字。5.4 结构化输出和Function Calling的配合现在不少平台还提供了严格的结构化输出能力可以保证模型最终回复内容的字段完全符合某个JSON Schema。它与Function Calling的定位不太一样Function Calling管的是“模型要调哪个动作”结构化输出管的是“模型最终说的内容是什么格式”。二者可以配合。比如你先用Function Calling让模型查了一圈数据最后一步可以要求模型以规定结构输出汇总结果方便前端直接渲染。这种组合在实际项目里比单纯靠“提示词要求格式”稳定得多。6. 选型与体感接口差异、模型选择和落地建议6.1 不同服务商接口格式的取舍虽然主流协议趋同但不同服务商在函数声明上仍然存在细节差异。一类是“函数声明调用ID”结构这是当前最主流的做法另一类会额外要求提供output_schema也就是除了入参还得声明返回值结构这样能增强返回的规范性。还有个别老牌服务仍在用“消息文本约定符号”的方式这种方式自由度最高但程序解析的稳定性也最差。如果你只是做一个原型或小工具选第一类主流通用格式就够了如果要上线生产、对结果规范性要求高优先考虑支持output_schema声明的那类服务它能在协议层面就把返回字段框死。切换不同服务的时候最省力的做法是在工程层封装一层统一的工具注册和结果适配器把各家的格式差异隔离在适配层内部。6.2 模型选择的基本盘工具调用对模型的底层能力要求不低不是所有模型都能做好。选型的核心标准是在工具调用评测集上的表现、对复杂schema的理解能力、以及多轮工具调用任务的成功率。从我实际体验来看第一梯队和第二梯队模型在工具调用上的差距非常明显具体表现为小模型或老旧模型经常选错函数、乱传参数、在流式场景下丢字段。如果你预算有限我建议采用分层策略用一个便宜的小模型做意图路由把最终的工具选择和参数生成交给能力更强的模型。这个组合在成本和效果之间能找到不错的平衡点。6.3 上线生产前一定要做的事最后整理几条落地经验都是我从线上踩坑换来的每个函数调用的入参和返回都要有日志排查线上问题时没有日志等于没有线索所有工具结果统一封装成字符串返回别把复杂对象直接塞给模型模型喜欢简洁文本对可能修改数据或产生副作用的工具在程序端做二次确认或权限校验上线前做“影子模式”让模型在测试流量里调用工具但不真正执行系统动作只记录决策日志对比人工标注后再放开设置全局超时时间函数执行超过15秒就该中断并回传错误避免用户长时间等待6.4 一点个人体会最后说点实际感受。做带Function Calling的系统最难的不是跑通代码而是让模型在你的一堆业务工具里做出稳定的决策。函数描述是你给模型画的地图描述写得越清楚模型走得越准程序端的校验是你给模型系的安全带真正稳住线上稳定性的从来都是这些不起眼的防御代码。我第一次把函数调用放进生产环境时最大的感触是以前担心模型乱说话现在更担心模型乱调用工具。好在Function Calling至少让“乱调用”这件事变得可追踪、可约束、可回滚。只要把工具定义做实、把循环和校验做严这套机制就能成为大模型应用里最靠谱的那块地基。
RELATED

相关推荐

从栅格地图到导航接入:机器人地图服务模块设计与实操

从栅格地图到导航接入:机器人地图服务模块设计与实操

1. 项目定位:为什么机器人项目需要一个独立的地图模块做机器人导航最怕什么?不是算法调不通,而是当你把传感器、底盘、规划器都调好之后,发现整套系统被一张乱七八糟的地图卡住了。lingbot-map这个项目,就是专门解决这…

📅 2026/10/21 4:45:55
Spring Boot海鲜烧烤商城系统设计:从数据库到订单库存的完整实践

Spring Boot海鲜烧烤商城系统设计:从数据库到订单库存的完整实践

每个做毕设或课设的同学,大概率都见过这类标题:“基于Spring Boot的XX商城系统”。我的待办事项列表里常年躺着类似的需求,其中“springboot167基于Java的水产品海鲜烧烤商城购物网站系统设计与实现”算是比较典型的电商类题目。你第一眼看到…

📅 2026/10/21 4:45:55
Spring Boot海鲜烧烤商城开发实战:从数据库设计到订单部署全覆盖

Spring Boot海鲜烧烤商城开发实战:从数据库设计到订单部署全覆盖

开篇先说实话,这个标题我看着挺亲切的——“基于Java的水产品海鲜烧烤商城购物网站系统”,一看就是典型的Spring Boot单体电商项目。别觉得它“只是毕业设计”就小看它,能把一个电商系统完整做下来,从前端页面到后端接口、从购物车…

📅 2026/10/21 4:45:55
MORE NEWS

更多资讯

📰

CactiEZ实战:华为、H3C、中兴交换机SNMP监控模板导入与排错

简介:面向CactiEZ 10.1监控平台用户,这份资源提供了一套开箱即用的主机监控模板包,专门解决网络运维人员在纳管华为、中兴、H3C等设备时手动编写SNMP模板的麻烦,也适用于需要同时监控UPS、打印机等辅助设备的机房场景。包内共16个…

📰

CVE-2024-38077补丁装不上?排查“此更新不适用于您的计算机”

如果你手头管着一批Windows Server,过去这几天多半被CVE-2024-38077这个编号搞得不太安生。这是远程桌面服务(RDP)相关的一个远程代码执行漏洞,微软公告评级为严重(Critical),攻击者不需要账号密…

📰

React企业级应用实战:技术选型、工程化搭建与性能优化

接手一个代号叫“rea”的项目时,我第一反应是这缩写也太随意了。但做着做着就发现,项目全称其实是React Enterprise Application,一个从零搭建的企业级前端应用,里边有商品展示、购物流程、后台配置、权限控制,还有一堆…

📰

美元黄金美债收益率联动分析:基于tushare的滚动相关性与背离识别实战

美元、黄金、美债收益率这三者之间的关系,几乎是每个做宏观对冲或者大类资产配置的人绕不开的课题。我从几年前开始用 tushare 拉数据做复盘,最初只是想验证一下"美元涨黄金跌"这个朴素认知到底靠不靠谱,后来越挖越深,发…

📰

从零搭建技能管理项目:熟练度分级、依赖关系与统计决策

1. 从“skills”这个标题说起:它到底在指什么“skills”这个词看起来简单,但恰恰因为它太常见,反而容易让人抓不住重点。我在不同场合看到过太多以“skills”命名的项目、文档、课程模块,有的指向编程能力矩阵,有的指向…

📰

Java支付宝沙箱支付接入教程:密钥配置、验签及避坑指南

Java接入支付宝沙箱支付图文详细教程:从零到跑通全流程,附避坑记录大概在2021年那阵子,我第一次把支付宝支付接进自己的Java项目时,最头疼的不是写代码,而是不知道从哪儿下手。网上搜出来的资料一半是过时的&#xff0…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬