云计算百科
云计算领域专业知识百科平台

【一起学AI · 03】Agent 总把请求派错?先把意图识别拆成三层

前言

用户一句话进来,系统要先判断这句话该交给哪个 Agent、还缺什么参数。这一步判错了,后面的 Agent 再聪明也白干。本文用一个售后助手,把意图识别从分层到兜底讲一遍。


文章目录

    • 前言
    • 一、意图识别最后要交出一张任务单
    • 二、一句话进来会遇到哪些麻烦
    • 三、规则、小模型、大模型按复杂度分层
    • 四、参数缺了,先追问再派活
    • 五、一句话里有两件事
    • 六、state、policy、fallback 各管一件事
    • 七、跑一个能验证的小例子
    • 八、路由错了,靠评估集发现
    • 写在最后

一、意图识别最后要交出一张任务单

假设我们在做一个电商售后助手,背后挂了三个 Agent:查订单的 order_agent、处理退款的 refund_agent、开发票的 invoice_agent。这是本文的教学场景,不对应某个已上线的系统。

用户不会按接口格式说话。他可能说“我的快递到哪了”,也可能说“先把上周那单退了,然后开张发票”。意图识别要做的,是把这种自然语言转成结构化的任务,再决定进哪个 Agent。

我更愿意把它的输出理解成一张任务单,而不是一个分类标签:

{
"tasks": [
{"intent": "refund_apply", "slots": {"order_id": null, "reason": null}, "confidence": 0.82},
{"intent": "invoice_issue", "slots": {"order_id": null, "title": null}, "confidence": 0.78}
],
"needs_clarification": true
}

只输出一个 refund 字符串的话,下游拿到它还得自己猜订单号、猜用户有没有说第二件事。任务单把这些信息摆在明面上:几个意图、每个意图要哪些参数、哪些还空着、模型有多确定。后面的路由、追问、确认,全都依赖这张单子。

#mermaid-svg-YDxsYJFlXevE7N4h{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YDxsYJFlXevE7N4h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YDxsYJFlXevE7N4h .error-icon{fill:#552222;}#mermaid-svg-YDxsYJFlXevE7N4h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YDxsYJFlXevE7N4h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YDxsYJFlXevE7N4h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YDxsYJFlXevE7N4h .marker.cross{stroke:#333333;}#mermaid-svg-YDxsYJFlXevE7N4h svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YDxsYJFlXevE7N4h p{margin:0;}#mermaid-svg-YDxsYJFlXevE7N4h .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YDxsYJFlXevE7N4h .cluster-label text{fill:#333;}#mermaid-svg-YDxsYJFlXevE7N4h .cluster-label span{color:#333;}#mermaid-svg-YDxsYJFlXevE7N4h .cluster-label span p{background-color:transparent;}#mermaid-svg-YDxsYJFlXevE7N4h .label text,#mermaid-svg-YDxsYJFlXevE7N4h span{fill:#333;color:#333;}#mermaid-svg-YDxsYJFlXevE7N4h .node rect,#mermaid-svg-YDxsYJFlXevE7N4h .node circle,#mermaid-svg-YDxsYJFlXevE7N4h .node ellipse,#mermaid-svg-YDxsYJFlXevE7N4h .node polygon,#mermaid-svg-YDxsYJFlXevE7N4h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YDxsYJFlXevE7N4h .rough-node .label text,#mermaid-svg-YDxsYJFlXevE7N4h .node .label text,#mermaid-svg-YDxsYJFlXevE7N4h .image-shape .label,#mermaid-svg-YDxsYJFlXevE7N4h .icon-shape .label{text-anchor:middle;}#mermaid-svg-YDxsYJFlXevE7N4h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YDxsYJFlXevE7N4h .rough-node .label,#mermaid-svg-YDxsYJFlXevE7N4h .node .label,#mermaid-svg-YDxsYJFlXevE7N4h .image-shape .label,#mermaid-svg-YDxsYJFlXevE7N4h .icon-shape .label{text-align:center;}#mermaid-svg-YDxsYJFlXevE7N4h .node.clickable{cursor:pointer;}#mermaid-svg-YDxsYJFlXevE7N4h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YDxsYJFlXevE7N4h .arrowheadPath{fill:#333333;}#mermaid-svg-YDxsYJFlXevE7N4h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YDxsYJFlXevE7N4h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YDxsYJFlXevE7N4h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YDxsYJFlXevE7N4h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YDxsYJFlXevE7N4h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YDxsYJFlXevE7N4h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YDxsYJFlXevE7N4h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YDxsYJFlXevE7N4h .cluster text{fill:#333;}#mermaid-svg-YDxsYJFlXevE7N4h .cluster span{color:#333;}#mermaid-svg-YDxsYJFlXevE7N4h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YDxsYJFlXevE7N4h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YDxsYJFlXevE7N4h rect.text{fill:none;stroke-width:0;}#mermaid-svg-YDxsYJFlXevE7N4h .icon-shape,#mermaid-svg-YDxsYJFlXevE7N4h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YDxsYJFlXevE7N4h .icon-shape p,#mermaid-svg-YDxsYJFlXevE7N4h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YDxsYJFlXevE7N4h .icon-shape .label rect,#mermaid-svg-YDxsYJFlXevE7N4h .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YDxsYJFlXevE7N4h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YDxsYJFlXevE7N4h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YDxsYJFlXevE7N4h :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

齐了

缺了

用户输入

意图识别

任务单意图 + 参数 + 置信度

参数齐了吗

路由到对应 Agent

追问用户

执行结果

这张图里,意图识别处在整个 Agent 系统的入口。入口判断错了,比如把“退货”当成“查物流”,后面的 Agent 会认真地做一件用户根本没要的事,而且回答得很流畅。这类错误比报错更难发现。

二、一句话进来会遇到哪些麻烦

把意图识别真正接进系统,会撞上一串问题。我按出现的先后顺序排了一下:

问题在售后助手里的样子
规则怎么定、用什么方法判 “订单 2024100801”能用正则,“东西有点问题”就不行
参数不完整 用户只说“我要退款”,没说哪一单、为什么退
单意图还是多意图 “先退款,然后开发票”是两件事,顺序还不能乱
单路由还是多路由 两件事分别交给两个 Agent,谁先谁后
闲聊 “谢谢”“你好”不该触发任何业务 Agent
信息不清晰 “那个东西你看下”,说不出是哪个意图
路由错了怎么办 写操作已经派出去了,才发现判错
怎么评估 换了模型或提示词,准确率是涨了还是跌了

这些问题轻重差得很远。闲聊和信息不清晰,用兜底话术就能处理;参数不完整和多意图,会直接决定系统的结构;路由错误和评估,决定这套东西能不能放心上线。所以下面不平均用力,重点放在分层、参数、多意图和评估上。

三、规则、小模型、大模型按复杂度分层

最常见的误区是所有请求都扔给大模型判断。能用,但贵、慢,而且同一句话两次调用可能给出不同结果。另一个极端是全写规则,几十个意图之后,正则会多到没人敢改。

比较稳的做法是按“被路由的内容有多复杂”分三层,从便宜的往贵的走:

#mermaid-svg-vPzP023KKU59wtnm{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vPzP023KKU59wtnm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vPzP023KKU59wtnm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vPzP023KKU59wtnm .error-icon{fill:#552222;}#mermaid-svg-vPzP023KKU59wtnm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vPzP023KKU59wtnm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vPzP023KKU59wtnm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vPzP023KKU59wtnm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vPzP023KKU59wtnm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vPzP023KKU59wtnm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vPzP023KKU59wtnm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vPzP023KKU59wtnm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vPzP023KKU59wtnm .marker.cross{stroke:#333333;}#mermaid-svg-vPzP023KKU59wtnm svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vPzP023KKU59wtnm p{margin:0;}#mermaid-svg-vPzP023KKU59wtnm .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vPzP023KKU59wtnm .cluster-label text{fill:#333;}#mermaid-svg-vPzP023KKU59wtnm .cluster-label span{color:#333;}#mermaid-svg-vPzP023KKU59wtnm .cluster-label span p{background-color:transparent;}#mermaid-svg-vPzP023KKU59wtnm .label text,#mermaid-svg-vPzP023KKU59wtnm span{fill:#333;color:#333;}#mermaid-svg-vPzP023KKU59wtnm .node rect,#mermaid-svg-vPzP023KKU59wtnm .node circle,#mermaid-svg-vPzP023KKU59wtnm .node ellipse,#mermaid-svg-vPzP023KKU59wtnm .node polygon,#mermaid-svg-vPzP023KKU59wtnm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vPzP023KKU59wtnm .rough-node .label text,#mermaid-svg-vPzP023KKU59wtnm .node .label text,#mermaid-svg-vPzP023KKU59wtnm .image-shape .label,#mermaid-svg-vPzP023KKU59wtnm .icon-shape .label{text-anchor:middle;}#mermaid-svg-vPzP023KKU59wtnm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vPzP023KKU59wtnm .rough-node .label,#mermaid-svg-vPzP023KKU59wtnm .node .label,#mermaid-svg-vPzP023KKU59wtnm .image-shape .label,#mermaid-svg-vPzP023KKU59wtnm .icon-shape .label{text-align:center;}#mermaid-svg-vPzP023KKU59wtnm .node.clickable{cursor:pointer;}#mermaid-svg-vPzP023KKU59wtnm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vPzP023KKU59wtnm .arrowheadPath{fill:#333333;}#mermaid-svg-vPzP023KKU59wtnm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vPzP023KKU59wtnm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vPzP023KKU59wtnm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vPzP023KKU59wtnm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vPzP023KKU59wtnm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vPzP023KKU59wtnm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vPzP023KKU59wtnm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vPzP023KKU59wtnm .cluster text{fill:#333;}#mermaid-svg-vPzP023KKU59wtnm .cluster span{color:#333;}#mermaid-svg-vPzP023KKU59wtnm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vPzP023KKU59wtnm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vPzP023KKU59wtnm rect.text{fill:none;stroke-width:0;}#mermaid-svg-vPzP023KKU59wtnm .icon-shape,#mermaid-svg-vPzP023KKU59wtnm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vPzP023KKU59wtnm .icon-shape p,#mermaid-svg-vPzP023KKU59wtnm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vPzP023KKU59wtnm .icon-shape .label rect,#mermaid-svg-vPzP023KKU59wtnm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vPzP023KKU59wtnm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vPzP023KKU59wtnm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vPzP023KKU59wtnm :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

命中

未命中

命中

没把握或疑似多意图

识别出意图

还是说不清

用户输入

规则层格式固定吗

生成任务单

小模型层单意图且有把握吗

大模型层按 schema 输出意图列表

兜底 / 澄清

规则层处理格式稳定、措辞固定的请求。订单号是固定 10 位,“订单 2024100801”这种输入用正则就能拿到意图和参数,置信度可以直接给 1.0。规则层的价值是确定性:同样的输入永远同样的结果,出问题也能一眼定位到哪条规则。

小模型层处理口语化但意图单一的请求,比如“我的快递到哪了”“这个想退货”。线上通常是一个在自家标注数据上训练的轻量文本分类模型,推理快,成本低。它的短板是只会在训练过的类别里选,碰到一句话里有两件事,或者类别之外的说法,容易硬选一个。

大模型层兜住剩下的长尾:多意图、指代、需要结合上下文理解的说法。这里要让大模型按固定 schema 返回意图列表,而不是自由发挥一段话,后面的代码才好校验。

分层的关键在于每一层要知道自己什么时候该放手。小模型层如果对一句多意图的话硬判出一个类别,大模型层就永远没有机会接手。下面这个例子是我写示例时真的踩到的:最初版本里,“先把上周那单退了,再给我开张发票”只命中了“发票”这个关键词,小模型层直接判成开发票,退款请求就这么丢了。后来给小模型层加了一条退出条件,看到“然后”“顺便”这类连接词就不判,交给下一层。

三层的取舍可以这样对照:

层适合的输入成本与延迟主要风险
规则 格式固定、关键词明确 几乎为零 规则膨胀,难维护
小模型 口语化的单意图 低 类别外输入被硬分类
大模型 多意图、指代、长尾说法 高 输出不稳定,需要校验

四、参数缺了,先追问再派活

识别出“退款”只完成了一半。refund_agent 要订单号和退款原因,用户只说了“我要退款”,这时候直接派过去,Agent 要么报错,要么自己编一个订单号。

所以每个意图要在注册表里声明必填参数,识别完先对照检查。缺什么就问什么,并且要把这个“正在等参数”的状态记下来,下一轮用户的回答才知道是在回答哪个问题。

refund_agent

会话状态

意图路由

用户

refund_agent

会话状态

意图路由

用户

#mermaid-svg-QaB0Hfhl9d3QOwko{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QaB0Hfhl9d3QOwko .error-icon{fill:#552222;}#mermaid-svg-QaB0Hfhl9d3QOwko .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QaB0Hfhl9d3QOwko .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QaB0Hfhl9d3QOwko .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QaB0Hfhl9d3QOwko .marker.cross{stroke:#333333;}#mermaid-svg-QaB0Hfhl9d3QOwko svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QaB0Hfhl9d3QOwko p{margin:0;}#mermaid-svg-QaB0Hfhl9d3QOwko .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QaB0Hfhl9d3QOwko text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QaB0Hfhl9d3QOwko .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QaB0Hfhl9d3QOwko .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QaB0Hfhl9d3QOwko #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QaB0Hfhl9d3QOwko .sequenceNumber{fill:white;}#mermaid-svg-QaB0Hfhl9d3QOwko #sequencenumber{fill:#333;}#mermaid-svg-QaB0Hfhl9d3QOwko #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QaB0Hfhl9d3QOwko .messageText{fill:#333;stroke:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QaB0Hfhl9d3QOwko .labelText,#mermaid-svg-QaB0Hfhl9d3QOwko .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .loopText,#mermaid-svg-QaB0Hfhl9d3QOwko .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QaB0Hfhl9d3QOwko .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QaB0Hfhl9d3QOwko .noteText,#mermaid-svg-QaB0Hfhl9d3QOwko .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QaB0Hfhl9d3QOwko .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QaB0Hfhl9d3QOwko .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QaB0Hfhl9d3QOwko .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QaB0Hfhl9d3QOwko .actorPopupMenu{position:absolute;}#mermaid-svg-QaB0Hfhl9d3QOwko .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QaB0Hfhl9d3QOwko .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QaB0Hfhl9d3QOwko .actor-man circle,#mermaid-svg-QaB0Hfhl9d3QOwko line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QaB0Hfhl9d3QOwko :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

先把上周那单退了

读取上一单订单号

记下待办任务,缺 reason

请问退款原因是什么?

尺码不合适

取出待办任务,补上 reason

确认对订单 2024100801 退款吗?

确认

refund(order_id, reason)

这里有个容易被忽略的细节:订单号是从上下文继承来的。用户前面查过 2024100801,现在说“上周那单”,系统猜是同一单。猜对的概率可能不低,但退款是写操作,猜错了就是给别人的订单发起退款。所以从上下文推出来的参数要打个标记,到了写操作前,要么让用户确认,要么重新问一遍。

追问还会遇到另一种情况:系统在等退款原因,用户回了一句“谢谢”。如果把“谢谢”当成退款原因填进去,就闹笑话了。处理顺序应该是先看这句话本身有没有可识别的意图,是闲聊就回闲聊,待办任务留着;什么都识别不出来,才当作对追问的回答。

五、一句话里有两件事

“先把上周那单退了,然后开张发票”包含两个意图,而且有先后关系,按常见的业务规则,发票要基于退款后的金额开。

多意图有两种处理方式。简单的情况,几个意图互不依赖,各自路由到对应 Agent,结果合起来返回就行。复杂一点的,意图之间有顺序、有数据依赖,就需要一个 planner 把它们排成执行计划:

#mermaid-svg-HZuhwc2aC1AbQJxf{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HZuhwc2aC1AbQJxf .error-icon{fill:#552222;}#mermaid-svg-HZuhwc2aC1AbQJxf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HZuhwc2aC1AbQJxf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HZuhwc2aC1AbQJxf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HZuhwc2aC1AbQJxf .marker.cross{stroke:#333333;}#mermaid-svg-HZuhwc2aC1AbQJxf svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HZuhwc2aC1AbQJxf p{margin:0;}#mermaid-svg-HZuhwc2aC1AbQJxf .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster-label text{fill:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster-label span{color:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster-label span p{background-color:transparent;}#mermaid-svg-HZuhwc2aC1AbQJxf .label text,#mermaid-svg-HZuhwc2aC1AbQJxf span{fill:#333;color:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf .node rect,#mermaid-svg-HZuhwc2aC1AbQJxf .node circle,#mermaid-svg-HZuhwc2aC1AbQJxf .node ellipse,#mermaid-svg-HZuhwc2aC1AbQJxf .node polygon,#mermaid-svg-HZuhwc2aC1AbQJxf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HZuhwc2aC1AbQJxf .rough-node .label text,#mermaid-svg-HZuhwc2aC1AbQJxf .node .label text,#mermaid-svg-HZuhwc2aC1AbQJxf .image-shape .label,#mermaid-svg-HZuhwc2aC1AbQJxf .icon-shape .label{text-anchor:middle;}#mermaid-svg-HZuhwc2aC1AbQJxf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HZuhwc2aC1AbQJxf .rough-node .label,#mermaid-svg-HZuhwc2aC1AbQJxf .node .label,#mermaid-svg-HZuhwc2aC1AbQJxf .image-shape .label,#mermaid-svg-HZuhwc2aC1AbQJxf .icon-shape .label{text-align:center;}#mermaid-svg-HZuhwc2aC1AbQJxf .node.clickable{cursor:pointer;}#mermaid-svg-HZuhwc2aC1AbQJxf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HZuhwc2aC1AbQJxf .arrowheadPath{fill:#333333;}#mermaid-svg-HZuhwc2aC1AbQJxf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HZuhwc2aC1AbQJxf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HZuhwc2aC1AbQJxf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HZuhwc2aC1AbQJxf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HZuhwc2aC1AbQJxf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HZuhwc2aC1AbQJxf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster text{fill:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf .cluster span{color:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HZuhwc2aC1AbQJxf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HZuhwc2aC1AbQJxf rect.text{fill:none;stroke-width:0;}#mermaid-svg-HZuhwc2aC1AbQJxf .icon-shape,#mermaid-svg-HZuhwc2aC1AbQJxf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HZuhwc2aC1AbQJxf .icon-shape p,#mermaid-svg-HZuhwc2aC1AbQJxf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HZuhwc2aC1AbQJxf .icon-shape .label rect,#mermaid-svg-HZuhwc2aC1AbQJxf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HZuhwc2aC1AbQJxf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HZuhwc2aC1AbQJxf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HZuhwc2aC1AbQJxf :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

完成或已确认

缺参数

缺参数

任务单refund + invoice

Planner 排执行顺序

任务队列

退款任务

开票任务

追问原因

追问抬头

planner 只负责排顺序和依赖,不负责执行。排好的任务进队列,前一个任务参数不齐,就停下来追问,后面的任务原样留在队列里,不会因为前一个卡住就被丢掉。这一点我觉得比“能识别出两个意图”更重要:识别出来又在流程里弄丢了,用户的体验和没识别是一样的。

不是所有多意图都值得上 planner。如果业务里九成请求都是单意图,偶尔出现的多意图可以直接回一句“我们先处理退款,处理完再帮你开发票”,只执行第一个,把第二个留给下一轮。等多意图真的多起来,再引入编排也不迟。

六、state、policy、fallback 各管一件事

到这里,入口已经不只是一个分类器了。我习惯把它拆成几个职责清楚的部件:

#mermaid-svg-4bYaDtEq3aZApMqM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4bYaDtEq3aZApMqM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4bYaDtEq3aZApMqM .error-icon{fill:#552222;}#mermaid-svg-4bYaDtEq3aZApMqM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4bYaDtEq3aZApMqM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4bYaDtEq3aZApMqM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4bYaDtEq3aZApMqM .marker.cross{stroke:#333333;}#mermaid-svg-4bYaDtEq3aZApMqM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4bYaDtEq3aZApMqM p{margin:0;}#mermaid-svg-4bYaDtEq3aZApMqM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4bYaDtEq3aZApMqM .cluster-label text{fill:#333;}#mermaid-svg-4bYaDtEq3aZApMqM .cluster-label span{color:#333;}#mermaid-svg-4bYaDtEq3aZApMqM .cluster-label span p{background-color:transparent;}#mermaid-svg-4bYaDtEq3aZApMqM .label text,#mermaid-svg-4bYaDtEq3aZApMqM span{fill:#333;color:#333;}#mermaid-svg-4bYaDtEq3aZApMqM .node rect,#mermaid-svg-4bYaDtEq3aZApMqM .node circle,#mermaid-svg-4bYaDtEq3aZApMqM .node ellipse,#mermaid-svg-4bYaDtEq3aZApMqM .node polygon,#mermaid-svg-4bYaDtEq3aZApMqM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4bYaDtEq3aZApMqM .rough-node .label text,#mermaid-svg-4bYaDtEq3aZApMqM .node .label text,#mermaid-svg-4bYaDtEq3aZApMqM .image-shape .label,#mermaid-svg-4bYaDtEq3aZApMqM .icon-shape .label{text-anchor:middle;}#mermaid-svg-4bYaDtEq3aZApMqM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4bYaDtEq3aZApMqM .rough-node .label,#mermaid-svg-4bYaDtEq3aZApMqM .node .label,#mermaid-svg-4bYaDtEq3aZApMqM .image-shape .label,#mermaid-svg-4bYaDtEq3aZApMqM .icon-shape .label{text-align:center;}#mermaid-svg-4bYaDtEq3aZApMqM .node.clickable{cursor:pointer;}#mermaid-svg-4bYaDtEq3aZApMqM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4bYaDtEq3aZApMqM .arrowheadPath{fill:#333333;}#mermaid-svg-4bYaDtEq3aZApMqM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4bYaDtEq3aZApMqM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4bYaDtEq3aZApMqM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4bYaDtEq3aZApMqM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4bYaDtEq3aZApMqM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4bYaDtEq3aZApMqM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4bYaDtEq3aZApMqM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4bYaDtEq3aZApMqM .cluster text{fill:#333;}#mermaid-svg-4bYaDtEq3aZApMqM .cluster span{color:#333;}#mermaid-svg-4bYaDtEq3aZApMqM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4bYaDtEq3aZApMqM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4bYaDtEq3aZApMqM rect.text{fill:none;stroke-width:0;}#mermaid-svg-4bYaDtEq3aZApMqM .icon-shape,#mermaid-svg-4bYaDtEq3aZApMqM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4bYaDtEq3aZApMqM .icon-shape p,#mermaid-svg-4bYaDtEq3aZApMqM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4bYaDtEq3aZApMqM .icon-shape .label rect,#mermaid-svg-4bYaDtEq3aZApMqM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4bYaDtEq3aZApMqM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4bYaDtEq3aZApMqM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4bYaDtEq3aZApMqM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

可以

需确认

识别失败

用户输入

分层识别规则 / 小模型 / 大模型

Planner多意图排序

Policy能不能直接执行

路由到业务 Agent

向用户确认

Fallback 兜底

State待办队列 / 上一单订单号

state 负责上下文。 待办任务队列、上一次提到的订单号、正在追问哪个参数,都放在这里。没有 state,每一轮对话都是从零开始,追问和指代都做不了。

policy 负责约束。 它回答“这个任务能不能直接执行”。只读的查询可以直接放行;写操作要看置信度够不够、参数是不是推断出来的,不够就转成确认。policy 和识别模型分开写,好处是改风控规则不需要动模型,换模型也不会把风控规则一起换掉。

fallback 负责兜底。 三层都识别不出来时,不要沉默,也不要随便猜一个。给用户一个有限的选项,比如“你是想查订单、退款,还是开发票?”,把开放问题变成选择题。fallback 触发的次数本身也是很好的监控信号,它突然升高,通常说明用户在问一类你还没覆盖的事。

七、跑一个能验证的小例子

下面把上面几块拼成一个能运行的最小版本。小模型层用关键词代替,大模型层写死一条样例返回,目的是看清分层、追问、多意图队列和 policy 是怎么串起来的,不是做一个真能用的分类器。代码基于 Python 3.10 及以上版本(用到了 str | None 写法)。

# crayontech/agent/intent_router.py
import re
from dataclasses import dataclass, field

# 意图注册表:意图名 -> 目标 Agent、必填参数、是否写操作
REGISTRY = {
"order_query": {"agent": "order_agent", "slots": ["order_id"], "write": False},
"refund_apply": {"agent": "refund_agent", "slots": ["order_id", "reason"], "write": True},
"invoice_issue": {"agent": "invoice_agent", "slots": ["order_id", "title"], "write": True},
"chitchat": {"agent": None, "slots": [], "write": False},
}

ORDER_ID = re.compile(r"\\d{10}")
MULTI_HINTS = ("然后", "再给", "顺便", "另外")

@dataclass
class Task:
intent: str
confidence: float
source: str
slots: dict = field(default_factory=dict)
inherited: set = field(default_factory=set) # 从上下文继承来的参数

@dataclass
class SessionState:
queue: list = field(default_factory=list) # 还没执行完的任务,按顺序排队
last_order_id: str | None = None

def rule_layer(text):
# 第一层:格式固定的请求,正则直接命中
m = re.fullmatch(r"(查询?)?订单\\s*(\\d{10})", text.strip())
if m:
return [Task("order_query", 1.0, "rule", {"order_id": m.group(2)})]
return []

KEYWORDS = {
"order_query": ["到哪", "物流", "发货"],
"refund_apply": ["退款", "退货"],
"invoice_issue": ["发票", "开票"],
"chitchat": ["你好", "谢谢"],
}

def small_model_layer(text):
# 第二层:线上通常是轻量分类模型,这里用关键词打分代替
if any(h in text for h in MULTI_HINTS):
return [] # 疑似多意图,不在这一层硬判
hits = [k for k, words in KEYWORDS.items() if any(w in text for w in words)]
return [Task(hits[0], 0.9, "small_model")] if len(hits) == 1 else []

def llm_layer(text):
# 第三层:线上调用大模型并要求按 schema 输出意图列表,这里写死一条样例
canned = {
"先把上周那单退了,然后开张发票": [
Task("refund_apply", 0.82, "llm"),
Task("invoice_issue", 0.78, "llm"),
],
}
return canned.get(text, [])

def classify(text):
for layer in (rule_layer, small_model_layer, llm_layer):
tasks = layer(text)
if tasks:
return tasks
return []

def missing_slots(task, text, state):
m = ORDER_ID.search(text)
if m:
task.slots.setdefault("order_id", m.group())
elif "order_id" in REGISTRY[task.intent]["slots"] and state.last_order_id:
if "order_id" not in task.slots:
task.slots["order_id"] = state.last_order_id
task.inherited.add("order_id")
return [s for s in REGISTRY[task.intent]["slots"] if s not in task.slots]

def policy(task):
# 写操作:置信度不足或订单号来自上下文推断,都先让用户确认
if not REGISTRY[task.intent]["write"]:
return "OK"
if task.confidence < 0.8 or "order_id" in task.inherited:
return "CONFIRM"
return "OK"

def handle(text, state):
tasks = classify(text)
if tasks and tasks[0].intent == "chitchat":
hint = f"({state.queue[0].intent} 还在等你补充信息)" if state.queue else ""
return ["CHITCHAT 不客气" + hint]
if tasks:
state.queue = tasks # 新意图覆盖旧队列
elif state.queue:
# 没识别出新意图,就把这句话当成上一轮追问的回答
head = state.queue[0]
slot = [s for s in REGISTRY[head.intent]["slots"] if s not in head.slots][0]
head.slots[slot] = text
else:
return ["FALLBACK 你是想查订单、退款,还是开发票?"]

out = []
while state.queue:
task = state.queue[0]
missing = missing_slots(task, text, state)
if missing:
out.append(f"ASK {task.intent} 缺少 {missing[0]}")
break
state.queue.pop(0)
if policy(task) == "CONFIRM":
out.append(f"CONFIRM {task.intent} {task.slots}")
continue
state.last_order_id = task.slots.get("order_id", state.last_order_id)
out.append(f"ROUTE {REGISTRY[task.intent]['agent']} {task.slots} via {task.source}")
return out

if __name__ == "__main__":
state = SessionState()
for line in [
"订单 2024100801",
"我的快递到哪了",
"先把上周那单退了,然后开张发票",
"尺码不合适",
"谢谢",
"蜡笔科技有限公司",
"那个东西你看下",
]:
print(f"> {line}")
for step in handle(line, state):
print(f" {step}")

我在本地用 Python 3.14 运行,输出如下:

> 订单 2024100801
ROUTE order_agent {'order_id': '2024100801'} via rule
> 我的快递到哪了
ROUTE order_agent {'order_id': '2024100801'} via small_model
> 先把上周那单退了,然后开张发票
ASK refund_apply 缺少 reason
> 尺码不合适
CONFIRM refund_apply {'order_id': '2024100801', 'reason': '尺码不合适'}
ASK invoice_issue 缺少 title
> 谢谢
CHITCHAT 不客气(invoice_issue 还在等你补充信息)
> 蜡笔科技有限公司
CONFIRM invoice_issue {'order_id': '2024100801', 'title': '蜡笔科技有限公司'}
> 那个东西你看下
FALLBACK 你是想查订单、退款,还是开发票?

逐行对一下前面讲的几件事。

第一句格式固定,规则层直接拿到订单号。第二句口语化但只有一个意图,小模型层接住,订单号从 state 里补上,因为是只读查询,policy 直接放行。

第三句带“然后”,小模型层主动放手,大模型层返回两个意图,进入队列。退款缺原因,系统追问,开票任务留在队列里没动。

第四句补上原因后,退款任务参数齐了,但订单号是从上下文继承的,policy 把它转成确认;队列继续往下走,轮到开票,缺抬头,继续追问。

第五句“谢谢”被识别成闲聊,没有被错填成发票抬头,还顺带提醒用户有一件事没办完。第七句什么都识别不出来,fallback 给出三个选项。

这个例子刻意省掉了几样东西:CONFIRM 只是打印出来,没有真正等用户回复“确认”;state 只存在内存里,进程重启就丢;“新意图覆盖旧队列”的策略也很粗暴,真实系统里可能要问用户“刚才的开票还要继续吗”。把它接到生产环境之前,这几处都要补。

八、路由错了,靠评估集发现

前面所有设计都在减少错误,但不会让错误消失。上线前后要有办法知道它错在哪。

我的建议是先攒一份标注好的评估集,每条样本写清楚期望的意图列表和参数。样本要覆盖几类情况:典型的单意图说法、多意图、需要上下文的指代、闲聊、以及故意说得含糊的句子。只拿“我要退款”这种标准说法去测,分数会很好看,但没有意义。

有了评估集,可以分层看指标:

看什么怎么算能发现什么
每个意图的准确率和召回率 按意图统计 哪个意图总被误判或漏判
混淆矩阵 期望意图 × 实际意图 退款和退货是不是经常互相串
多意图完全匹配率 意图集合和顺序都对才算对 planner 和大模型层是否可靠
参数抽取准确率 按参数统计 订单号、原因是不是经常抽错
每层的命中占比 规则 / 小模型 / 大模型各接住多少 大模型层是不是被用得太多
fallback 与澄清比例 占总请求的比例 覆盖不足还是追问过度

评估大模型层时,模型版本和提示词版本都要固定下来,并且记录在每次评估结果里。换了任何一个,就重新跑一遍整个评估集,再和上一次对比。单看几条样例觉得“好像更准了”,是最靠不住的判断方式。

线上也有现成的信号:用户在下一轮说“不是这个”“我说的是退款”,或者同一个问题换个说法重复问,基本可以认定上一轮路由错了。把这些对话捞出来,人工确认后补进评估集,评估集就会跟着真实用户的说法一起长。

路由错误发生在写操作上时,代价最高。这也是为什么第四节和第六节反复强调写操作前的确认:评估集只能告诉你错误率是多少,确认步骤才能拦住具体某一次错误。

写在最后

意图识别看起来只是 Agent 系统入口处的一个分类,真做下去会发现,它决定了系统怎么记上下文、什么时候追问、哪些操作必须确认。

我的看法是别一上来就全交给大模型。先用规则和小模型把高频、明确的请求接住,大模型只处理剩下那部分说不清的;写操作前的确认和一份持续更新的评估集,比换一个更强的模型更能让人放心。

本系列会继续往 Agent 的工程细节里走,感兴趣的朋友记得点个关注,后面的文章别错过。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【一起学AI · 03】Agent 总把请求派错?先把意图识别拆成三层
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!