意图路由的三次重构:从规则到 Agent,再回到规则
太长不读:过去一年我在几个 AI 项目里反复处理同一个问题——用户说了一句话,系统怎么判断这活儿该谁干。这个模块我推翻重写了三次:先是关键词规则,然后押注纯 Agent 化,最后回调到框架驱动。整理下来,有几处体会和我原来想的不太一样:一是关键词路由真正的瓶颈不是准确率,而是「枚举不完」这件事本身;二是 LLM 自主路由的失控不在 Demo 里,在复现、审计和成本这三本账上;三是人机分工最划算的切法,是「理解给模型,决策给规则,歧义交给人」。读完这篇,大伙对这几个问题应该能有些更清晰的感知。
一、先对齐问题:什么是意图路由
一个 AI 应用通常挂着一堆能力:生成用例、跑任务、查数据、建流程。用户随手输入一句话,系统第一步要做的不是回答,而是路由——判断这句话该交给哪个能力处理。
打个比方,这像装修公司的前台:客户打电话来,前台得先听明白是「修水管」还是「全屋定制」,再转给对应的师傅。前台听错了,师傅手艺再好也白干。
难的地方在于,客户的说法是无穷无尽的。「我家水管漏了」「卫生间一直滴水」「楼下邻居找上来了」,三件事是同一件事,前台都得听出来。三次重构,本质上都是围绕这个「前台」在打转。
二、第一次:关键词打分,可控但织不完的网
最早的一版很朴素,一个关键词打分路由器:
def route(user_input: str, project_id: str):
scores = {}
for intent, keywords in INTENT_KEYWORDS.items():
# 每个关键词命中加一分,按权重累计
scores[intent] = sum(w for kw, w in keywords.items() if kw in user_input)
best_intent, best_score = max(scores.items(), key=lambda x: x[1])
confidence = best_score / max_total
if confidence >= 0.7:
return best_intent # 高置信,直接路由
elif confidence >= 0.5:
return ask_or_fallback(best_intent) # 中置信,走降级
else:
return route_by_project(project_id) # 低置信,按项目猜
机制就三件事:关键词命中打分;置信度按 0.7 和 0.5 分两档;分数实在太低就按用户所在的项目兜底猜一个。
这个方案的好处是实打实的:简单、可控、可解释。路由错了,日志里一眼能看到哪个关键词没命中,改一行配置修好。上线初期它工作得相当不错。
问题随后暴露,而且比预想严重:用户的表达方式枚举不完。用户说「帮我造点数据」,关键词里有「造数据」,命中;用户说「帮我准备一下测试物料」,懵了;用户说「老规矩,还是昨天那个场景」,彻底没办法。那段时间的维护日常是:看日志、发现新说法、加关键词、发版,循环往复。像拿一张永远织不完的网,去捞一条会变形的鱼。
更麻烦的是关键词互相打架。「生成一个流程」里的「生成」和「流程」分别命中两个不同的意图,分数接近,置信度卡在 0.5 上下,大量请求被迫走降级通道。
现在回头看,这版的瓶颈不是准确率不够高,而是它假设语言是可以枚举的——这个假设本身就是错的。
三、第二次:纯 Agent 化,和那个消失的 v2
被关键词折磨够之后,团队走向了另一个极端:纯 Agent 化,零规则判断。思路很直接——既然大模型理解能力强,路由这件事干脆全部交给它。
设计了一版相当「教科书」的四层架构:
| 层 | 职责 |
|---|---|
| L1 理解层 | LLM 深度理解用户意图,拆解任务 |
| L2 执行层 | 多个 Agent 各司其职,通过 BlackBoard 黑板交换中间结果 |
| L3 反馈层 | 执行结果回流,LLM 自我评估、自我纠正 |
| L4 数据层 | Capability Registry 注册所有原子能力,Agent 按需组合 |
设计文档写了三份,评审开了好几轮,大伙都挺兴奋。然后,这个 v2 就消失了。
不是修辞,是真的消失了:后来翻仓库,v2 的代码整个被删掉,本地只剩几个 __pycache__ 缓存残骸。考古现场只剩陶片,陶罐没了。
它死在哪儿?不是 Demo 效果——Demo 里 LLM 自主路由的表现堪称惊艳。它死在三本生产环境的账上:
- 复现账:同一句话,今天路由给 A,明天路由给 B。用户问「昨天还好好的,今天怎么不对」,没人能回答。
- 审计账:关键词路由错了有分数可查;LLM 路由错了,只有一段「看起来很有道理」的推理,为什么选 B 不选 A,说不清。
- 成本账:每次路由都是一次完整的大模型调用,黑板协作几轮下来上下文越滚越大。Demo 里十次调用无所谓,生产环境乘以用户量,账单会教你做人。
跑通一个 Demo 和做出一个能用的产品,是两回事。这句话说过很多遍,但直到亲手埋掉一个 v2,才算听懂。
四、第三次:框架驱动,把「怎么走」收回来
第三代的思路是一次回调——不是退回关键词,而是退到中间态。核心原则一句话:LLM 负责理解,框架负责执行,规则负责决策。落到两个具体做法上。
4.1 结构化定义,替代自然语言说明书
早期试过让 Agent 读自然语言写的能力说明书,一大篇 Markdown。效果有,但执行太自由——模型状态好就按步骤走,状态不好就自由发挥。
改成结构化定义,每个能力配一个 yaml:
name: testcase-gen
steps:
- id: analyze
action: run_analysis
on_fail: retry # 失败策略:abort / skip / fallback / retry
- id: generate
action: gen_cases
depends_on: [analyze]
parallel_group: gen # 同组步骤可并行
- id: verify
action: validate_output
validator: case_schema_check # 每步带验证器
框架拿到定义后做 DAG 解析,算出依赖和并行组,然后强制按步骤执行。LLM 只负责一件事:内容生成。往哪走、失败了怎么办,由框架和配置决定,每步产出还要过验证器。
一句话:把「怎么走」从模型手里收回来,只留「写什么」给它。
4.2 理解引擎 + 分发器:理解和决策拆开
另一个服务里,把「理解」和「决策」拆成了两个模块:
// 第一步:理解引擎(LLM)只输出结构化理解结果,不做决策
interface Understanding {
task_intent: string; // 用户想干嘛
complexity: "low" | "medium" | "high";
requires_context: boolean;
// ...一共 10 个字段
}
// 第二步:分发器(纯规则)拿理解结果做路由
function dispatch(u: Understanding): Route {
if (u.complexity === "low" && isBuiltin(u.task_intent)) return LOCAL;
if (u.requires_context && !hasContext()) return CLARIFY;
// ...7 条规则按优先级逐条匹配,全不命中走兜底降级
}
LLM 产出的是一份「体检报告」:10 个字段,把这句话的意图、复杂度、上下文依赖都结构化出来。分发器拿着报告走 7 条有优先级的规则,规则全不命中,还有规则引擎兜底。
这个组合的合理性在于能力互补:LLM 擅长处理语言的模糊性(恰好是关键词的死穴),规则擅长保证决策的确定性(恰好是 LLM 的死穴)。各司其职,互不越界。
五、另一条路:猜不准,就问人
除了「机器怎么猜」,还有一条常被工程师忽视的路:猜不准的时候,直接问人。
在我们做的 CLI 工具里,每个能力配触发词做初筛,加优先级仲裁。但触发词只能解决「明显是 A」和「明显是 B」,中间地带永远存在。处理办法很朴素:歧义时弹一个选择框——「找到两个可能匹配的能力:①生成测试用例 ②创建测试流程,你想用哪个?」用户点一下,两秒结束。
算一笔账就会发现这个方案划算得离谱:模型猜测要消耗 token、要承担猜错风险;而用户点一下的成本几乎为零。最便宜的意图理解,是问用户。
这里有一个踩过的坑值得说:路由规则要锚在宾语上,不要锚在动词上。最早的匹配规则按动词写,用户说「生成用例」就路由到用例生成。直到有人说「生成一个流程」,同一个动词「生成」,规则把它送去了用例生成,完全跑偏。改成按宾语锚定后——「用例」「流程」「任务」才是意图的载体,动词只是动作——用户换动词说法(「建一条流程」「跑一下那个流程」),宾语不变,路由就稳。
推广开来:设计路由规则时,要锚定用户表达里不变的部分,而不是多变的部分。
六、什么时候才该打断用户:一条不等式
问用户也不能滥用。系统动不动弹窗「你确定吗」,用户很快就会关掉它。什么时候该问,什么时候自己扛,需要一条明确的规则。
我们最后定的原则分两步:
第一步,先用上下文和记忆消解歧义。用户说「还是昨天那个场景」,如果记忆里能翻出昨天的场景,就别问,直接干。很多歧义放进更大的上下文里,根本不是歧义。
第二步,消解不掉的,算一笔账:
猜错代价 × 不确定度 > 打断成本,才打断用户。
三种典型情况:
| 场景 | 处置 |
|---|---|
| 猜错代价低、不确定度低 | 带默认假设继续,把假设写进输出方便纠正 |
| 猜错代价高、不确定度高 | 必须停下来问,比如删数据、群发消息 |
| 打断本身 | 是真实成本,每次打断都在消耗用户耐心 |
这条不等式把「要不要加确认弹窗」从拍脑袋变成了可讨论、可调参的工程决策。后来评审任何这类需求,第一句话都是:这个场景的猜错代价是多少。
七、最后
回头看,三次重构表面是技术方案更迭,本质是对 LLM 能力边界的认知更新:第一代假设语言可枚举,错了;第二代假设模型可托付一切,也错了;第三代承认两边都有边界,规则与自主不是开关,是一个光谱,架构的工作是在光谱上给每块职责找到位置。
如果只能留一句话,就留这句:理解给模型,决策给规则,歧义交给人。 架构的成熟度不看它多聪明,看它承认不确定性的位置有多精确。那个消失的 v2 一行代码都没留下,但这句话留下了。