如果你正在做AI原生应用,函数调用的可扩展性迟早会卡你一下。团队刚起步的时候,二三十个函数接口全塞进提示词里,模型也能用得有模有样;可一旦业务线铺开、工具数量涨到上百,你会发现模型开始“花式选错”——明明该查库存的,它去调了订单查询;参数格式也经常报错。我在一个智能助手项目里撞上过这道坎,前前后后排了两周,最后发现问题不在模型弱,而是函数调用这个链路从一开始就没考虑扩展。这篇内容就是那次重构的复盘,核心聊三件事:规模变大之后链路会裂在哪、怎么把候选集从全量换成按需路由、以及执行和运维侧要补什么,适合正在做AI原生应用、或者准备把助手能力铺到多条业务线的开发者参考。
1. 扩展瓶颈不会突然出现:三个先兆信号
很多团队发现函数调用撑不住,往往是通过用户反馈的“模型变笨了”来判断。但真正的根因藏在更底层:不是模型推理能力下降,而是函数调用这条链路的规模上去了,原来被忽略的问题被成倍放大。这里说三个我在项目里真实撞到的信号,你可以对照一下自己的系统。
1.1 信号一:工具从几十个涨到上百个,模型开始频繁选错
函数调用这套机制的设计初衷,是让模型在一个候选集合里选一个合适的函数,并且把参数填对。候选集合小的时候,比如20个工具,模型基本不会认错;但到了80个、100个的时候,每个函数都有name、description、参数schema定义,模型要在这么长的“菜单”里做区分,相似语义的工具就非常容易撞车。“查询订单状态”和“查询退货进度”这种,描述稍微写得不清楚,模型就会靠猜。
我这边观测到的情况是,工具量从20涨到80以后,函数选准率从九成以上掉到八成出头,这还只是模型端“选对函数”的准确率;再叠加参数填写错误,端到端成功率更难看。更麻烦的是维护成本:功能多了以后,不同人写的工具描述风格不统一,有的人写“获取商品信息”,有的人写“查询SKU详情”,看起来是两个函数,模型眼里其实是一团浆糊。
1.2 信号二:函数声明把上下文预算吃掉大半
第二个信号更隐蔽,也更费钱。一个函数声明看起来不大,但把name、description、参数、枚举值、示例一算,平均200到400个token很正常。100个工具就是2万到4万token。可对话历史、检索回来的文档块也要塞进上下文,模型还要留空间输出。
结果就是要么截断历史,要么牺牲检索内容,要么每次请求的token成本翻着跟头涨。更无奈的是,用户问的明明是电商售后,你却把全部两百个工具的定义都带上了,这中间绝大部分都是无效开销。类比一下就是:你进商场只是想买个螺丝刀,结果导购先把整栋楼的商品目录给你背了一遍。
1.3 信号三:一次失败被放大成多轮“补救表演”
第三个信号在链路上。工具调用失败之后,模型通常不会认输,而是会尝试用另一个函数补救,或者把同一个函数再调一次。单看一次交互好像没事,但它把一次失败放大成了两三轮往返:延迟翻倍、token消耗翻倍,用户感知就是“这助手越用越笨”。
如果出错的函数还带副作用——比如已经发出了短信、创建了工单——补救本身可能造成重复操作,这一步的错误成本就远超一次对话了。工具多了以后,这类失败回路出现的频率会显著上升,因为选错工具本身就是最常见的失败源。我见过最离谱的一次,模型为了查物流,先调了下单接口,再调了取消订单接口,一次性演出了一套完整的“错误闭环”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一刀动在候选集:从“全量注入”改成“按需路由”
要解决上面的问题,第一个思路是别把所有工具都摆上台面。与其让模型在一份超长菜单里选菜,不如我们从外围先筛一道。这道工序我建议分两步走:先做硬过滤,再做语义检索。这两步不是二选一,而是先后的关系。
2.1 硬过滤:权限、平台、业务域先把候选集砍一半
硬过滤是最容易做、收益却最被低估的一步。所谓硬过滤,就是用代码逻辑而不是模型判断,把当前请求不可能用到的工具直接挡在外面。典型的过滤条件有三类:一是权限,用户没开通的能力,对应函数根本不出现在候选列表里;二是上下文,用户当前在哪个页面、用的什么设备、处在什么业务域,相关的工具才进来;三是状态,比如订单流程还没进入售后环节,售后相关函数就不该出现。
很多团队忽略这一步,把过滤全交给模型,等于让模型在一个藏满烟雾弹的房间里找路。硬过滤做完,候选集通常能先降30%到50%,而且这种降低是确定性的,不依赖模型发挥,也不存在召回概率问题。
2.2 语义检索:给每个函数写“路由描述”,再按需召回Top-K
剩下的一部分工具没法用规则过滤,得靠检索。做法是给每个函数单独写一段路由描述,重点说清楚“什么时候该用我”,把这段话嵌入成向量存起来。每次请求时,把“用户最新意图加最近上下文”也做嵌入,去向量库里召回最相关的Top-K个函数,再和常驻的通用工具一起送给模型。
这样候选集就从100个降到20个以内,模型的选择压力小很多。常驻工具一般放5到10个最通用、最高频的函数,比如通用搜索、帮助、当前页面的主操作;剩下的全部走检索。
2.3 描述质量决定检索质量,并且不要写否定句式
这个方案里最花工夫的不是选什么向量模型,而是函数的路由描述。描述写得好不好,直接决定召回准不准。我经常看到一种典型错误:在描述里写否定句,比如“不要用于查询天气”。我实测过很多次,模型在模糊场景下反而更容易因为这句话去猜那个函数;检索模型同样会被否定词带偏。
正确做法是正面描述适用场景,把常见的用户问法写进去:“当用户询问今日天气、未来几天预报、降雨概率时使用”。另外,检索的阈值不要设死,当召回分数都偏低时,与其硬返回一堆不相关工具,不如只保留常驻的兜底工具,让模型明确知道自己“无工具可用”。这时候模型通常会主动向用户澄清问题,而不是瞎调用。
2.4 要不要上“分级路由”
在检索之上还有一种更重的做法:先让模型判断“属于哪个业务域”,再去指定域里选工具,也就是两层路由。好处是每一级的候选都很小,坏处是每一级都多一次模型调用,而且分类一旦错了就彻底翻车,连补救的机会都没有。
我的看法是:除非你的业务域边界非常清晰、域间几乎不交叉,否则先用硬过滤加检索就够了,不要为了架构好看上个两级模型调度。延迟和错误的双重放大往往会吃掉架构收益。我们一开始也规划过分级路由,后来发现很多用户请求天然跨域,比如“查一下附近门店的营业时间顺便看看库存”,分级路由对这类请求非常不友好。
3. 执行层的可扩展改造:并行、幂等和返回结果减肥
候选集压下来之后,模型端的选择问题会缓解大半,但执行链路还有三个坑,是工具数量涨上去之后必然遇到的。这一层的问题不像选错函数那么显眼,但每一个都会在工具多的时候变成事故高发区。
3.1 并行函数调用看着香,先看依赖关系再动手
现在不少模型支持一次返回多个函数调用,由应用端并行执行。这对无依赖的工具组合很友好,比如同时查天气和路况。但一旦工具之间存在数据依赖,比如先查订单号、再根据订单号催物流,强行并行就会出问题:第二个函数拿到的可能是第一个函数执行之前的数据。
项目里我会在工具元数据里加一个depends_on字段,用来标注依赖关系。执行框架先跑没有依赖的这批函数,拿到结果再跑第二批,效果上接近串行的可靠、又尽可能并发。千万别指望模型自己替你维护依赖关系,它是真的会忘。我们在早期版本里遇到过,模型同时输出两个调用请求,第二个函数的入参引用的是第一个函数的返回值,但由于并行执行,参数实际是空的。
3.2 重试救场的铁律:先确认函数是否幂等
工具调用的网络超时、上游抖动是家常便饭,重试几乎是标配。但重试前一定要确认一件事:这个函数有没有副作用。查询、计算这类只读函数,重试多少次都没问题;但发短信、创建订单、扣款这些函数,无脑重试会制造重复操作。
处理办法大致有三种:一是给函数规定幂等键,比如请求带上request_id,上游根据它去重;二是重试前先调一个查询接口确认状态;三是把函数按副作用等级打标,READ、WRITE、CRITICAL分级管理,只有READ级别的函数允许自动重试,其他级别一律由上层策略决定。我们犯过的错误就是把所有函数一视同仁地自动重试,结果重复订单让客服团队排查了一个下午。
3.3 返回结果要“瘦身上交”,否则上下文二次爆炸
执行完函数后,工具返回结果会被拼回上下文里继续交给模型。这是整个链路里最容易被忽视的token黑洞。一个搜索接口如果把整页内容返回给你,随随便便就是几十KB,扔给模型不仅费钱,还会把对话历史里真正重要的信息挤出去。
我们做的改造分三层:第一,每个函数的返回结构做成精简的schema,能只回“标题加摘要加链接”的,绝不回正文字;第二,对单个工具结果设置截断上限,比如2000字,超出就截断或做摘要;第三,对特别大的结果用一次专门的摘要函数处理后再回填。这一步做完,整个调用的平均上下文消耗能降一半以上,模型的“记忆连贯性”也明显变好。
4. 规模化之后,运维侧的四件套:指标、回归、降级、版本
工具一多,凭感觉判断“模型有没有变笨”非常危险,得像对待一个线上服务那样对待函数调用。这一节说的四件事,是我们吃了不少亏之后才补齐的,缺一件都容易在深夜被报警电话叫醒。
4.1 四类指标:选准率、参数通过率、执行成功率、端到端恢复成功率
建议每个工具调用都埋点,至少聚合下面四类指标:
| 指标 | 统计口径 | 常见劣化原因 |
|---|---|---|
| 工具选准率 | 实际调用是否等于期望调用的函数 | 描述相似、候选集过大 |
| 参数通过率 | 生成参数是否通过schema校验 | 描述不完整、示例缺失 |
| 执行成功率 | 下游服务是否成功返回 | 超时、上游抖动、权限受限 |
| 端到端恢复成功率 | 失败后是否在后续轮次被纠正 | 缺少降级兜底、模型连续猜错 |
注意选准率是函数调用特有的指标,不能用“对话是否成功”来替代。很多时候模型选错了工具但能自圆其说,用户不说破,你根本不知道,只有对工具的期望调用做标注才能暴露出来。
4.2 每次改动工具描述,都要跑一遍回归
函数调用系统里有一类隐蔽的根因:不是模型坏了,而是有人改了一句工具描述。描述对于模型来说是提示词,对于检索系统来说是索引,改一个词可能就同时改变召回和选择的结果。
我们这边定了一个规矩:凡是涉及工具描述、新增工具、路由阈值、向量模型、大模型版本这几类改动,都必须跑一次离线回归。回归集不用特别大,挑200到500条有代表性的真实问题,标注好期望调用的工具和关键参数,写个脚本批量跑,对比改动前后四类指标。这套流程前期搭建有点费事,但工具过百之后,它能帮你拦住大量肉眼看不出来的劣化。
4.3 降级方案要提前设计,别等翻车再补
扩展性的另一个隐藏含义是“出问题时不至于全线崩掉”。我一直建议在架构里预留降级开关:当监测到模型连续选错工具、或者工具调用成功率低于阈值时,自动把候选集收缩到当前业务域的最小集合;更极端的情况,还可以关闭模型直接调用,改成在界面上给用户展示确定性的功能按钮。
这个兜底路径看起来“不够智能”,但用户体验反而会稳定。用户要的是把事情办成,不是看模型表演猜测。我们在降级模式下把“点击按钮走老流程”作为底线能力,效果出乎意料地好——因为那条路径的每一步都是确定性的,不会产生幻觉。
4.4 函数协议会漂移,校验层要兜住
大模型的函数调用协议在各家实现上大同小异,都会走“声明、解析、执行、回填”这个循环,但参数格式的细节经常变。项目迭代中,函数定义本身也会改。你会遇到一种尴尬的情况:模型生成的参数还是老格式,服务端已经按新schema校验了,然后开始报错。
我们的做法是给每个函数的入参解析加一个兼容层:老字段做映射,多余字段忽略,关键字段缺失时用默认值补齐。别让参数解析成为整个链路里最脆的一环,这个位置出错的排查成本特别高,因为你会先怀疑模型、再怀疑网络,最后才想起是schema版本不匹配。
5. 重构之后,两个反直觉的观察
整个重构做完,有两个结论和我一开始的直觉相反,单独拿出来说一下,希望能帮读者少走弯路。
5.1 候选集不是越小越好
我一开始直觉认为工具候选集越小越好,最好只给5个。后来发现这是个误区。候选集太小,模型面对的选项不够,它会硬往现有工具上凑,反而出现更多牵强调用。我这边实验下来,常见的中大规模函数库下,召回Top-K设置在12到20之间是比较稳的区间,既能给模型足够的选择空间,又不会让它迷失在长菜单里。
K的最优值跟模型能力和函数描述质量强相关,建议你在自己的数据上做一次小批量对比,不要照抄别人的数值。我们内部每换一次模型,都会把K重新扫一遍,这属于性价比很高的调优动作。
5.2 函数选不准,很多时候是“描述设计”的问题,不是模型的问题
当你发现模型老是选错函数,第一反应别是换更大的模型。绝大多数情况下,把两个易混淆函数的描述重写一遍,让它们的适用场景边界更清楚,效果比换模型明显得多。
我们在一次排查里发现,“创建订单”和“预下单”两个函数被模型混用,改完描述之后错误率直接掉了四成。先检查路由描述、再调K、再考虑换模型,这个顺序基本不会错。换模型是最后的手段,而且换完之后大概率还要回来改描述,不如一开始就在描述上花功夫。
最后再分享一条踩出来的经验:不要等工具超过50个才考虑扩展性,那时候路由、执行、观测要同时改,还要安抚线上效果,改动成本是最高的。如果你现在工具量还在两位数,优先把两件事做掉:一是给每个函数写出高质量的路由描述,二是把选准率、参数通过率这些埋点加上。等工具涨到三位数,你会感谢当初这两个决定。函数调用本质上是在帮模型降低自由发挥的空间,扩展性设计的全部目标,就是让模型在更多工具面前依然选得准、跑得稳、错了能回来。
