博客.
博客.
(联系方式)

保持联系

如果你想联系我,无论是讨论合作机会、分享想法,还是只是打个招呼,都可以通过以下方式找到我。

(最后更新)

版权所有 / 2026

(快捷键)C
返回文章列表
2026年8月28日

意图路由的三次重构:从规则到 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 自主路由的表现堪称惊艳。它死在三本生产环境的账上:

  1. 复现账:同一句话,今天路由给 A,明天路由给 B。用户问「昨天还好好的,今天怎么不对」,没人能回答。
  2. 审计账:关键词路由错了有分数可查;LLM 路由错了,只有一段「看起来很有道理」的推理,为什么选 B 不选 A,说不清。
  3. 成本账:每次路由都是一次完整的大模型调用,黑板协作几轮下来上下文越滚越大。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、要承担猜错风险;而用户点一下的成本几乎为零。最便宜的意图理解,是问用户。

这里有一个踩过的坑值得说:路由规则要锚在宾语上,不要锚在动词上。最早的匹配规则按动词写,用户说「生成用例」就路由到用例生成。直到有人说「生成一个流程」,同一个动词「生成」,规则把它送去了用例生成,完全跑偏。改成按宾语锚定后——「用例」「流程」「任务」才是意图的载体,动词只是动作——用户换动词说法(「建一条流程」「跑一下那个流程」),宾语不变,路由就稳。

推广开来:设计路由规则时,要锚定用户表达里不变的部分,而不是多变的部分。

六、什么时候才该打断用户:一条不等式

问用户也不能滥用。系统动不动弹窗「你确定吗」,用户很快就会关掉它。什么时候该问,什么时候自己扛,需要一条明确的规则。

我们最后定的原则分两步:

第一步,先用上下文和记忆消解歧义。用户说「还是昨天那个场景」,如果记忆里能翻出昨天的场景,就别问,直接干。很多歧义放进更大的上下文里,根本不是歧义。

第二步,消解不掉的,算一笔账:

猜错代价 × 不确定度 > 打断成本,才打断用户。

HITL 门卡决策逻辑

三种典型情况:

场景处置
猜错代价低、不确定度低带默认假设继续,把假设写进输出方便纠正
猜错代价高、不确定度高必须停下来问,比如删数据、群发消息
打断本身是真实成本,每次打断都在消耗用户耐心

这条不等式把「要不要加确认弹窗」从拍脑袋变成了可讨论、可调参的工程决策。后来评审任何这类需求,第一句话都是:这个场景的猜错代价是多少。

七、最后

回头看,三次重构表面是技术方案更迭,本质是对 LLM 能力边界的认知更新:第一代假设语言可枚举,错了;第二代假设模型可托付一切,也错了;第三代承认两边都有边界,规则与自主不是开关,是一个光谱,架构的工作是在光谱上给每块职责找到位置。

如果只能留一句话,就留这句:理解给模型,决策给规则,歧义交给人。 架构的成熟度不看它多聪明,看它承认不确定性的位置有多精确。那个消失的 v2 一行代码都没留下,但这句话留下了。