ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent意图识别分层漏斗架构:从62%到91%准确率的工程实践

Agent意图识别分层漏斗架构:从62%到91%准确率的工程实践 1. 为什么单层意图识别在工业场景里一定会崩做过 Agent 项目的人大概都有过这种体验Demo 阶段准确率 95%一上生产环境直接掉到 60% 出头用户随便换个说法、加个错别字、夹带一句闲聊整个链路就开始胡言乱语。我最早做客服 Agent 的时候就是拿一个 Prompt 硬扛所有意图前面挂个分类模型后面接一堆 if-else结果上线第一周就被真实流量教做人。问题出在哪单层意图识别本质上是在用一次判断解决一个多维度的问题。用户的输入里同时包含我要干什么、我在说什么领域、我情绪怎么样、我这句话是不是在问同一个东西、我是不是在闲聊这些正交的信息你让一个模型一次性输出它必然会在某些维度上妥协。就像你让一个人同时判断一道菜是川菜还是粤菜、辣度几级、食材新不新鲜、厨师心情好不好他大概率只能抓住最显眼的那一个。工业级场景和 Demo 最大的区别在于流量分布是长尾的。头部意图可能占 70%但剩下 30% 里藏着几百种变体每一种单独看都不多加起来就是灾难。单层模型在头部意图上表现很好一到长尾就开始乱分类而长尾恰恰是用户真正需要被正确处理的场景——头部意图用户自己都能自助解决长尾才是需要 Agent 介入的地方。分层漏斗这个思路说白了就是把一次复杂的判断拆成多次简单的判断每一层只解决一个维度的问题逐层收敛。这个思路不新鲜搜索引擎、推荐系统、风控系统早就在用但搬到 Agent 意图识别上很多人没想清楚每层该放什么、层与层之间怎么衔接、什么时候该短路、什么时候该兜底。我踩过的坑基本都集中在这几个问题上。下面我按实际落地的顺序把整套分层漏斗的设计思路、每层的具体实现、参数怎么调、坑在哪完整拆一遍。这套方案我在两个日活十万级的 Agent 项目里跑过意图识别准确率从单层的 62% 提到了 91%误路由率从 18% 压到了 4% 以内。2. 分层漏斗的整体架构与每层职责划分2.1 四层漏斗的分工逻辑我最终定下来的结构是四层从粗到细依次收敛层级名称核心职责典型耗时拦截比例L1规则前置层拦截明显无效、敏感、超短输入1ms15%~25%L2粗分类层判断领域大类售前/售后/闲聊/其他5~15ms分流 100%L3细意图层在领域内判断具体意图30~80ms收敛到 3~8 个候选L4LLM 兜底层处理低置信度、多意图、模糊表达300~800ms5%~15%这个比例不是拍脑袋定的。L1 拦截 15%~25% 是因为真实流量里确实有大量你好、在吗、这种无意义输入用规则拦掉成本几乎为零。L2 到 L3 是主力L4 只处理前面搞不定的硬骨头这样整体平均耗时能控制在 100ms 以内比全量走 LLM 快一个数量级。注意漏斗不是越深越好。我见过有人搞七层每层都调模型结果延迟爆炸、维护成本高到没人敢改。四层是我实测下来性价比最高的分界点再往下拆收益递减明显。2.2 为什么是漏斗而不是并联很多人第一反应是并联同时跑规则、小模型、大模型然后投票。这个方案在离线评测里看着很美线上就是灾难。并联意味着每次请求都要付出最慢那条路径的成本LLM 那一路 800ms整体就是 800ms前面省的全白费。漏斗的核心价值是短路。L1 能拦掉的绝不进 L2L2 高置信度的绝不进 L3L3 明确的绝不进 L4。实测下来只有 5%~15% 的请求会走到 L4剩下 85% 都在 100ms 内解决。这个短路机制是漏斗能扛住并发的根本原因。另一个原因是可解释性。并联投票出结果你很难说清楚为什么这么判。漏斗每一层的判断都有明确依据出问题能快速定位是哪一层错了改起来也有的放矢。2.3 层与层之间的数据契约这是最容易被忽略但最要命的部分。每层输出的数据结构必须统一否则后面接起来就是一团乱麻。我用的契约是这样的{ query: 原始输入, normalized_query: 归一化后的输入, l1_result: {passed: True, reason: None}, l2_result: {domain: after_sales, confidence: 0.92}, l3_result: {intent: refund_request, confidence: 0.87, candidates: [...]}, l4_result: None, final_intent: refund_request, final_confidence: 0.87, route_path: [L1, L2, L3], latency_ms: 47 }route_path这个字段特别重要线上排查问题时一眼就能看出这个请求走了哪几层、在哪层被拦下的。没有这个字段你面对一堆日志根本无从下手。3. 每一层的具体实现与参数调优3.1 L1 规则前置层用最便宜的方式干掉最脏的流量L1 的目标不是准确分类而是快速排除。这一层用纯规则实现不调任何模型耗时控制在 1ms 以内。我实际用的规则集分几类长度规则字符数小于 2 的直接拦在、、嗯字符数超过 500 的走特殊处理大概率是粘贴的长文本黑名单规则纯表情、纯标点、重复字符aaaaa敏感词规则命中敏感词库的直接转人工不进后续流程格式规则纯数字、纯链接、纯手机号走对应的专用处理链路这里有个坑黑名单不能太激进。我一开始把你好、谢谢都放进闲聊拦截结果用户说你好我要退款也被拦了。后来改成前缀匹配 长度判断组合只有你好单独出现且长度小于 4 才拦问题就解决了。def l1_filter(query): q query.strip() if len(q) 2: return {passed: False, reason: too_short} if len(q) 500: return {passed: False, reason: too_long, action: truncate} if re.fullmatch(r[\s\W], q): return {passed: False, reason: pure_symbol} if q in SENSITIVE_WORDS: return {passed: False, reason: sensitive, action: human} return {passed: True, reason: None}实操心得L1 的规则一定要做成配置化别写死在代码里。线上流量变化很快今天有效的规则下个月可能就误伤了。我用的是一个 YAML 配置文件加热加载改规则不用发版运营同学自己就能调。3.2 L2 粗分类层领域判断是分流的命门L2 的任务是把请求分到几个大领域里比如售前咨询、售后服务、订单物流、闲聊、其他。这一层不需要判断具体意图只需要判断大概属于哪个方向。为什么要有这一层因为领域信息能极大缩小 L3 的搜索空间。如果 L3 要在 200 个意图里选准确率会很难看但如果 L2 已经告诉它这是售后领域L3 只需要在 30 个售后意图里选难度直接降一个量级。L2 的实现我试过三种方案方案准确率耗时维护成本适用场景关键词匹配70%1ms低领域边界清晰小模型分类BERT-base88%10ms中主流选择LLM few-shot93%400ms低领域多且变化快我最终选的是小模型 关键词兜底的混合方案。小模型用 BERT-base 微调训练数据每个领域 2000 条左右准确率能到 88%。关键词兜底处理那些小模型置信度低于 0.6 的样本用领域关键词表再判一次能再捞回来 5% 左右。这里的关键参数是置信度阈值。设太高大量请求会漏到 L3 甚至 L4延迟上去了设太低错分到错误领域L3 再准也救不回来。我实测下来 0.75 是个比较平衡的点低于这个值就走兜底逻辑。def l2_classify(query): probs domain_model.predict(query) top_domain, top_prob max(probs.items(), keylambda x: x[1]) if top_prob 0.75: return {domain: top_domain, confidence: top_prob, source: model} # 兜底关键词匹配 kw_domain keyword_match(query, DOMAIN_KEYWORDS) if kw_domain: return {domain: kw_domain, confidence: 0.6, source: keyword} return {domain: unknown, confidence: top_prob, source: fallback}3.3 L3 细意图层候选集收敛是核心L3 是主力层负责在领域内判断具体意图。这一层的输入是 L2 给的领域标签输出是具体意图加置信度。我的做法是每个领域一个独立的分类模型而不是一个大模型管所有意图。原因很简单领域内意图数量可控一般 20~50 个单独训练准确率高、推理快、互不干扰。售后模型更新不影响售前模型这是工程上的巨大优势。L3 的输出不只是单个意图而是Top-K 候选列表。这个设计很关键因为 L4 需要候选列表来做最终裁决。如果 L3 只输出一个意图L4 就没有参考信息只能从头判断那 L3 就白做了。def l3_classify(query, domain): model DOMAIN_MODELS[domain] probs model.predict(query) candidates sorted(probs.items(), keylambda x: x[1], reverseTrue)[:5] top_intent, top_prob candidates[0] if top_prob 0.85: return {intent: top_intent, confidence: top_prob, candidates: candidates, need_l4: False} return {intent: top_intent, confidence: top_prob, candidates: candidates, need_l4: True}0.85 这个阈值是调出来的。低于这个值说明模型自己都不确定硬判大概率错不如交给 L4。实测下来L3 置信度高于 0.85 的样本最终正确率 96%低于 0.85 的正确率只有 71%。这个分界线非常清晰。3.4 L4 LLM 兜底层只处理真正难的样本L4 用 LLM 处理前面搞不定的样本包括L3 置信度低的、L2 判为 unknown 的、多意图混杂的、表达特别口语化的。这一层的关键是Prompt 设计。我用的结构是你是一个意图识别专家。用户输入属于【{domain}】领域。 候选意图列表{candidates} 用户输入{query} 请判断用户最可能的意图并给出置信度。 如果候选列表中没有合适的返回 other。 输出 JSON 格式{intent: ..., confidence: 0.0-1.0, reason: ...}把 L2 的领域和 L3 的候选列表塞进 PromptLLM 的判断准确率比裸判高 20 个百分点以上。这就是漏斗的价值——每一层都在给下一层提供先验信息。L4 的耗时是 300~800ms所以必须严格控制流量。我的做法是设一个总预算如果 L4 的调用量超过总请求的 15%就触发告警说明前面几层出问题了需要回头调 L2/L3 的阈值。注意L4 一定要设超时和降级。LLM 服务不稳定是常态超时了就返回 L3 的 Top-1 结果虽然可能不准但总比整个链路卡死强。我设的超时是 1.5s超过就走降级。4. 完整链路的串联与工程实现4.1 主流程代码把四层串起来的主流程大概长这样def intent_recognition(query): start time.time() result {query: query, route_path: []} # L1 l1 l1_filter(query) result[l1_result] l1 result[route_path].append(L1) if not l1[passed]: result[final_intent] l1.get(action, reject) result[latency_ms] (time.time() - start) * 1000 return result # L2 l2 l2_classify(query) result[l2_result] l2 result[route_path].append(L2) # L3 if l2[domain] ! unknown: l3 l3_classify(query, l2[domain]) result[l3_result] l3 result[route_path].append(L3) if not l3[need_l4]: result[final_intent] l3[intent] result[final_confidence] l3[confidence] result[latency_ms] (time.time() - start) * 1000 return result # L4 l4 l4_llm_classify(query, l2.get(domain), l3.get(candidates) if l3 else None) result[l4_result] l4 result[route_path].append(L4) result[final_intent] l4[intent] result[final_confidence] l4[confidence] result[latency_ms] (time.time() - start) * 1000 return result4.2 并发扛压的关键设计Agent 场景的并发特点是突发性强可能前一秒 QPS 是 10下一秒就冲到 500。分层漏斗在扛并发上有天然优势因为大部分请求在前两层就返回了不会打到后面的重资源层。但有几个地方必须做保护L2/L3 模型服务要做批处理。单条推理 GPU 利用率很低攒批到 32 条一起推吞吐能提升 5~8 倍。我用的是 Triton 的动态批处理延迟增加 5ms 左右吞吐提升明显。L4 要做队列和限流。LLM 调用是瓶颈必须用信号量控制并发数超过就排队或降级。我设的并发上限是 50超过直接走 L3 降级结果。全链路要有超时预算。L1 给 5msL2 给 20msL3 给 100msL4 给 1500ms任何一层超时都走降级不能让单个请求拖垮整个服务。4.3 监控与迭代闭环上线不是终点是起点。我搭的监控体系包含几个核心指标指标含义告警阈值L4 流量占比走到 LLM 层的比例15% 告警各层置信度分布判断阈值是否合理分布偏移 10% 告警端到端 P99 延迟用户体验500ms 告警意图分布突变是否有新意图出现单意图占比变化 5% 告警意图分布突变这个指标特别有用。有一次线上突然出现大量other意图一查发现是竞品上线了新功能用户开始问一些我们没覆盖的问题。如果没有这个监控可能要等用户投诉才发现。迭代闭环是这样的每天把 L4 处理的样本捞出来人工标注一批正确的加入 L3 训练集错误的分析原因——是 L2 分错领域了还是 L3 候选集里根本没有这个意图。前者调 L2后者加新意图。这个循环跑起来模型每周都在变好。5. 踩过的坑与排查技巧实录5.1 常见问题速查表现象可能原因排查方法解决方案L4 流量暴涨L2/L3 阈值过严看各层置信度分布下调阈值或补训练数据某领域准确率骤降该领域模型过拟合看混淆矩阵补充长尾样本重训延迟毛刺L4 排队看 L4 队列长度扩容或提高降级比例意图漂移用户表达变化看 L4 样本定期增量训练误拦正常请求L1 规则过激看 L1 拦截样本放宽规则或加白名单5.2 三个印象最深的坑第一个坑L2 和 L3 的领域定义不一致。我一开始 L2 分售前/售后/物流L3 的售后模型里又包含了物流相关的意图结果物流问题在 L2 被分到物流L3 却没有对应模型直接掉到 L4。后来统一了领域定义L2 和 L3 用同一套领域划分问题解决。领域定义必须全局唯一这是架构层面的约束不能各层各搞一套。第二个坑L3 候选集太小。我一开始 Top-K 设的是 3结果有些样本正确答案排在第 4、第 5 位L4 拿不到正确候选只能瞎猜。后来改成 Top-5L4 准确率提升了 8 个百分点。K 值不是越小越好要给 L4 留够信息。第三个坑LLM 的 Prompt 里塞了太多候选。有次一个领域有 50 个意图我把全部候选塞进 Prompt结果 LLM 反而懵了准确率比只给 Top-5 还低。LLM 的注意力是有限的候选太多它会分心。后来固定只传 Top-5效果最好。5.3 独家避坑技巧阈值不要一次调到位。我习惯先设一个保守值比如 L3 的 0.9上线跑一周看数据再逐步下调。一次调太激进线上出问题很难回滚。L1 的规则要留观察模式。新规则先只记录不拦截跑一周看会误伤多少确认没问题再开启拦截。这个习惯帮我避免了好几次线上事故。L4 的样本要定期人工抽检。别完全信 LLM 的判断每周抽 200 条人工看能发现很多模型发现不了的问题。给每个意图设一个最小样本量。意图样本少于 50 条的不要单独建类合并到other里否则模型学不好还干扰其他意图。6. 效果验证与不同规模场景的适配建议6.1 实测效果对比这套方案在两个项目上的实测数据指标单层方案分层漏斗提升意图识别准确率62%91%29pp误路由率18%4%-14pp平均延迟420ms87ms-79%P99 延迟1200ms480ms-60%单机 QPS15120700%准确率的提升主要来自 L4 兜底延迟和吞吐的提升主要来自短路机制。这两个收益是分层漏斗最核心的价值。6.2 小团队怎么落地如果你团队只有两三个人别一上来就搞四层。我的建议是先做两层规则 LLM。规则拦掉明显无效的剩下的全走 LLM。这样开发成本最低准确率也能到 80% 左右。等流量上来了、LLM 成本扛不住了再把 LLM 那层拆成小模型 LLM 两层逐步演进。演进路径我建议是规则LLM → 规则小模型LLM → 规则粗分类细分类LLM。每一步都基于当前的实际瓶颈来拆不要为了架构好看而拆。6.3 大流量场景的优化点日请求过百万之后瓶颈会转移到模型推理和 LLM 调用上。这时候几个优化方向L2/L3 模型量化。INT8 量化后推理速度提升 2~3 倍准确率掉 1 个点以内非常划算。L4 结果缓存。相似 query 的 LLM 结果可以缓存用向量相似度匹配命中率能到 20% 左右直接省掉这部分 LLM 调用。L3 模型蒸馏。用大模型蒸馏小模型小模型准确率能再提 3~5 个点同时保持推理速度。分层异步。L2 和 L3 如果领域判断很明确可以并行跑省掉一层串行延迟。但这个优化要谨慎并行意味着资源消耗翻倍。这套东西说到底就是一句话别指望一个模型解决所有问题把问题拆开每层解决一个用工程手段把简单问题挡在前面把复杂问题留给贵的资源。这个思路不只适用于意图识别Agent 里很多环节都能套用。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进