☰
函数调用与工具声明:大模型应用开发的工程要点
2026/10/20 5:00:18 网站建设 项目流程

最近在跟读一门大模型应用开发的公开课,课程代号是CS329Z,第四讲的主题正好是“工具使用与函数调用”。这一讲是我暂停次数最多的一节,因为代码示例看起来都很短,背后的协议往返却要把我第一次做智能体时踩过的坑全部串起来:模型为什么能“调用”函数?工具声明里到底该写什么?返回结果该以什么姿态再塞回对话?如果你也在做LLM应用,无论是搭一个会查天气的小助手,还是给业务系统接上自动操作能力,函数调用(Function Calling)和工具声明(Tool Schema)都是绕不开的地基。这篇文章不替你把课程重讲一遍,而是把我从这一讲里提炼出来的工程要点,以及回归项目后实测中遇到的那些“课堂不会写但上线躲不掉”的问题,一起整理出来。

1. 为什么“让模型学会调用工具”成了这一讲的题眼

1.1 语言模型的问题不是“不够聪明”,而是“没有手”

这一讲的开篇没有急着给代码,而是先抛出一个对比:模型的长板是语义理解和文本生成,短板是事实约束和执行动作。你可以让模型写出条理清晰的计划书,但它不知道当前城市此刻在下雨;你让它计算一笔复杂优惠,它会给你一个看起来合理但完全不对的数字。原因不复杂——语言模型本质上是在预测Token序列,所有知识都被压缩在参数里,它既看不到实时世界,也无法执行用户真正需要的操作。

工具使用要为这个缺口补上的是“外部世界接口”:真实数据、业务动作、精确计算,全都可以放出去,让程序来回答。模型负责生成意图,程序负责兑现意图。这个分工在课程里被反复强调,因为很多人一开始会误以为模型真的“学会了用工具”,但实际上模型只是学会了在合适的时机说出“我想用哪个工具、参数是什么”,剩下的事情全部由你的代码完成。

1.2 函数调用、工具使用、插件,本质上是一回事

术语有点乱,有人叫Function Calling,有人叫Tool Use,还有人叫Plugin。课程里统一给的看法是,这些名字都在描述同一个协议:模型在生成回复时,除了输出自然语言,还可以输出一个结构化的“调用请求”,请求里标明工具名和参数;应用收到这个请求后执行真实函数,再把结果作为新一轮对话消息交还给模型。模型并不真正执行任何代码,它只是训练成了会在特定格式里表达“我想调用这个工具”的决策体。

这也是为什么它比普通Prompt拼接更高级:普通拼接只是把外部结果当背景资料喂给模型,模型没有“选择动作”的空间;函数调用则把工具选择权交给模型,让它根据对话状态动态决定用哪个、用几次。课程用了一个很朴素的比喻:思维链是模型的大脑,工具调用是模型的手。

1.3 为什么不能只靠“模型越来越大”解决

有人会问,模型能力继续提升,是不是就不需要工具了?这一讲的反驳很直接:再强的语言模型也无法替你去查数据库权限之外的数据,更无法替你按下转账确认按钮。需求的本质是把模型接入真实系统,这个接入口必须有清晰边界,而函数调用正好提供了一层被模型原生支持的边界协议。后面所有安全、稳定、可观测的话题,都是围绕这个边界展开的。

2. 一次函数调用请求的前后半场:模型想,程序做

2.1 前半场:模型如何“说出”调用意图

把一次函数调用拆开看,前半场是模型输出调用意图。当用户说“帮我查一下某城市的天气”,你传给模型的内容里除了用户消息,还有一份当前会话允许使用的工具清单。模型读到这些内容后,会自己判断当前该不该调用工具。

如果它决定调用,返回的消息里会多出一个结构,大致长这样:

{ "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"某城市\", \"unit\": \"celsius\"}" } } ] }

这里有个关键点要提醒刚接触的人:模型没有连接天气服务,也没有执行任何请求,它只是返回了一个包含工具名和参数的结构化文本。真正去查天气的,是你的代码。这一段返回往往和正常文本消息一起出现,有些平台还会在消息里附上“要不要调用工具的思考”,但应用层只需要关注结构化字段即可。

2.2 后半场:程序接住、执行、再把结果交给模型

函数调用的后半场完全由你掌控。读取返回里的工具调用标记,解析工具名和参数,在自己的运行环境里找到对应函数,执行,然后把结果以独立的工具消息写回对话。这个循环用伪代码表达会更直观:

messages = [{"role": "user", "content": user_input}] for _ in range(MAX_ROUNDS): resp = llm.chat(messages, tools=TOOL_DEFS) if not resp.tool_calls: # 模型认为不需要再调用工具,直接输出最终回答 return resp.text for call in resp.tool_calls: result = run_tool(call.function.name, call.function.arguments) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result })

注意两个细节。第一,tool_call_id必须原样带回,平台要拿它把工具结果和对应的调用请求关联起来,丢了或者串了,上下文就乱了。第二,工具结果是作为一条新消息进入对话的,不是简单拼接在上一句后面,这样模型才能清楚区分“用户说了什么”和“工具返回了什么”,进而判断下一步动作。

2.3 这个循环会持续多久

一次请求可能只触发一次工具调用,也可能触发一串:先查订单,再查物流,再发起退款。模型拿到上一轮工具结果后,会继续判断还需要调用哪个工具,或者已经可以生成最终回答。这也是为什么外面那层循环需要轮数上限——没有上限的循环在异常情况下会变成一笔失控账单。课程给了个经验判断:多数业务任务在三四轮内就能收敛,超过六轮还收敛不了,多半是工具选择出了问题,或者是任务本身过于开放。

3. 工具声明这件事,值得当产品需求来写

3.1 工具名和描述是模型最重要的“路标”

不少人第一次接触函数调用,把工具声明当成接口文档随便填一下,结果模型频繁调错工具,就以为是模型能力不行。实际上,工具声明就是喂给模型的“选项菜单”,菜单写得含糊,点菜的人当然容易点错。下面这两种写法,模型调用表现会有肉眼可见的差距:

类别粗糙写法合格写法
工具名get_infoget_weather_by_city
描述获取天气信息根据城市名查询当日天气,适用于用户询问天气、气温、降水、穿衣建议时;不适用于查询历史气候
参数city: stringcity: string,城市标准中文全称,如“某城市”,必填
返回天气信息返回当前温度、天气状况、湿度、风力,面向用户直接展示

工具名最好包含领域和动作,get_info这种名字对模型没有信息量;描述里写清“什么时候用”和“什么时候不要用”,模型才能区分相似工具;字段描述最好带格式示例,能明显减少模型传错参数的现象。

3.2 描述里藏着模型选择的潜规则

我实测下来的经验是:把工具描述当成写给一个没见过你们系统的人的说明书。你告诉它“该工具支持什么输入,返回什么信息,最适合解决哪种问题”,它就能做出更接近专家判断的选择。

还有一个常见误区是描述写得太短,比如“查询天气”,模型会在用户问“今天适合穿什么”时犹豫要不要调它;但如果描述里写了“查询某城市当前气温、天气状况,当用户提到天气、气温、穿衣建议时可调用”,选择就稳定得多。这不是玄学,是因为模型在做选择时,很大程度上依赖描述与用户问题的语义重叠度,描述越贴近真实触发场景,匹配准确率越高。

3.3 参数Schema的工程化:少嵌套、多枚举、必填清晰

参数一般用JSON Schema描述,看着简单,细节却不少。第一,尽量保持扁平结构,嵌套对象是模型最容易出错的地方,模型对深层字段的填充一致性远不如对顶层字段。第二,凡是取值范围有限的字段,都显式给枚举值,比在描述里写“只能填1到5”管用得多。第三,必填参数标清楚,可选参数给出默认值说明。工具函数入口再加一层校验,把模型给的不合法参数挡在业务代码之外。

这一条其实是从“函数调用不稳定”的反馈里反推出来的。很多所谓的不稳定,都是参数约束没设计好导致的,模型不是不会填,是你没告诉它能填什么。把边界条件写清楚,比换更大的模型更有效。

4. 出了课堂才会遇到的三个坑

4.1 并行调用与结果串位:每条返回都要对上id

课堂Demo一般只有一个工具,一次调用,代码随便写写都能跑。但真实场景里,一个回合内模型可能同时发起多个工具调用,尤其是你提供了多个独立查询工具时。比如用户问“对比某两个城市今天的气温和湿度”,模型可能同时返回两个get_weather调用。

这时候如果没有按ID对应回传,对话上下文会彻底乱掉。排查思路很简单:看看工具结果是不是放错了对话位置,检查回传时是否带上了每条调用对应的ID。我一开始就吃过这个亏,把两个工具结果顺序固定按返回顺序回传,但平台上并发的调用ID顺序并不稳定,最后逐条用tool_call_id对应才解决。这里没有什么黑魔法,就是老老实实做一个id -> result的映射。

4.2 工具抛错时,把错误当成结果回传

另一个高频坑:工具函数执行到一半抛出异常,代码直接中断,没有往messages里写任何内容。模型那边等不到工具返回,要么反复发起同一次调用,要么直接编造一个结果。

正确做法是把异常包装成普通的工具结果文本,明确告诉模型这次调用失败了、失败原因是什么。比如天气服务超时,就返回“get_weather调用失败:上游服务超时,请重试或询问用户是否需要其他方式查询”。注意,错误信息要面向模型写得足够清楚,别只丢一个空Exception,模型分不清是参数问题还是网络问题,就会在一个错误参数上反复兜圈子。

4.3 工具结果太长,几轮就把上下文吃穿

数据库查询、日志检索类工具返回结果很容易超出预期,动辄几百行JSON。如果不做处理,两条工具调用就占满了模型上下文,后面所有对话质量断崖式下降。我在自己项目里试过几个办法:只保留前端呈现需要的字段、对长列表做摘要、限制返回条数。

可以专门写一个“结果精简”步骤,把原始结果先归约成适合交给模型的形态,再放进工具消息。比如查询订单明细时,先取订单号、状态、金额、时间这几个字段,而不是把整条数据库记录原样丢给模型。这个步骤不是可选项,只要你的工具接的是真实业务系统,它就一定是必需品。

4.4 循环没有上限,异常时账单失控

最后一个坑和时间、钱有关。工具调用循环如果没有轮数上限,遇到模型在一个错误参数上反复重试时,你的请求就会变成一场无休止的对话。给外层循环设一个合理上限,比如简单任务6轮、复杂任务12轮,到达上限就把当前状态保存下来、结束对话,交给人工或下一步流程处理。

我习惯在每轮循环后打印一行日志,记录当前轮次、刚执行的工具名、返回结果摘要。这样即使真的出现循环失控,也能从日志里回放完整链路,而不是面对一个空空的超时异常。这个习惯成本极低,但救过我很多次。

5. 安全边界与多轮编排:让工具调用从能用变成可靠

5.1 工具是智能体的手,手不能什么都碰

函数调用给了模型触达外部系统的通道,但这个通道必须有边界。工具声明里暴露什么函数,决定了模型能做什么。写智能体的时候,不要一股脑把所有接口都注册成工具,坚持最小权限:只把当前场景必要的能力暴露给模型。

涉及转账、删除、发送消息这类敏感操作,就算注册了工具,也要在应用层加二次确认,不能让模型拿着用户一句话就直接执行不可逆动作。这一点课程里没有展开太多,但工程上必须自己补上。我把所有危险操作都套了一层“需要用户明确确认”的壳,确认前模型只能生成待执行的草稿,确认后才真正调接口。

5.2 多轮调用的编排要把状态掌握在业务层

复杂任务往往需要多次工具调用,模型只是根据上下文不断输出“下一个动作”。业务系统里,建议在模型外层做简单的状态机:记录当前阶段、工具成功还是失败、用户有没有确认。课程里也很强调一点:函数调用只是完成任务的其中一个环节,真正稳定的流程控制必须放在代码里,而不是依赖模型“自由发挥”。

举个例子,一个“查询库存后下单”的流程,如果库存查询返回缺货,模型可能会自作主张改成建议用户购买替代品。这本身没问题,但下单动作必须经过状态机确认:库存是否充足、用户是否同意、价格是否有效。把业务规则写在代码里,模型只负责推动对话和传递结构化意图,这样整个流程才能被审计和兜底。

5.3 可观测性:没有日志的工具调用等于没调

把每一步工具调用都记录下来:工具名、参数、耗时、返回结果状态。我在项目早期没有做这一步,模型行为一变只能靠猜。做了日志之后,很多问题一眼就能定位——是不是描述更改导致选错了工具、是不是某个接口超时让结果变空。

日志不仅能排障,也能沉淀出测试样本,用来做后续的回归验证。我现在的做法是把每条调用日志同步到一个单独的事件表,字段包括session_id、tool_name、arguments、result_status。线上出问题时,先查这张表,基本能覆盖八成以上的“模型怎么突然不听话”类问题。

6. 课程之外:三个让函数调用更好落地的习惯

6.1 维护工具目录,按场景暴露子集

工具清单不是越多越好。我自己的经验是,单个对话里暴露超过二十个工具时,模型选错的概率会明显上升,因为每个工具的description都在跟其他工具抢注意力。更可控的做法是维护一个全量工具目录,再按场景切出子集。

比如客服场景只暴露订单查询和退换货相关工具,运营场景只暴露数据分析相关工具,让模型每次面对的选项尽量少而精准。这个思路和给用户做菜单一样,菜单上百个菜不如一页十个菜容易点对。工具子集放在配置里管理,切换场景时直接换一租即可,不需要改代码逻辑。

6.2 固定回归集,防止“加一个工具坏了三个”

工具定义一旦变化,模型的选择行为会跟着变。维护一组固定的回归样本,里面记录每个测试输入应该调用哪个工具、期望什么参数。每次调整工具声明之后跑一遍,能很快发现“新增工具把老请求抢走了”之类的回归问题。

这类样本我会直接从真实日志里挑,比手工编的场景更能暴露问题。攒上一百条左右基本够用,跑一遍也就几十次请求、几分钱,却能省下大量线上排查的时间。

6.3 一个几乎不花钱的好习惯:在工具入口校验参数

最后分享一个自己的习惯:工具函数入口一定要做参数校验。模型给出的参数只是概率采样产物,格式对不代表业务对,可能城市名不存在、订单号不是你家的,也可能数值超出合理范围。像校验用户表单输入一样去校验模型参数,能让你的工具调用从“大多数时候对”变成“稳定的对”。

校验不过时,直接返回一个清晰的结构化错误给模型,比如“参数city不是已知城市名,请从列表中选择:某城市、某城镇”,模型通常会自动修正参数并再次尝试。这一步加在业务逻辑之前,几行代码的事,却能让整个工具链路的稳定性上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询