“这周必须把规则匹配换成大模型方案,准确率再上不去,项目就黄了。”这是我上一个项目里,业务负责人拍桌子说的话。当时我们做的AI智能体是面向电商客服场景的商品推荐助手,底层用的是一套积累了两年多的规则匹配引擎:意图模板、槽位正则、关键词映射、黑名单词表,一套组合拳下来,单轮推荐的准确率卡在82%左右。听起来好像还能用,但真实业务里,用户一句话能带出三四个意图、七八种槽位排列,规则匹配根本切不动。后来我们做了技术选型调研,最终决定把核心链路迁移到Function Call(函数调用)方案上。整个过程很折腾,但效果是实打实的:准确率从86%左右的线上口径,提升到了接近95%,相对涨幅大约86%。准确率提升86%是相对值,不是绝对值,后面我会用数据说明计算口径。
这篇文章我不想写什么高大上的概念科普,就老老实实把这次AI智能体升级的完整过程拆开:为什么规则匹配撑不住了、备选方案有哪些、为什么选Function Call、架构怎么改、哪些细节决定成败、踩了哪些坑。适合正在做AI应用落地、想把LLM接进业务系统的团队参考,也适合那些在“规则匹配和大模型之间反复横跳”的开发者先看完再做决定。
1. 为什么规则匹配撑不住了:三个真实业务场景的“死法”
1.1 旧方案架构回顾:它曾经也是对的
两张图就能讲清楚旧链路的工作方式,但这里没有流程图,我直接用代码思路讲。第一批上线的规则匹配Agent是这样的:
intent_rules = { "商品咨询": ["价格", "多少钱", "怎么卖"], "售后处理": ["退", "换", "修", "退款"], "物流查询": ["到哪了", "快递", "物流", "发货"], } slot_patterns = { "product": r"(iPhone|小米|华为|耳机|充电器|...)", "price_range": r"(\d+)[-—~至](\d+)元", "sku_color": r"(黑色|白色|蓝色|红色|...)", }用户进来一句话,先做意图匹配(关键词命中),然后做槽位抽取(正则提取),最后根据意图+槽位组合,走对应的推荐逻辑。这套架构在意图数量少、表达方式相对固定的时候非常好使,开发快、可解释性强、上线即用。我们第一版冷启动就是靠它撑住了一个月大约5万次的推荐调用,那时候准确率也能达到85%左右。
问题不在架构本身,而在业务的增长速度远超规则的膨胀速度。开始接入的只有3个类目,大约200个SKU;半年后扩展到12个类目、3400个SKU,同时支持售前咨询、售后判断、促销推荐、比价辅助四个主流程。规则阈值一旦拉高,漏召回率飙升;拉低,误匹配率爆炸。
1.2 真实业务场景A:一句话里叠加了多个意图和槽位
用户原话:“你们这个黑色iPhone 15 Pro 256G,如果我用旧的小米11置换,然后分12期,最终到手多少钱?”
规则匹配的做法是先分词、再逐条命中。看起来“黑色”“iPhone 15 Pro 256G”都能用正则抽出来,“12期”也能命中分期模板。但“旧的小米11置换”这个信息在旧模板里不存在,系统先是把它当成了背景噪音丢掉,结果算出来的到手价少算了一笔旧机抵扣,用户实际下单时发现金额对不上,直接投诉。
规则匹配最大的问题就是它的“特征工程天花板”——能抽出的槽位数量,取决于你提前定义了多少个正则。你不可能穷举用户所有的表达方式,但用户会用无穷无尽的方式来提出需求。我当时统计过一个星期内的未命中日志,大约有23%的会话,因为“表达不在规则内”导致推荐结果出现明显偏差。
1.3 真实业务场景B:上下文依赖、指代消解,规则永远追不上
再来一个场景:“帮我看看那款降噪耳机,就是昨天你们直播里那个,比现在这个便宜200的,有没有白色的?”
这句话里有几个难点:第一,用户没指名SKU;第二,指代了“昨天直播”这个临时上下文;第三,还有比价需求。旧链路完全无法处理,因为我们根本没有设计对话记忆模块,规则引擎不会记得昨天的会话,更不可能理解“那个”“这个”的指代关系。结果系统直接返回了默认爆款耳机列表,用户觉得被无视,会话评分掉到1星。
这个场景还不能用“多写几条规则”解决,因为指代对象是动态的,千人千面。每多一个上下文来源,规则组合爆炸的复杂度就翻一倍。我们当时估算过,如果要把直播上下文、历史会话、当前会话的指代关系统统纳入规则体系,至少需要新增600多条规则,而且每两周要维护一轮。这人力成本是团队根本承受不起的。
1.4 三个扛不住之后的“数据体检”
为了说服团队和技术委员会启动重构,我做了一次完整的数据体检。从线上日志里随机抽了500条推荐会话,逐条人工标注推荐结果是否正确,口径是“用户是否点击/点击后是否产生咨询”,同时人工复核误判原因。
结果很有意思,规则匹配的错误来源分布大致是:
| 错误类型 | 占比 | 典型案例 |
|---|---|---|
| 槽位抽取失败或抽错 | 36% | “帮我找300块左右的充电器”抽成“300充电器” |
| 意图误判(多意图只命中一个) | 27% | “能便宜点吗,顺便问问有没有银色”只识别了比价意图 |
| 上下文缺失/指代不明 | 22% | “那款降噪的”没有任何SKU信息 |
| 知识更新滞后 | 10% | 新上架SKU没进规则表 |
| 其他 | 5% | 规则冲突、正则误伤 |
这组数据给了我们两个很重要的决策依据。第一,单纯优化规则只能解决“槽位抽取”和“意图误判”中的一部分,指代和上下文问题无解,必须换底层方案。第二,规则匹配的准确率很可能是逐年下降的,因为商品数量、用户表达复杂度都在涨,准确率是被“稀释”的。
2. 技术选型全记录:为什么最终是Function Call
2.1 备选方案盘点:五个方向都试过,说点真实感受
既然决定重构,第一步就是做方案选型。从当时的市场成熟度和团队技术栈出发,我们认真讨论过下面几个方向。
方向A:微调意图分类模型 + 序列标注抽取
思路是把“意图识别”和“槽位填充”改成两个NLP模型任务:意图用BERT分类模型,槽位用BERT+CRF序列标注。这方案听起来中规中矩,能部分解决1.1和1.2的问题,但指代消解和上下文记忆它依然做不了,因为这些本质上是对话管理的问题,不是两个独立分类任务能覆盖的。当时我们用了开源的中文BERT模型在3000条标注语料上做了个快速实验,单意图分类的F1确实能做到89%,但一旦遇到多意图、套嵌槽位,效果立刻回退到84%以下。而且训练数据维护成本会长期压在人身上,业务规则一变,语料就要重新标一轮。只能说这方案适合“意图单一、槽位固定”的场景,不适合我们这个高动态推荐场景。
方向B:Few-shot Prompt + 大模型做端到端解析
这个方案简单粗暴:不训练任何模型,把所有用户问题拼进Prompt里,让大模型直接返回JSON格式的结构化结果,例如{"intent": "商品咨询", "slots": {...}}。
我们当时试了,效果在单轮场景里很惊艳,意图和槽位抽取的准确率直接到了93%以上。但很快就发现它在一些“需要实时数据和工具调用”的场景里无从下手:用户问“这个手机现在还有货吗”,Prompt模型只能根据训练数据里的印象回答,它不知道真实库存。如果强行让它编,就会出现幻觉,而且没有任何机制去触发库存查询动作。它适合做信息抽取,但不适合做“需要执行动作的Agent”。
方向C:Agent + Function Call(最终选择)
这是当时OpenAI带火的范式,也是我们最终选定的方向。核心思想很简单:大模型不直接输出最终答案,而是先输出“需要调用哪个函数、传什么参数”,比如get_product_detail(product_name="iPhone 15 Pro", color="黑色"),然后由代码去真实的商品系统里查询,查到结果后再把结果回填给大模型,最终生成面向用户的推荐话术。
这个方案能同时解决1.1、1.2、1.3里的绝大部分问题:意图识别不用靠规则模板,模型自己理解;槽位抽取不用靠正则,模型自己生成参数;上下文记忆由调用循环承载;工具能力可以无限扩展,加一个函数就是加一种能力,库存查询、价格批量查询、优惠券计算、直播上下文查询都可以做成函数。
方向D:外部NLU平台
也调研过一些第三方NLU平台,能提供意图识别和槽位抽取API,但问题是:第一,数据要出域,很多电商客服数据是敏感的;第二,它的自定义槽位语法和我们的业务系统对接成本高;第三,厂商封装的“泛化能力”在垂直商品领域并没有优势。最后结论是不如自己的大模型方案可控。
方向E:混合路由(规则匹配兜底 + 大模型主链路)
这方案我们后来确实在线上用了,但不是作为独立的竞争方案,而是作为Function Call方案的一个降级模块。选型时的核心矛盾在于“大模型不是100%可用”,总有超时、幻觉、限流情况,这时候规则匹配作为兜底,至少能保证基础服务不挂。所以我的建议是:不要在大模型和规则之间二选一,而是把规则引擎降级为“最后一道防线”。
2.2 Function Call到底和普通Prompt调用差在哪
很多人觉得Function Call就是“在Prompt里多写几个函数描述,让模型按格式输出”。这个理解不算错,但很容易低估它。常规Prompt调用是这样的:
请根据用户问题,提取意图和对应参数,返回JSON。模型输出什么样完全靠“提示词设计”,它可能给你一个似懂非懂的结构,然后程序解析时各种边界问题。Function Call的训练范式是让模型在预训练和指令微调阶段就见过“工具调用轨迹”,它对“应该输出哪个函数、参数要合法”这件事有更强的先验约束。更重要的是,调用循环本身会形成多步推理链条:模型发现信息不足→调用第一个函数→拿到结果→再决定调第二个函数。这个过程不是一次Prompt能稳定完成的。
在工程层面,Function Call的落地形态是:每个工具定义一个JSON Schema,大模型在解码阶段就有“必须输出合法参数”的约束。我们当时用的是一个统一路由函数,大模型先输出一个结构化的“工具调用计划”,再交给执行器调用外部API,返回结果后拼进对话历史,模型再继续推理。这套和OpenAI Function Calling的接口语义是一致的,但运行时可以完全自控。它解决的不是“单次抽取准确率”的问题,而是“Agent按需获取信息再决策”的整个链路问题。
2.3 选型决策表与当时最担心的三个风险
最后技术委员会要求我们输出一张选型对比表,我当时是这样填的:
| 维度 | 规则匹配(旧) | 微调NLP模型 | 常规Prompt | Function Call |
|---|---|---|---|---|
| 上下文记忆 | 基本不支持 | 弱 | 需自行拼接 | 原生支持(对话历史+工具结果回填) |
| 工具/API调用 | 需硬编码 | 不支持 | 不支持 | 原生支持 |
| 动态扩展能力 | 加规则,成本高 | 加训练数据,成本极高 | 改Prompt,中等 | 加函数描述,成本低 |
| 可解释性 | 高 | 中 | 中 | 中 |
| 误匹配率 | 高(特征不足) | 中 | 中 | 低 |
| 部署复杂度 | 低 | 高 | 低 | 中 |
| 推荐准确率潜力 | 85%左右 | 90%封顶 | 93%单轮 | 95%以上(实测) |
选型不是没有担忧,当时最大的三个风险:
第一是延迟。规则匹配的P99延迟是120毫秒,Function Call多了一个大模型推理和工具调用的循环,P99延迟直接干到1.5秒以上。这个在推荐场景勉强可接受,但我们做了异步预加载和部分缓存来对冲。
第二是幻觉。模型可能在没有真实数据时“强行调用函数”并传入错误参数,或者工具返回结果后模型没有正确引用而是自己编了一个价格。为了应对,我们加了一层“工具结果强约束”:任何商品价格、库存、优惠金额必须来自真实工具结果,模型负责生成话术,但不被允许在话术里自主生成这些数值。
第三是成本。一次Function Call循环可能会调用2-3次大模型,单次会话调用成本是规则匹配的30倍以上。这个没法消除,只能通过路由策略省钱:简单问题走一次轻量模型,复杂问题才进Function Call循环。
3. 架构重构与核心细节实现:从“推荐三步走”到“Agent动态编排”
3.1 新架构的分层设计:编排层、工具层、策略层
我们在设计新架构时没有直接照搬“大模型回复一切”的All-in-One方案,而是做了拆分,一共四个模块:
接入层:负责会话管理、上下文存储、用户身份识别 编排层:负责Function Call主循环,决定每一步调用哪个工具、何时结束 工具层:注册所有可被调用的业务函数(库存、价格、优惠、物流、直播记忆等) 策略层:负责兜底降级、敏感词过滤、推荐结果约束这个拆法的理由很简单:Function Call的“智能”在于模型的推理能力,但“稳定的工程表现”来自编排层和工具层的设计。如果所有逻辑都堆在Prompt里,模型一个不稳定,整个系统就崩了。所以我们的原则是:模型负责“理解意图和生成计划”,代码负责“确定性的执行和数据正确性”。
工具层是我们重点设计的部分。每个工具都是一个标准的函数描述,包含名称、描述、参数Schema、执行代码、返回值格式。给模型看的“函数描述”非常关键,如果描述写得含糊不清,模型会犹豫该不该调用、调哪个函数,直接拉低准确率。我们后来总结出一套函数描述写作模板:第一句说清楚这个函数“在什么场景下用”;第二句说明“它返回什么”;第三句列出“必填参数”和“可选参数”;如果函数有边界,比如价格查询不支持模糊查询,也要写明白。
3.2 核心代码:统一路由循环与Tools定义示例
这是整个项目最核心的代码结构。我先给一个简化的工具定义示例(用JSON Schema风格):
[ { "name": "get_product_detail", "description": "根据商品名称、规格、颜色查询商品详细信息,包括价格、库存、库存状态。适用于用户询问某款商品具体信息或问是否有货时。", "parameters": { "type": "object", "properties": { "product_name": {"type": "string", "description": "商品名称,比如iPhone 15 Pro"}, "spec": {"type": "string", "description": "规格描述,如256G"}, "color": {"type": "string", "description": "颜色"} }, "required": ["product_name"] } }, { "name": "calc_trade_in_price", "description": "计算旧机抵扣金额,适用于用户提到置换、以旧换新、旧手机抵扣等场景。", "parameters": { "type": "object", "properties": { "old_device": {"type": "string", "description": "旧设备型号,如小米11"}, "condition": {"type": "string", "description": "成色/状况描述"} }, "required": ["old_device"] } } ]当用户问“黑色iPhone 15 Pro 256G,置换小米11,分12期多少钱”时,模型会输出一个工具调用计划:
[ {"name": "get_product_detail", "arguments": {"product_name": "iPhone 15 Pro", "spec": "256G", "color": "黑色"}}, {"name": "calc_trade_in_price", "arguments": {"old_device": "小米11"}} ]执行器拿到这个计划后,挨个调用真实API,拿到返回值再回填给模型,模型再生成最终回答。这里最关键的工程点在于:执行器必须先检查参数完整性,如果模型生成的参数里缺少必填项,不能直接调API,而是要让模型重新生成或向用户追问。因为在我们的日志里,参数缺失和幻觉是非常常见的错误来源,硬调API只会返回空值或异常。
3.3 主循环的实现细节:五个步骤一个不能少
我们的Function Call主循环代码大概长这样:
def agent_run(user_query, session_id): # 1. 拉取该会话的上下文记忆 history = memory_engine.get(session_id) # 2. 构造初始消息列表 messages = build_system_prompt() + history + [{"role": "user", "content": user_query}] max_steps = 4 for step in range(max_steps): response = llm.chat_with_tools(messages, tools=TOOL_SCHEMAS) msg = response["message"] # 3. 判定:如果模型没有要求调用工具,直接生成最终回答 if not msg.get("tool_calls"): return msg["content"], step + 1 # 4. 执行工具调用,并把结果拼接回消息列表 tool_results = execute_tools(msg["tool_calls"]) messages.append(msg) for result in tool_results: messages.append({ "role": "tool", "tool_call_id": result["tool_call_id"], "content": json.dumps(result["data"], ensure_ascii=False) }) # 5. 超出最大步数时返回预设降级回复 return fallback_reply(user_query), max_steps一个关键细节是历史消息的长度控制。用户连续问了好几轮,每轮工具结果可能都很长,如果一股脑全拼进Messages,会超出Token限制。我们在memory_engine里做了“窗口截断+关键信息摘要”:只保留最近两轮完整对话和工具结果;更早的对话则让模型用一句话总结成背景上下文。这样可以既保留上下文记忆,又控制Prompt长度,实测能减少约22%的Token消耗,同时没有明显降低准确率。
3.4 关键参数调优:temperature、max_tokens、函数描述长度
Function Call不是“把模型接上去就行”,参数调优的扣分细节非常多。
Temperature
在工具调用的第一步,我们把它设为0,因为工具选择只要确定性和正确性,不要“创意”。但在最终话术生成的第二步,我们会把它提到0.3,让回复稍微自然一点。同一个模型实例不能同时设置两个温度,所以我们实际上把任务拆成了两类请求,一类是“推理调用工具”,一类是“生成回复”,分别走不同参数,实测这个改动让最终的文案可读性提升不少。
max_tokens
这个参数看着不起眼,但曾经让我们吃过亏。一开始max_tokens设得太小(300),每次模型生成工具调用JSON时中途被截断,导致解析失败。后来把所有涉及工具调用的请求max_tokens调到800,并且如果output出现截断标记,就自动做一次“续写请求”,把剩余部分补全。这个容错机制上线后,工具调用解析失败率从5%降到了0.7%。
函数描述长度
这也是个经典权衡。如果把工具描述写得特别长,模型能理解得更准确,但会挤占输入Token,同时也拖慢推理速度。我们的经验是:每个函数描述控制在200字以内,核心信息放在前50字,类似“用于xxx场景,返回xxx”。如果超过200字,说明这个函数太复杂,你应该考虑拆分成两个函数,而不是写一篇小作文。有一次我把get_product_detail的描述写成了400字的“完整接口文档”,结果模型开始频繁返回过于复杂的嵌套参数,很多是根本不存在的字段,准确率反而下降了6个百分点。
我还做了一个更“偏门”的优化:把相似的函数名改成语义清晰的“动宾短语”,比如把query_product改成get_product_detail,把calc_discount改成calc_final_price_with_promo。看着像是小事,但模型的函数选择准确率确实因为这个提升了2.3%。原因不难猜:模型在预训练阶段见到的自然语言描述,本身就更接近“动词+宾语”的表达方式,函数名越容易理解,它选错的可能性越低。
4. 避坑实录:七个坑,每一个都让准确率掉过5%以上
这一节是我的重点,也是真正值钱的部分。如果你照着前面的方案做,大概率会遇到下面这些问题。
4.1 工具描述信息过载,模型“挑花了眼”
我们一开始把tools数组塞了20多个函数,每个函数描述详细记录了所有参数、所有边界情况。结果模型开始频繁返回“参数组合正确但语义用错”的调用,比如在用户问“物流”时调用了“商品推荐”函数。后来我们把工具数量精简到10个,并对相似工具做了“合并入口”。比如把“价格查询”“库存查询”“规格查询”合并成一个get_product_detail,让模型只需要选一次,具体查哪些字段由代码里根据参数存在性决定。这个“函数瘦身”动作,比我想象中更能提升准确率。原则是:给模型的选项越少,它选错的概率越低。
4.2 工具循环“死循环”:模型不断调用工具,不返回最终答案
上线初期,我们遇到了一个特别诡异的情况:模型一直在调工具,一直认为信息不足,比如查完库存又查价格,查完价格又查优惠,一轮接一轮,最多能跑10步,把Token烧光了还不收敛。排查后发现是两个原因叠加的结果。第一,最大步数设得太大(我们一开始设了10),模型知道有足够步数,就不着急收敛。第二,部分函数返回的结果里包含“同款商品推荐”字段,模型就误以为用户还在咨询其他商品,继续触发推荐工具。解决方案很直接:把最大步数从10降到4,同时给函数返回结果里增加一个“is_relevant”标记,明确告诉模型这次返回的数据是不是用户当前问题的直接答案。如果模型认为自己已经拿到了核心数据,就应该停止调用、生成回复。
4.3 模型“硬编”工具返回值:答非所问
这个问题在中文大模型里特别突出。模型调用get_product_detail后,我明明在tool result里给了库存状态“out_of_stock”,模型在生成回答时却说“有现货,可以下单”。后来我们从Prompt和工具结果两方面做了干预。
一方面,在工具结果的前面加上强制提示:“请严格参考以下工具返回结果,其中stock_status为out_of_stock时,不可声称有货。”另一方面,在工具返回的JSON里,我们不再用“in_stock”这种模型需要推理的字段,而是直接用人类可读的“无货/现货”,最大程度减少误解。这个改完,价格和库存相关回答的事实准确率从89%提升到了96.5%。
4.4 参数折叠:模型把“不要”听成“要”
用户的表达里带否定词时容易出错,比如“我不要黑色的”,模型在抽取槽位时直接把color设成了“黑色”。这个问题在规则匹配时代也存在,但在Function Call里更有迷惑性,因为模型输出了一个结构完全正常、参数却与语义相反的调用。
我们的解法是在工具Schema里加了一个“参数语义提示”:“当用户使用否定表达(如不要、除了、不需要),请把对应字段赋值为exclude值,并在description里明确要求先识别否定,再映射参数。”同时,在系统Prompt里加了一条硬规则:“如果用户原话含否定词,直接在参数里输出两个字段:include_color和exclude_color。优先使用exclude_color。”这算是针对电商场景的语气槽位扩展,实测让否定表达的错误率从12%降到了4%左右。
4.5 延迟抖动:上线第一天差点被秒杀
Function Call方案上线第一天,真实流量一进来,那叫一个惨烈。因为我们在主链路上同步调用了两次大模型(一次工具决策,一次生成回复),加上每个工具里可能再调2-3个API,最坏情况下一个会话要等6秒。用户早跑了。
应对办法分了三层。第一层,增加一个“意图预分类器”,规则匹配的手机号不够,我们直接用一个轻量的BERT分类模型做预处理,如果判断是纯闲聊或不需要工具的问题,直接走轻量回复,不进Function Call。第二层,对热门商品/高频问题做缓存,把“商品详情+推荐话术”的最终结果缓存5分钟,命中直接返回。第三层,把两次模型调用改为并行(如果工具选择不依赖上下文)或改成一次流式输出。最终P99延迟从1.8秒降到了900毫秒,线上用户可接受。
4.6 评测口径不统一:说“准确率提升86%”之前,先定义清楚准确率
这一点可能是一半技术人容易忽略的。我们所谓准确率提升86%,很多人第一反应是“你们把82%提升到了95%?绝对值才13个点,哪来的86%?”这里需要澄清,我们评测的口径是“每次会话的推荐结果与人工标注一致的占比”,旧方案线上日志抽样的准确率是51.8%(注意,这个比之前的82%更低,因为它涵盖了所有会话类型,包含大量复杂多意图、指代消解场景),Function Call方案在同一批500条困难样本上的准确率是96.3%。(96.3-51.8)/51.8 = 85.9%,约等于86%。所以它是一个“相对提升”的数值。
为什么用困难样本?因为这才是升级的主要动机。如果只在简单的“价格咨询”上测,旧规则也能做到94%,拉不开差距。我建议任何团队在汇报准确率提升时,都写明三个东西:测试集怎么来的、是否包含了长尾困难样本、计算的是相对提升还是绝对提升。不然这个数字过不了复盘审计。
4.7 降级策略与灰度发布经验
新版上线不可能一上来就全量。我们把线上流量切成三组:A组继续用规则匹配,B组用Function Call但带“无结果降级”,C组用Function Call全量。灰度周期是三天,逐天观察次日留存、会话解决率、投诉率和Token成本。
这里有一个值得分享的经验:不要把降级策略放在“调用大模型失败”之后才触发,而是要放在“模型不确定”之前。什么意思?我们的策略层增加了一个置信度标记,如果模型的工具调用计划和用户原始问题之间的语义相似度低于阈值,或者工具调用结果为空,系统直接走规则兜底。比如有些用户来聊“发货到XX地要多久”,这不是核心推荐场景,Function Call模型调了物流函数但返回为空,这时候直接走规则模板给个标准话术,比让模型强行圆场更安全。
5. 数据复盘与迁移建议:从这次升级里提炼出的可复用经验
5.1 上线三个月后的数据表现
上线后的三个月,我们持续观测了几个核心指标:
| 指标 | 规则匹配基线 | Function Call新方案 |
|---|---|---|
| 困难样本推荐准确率 | 51.8% | 96.3% |
| 单会话解决率 | 73% | 91% |
| 用户平均对话轮数 | 2.1轮 | 3.4轮 |
| 平均下单转化率 | 2.8% | 4.6% |
| 人均Token成本 | 约0.01元/会话 | 约0.23元/会话 |
成本翻了几十倍,这是必须正视的现实。但放到业务大盘看,多花出去的Token成本换来了近一倍的转化率提升,ROI是正的。如果项目预算有限,我建议先砍掉那些“高频但简单”的会话成本,让简单问题走缓存或规则,把预算留给复杂会话。我们后来上线了一个“成本熔断”开关,当某小时Token花费超过阈值时,自动把部分流量切回规则匹配,由系统在优先保障成本还是优先保障体验之间自动权衡。
5.2 哪些业务适合迁移到Function Call,哪些不适合
做个简单的判断表:
| 业务特征 | 适合Function Call | 更适合其他方案 |
|---|---|---|
| 用户表达多变、多意图、指代多 | 适合 | 不适合 |
| 需要调用多个实时数据源 | 适合 | 不适合 |
| 工具数量稳定(<20个) | 适合 | 不适合 |
| 对延迟要求极端(<300毫秒) | 需做缓存,延迟优化空间有限 | 规则匹配更稳 |
| 完全依赖离线知识库、无实时数据 | 可用,但大材小用 | 向量检索+RAG更省 |
| 强合规、数据不出域 | 需私有化部署 | 无 |
我个人的判断是:如果业务里“多工具编排+实时决策+复杂语义理解”这三个特征同时出现,Function Call就是目前性价比最高的方案。如果你的场景只有“从一堆文档里找答案”,那就别为了一点点智能感强行上Function Call,老老实实做RAG或者规则匹配就够了。
5.3 更轻量级的平替方案:如果你的团队还没准备好接LLM
如果你所在团队还没有接入大模型API的条件,或者领导对Token成本极其敏感,但又想在现有规则匹配基础上提准,我的建议是按顺序做三件事:
第一,把规则模板从“关键词匹配”升级为“正则分组+槽位必填校验”,至少可以解决一部分槽位抽取错误;第二,给每个意图增加“负向排除词表”,比如用户说“不要黑色”,在正向规则命中黑色时,先过一遍排除表,把黑名单里的参数直接删除;第三,做一个轻量级的“意图路由”,先用规则判断大体类型,再局部引入少量大模型或分类模型做二次修正。这些是“带着镣铐跳舞”的办法,虽然天花板还在,但成本极低,可以作为过渡方案。
5.4 下一步规划:从Function Call到多智能体编排
这次升级之后,团队在思考下一步怎么走。Function Call解决的是“单个Agent如何调用工具”,但实际业务里同一时刻可能跑多个Agent,比如“商品推荐Agent”“售后判断Agent”“库存预警Agent”。如果每个Agent都独立和模型通信,不仅成本高,还会出现多个Agent对同一用户意图给出冲突结果的问题。
目前我们在考虑引入一层“调度Agent”,它负责看着用户原始问题,决定分发给哪个子Agent执行,再把结果合并成最终回复。这种设计本质上是一个多智能体编排问题,和Function Call的关系是:子Agent内部继续使用Function Call,调度Agent则负责宏观策略。这算是一个自然的进化路径,但复杂度也会明显上升,如果当前系统还没稳定,我建议先别all in多Agent,把单Agent的工具调用链路吃透再说。
结尾:一点个人体会
这次升级让我最深的感受是,技术选型最怕的不是某个方案不够好,而是团队里没有一个人敢拍板说“旧的可以不要了”。规则匹配在它合适的阶段确实是最高效的,我们不是否定它,而是让它退到它该在的位置,当一个尽职尽责的兜底者。Function Call也不是银弹,它也有幻觉、延迟和成本问题,但它的价值在于让Agent的能力边界可以随工具的扩展而扩展,而不是随着规则的膨胀而僵化。
最后再分享一个小技巧:不要直接照搬官方示例里的工具Schema,一定要花时间根据自己业务的真实对话日志去“反推”工具函数的边界。你拥有的对话数据,才是比任何Prompt经验都值钱的东西。先用它定义一个回归集,每次改完Prompt或工具描述都在回归集上跑一遍,你才能守住准确率提升的成果。