大模型函数调用失败的兜底设计
先把结论摆这儿function calling 这东西,默认就当它会出错来设计。模型该返回get_weather(city北京)的地方,它有可能给你来个city北京市朝阳区,或者参数名拼成citys,再或者干脆把 JSON 写崩了。线上跑久了你会发现,失败率没你想的低。我自己那套客服流程,头一周统计下来,函数调用相关的异常占了所有报错的三成多。所以兜底和降级不是锦上添花,是必须先铺好的地基。下面是我踩过坑之后定下来的几层防护,按问题→设计→结论说。问题:到底会错在哪我把线上日志捞出来分了一下类,大模型调函数翻车,无非这么几种,频率从高到低:错误类型长啥样我这边占比参数值不合法枚举传了不存在的值、日期格式乱、city 带了多余后缀~40%JSON 结构坏括号没闭合、多个逗号、字段名拼错~25%幻觉调函数调了一个我压根没注册的工具~15%该调没调明明要查库存,它自己编了个数~12%调用超时/外部 API 挂了函数本身没问题,下游 500 了~8%最阴的是第四种——该调没调。前三种至少会报错,你能 catch 到;这种它静悄悄给你编一个数,用户还信了。我有次测试库存查询,模型直接回还剩 23 件,我后台一看那商品早下架了,根本没触发查询函数。背后一身汗。设计:四层兜底,从轻到重我的原则是,每一层尽量在原地把问题解决掉,实在不行再往下掉一级,最后兜不住才认输给用户一句人话。第一层:参数校验 自动修正。函数入口先用 JSON Schema 卡一遍。能自动修的就别麻烦模型——比如city北京市我直接 strip 掉市字再查;枚举值做个模糊匹配(Levenshtein距离小于 2 就纠到最近的合法值)。这一层能把上面那 40% 的参数错砍掉一大半,而且不用多花一次 token。def coerce_args(raw, schema): fixed {} for key, spec in schema.items(): val raw.get(key) if spec[type] enum and val not in spec[values]: val nearest(val, spec[values]) # 模糊纠正 if spec[type] city: val val.rstrip(市区县) fixed[key] val return fixed第二层:报错回喂,让模型自己改一次。修不了的(比如 JSON 直接崩了),我把校验错误信息原样塞回去,追加一轮对话:你刚才调用xxx时start_date不符合 YYYY-MM-DD 格式,请重新生成。 给它一次机会。注意只给一次,别开 while 循环,我见过有人没设上限,模型俩参数互相纠结,转了七八圈烧了一大把 token。第三层:降级到弱能力路径。重试还不行,就别硬刚函数调用了。要么换个更笨但更稳的提示词模板,把工具调用拆成先让模型只输出意图,我代码去拼参数;要么直接走规则引擎兜底。查天气这种,我备了个纯关键词匹配的兜底分支,虽然蠢,但能用。第四层:认输,但要体面。全挂了,给用户一句这个我暂时查不了,要不你换个说法/稍后再试,同时打点告警。千万别把原始 traceback 甩用户脸上。针对最阴的该调没调,我额外加了个事后核验:凡是涉及真实数据(库存、价格、订单)的回答,过一道正则关键词检测,发现回答里有具体数字但本轮没触发对应函数,直接拦下重走。结论:别让一次错误穿透到用户说实话这套东西我搭了挺久,最费劲的不是写代码,是把那张错误分布表跑出来——你得先知道自己到底错在哪,才知道防哪层。盲目加 retry 是没用的,参数值错的你 retry 八次还是错。还有个小插曲值得说:这套兜底逻辑我最早是硬编码在业务代码里的,改一次提示词要动一堆地方,烦得很。后来我把整个客服智能体挪到了一个零代码搭智能体的平台上重做,拖拽配节点,参数校验、知识库(RAG)挂载、降级分支都在画布上配,改逻辑不用重新发版。一个下午就把原来两周写的流程复刻了七七八八。当然它也不是万能,复杂的自定义纠错逻辑还得我自己写函数补进去,那平台只帮我把编排和兜底框架这些杂活省了——但说真的,省这些杂活已经够香了。第一版搭出来响应也偏慢,优化了下知识库召回才顺。兜底设计的核心就一句:任何一层的错误,都不该原封不动地穿透到用户那一端。每一层都要么解决它,要么优雅地往下一层让。(模型和 API 我直接走的讯飞星辰MaaS,现成调,没自己折腾部署算力)你们做 function calling 兜底,retry 上限一般设几次?评论区聊聊,我好奇大家是不是也被该调没调坑过。

相关新闻