ARTICLE DETAIL

资讯详情

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

生产级意图路由:三层漏斗架构设计与落地实践

生产级意图路由:三层漏斗架构设计与落地实践 1. 为什么“意图路由”不是加个 if-else 就能上线的在刚接触 Agent 开发时我见过太多团队把“意图识别”当成一个 NLP 分类任务来处理训练一个微调过的 LLM 分类器输入 query输出 intent 标签比如 search / calculate / report / tool_call然后用 switch-case 跳转到对应逻辑。看起来干净利落跑通 Demo 五分钟PR 合并前还被夸“架构清晰”。结果上线第三天客服后台就堆了 47 条报错“用户说‘帮我查下上个月张三的报销单’系统却进了计算器模块”“用户问‘怎么重置密码’被路由到知识库搜索但返回的是医保政策原文”更典型的是“我要投诉”和“我要表扬”模型打分只差 0.03路由决策反复横跳——用户刚点完投诉入口页面又刷出表扬表单。这根本不是模型精度问题。而是把意图路由误当作静态分类任务来解忽略了它在生产环境中的三个本质属性语义模糊性、上下文依赖性、业务敏感性。语义模糊性用户说“这个月工资条不对”到底是查明细search、比对历史compare、还是申诉异常complain同一句话在 HR 系统里是查询在财务系统里可能是风险预警信号。上下文依赖性新员工第一次登录问“我的账号怎么登不上”和老员工连续三次失败后问“我的账号怎么登不上”路由目标完全不同——前者该走自助引导后者该直连人工坐席。业务敏感性把“我要报警”路由到 FAQ 模块技术上可能只有 0.2% 的误判率但业务上就是 100% 的事故。路由错误的成本远高于模型推理延迟。所以“生产级意图路由”的核心矛盾从来不是“怎么让模型更准”而是“如何在可控成本下把路由决策的不确定性显式建模、分层收敛、可审计回溯”。这正是“三层漏斗架构”要解决的问题——它不追求一步到位的完美分类而是用三道物理隔离的过滤机制把高风险、高歧义、高成本的路由决策逐步压缩到可穷举、可验证、可兜底的确定性区间。这不是算法优化是工程架构设计。你看到的 LangGraph 图底层其实是状态机 规则引擎 人工干预通道的混合体。后面我会拆开每层的螺丝告诉你为什么必须是三层为什么不能合并成两层以及每一层的边界线画在哪才不会被业务方半夜打电话骂醒。2. 三层漏斗的物理边界每一层都在解决一个不可妥协的工程约束很多团队在落地时会问“能不能把第一层规则过滤和第二层 LLM 意图识别合并”或者“第三层兜底是不是太重了直接用 fallback prompt 不就行了”——这些想法背后是对三层各自承担的不可替代性约束缺乏认知。我用我们实际部署的医疗问答 Agent 为例说明每层的硬性边界是怎么划出来的。2.1 第一层正则关键词结构化校验——解决“绝对不能错”的硬拦截这一层的目标只有一个在毫秒级内拦截所有明确违反业务红线的请求且零误杀。它不处理“理解”只做“匹配”。我们定义了三类硬规则身份强校验所有含“身份证号”“社保卡号”“病历号”的 query必须同时包含“查询”“打印”“导出”等动词否则直接拦截并返回合规提示“根据《个人信息保护法》第X条您需先完成实名认证才能访问个人健康数据。”时效强约束含“今天”“现在”“立刻”等词的 query若未携带时间戳或设备定位信息则拒绝进入后续流程防止恶意刷单。实体白名单仅允许对预设的 87 个药品名、42 个检查项目、19 个科室名称进行模糊搜索其余全部拦截。例如用户输入“查一下那个叫阿莫西林的药”能过但输入“查一下那个叫蓝色小药丸的药”直接拦截。提示这一层严禁使用任何 LLM 或 embedding。我们用 Rust 编写的轻量级匹配引擎P99 延迟 3ms内存占用 2MB。曾有团队试图用 Sentence-BERT 做相似度匹配结果单次请求内存暴涨 1.2GB被运维直接 kill。规则引擎的代价是维护成本高但换来的是确定性——这是生产环境的底线。2.2 第二层轻量级 LLM 意图打分 置信度阈值——解决“可以试错但必须可控”的灰度决策过了第一层的 query进入第二层。这里的核心任务是给出 3~5 个候选意图并为每个意图打分再根据动态阈值决定是否放行。我们没用 full-size LLM而是蒸馏了一个 1.3B 参数的专用模型基于 Qwen-1.5 微调输入格式固定为[USER_ROLE]患者[CONTEXT]已绑定医保卡近3次咨询均为药品用法[QUERY]这个药饭前吃还是饭后吃模型输出 JSON{ intents: [ {name: drug_usage, score: 0.82, reason: query 明确指向药品服用方式}, {name: side_effect, score: 0.15, reason: 未出现‘副作用’‘过敏’等关键词}, {name: price_inquiry, score: 0.03, reason: 无价格、费用、报销等词} ], confidence: 0.82 }关键在动态阈值机制对高风险意图如 complain / refund / cancel要求 confidence ≥ 0.92 才放行对中风险意图如 report / schedule阈值设为 0.75对低风险意图如 search / faq阈值为 0.60。未达阈值的 query 不丢弃而是打上low_confidence标签进入第三层。这个设计让我们把误路由率从 12.7% 降到 1.3%同时将人工审核量减少 68%——因为 89% 的低置信请求其实只是用户表达不规范比如“那个红盒子的药怎么吃”第三层能自动补全。2.3 第三层人工可干预的决策沙盒——解决“必须有人类最终拍板”的责任闭环这是最容易被砍掉的一层也是最不该砍的。它的存在不是为了“救火”而是为了建立可追溯、可复盘、可追责的决策链路。我们用 LangGraph 实现了一个状态机沙盒所有low_confidence请求进入review_state系统自动生成 3 个辅助决策项① 最近 7 天同类 query 的路由分布如 83% 路由到 drug_usage② 用户历史行为聚类标签如“高频药品咨询者”③ 规则引擎的原始匹配日志如“命中关键词‘吃’未命中‘副作用’”审核员只需点击 1 个按钮Accept / Reject / Escalate选择后立即生效并记录操作人、时间、依据所有决策日志实时写入 ClickHouse支持按 intent、user_id、time_range 多维下钻分析。注意这一层必须与业务系统深度耦合。我们把审核界面嵌入医院 HIS 系统的护士工作站审核员在处理工单时顺手就能处理路由异常。如果做成独立后台响应延迟超过 2 分钟就会导致用户重复提交形成恶性循环。三层不是简单的流水线而是带反馈回路的闭环第三层的每一次人工决策都会触发两个动作——① 自动更新第二层模型的 fine-tuning 数据集② 若某条规则连续 5 次被人工覆盖系统自动告警并建议修改第一层规则。这才是“生产级”的真实含义它不是一个静态架构而是一个持续进化的决策器官。3. LangGraph 如何成为三层漏斗的“神经中枢”状态机设计的 3 个反常识细节很多人以为 LangGraph 就是画个节点图、连几条边然后 run() 就完事。但在三层漏斗架构里LangGraph 承担的是状态协调、异常熔断、审计埋点三重职责。我拆解三个实际踩坑后才明白的设计细节3.1 状态不是“变量”而是“带版本号的决策快照”初版实现时我们把用户 query 当作 state 传入每个节点修改 state 字段。结果出现严重 bug当用户快速连续发送两条消息如“查血压”→“再查血糖”第二条消息的 state 里竟混入了第一条的意图打分结果。根源在于 LangGraph 的 state 是引用传递且默认不 deep copy。解决方案为每个 request 生成唯一 trace_id并强制 state 结构化为不可变快照class RoutingState(TypedDict): trace_id: str user_query: str user_context: dict # 包含角色、历史、设备等 layer1_result: Optional[dict] # {status: pass/block, reason: ...} layer2_result: Optional[dict] # {intents: [...], confidence: float} layer3_decision: Optional[dict] # {action: accept, intent: blood_pressure, operator: nurse_001} version: int # 每次 state 更新 1用于幂等校验每次节点执行前state.version 自增下游节点通过 version 判断是否处理过期状态。这个设计让并发请求的路由结果 100% 可重现也为后续的 AB 测试打下基础——你可以精确对比“旧版规则 vs 新版规则”在相同 trace_id 下的决策差异。3.2 边不是“连接线”而是“带熔断策略的契约”默认的 LangGraph edge 是无条件跳转。但在生产环境我们必须控制每条路径的最大尝试次数、超时阈值、降级策略。例如第一层规则引擎的 timeout 设为 5ms超时则直接走 fallback返回“系统繁忙请稍后再试”第二层 LLM 调用设置 max_retries1且 retry 时强制切换到备用模型实例避免单点故障第三层人工审核的等待 timeout 为 90s超时自动升级至值班组长。我们用自定义 edge 函数实现def layer2_edge(state: RoutingState) - str: if state.get(layer1_result, {}).get(status) block: return to_layer1_fallback # 熔断检查过去1分钟内 layer2 错误率 5% if circuit_breaker.is_open(layer2): return to_layer3_review # 熔断时直接进人工 return to_layer2_llm这个 circuit_breaker 是独立服务与 LangGraph 解耦但通过 Redis 共享状态。它让整个漏斗具备了“自我保护”能力——当第二层模型因 GPU 故障开始抖动系统会自动把流量切到第三层而不是雪崩式失败。3.3 图不是“拓扑”而是“可热插拔的决策协议”最初我们把三层写死在同一个 Graph 里。结果业务方提了个需求“门诊部想把投诉路由到院长信箱但住院部要路由到医务科”。如果改代码就得停服发布。后来我们改成协议驱动的图编排每个业务单元门诊/住院/体检注册自己的routing_policy.json定义{ intent_mapping: { complain: {target: director_mailbox, threshold: 0.85}, refund: {target: finance_dept, threshold: 0.90} }, layer3_handlers: [nurse_supervisor, admin_officer] }LangGraph 启动时加载所有 policy运行时根据state.user_context[department]动态选择 policy新增部门只需上传 policy 文件无需重启服务。这个设计让我们的路由系统支撑了 12 个业务线的差异化策略且策略变更平均耗时 3 分钟。真正的生产级不是功能多强大而是变更多安全。4. 从 Demo 到生产5 个必须跨过的“非技术”深坑技术方案再漂亮落地时也会被现实撞得头破血流。这 5 个坑没有一个写在 LangGraph 官方文档里但每个都让我团队加班超过 40 小时4.1 坑一业务方永远认为“意图”是名词而你要教他们理解“意图”是动宾短语市场部提需求“用户问‘挂号’就路由到挂号模块”。但实际中用户说“我想挂明天上午张医生的号”这是挂号说“挂号费多少钱”这是价格咨询说“挂号总是失败”这是技术支持。我们花了整整两周带着业务方一起标注 2000 条真实对话教会他们用“主语谓语宾语状语”的结构拆解 query。最终产出的《意图定义手册》里每条意图都附带 3 个正例、3 个反例、2 个边界案例。没有这一步后面所有模型训练都是空中楼阁。4.2 坑二日志不是记录“做了什么”而是记录“为什么这么做”初期日志只记{intent: search, confidence: 0.78}。当出现误路由时根本无法复现。后来我们强制要求每层输出决策依据链第一层{matched_rule: keyword_drug_name, keywords: [阿莫西林], whitelist_hit: true}第二层{model_version: v2.3.1, input_tokens: 47, top_intent_reason: query contains 怎么吃 pattern}第三层{reviewer: nurse_zhang, review_time: 2024-06-15T14:22:33Z, override_reason: 用户历史 5 次咨询均为用法本次应优先保障体验}现在每次故障复盘都能在 10 分钟内定位到是规则缺陷、模型偏差还是人工判断失误。4.3 坑三测试不是测“能不能跑”而是测“会不会骗你”我们设计了一套“对抗测试集”同音字攻击“挂好号” vs “挂号”前者是完成态不应路由到挂号否定句陷阱“不是要查报告是要取消预约”跨域混淆“微信支付不了”表面是支付问题实际是网络问题应路由到 IT 支持。这套测试集发现 23% 的误路由来自语言学边缘 case远超模型本身的误差率。4.4 坑四监控不是看“成功率”而是看“决策熵”传统指标如“路由准确率 92%”毫无意义。我们新增了决策熵监控对每个 request计算第二层输出的 intent 分数分布熵值 H -Σp_i * log(p_i)。H 0.3决策高度集中系统很稳H ∈ [0.3, 0.7]正常灰度区间H 0.7模型陷入犹豫需人工介入。当某天熵值突增我们发现是新上线的医保政策文档引入了大量“报销”“结算”“垫付”等近义词导致模型无法区分 intent。这个指标比准确率提前 3 小时预警了问题。4.5 坑五上线不是“发布”而是“渐进式信任移交”我们从不全量切流。而是第 1 天1% 流量走新漏斗其余走旧逻辑第 2 天5% 流量但只开放低风险 intentsearch / faq第 5 天20% 流量开放中风险 intent第 10 天100% 流量但保留“一键回滚”开关。每次增量都同步向业务方推送《决策对比报告》列明新旧路由差异及原因。信任不是靠 PPT 建立的是靠每天 3 份可验证的报表积累的。5. 三层之后当漏斗不再够用我们如何扩展架构运行 8 个月后系统日均处理 24 万次路由请求三层架构依然稳定。但业务方提出了新需求“希望用户说‘帮我处理上次的投诉’时能自动关联历史工单”。这意味着路由决策需要依赖外部状态——而三层漏斗是纯 stateless 的。我们的解法不是加第四层而是在第二层嵌入“状态感知代理”当 query 含“上次”“历史”“之前”等时序词第二层模型不直接输出 intent而是触发一个轻量级 RAG 查询# 在 layer2_llm 节点内 if has_temporal_word(query): recent_cases vector_db.search( query_embeddingembed(query), filter{user_id: state[user_id], type: complain}, top_k3 ) # 将最近 3 个工单摘要拼接到 prompt 中 augmented_prompt f用户历史投诉{recent_cases[0][summary]}...{query} return llm.invoke(augmented_prompt)这个 RAG 查询走专用缓存集群P95 80ms不影响主漏斗 SLA。关键是RAG 结果不改变三层架构只是作为第二层的“增强输入”。所有决策依然遵循原有三层规则审计日志也完整记录 RAG 的调用痕迹。这个设计证明了一点好的架构不是无限堆叠层级而是在核心范式上做精准增强。我们至今没加第四层但通过在第二层注入状态感知能力解决了 92% 的上下文依赖需求。真正的工程智慧往往藏在“不做”的克制里。我在实际项目中最大的体会是意图路由从来不是 AI 问题而是组织问题。当你能把业务方拉到标注现场能让他们看懂熵值监控图能让他们在决策沙盒里亲手修正一次错误路由——这时候技术才真正落地了。
返回列表