ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从规则匹配到Function Call:AI智能体意图识别升级实践复盘

从规则匹配到Function Call:AI智能体意图识别升级实践复盘 做这件事的起因其实挺现实的我们团队维护一个电商导购类的AI智能体最初用的是纯规则匹配方案。上线半年规则从200条膨胀到1000多条准确率却一路下滑。当时我拿一周的真实用户日志做了一次评测结果只有51.3%的意图识别准确率——也就是说将近一半的用户问题被系统错误理解或者直接打回抱歉我不太理解。后来我们决定把那套规则系统整个推倒换成了Function Call方案。这个决定不是拍脑袋做出来的中间对比了微调、传统NLU、Function Call三条技术路线又跑了一个多月的小规模验证最后才在灰度中逐步替换。最终在同一个评测集上新方案准确率达到95.4%相对提升86%。这篇文章就是把整个选型、验证、迁移、踩坑的过程完整记录下来。1. 规则匹配的天花板半年从200条正则膨胀到1000条想理解我们为什么要动这么大一刀得先看看旧系统是怎么一步步走到崩溃的。这中间有选型时的合理性也有规则系统天然的结构性问题。1.1 当初选规则匹配的正确理由项目刚启动时团队对AI智能体的定位很明确先把查价格、查库存、查订单、领优惠券这几个高频动作做好。当时市面上能调用的模型API还不像现在这么普及我们用规则匹配其实是当时最优的选择。理由很直接对话场景固定高频意图不超过10个每个意图用几条正则就能覆盖。比如XX多少钱XX价格XX售价归到价格查询写个正则就能搞定。规则匹配的延迟在10毫秒以内成本几乎为零逻辑完全透明出问题了一行正则就能改。对一个早期项目来说这种可控性比什么都重要。另一个隐性原因是团队当时没有专门的算法工程师后端同学写正则的水平都很熟练。规则匹配的上手门槛低到可以忽略不计任何一个会写代码的人都能快速贡献规则这在项目起步阶段极大降低了协作成本。1.2 崩盘前夜那些规则处理不了的真实用户表达系统上线后真实用户的表达方式远比我们最初设想的丰富。举几个后来评测日志里反复出现的例子。用户问你们家有没有那种戴在手腕上的能测心跳还能收微信消息的东西——这句话里没有任何一个关键词能直接命中智能手表这个类目。我们规则里写的是手表智能手表运动手表根本没有手腕测心跳收微信消息这种描述性表达。再比如那个最大屏的水果手机最新的存256G的现在啥价格——关键词水果可能命中手机品牌但最大屏和最新的需要结合商品库数据才能解析正则对这种模糊描述完全无能为力。还有大量用户会带着错别字和口语化缩写来提问苹过15 pro max 256 几多钱好几这类方言词也频繁出现。我们为了覆盖这些变体只能不断加同义词表、加替换规则。每加一条规则只能覆盖一小撮新表达但规则之间的优先级冲突和维护复杂度在指数级上升。1.3 一千条规则的真实维护成本半年后代码仓库里的规则文件已经有1000多条。你以为规则多覆盖就一定广实际上恰恰相反。规则之间存在大量隐性冲突。比如有一条规则把手表映射到手表类目后来为了支持Apple Watch又加了一条更具体的规则。但用户问买手表还是买手环好时两条规则同时触发系统按加载顺序随机选一条执行结果有时展示手表有时展示手环。更麻烦的是规则维护对业务变动的响应速度极慢。电商平台每月都有新品类、新玩法比如新增了以旧换新业务需要在所有相关规则里同步加关键词。这些改动分散在1000多条正则里没人敢保证不漏。每次大促前我们都要花两三天回归测试纯粹靠人肉排查。如果只是维护成本高也就罢了真正让我下定决心重构的是那次评测拿出的数据。1.4 用真实日志评测准确率只有51.3%那次评测的方法其实很简单从最近一周的生产日志里随机抽了1000条用户消息由两名产品同学独立标注每条消息对应的正确意图标注不一致的讨论到一致为止。然后拿旧系统对这1000条消息跑一遍系统返回内容的意图与标注一致就算正确。结果非常残酷51.3%。抽中的1000条消息里有相当一部分是闲聊、无意义消息、超长复合问题。旧系统要么识别成错误意图要么直接落入兜底话术。这意味着将近一半的用户在第一轮对话就得不到有效响应——对一个导购智能体来说基本上等于把用户往外推。有了这个数据做基准重构这件事才真正被提上日程。后续所有方案对比、技术选型都是以这个评测集和这套标注口径为基准来评估的。2. 为什么不是微调也不是NLU偏偏是Function Call确定要重构之后我们面临三条技术路线传统NLU意图分类加槽位抽取、微调模型、Function Call。三选一的过程说白了就是不断做减法。2.1 Function Call的工作机制一句话讲透先用最简单的方式解释Function Call。我们不再维护任何正则或分类器而是把系统里能提供的功能列成一份工具清单每个工具包含名称、功能说明、参数定义JSON Schema。模型收到用户消息后会自己判断应该调用哪个工具然后以结构化JSON的形式返回我想调用这个函数参数是这些。比如用户说iPhone 15 Pro Max 256GB多少钱模型可能返回{ function: query_product_price, arguments: { product_name: iPhone 15 Pro Max 256GB } }后端拿到这个JSON执行真正的价格查询把结果回传给模型模型再组织成自然语言回复用户。本质上是把意图识别和参数抽取这两件事从传统的多模块管线压缩进了模型的一次生成过程中。模型同时决定了用户想干什么和这件事需要哪些参数。2.2 微调为什么被排除先聊微调。当时市面上确实有模型开放了微调能力我们也动过这个心思。但深入评估后发现微调要解决的问题和我们的问题并不完全匹配。微调适合让模型学会一种特定的输出格式或表达风格比如让它总是用JSON回复。但我们要的是根据用户五花八门的表达正确映射到某个函数并抽出参数这件事模型的基础能力已经具备缺的不是知识而是对当前业务场景和工具语义的理解。微调还需要准备大规模高质量训练数据。我们当时连1000条标注数据都还没有要做微调至少要上万条覆盖各种函数组合、参数边界、异常情况的样本标注成本和数据清洗成本都是硬伤。而且业务每周都可能新增工具每次新增都要重新走一遍数据准备、微调、评测、上线的流程迭代周期以周为单位完全跟不上业务节奏。还有一个现实问题微调后模型的通用能力会受影响尤其是中文口语理解和常识推理能力。这对导购场景是致命的因为用户问题里充满了口语和隐含语义通用能力必须有。综合下来微调方案被排除不是因为做不到而是因为成本收益比太差。2.3 传统NLU管线为什么被排除传统NLU方案是另一条看起来更正规的路线先做一个意图分类模型再做一个槽位抽取模型两个模型串联输出结果进入对话管理模块由对话管理决定调用哪个业务API。这套架构在工业界非常成熟理论上比规则匹配稳定得多。但落地时有一个绕不开的问题需要同时维护两个模型的训练、评测、部署和版本迭代。意图分类要标注数据槽位抽取要标注数据两个任务的标注规范还不一样。当时团队没有人有完整的NLU工程经验贸然引入只会制造更大的运维黑洞。更关键的是NLU管线是分阶段处理的意图错了后面的槽位抽取和对话管理全部跟着错。错误在链路里会累积放大。我记得当时产品同学提出一个很尖锐的问题如果意图分对了但槽位抽错了用户会怎么骂我们——用户只关心最终结果不关心你哪一步出了问题。传统NLU并不是不好但对当时的我们来说太重了。我们需要的是一条让业务能在几天内接入新工具、效果可快速验证的轻量路线。2.4 Function Call刚好卡在理解和执行之间Function Call方案最吸引我的地方是它把理解用户意图和映射到业务工具合并成了同一个任务。模型天生具备强大的语义理解能力我们不需要再单独训练一个意图分类器函数定义本身承担了意图列表的作用新增一个工具就是在工具清单里加一个JSON定义不用重新训练任何模型。举个直观的例子用户说我上周买的那双鞋能退吗规则系统要命中退货意图需要一堆同义词NLU系统需要标注大量退换货样本来训练分类器。但Function Call方案只需要在售后函数里写清楚当用户询问退货、退款、换货、售后问题时调用本函数模型凭借已有的语言理解能力就能准确判断。这等于把传统NLU系统中需要数据积累才能实现的能力用一段描述就解决了。当然这不是说Function Call万能它的上限取决于模型本身的推理能力。但对我们这种业务复杂度中等、工具数量可控的导购场景来说它确实是性价比最高的路径。3. 定方案前的技术验证小成本跑通最小Demo方案选型不能只看PPT得跑真实数据。我们在拍板之前做了一轮最小成本的验证就是为了回答三个问题市面上哪些模型对Function Call支持得好在真实用户表达下效果到底行不行成本延迟能不能接受3.1 选型前先定评测口径前期评测如果口径不统一后面拿到的所有对比数据都是废的。我们当时定了两条原则。第一评测用例必须来自真实线上日志不能开发自己编。自己编的用例一定会无意识避开自己的知识盲区而真实用户的表达才是系统要面对的真实挑战。我们从之前那1000条标注数据里按意图分布重新抽了200条覆盖价格查询、库存查询、商品搜索、推荐、售后、闲聊、模糊表达等类型作为候选模型的评测集。第二每个用例要跑多次。模型有随机性同一个问题可能这次返回正确函数、下次就返回另一个。我们当时每个用例跑3次只要3次中有2次能正确返回函数和参数就算这个用例通过。这个口径避免了单次结果带来的偶然性。3.2 三个候选模型的实测对比我们锁定了三个候选模型做对比测试分别来自不同厂商的中大杯型号。当时入口条件是API能用、支持Function Calling、上下文长度至少8K。测试方式完全模拟线上运行流程把同一段System Prompt、同一组函数定义、同一个用户问题发给模型解析返回结果判断函数选择和参数抽取是否正确。评测结果让我印象很深模型函数选择准确率参数抽取准确率平均延迟秒备注模型A88.5%82.0%1.2函数较多时偶尔选错模型B83.0%79.5%2.8延迟偏高中文表达偏生硬模型C90.5%87.0%0.8综合表现最好模型A的优势是生态成熟但函数一多就开始犯迷糊。模型B的响应速度本身就慢用户等3秒以上基本就要流失直接出局。模型C在准确率和延迟两个指标上都领先最终成为我们的首选。3.3 二十个真实问题验证的第一印象正式大评测之前我先拿20个线上真实问题做了冒烟测试。这20个问题覆盖了那段时间系统兜底率最高的一些表达比如有没有便宜点的跑步耳机你们这个能送人吗我上次看到的东西还在不在。第一印象非常直观模型对口语化表达的容错远超市里任何正则规则。上面那些问题几乎没有一个是旧规则能正面识别的但Function Call方案基本都能映射到正确的函数。当时有一个问题印象最深你们那个手表能测血压不——正确动作其实是查询商品参数模型准确调用了商品详情函数并传入了血压监测作为参数关键词。这种理解能力是规则系统完全不具备的。也正是这组冒烟测试结果让团队里原本对重构持怀疑态度的同学彻底转变了立场。3.4 86%的相对提升是怎么算出来的最后回到大家最关心的那个数字86%的相对提升到底怎么来的。旧规则系统在1000条标注评测集上的准确率是51.3%Function Call方案在同一评测集上的准确率是95.4%。相对提升的计算方式是(95.4% - 51.3%) / 51.3% 86.0%准确率的概念要保持一致系统返回内容对应的业务意图与人工标注一致才算正确。同一个模型在同样200条小评测集上的函数选择准确率是90.5%到了1000条大评测集变成95.4%。这个差异主要来自大评测集中包含更多简单明确的问题模型表现自然会更好。这里想提醒一句网上经常看到各种提升数字但如果不说明提升是绝对值还是相对值很容易误导人。相对提升86%听起来很猛实际是提升了44.1个百分点。两种说法都对但含义完全不同写复盘的时候一定要讲清楚口径。4. 迁移落地从规则库到JSON Schema的关键动作方案验证通过后真正的工程改造才刚开始。把一个运行了半年的规则系统安全替换成Function Call方案不是写几个函数定义就完事的。这里记录几个关键动作都是我们在实践中一点点摸索出来的。4.1 意图盘点把1000条规则收敛成9个函数第一步不是写代码而是做减法。我们把1000条规则里涉及的意图全部列出来按用户真实需求合并同类项。比如价格查询优惠价查询活动价查询到手价查询本质上都是查价格合并成一个query_product_price函数。库存查询有没有货还补货吗合并成check_stock。对比两款手机帮我看看A和B哪个好合并成compare_products。最终收敛出9个函数search_product商品搜索、query_product_price查价格、check_stock查库存、compare_products商品对比、recommend_by_scene场景推荐、query_order查订单、create_order下单、after_sale售后、transfer_human转人工。每个函数都对应着后端已经存在的查询或操作能力。这一步的目的是让函数数量和业务能力一一对应不搞重复定义。9个函数对模型来说是很舒服的规模既能覆盖业务需求又不至于让模型在几十个函数里挑花眼。4.2 函数参数的语义学description写不好模型就乱来函数定义里最容易被低估的是参数的description。一开始我们写得特别随意比如在query_product_price的参数里写product_name: 商品名称。结果模型经常把用户的模糊描述直接塞进来比如便宜的手机那个能测心跳的后端拿到这种参数根本查不了库。后来我们对所有参数的description做了细化规则只有一条把什么样的值可以填进来说清楚。改进后的定义长这样{ name: query_product_price, description: 查询商品当前售价。适用于用户询问价格、多少钱、贵不贵、活动价等场景。, parameters: { type: object, properties: { product_name: { type: string, description: 商品名称、型号或用户口头描述的商品信息例如iPhone 15 Pro Max 256G、那个最大屏的水果手机。若用户描述过于模糊无法对应具体商品填入用户的原话由后端做模糊匹配。 }, sku_id: { type: string, description: 用户直接给出SKU编号时填写例如SKU-2024-0001。若用户未提供不要填写。 } }, required: [product_name] } }注意两个细节。第一required里只填了product_name因为用户可能只给模糊描述我们要让模型一定有东西可传传错了后端还能兜底。第二description里明确写了若用户描述过于模糊无法对应具体商品填入用户的原话这句话相当于给模型留了一个合法的退路避免它编造不存在的信息。4.3 System Prompt里的工具使用说明书函数定义之外System Prompt也承担了一部分使用说明书的职责。我们遇到的问题是模型偶尔会在不该调用函数的时候乱调用。比如用户只是闲聊今天天气不错模型可能因为某个描述有推荐两个字就调用了recommend_by_scene。还有用户问你们一般多久发货正确动作是查售后规则或直接回答但模型有时会去调query_order。我们的解法是在System Prompt里加了一段工具使用边界说明你是一个电商导购助手。你有以下工具可供调用search_product、query_product_price、check_stock、compare_products、recommend_by_scene、query_order、create_order、after_sale、transfer_human。使用原则仅当用户明确表达购物相关意图时才调用工具。若用户只是闲聊、问好、表达情绪不调用任何工具直接友好回复即可。若用户需求不在任何工具能力范围内调用transfer_human转人工。若用户需求对应的工具能力不明确优先通过追问澄清不要强行调用。这段说明看着简单但对模型的行为约束非常明显。函数定义让模型知道有什么工具Prompt里的使用原则让模型知道什么时候不该用工具。两者缺一不可。4.4 执行层改造旧查询逻辑不要动迁移过程中最大的架构教训是核心查询逻辑千万别在功能切换时顺手重写。我们最初的想法是借这次升级把商品查询逻辑也优化一遍后来被资深同事拦住了一次只改一件事要么换交互方案要么改业务逻辑两个一起改出了问题都不知道查哪里。最终执行层的改造只做了一件事写一个适配层把模型返回的JSON参数转成后端查询函数需要的参数格式。比如模型返回{product_name: iPhone 15 Pro Max 256G}适配层负责把256G标准化成256GB再调用老的价格查询函数。这样做的好处是新方案上线后即使出了问题排查范围也被限制在适配层和函数定义里不会牵扯到复杂的业务逻辑。灰度期间的几次小问题都是几分钟内就定位修复了。5. 压测和灰度中踩过的五个坑任何技术方案在灰度阶段都会暴露出测试环境发现不了的问题。我们这轮迁移踩的坑不少挑五个最典型的记录下来给同样在做Function Call迁移的人一个参考。5.1 坑一参数描述太泛模型开始自创商品名灰度第一天就发现一个问题用户问有没有适合跑步戴的耳机模型调用了search_product参数传的是跑步耳机。听起来没毛病但后端商品库里的类目叫运动耳机不叫跑步耳机结果返回空。问题根因是我们描述里写了若用户描述模糊填入用户原话但没告诉模型系统可支持的类目范围。后来在search_product的参数描述里加了一句商品类目仅支持手机、电脑、耳机、运动耳机、智能手表、手环、音箱、电视、冰箱、洗衣机。若用户描述不属于以上类目调用transfer_human。加了这句话之后模型会先判断用户说的是不是我们能卖的再决定调用哪个函数。这类描述直接影响模型参数抽取的上限值得花时间打磨。5.2 坑二函数一多模型反而不会选工具内部联调时我们发现当函数数量从5个增加到9个之后模型在某些边界问题上开始选择困难。典型表现是用户问这手机现在买划算吗模型有时调用query_product_price有时调用recommend_by_scene还有极少数情况会同时返回两个函数调用。后来逐个分析失败用例发现是函数描述之间出现了语义重叠模型不知道怎么严格区分。比如recommend_by_scene的描述里写了根据用户场景推荐合适的商品而query_product_price的描述里写了查询商品当前售价。用户问现在买划算吗确实既涉及价格又涉及时机判断。解法是给每个函数加了何时使用和何时不使用两段说明。query_product_price的不使用场景明确写当用户表达的是现在买是否划算这类综合决策需求时不要使用本函数应使用recommend_by_scene。语义边界清晰之后选择准确率明显回升。5.3 坑三忘记调temperature输出像在抽卡这算是最低级但也最容易踩的坑。模型API默认的temperature是0.7这个值用于文本生成很合适但用在Function Call任务上就是灾难。同样的用户问题同一个模型10次调用能给出两种不同的函数选择。有一次测试时用户说我要退货模型前5次都正确调用了after_sale第6次突然调用了transfer_human。排查了半天最后发现是temperature在捣乱。函数调用本质上是分类决策任务决策需要的是确定性不是随机性。我们后来把temperature统一调低到0.1稳定性立刻上来了。如果你也在做Function Call相关应用第一件事就是确认temperature设置。5.4 坑四评测集是开发自己编的指标虚高上线前的内测里我们新增了一批开发同学自己写的用例跑出的函数选择准确率一度达到97%当时还挺高兴的。结果拿这批用例和真实线上日志对比发现一个扎心的事实开发编的用例都是语法完整、意图明确、格式标准的标准问而真实用户问题里夹杂着错别字、省略主语、中途换话题、甚至发一串表情包。我们拿开发自编用例和真实日志用例分别评测准确率差了将近15个百分点。真实问题比编的用例难得多。这个教训让我定了一条规矩任何评测集都必须从真实流量中抽样标注完成后永久锁定不允许开发同学往里面加友好用例。只有这样才能保证评测结果反映真实水平。5.5 坑五兜底逻辑拍脑袋用户被反复听不懂规则系统时代的兜底是一句固定的抱歉我不太理解请换个说法。换成Function Call方案后我们原以为模型理解能力强兜底率会自然下降。结果灰度后发现兜底率确实降了但一部分兜底场景让人哭笑不得比如用户连续两次问类似问题模型两次都调用了transfer_human。后来专门分析兜底日志发现原因是模型在不确定时倾向于转人工有时用户只是换了个表达方式重问一遍模型不了解上文还是当成新问题处理再次选择转人工。用户会觉得自己被敷衍了。我们的解决办法是加了一层会话级别的重新判断如果一个会话里transfer_human被连续调用两次系统自动改成继续追问澄清让用户换一种更具体的说法给模型第二次机会。这个小改动让兜底后的用户留存率提高了不少。这个坑的核心教训是不要以为模型推荐了什么就一定是对的兜底策略需要结合产品体验做专门的流程设计。6. 上线后的真实数据与长期稳定性保障灰度两周后我们逐步把流量从10%提升到50%再到全量。这期间记录了一些关键数据也搭建了一套保障长期稳定性的机制。6.1 效果数据准确率、兜底率、对话轮次上线四周后的统计结果比评测数据更能说明问题。因为评测集始终是那1000条固定样本但线上流量是持续变化的能反映系统在真实场景下的长期表现。指标规则匹配时期Function Call时期变化意图识别准确率51.3%95.4%提升44.1个百分点兜底率无法理解而转人工38.2%6.8%下降31.4个百分点平均解决轮次2.7轮1.6轮减少1.1轮用户满意度好评率82%91.3%提升9.3个百分点最直观的变化是用户不再需要反复表达自己的需求了。以前问那个手表多少钱要经历哪款手表什么颜色的多大尺寸三连追问现在模型能直接理解用户指的是哪款甚至能从用户的上下文判断出具体配置。这里的准确率95.4%和评测时的数据基本一致说明模型效果在不同时间段的稳定性是可信的。6.2 延迟和成本账单值不值一目了然规则匹配的延迟是10毫秒成本几乎为零。换成Function Call方案后单次调用的平均延迟是0.8秒左右P95在1.5秒以内——用户感知上会有轻微等待但完全在可接受范围内。成本方面每一次完整的对话含工具调用和结果组织大概消耗1000到2000个token换算成人民币大约是每次0.01到0.03元。按我们当时的日活和人均对话轮次一天的模型调用成本几百元。这个成本换来的准确率提升在业务上完全划算。如果你也要做类似选型建议算一笔账旧方案因为识别错误流失的用户价值和每天几百块的模型调用成本哪个更大。这比纯粹讨论技术方案的优劣更有说服力。6.3 灰度发布与回滚先10%再50%再全量整个上线过程分了四个阶段。第一阶段是10%流量灰度跑满7天主要观测兜底率是否有异常波动。第二阶段扩大到50%对比新旧两个方案在同一时段的效果数据。第三阶段全量切换保留旧规则系统在后台随时可以回滚。第四阶段观察两周确认稳定后旧系统才真正下线。灰度期间有一次差点回滚。全量切换后第一天兜底率突然从6.8%跳到了11%。排查后发现是触发了部分模型服务端的限流导致一部分请求在模型那边直接超时系统走了兜底。后来加了重试机制和超时降级方案兜底率才回到正常水平。这次事件证明了一点Function Call方案的稳定性不只在模型本身还依赖模型服务的可用性。如果贵司对SLA要求很高建议提前接好降级方案最低限度也要保证用户问题能被友好接住而不是直接报错。6.4 日志与回放机制模型出错不能只靠猜模型应用最大的问题是不确定性。同样的输入今天的输出和明天可能不一样。为了在出问题时能快速定位我们建了一套完整的日志系统。每一次模型调用都会记录用户原文、函数定义版本、System Prompt版本、模型返回的原始JSON、适配层处理结果、后端返回结果、最终回复给用户的话术。所有记录汇总到日志平台支持按用户ID或会话ID一键回放整个链路。这套机制在后续的无数次问题排查中帮了大忙。比如用户投诉我明明问的是这个你回复的是那个我们只需要按会话ID查日志就能看到模型当时返回了什么函数、参数是什么、后端又执行了什么。定位时间从以前的小时级别缩短到分钟级别。这里给一个建议函数定义和Prompt都要做版本管理发布新版本要在日志里记录版本号。别问我为什么——当你发现上线三天后效果变差了结果排查了半天才发现是有人偷偷改了一行函数描述那种感觉真的很崩溃。回看整条技术选型之路最大的收获其实不是那86%的准确率提升而是我们找到了一条可持续演进的路线。规则系统是一条死胡同越走越窄Function Call是一条快速路业务的每一次新增都只是加一份JSON定义。如果你也在做智能体升级我的建议是先不要铺开所有场景挑两三个用户流量最大、规则匹配最吃力的场景把从评测、验证、迁移、灰度的整条链路跑通用真实数据说服自己和团队再逐步扩大替换范围。旧规则系统也不是敌人在很长一段时间里它都是我用来校验新方案结果的参照系。
返回列表