智能路由:意图分类加置信度门控
上一课的并行提问解决了「一次把问题问全」。这一课解决接下来的事:拿到答案之后,消息该交给谁处理——确定性代码、专家 LLM,还是人工。
官方把这件事拆成两个模式:Intent routing(意图路由) 先分类再分发;Confidence-gated routing(置信度门控) 再加一道闸——「答案告诉你是什么,confidence 告诉你要不要做」。两个模式合起来,就是本课的智能路由。全程 12 分钟。
其实你在第一个请求里已经做过一次最朴素的路由:route_ticket.py 用 department 这道 Choice 把工单分给 technical 团队。这一课把它扩展成一套完整的、可以放进生产环境的路由系统。
学完你能做什么
- 用 Choice 完成 Jev 意图路由:把入站消息分到固定意图集(order_status / product_question / return_exchange / complaint),用 Score 同时评出复杂度
- 写出带兜底分支的路由代码:confidence 低于门槛 → 人工;过了门槛 → if/elif 分发到三类处理方
- 按动作风险设置不同阈值:低危动作(查余额)0.6 就放行,高危动作(批准转账)0.85 以上才免确认
- 说清什么时候不该设 confidence 阈值,以及路由不符预期时官方排查路径
- 用 probabilities 做「第二意图抄送」这类门控之外的副路由
你现在的困境
- 我用大模型做意图路由,prompt 里写死了「只输出四个选项之一」,它还是时不时回我一句「这个是订单查询」、在后面跟一段解释。我写了几十行正则加 try/except 去兜,解析代码比业务代码还长
- 我也试过关键词规则路由:「退款」→售后,「物流」→订单。第一周很好用,第二周用户说「我上次买的东西还没到」,规则表从 20 条涨到 200 条,命中率还是上不去
- 模型从不告诉我它「没把握」:百分之五十二和百分之九十八的确信,返回到我手里长得一模一样,都只能硬着头皮执行。查订单错了用户重问一句就完事,可把一笔转账执行错了就是真金白银的资损
什么时候用这一招
当你需要:
- 把入站请求分发给不同处理方:客服工单、语音指令、agent 技能选择
- 模型没把握时能安全退下来:转人工、要确认、或者换个问法
- 贵的资源只花在真正需要的请求上——专家 LLM 不该去回答「查订单」
而且不想:
- 每条消息都过一遍大模型、再写解析代码去抠它的文本输出
- 维护一张永远补不完的关键词对照表
- 模型没有不确定性信号,错了也只能事后救火
🎒 开始前的准备
- 完成了 并行提问与投机扇出:知道多个问题在一次请求里并行、独立评估(本课的 Choice + Score 就靠它)
- 跑通过第一个请求:
.venv里装好了typesafe-sdk,TYPESAFE_API_KEY已在环境变量里 - 大致记得 Choice 和 Score 的写法(忘了也没关系,第 1 步会带着写)
核心思路
先分类,再分发。 官方对 Intent routing 的定义:
对入站请求分类,并把每个请求路由到最优处理方:确定性逻辑、专家 LLM,或人工。
不是每条消息都需要同一种处理方:有些查一下数据库就答了,有些需要带领域上下文的 LLM,有些必须人来。Jev 坐在所有处理方前面,当那个「快而便宜」的分类器——官方文档说多数调用大约 100 毫秒完成(docs/concepts/how-to-build-with-system-one.md:55),贵的东西只被真正需要它的请求触发。
再加一道闸。 官方对 Confidence-gated routing 的定义:
把 confidence 当作第二个轴。答案告诉你「是什么」,confidence 告诉你「要不要做」。
Jev 的每个 Choice 和 Score 答案都自带 confidence(0–1,由概率分布的集中程度算出)。官方的三档用法:
| confidence | 行为 |
|---|---|
| 高 | 自动执行,不需要人参与 |
| 中 | 谨慎进行:请用户确认、标记复核、或先补信息 |
| 低 | 不要行动:转人工、要求澄清、或回退到别的系统 |
两个模式拼起来,就是一条完整的路由流水线(官方客服路由示例的结构):
customer message
│ 一次请求:消息 + 2 个问题
▼
┌─────────────────────────────┐
│ intent(Choice) │ ← 并行评估
│ complexity(Score) │
└─────────────────────────────┘
│ 一次响应:2 个带 confidence 的答案
▼
intent confidence ≥ 0.5?──否──▶ 人工
│是
▼ 按 intent.choice 分发
├─ order_status → 订单查询(确定性代码,无 LLM)
├─ product_question → 产品专家 LLM
├─ return_exchange → 退换货专家 LLM
└─ complaint → complexity > 1 或它自己没把握?──是──▶ 人工
│否
▼
投诉处理 LLM为什么不用大模型 prompt 直接路由?
| 对比维度 | LLM prompt 路由 | Jev 意图路由 |
|---|---|---|
| 返回格式 | 自由文本,输出 order_status 还是「订单查询」随缘 | 类型化答案,必是预定义选项之一 |
| 解析代码 | 正则 + try/except + 重试 | 拿选项名直接 if/elif,零解析 |
| 意图之外的信息 | 另发一次请求再解析一次 | 同一请求并行加一道 Score |
| 不确定性信号 | 拿不到 | 每个答案自带 confidence |
| 加新意图 | 改 prompt 重新调试 | 请求里加一个选项,分发代码加一个分支 |
那为什么还要留 confidence 这道闸?拿掉它,top1 概率 0.52 和 0.98 的两个答案在你手里长得一模一样。四类消息里混进一条「既问订单又情绪激烈」的,意图在 order_status 和 complaint 之间分裂,答案照样给你一个 winner——只是概率摊薄了、confidence 掉下来了。门控让你在这种时刻选择「先别做」。
confidence 的计算公式和完整的三档解读在 Confidence 置信度 课;本课专注怎么把它用在路由上。
跟我做
第 1 步:定义意图分类与复杂度两个问题
为什么:路由能落地,前提是答案是代码能直接对照的东西。Choice 给你「四选一」的意图,Score 给你「多复杂」的尺子——这两个信息分别决定「交给谁」和「能不能自动」。
这是官方模式页 Playground 里给出的两个问题定义(从渲染页提取,选项描述为官方原文):
from typesafe_sdk import Choice, Score
ROUTING_QUESTIONS = {
"intent": Choice(
instructions="The primary intent of this customer message",
criteria={
"order_status": "Asking about an existing order",
"product_question": "Asking about a product before buying",
"return_exchange": "Wants to return or exchange something",
"complaint": "Unhappy with experience, wants resolution",
},
),
"complexity": Score(
instructions="How complex is this request to resolve",
criteria=[
"Simple lookup or standard procedure",
"Requires some judgment or multi-step process",
"Unusual situation, edge case, or escalation needed",
],
),
}三个细节:
- 选项描述要能互相区分。 模型看得到选项名和描述,看不到问题 ID。描述写「Asking about an existing order」(问已有订单)而不是「订单相关」,四个意图才不会挤在一起
- 复杂度量表的第三级是「escalation needed」。 路由代码正是拿这一端当升级人工的信号(第 4 步)
- 列表可能不全时加
other/none of the above选项,让模型能说「都不对」——官方建议,详见 Choice 课。本例四个意图覆盖了示例系统的全部入口,所以没加
你应该看到:ROUTING_QUESTIONS 里两道题,一道 Choice 四个意图选项,一道 Score 三级复杂度量表。
- 我的每个意图选项都有一句能和其他选项区分开的描述
第 2 步:一次请求拿到两个类型化答案
为什么:意图和复杂度互不依赖——上一课讲过,独立问题就该并进一次请求:几乎不增加响应时间,只多花一点问题 token。
新建 smart_route.py,接上第 1 步的问题定义,发一条清晰的消息试试:
from typesafe_sdk import TypeSafeClient
# ROUTING_QUESTIONS 的定义见第 1 步,此处省略
message = "Hi, where is my order? It was supposed to arrive last Tuesday and tracking hasn't updated."
with TypeSafeClient() as client:
response = client.system_one(
state=message,
questions=ROUTING_QUESTIONS,
)
intent = response.answers["intent"]
complexity = response.answers["complexity"]
print(intent.choice, intent.confidence)
print(complexity.score, complexity.confidence)运行它:
.venv/bin/python smart_route.py你应该看到:第一行是 order_status 加一个接近 1.0 的 confidence——这条消息只有一个清晰的意图,概率会全部集中在这个选项上(官方 quickstart 示例里那条只谈 API 500 报错的工单,department 的 confidence 就是 1.0)。第二行是复杂度分数和它自己的 confidence,本步先拿到手,第 4 步再用。具体数值以你的实际返回为准,随 Jev 版本可能变化。
换一条混合意图的消息再跑一次:
message = "This is the third time I'm writing and nobody has replied. Where is my order? I'm done waiting — I want my money back."你应该看到:intent.choice 还是会给出一个赢家的名字(它必须给——choice 永远是概率最高的选项),但这次概率摊在 order_status / return_exchange / complaint 好几个选项上,intent.confidence 明显掉下来。官方 Choice 课文档对同类情况的记载:一条同时提到尺码错误和重复扣费的工单,returns 0.61 / billing 0.35,confidence 只有 0.42——消息属于两个团队,分布就诚实地分裂。
这个掉下来的 confidence,就是第 3 步门控要吃的信号。
- 清晰消息:confidence 高;混合消息:confidence 明显下降
第 3 步:写出 Jev 意图路由的分发代码
为什么:路由的本质就是 if/elif——前提是答案干净到不用解析。Jev 保证 choice 必是 criteria 里的选项之一,所以分发代码可以穷举。
下面是官方模式页的 route_ticket 原样代码(docs/patterns/intent-routing.md)。处理函数(handle_order_status、handle_with_llm 等)是官方占位名,替换成你自己的业务逻辑:
def route_ticket(ticket_id, response):
intent = response.answers["intent"]
complexity = response.answers["complexity"]
if intent.confidence < 0.5:
# If we don't have enough confidence to classify, route to a human agent
return route_to_human_agent(ticket_id)
if intent.choice == "order_status":
handle_order_status(ticket_id)
elif intent.choice == "product_question":
handle_with_llm(ticket_id, PRODUCT_SPECIALIST)
elif intent.choice == "return_exchange":
handle_with_llm(ticket_id, RETURNS_SPECIALIST)
elif intent.choice == "complaint":
low_confidence = complexity.confidence < 0.5
# A higher complexity.score leans toward the "escalation needed" end of the scale.
if complexity.score > 1 or low_confidence:
# Too complex for safe automation, or we're not sure about the complexity; route to a human.
route_to_human_agent(ticket_id)
else:
handle_with_llm(ticket_id, COMPLAINT_RESOLUTION)对着第 2 步的两条消息跑:
route_ticket("T-1024", response)你应该看到:
- 清晰消息(order_status、高 confidence)→ 走人工的分支不触发,交给
handle_order_status——确定性代码,一次数据库查询就答了,零 LLM 参与 - 混合消息(confidence 掉到 0.5 以下)→ 直接
route_to_human_agent,错误在执行前就被拦下
读一遍这段代码的结构,四个分支正好对应三类处理方:
| intent | 处理方 | 代价 |
|---|---|---|
order_status | 确定性代码(订单查询) | 最便宜,无 LLM |
product_question / return_exchange | 各自的专家 LLM | 每个装着不同的领域上下文 |
complaint(复杂或没把握) | 人工 | 最贵,只留给真正需要的 |
官方对这段代码的总结:分类在一次快速调用里全部完成,昂贵的资源只被真正需要它们的请求触发。
- 我的路由代码第一条路径是「confidence 不够 → 人工」,而不是直接分发
第 4 步:给 complaint 分支加复杂度二次门控
为什么:第 3 步的门控只看了意图自身的把握。但有些意图天然要再问一层:投诉可能是「物流慢了发句牢骚」,也可能是「威胁要发社交媒体曝光」——同一个意图标签,处理代价天差地别。这层判断交给同一次请求里已经拿到的 complexity 答案,不用第二次调用。
回看第 3 步代码里的 complaint 分支:
elif intent.choice == "complaint":
low_confidence = complexity.confidence < 0.5
# A higher complexity.score leans toward the "escalation needed" end of the scale.
if complexity.score > 1 or low_confidence:
# Too complex for safe automation, or we're not sure about the complexity; route to a human.
route_to_human_agent(ticket_id)
else:
handle_with_llm(ticket_id, COMPLAINT_RESOLUTION)两个门控条件,各拦一种情况:
complexity.score > 1:分数越过了中间级,偏向量表「escalation needed」那一端——能自动,但不敢自动的,转人工complexity.confidence < 0.5:连复杂度本身都没评估明白——信息不够,也转人工
官方在这段代码后面特别提醒:对 Score 答案同样要做 confidence 检查,低 confidence 的含义要结合系统语境和决策代价来读。
你应该看到:对第 1 步那条「第三次写信了,没人理,我要退钱」的消息,意图大概率落在 complaint 一带;复杂度高或没把握时,它落进人工分支而不是投诉 LLM——多花一个人工席位,换一次不激化客诉的安全处理。
- 我能说出 complaint 分支两个转人工条件分别拦的是「太复杂」和「没把握」
第 5 步:按风险缩放置信度阈值(语音银行示例)
为什么:前面 0.5 的门槛是给「分类这件事」兜底的。但即使分类对了,不同动作的容错也不同:错播一遍余额,用户再听一次就完事;错批一笔转账,就是资损。官方的第二个模式页把这层逻辑做成了语音银行示例。
它的意图问题(同样从官方 Playground 提取):
BANK_INTENT = Choice(
instructions="What action is the user requesting?",
criteria={
"check_balance": "Check the balance of an account",
"approve_transfer": "Approve the pending transfer request",
"other": "Something else",
},
)门控代码(官方原样,docs/patterns/confidence-routing.md):
action = response.answers["intent"]
# Below 0.6 confidence on any action, route to a human
if action.confidence < 0.6:
route_to_support_agent(account_id)
elif action.choice == "check_balance":
# Low stakes. 0.6 confidence is sufficient.
show_balance(account_id)
elif action.choice == "approve_transfer":
if action.confidence > 0.85:
# High stakes, but high confidence. Safe to act automatically.
approve_transfer(account_id)
else:
# High stakes, moderate confidence. Verify intent first.
ask_user_to_confirm("Just to confirm: you would like to approve this transfer, is that correct?")
else:
route_to_support_agent(account_id)同一套选择题,四个出口,threshold 随风险走:
| 情况 | 阈值 | 出口 |
|---|---|---|
| 任何动作,confidence < 0.6 | 兜底线 | 转人工客服 |
check_balance,≥ 0.6 | 低危 | 直接播报余额 |
approve_transfer,0.6–0.85 | 高危中等把握 | 先向用户口头确认再执行 |
approve_transfer,> 0.85 | 高危高把握 | 自动批准 |
other(列表外意图) | — | 转人工客服 |
官方对这套数字的解释:0.6 的兜底线接住模型真正不确定的所有情况;底线之上,每种动作按「错误执行的代价」有自己的阈值——查余额 0.6 就够,因为最坏情况只是用户多听一遍播报;批准转账需要 0.85 以上,否则系统应当先确认。
试试这三条命令(替换第 2 步的 state 和问题定义,逐条发请求):
for message in [
"What's my account balance?", # → check_balance
"Yes, go ahead and approve the transfer.", # → approve_transfer
"Can you help me with something else?", # → other
]:
response = client.system_one(state=message, questions={"intent": BANK_INTENT})
# …接下面的门控代码你应该看到:第一条自动播余额;第二条大概率落在 > 0.85 自动批准或 0.6–0.85 先确认两个分支之一——两个分支都是合理出口,这正是门控的意义;第三条走 else 转人工。
两套数字不要混淆:官方另一个同场景示例(docs/confidence.md 的「Thresholds scale with risk」节,也在 Confidence 课 讲过)用的是 0.5 兜底 / 转账 > 0.9。两个都是官方示例,数字不同、模式相同——差异本身就是官方想说的,它在同节的 NOTE 里明确写道:
正确的阈值取决于你的领域和模型在你的用例上的表现。从保守的阈值开始,用自己的数据测试,再随着观察调整。
💡 中文消息路由
官方说明英语是主要训练语言、准确度最好,包括中文在内的其他语言能用但准确度较低。中文消息的路由系统上线前,先用自己的数据跑一批,重点看 confidence 的分布——门控是否误拦、误放,比看单条答案对错更重要。
检查点 ✅
- 我能说出智能路由的两步:Choice/Score 分类意图与复杂度,confidence 决定放不放行
- 我知道三类处理方:确定性代码(无 LLM)、专家 LLM、人工——以及各自适合的 intent
- 我的路由代码第一条路径是人工兜底(如
intent.confidence < 0.5→ 人工) - 我知道阈值随风险缩放:低危 0.6 即可,高危(转账审批)要 0.85 以上
- 我知道路由不符预期的官方排查顺序:先查阈值和问题,而不是先怀疑模型
踩坑提醒
❌ 到处设 confidence 阈值
把「低于 X 就转人工」的判断塞满所有代码路径,是最常见的滥用。官方 agent-skill 文档的提醒:
如果你只关心选出最优选项,直接选 confidence 最高的那个即可(而不是设一个 confidence 阈值)。如果你有特定的统计算法需求,大概率应该用 probabilities 而不是 confidence。
什么时候才用门控:答案要触发真实动作、且动作有代价差异时。只做排序、只要 top1 的场景,直接取 choice 就是了。
❌ 路由不符预期,先去改问题措辞
症状:召回率不对——该转人工的没转(假阴性),不该转的转了(假阳性)。官方排查顺序是先查阈值,再查问题:
- 阈值设太高 → 假阴性(大量该自动的被转人工)
- 阈值设太低 → 假阳性(大量没把握的被自动执行)
- 阈值真的没问题,再把问题写得更具体(比如选项描述区分度不够)
❌ 一个系统只用一个阈值
0.5 给分类兜底、0.6 给低危动作、0.85 给高危动作——上面的语音银行示例里这三个数字同时存在。官方明确说「confidence threshold 不是一个数字」:同一个系统里不同动作按「做错的代价」分别设门槛,你的代码体现的是风险容忍度。只有一个阈值的系统,要么拦太多(人工量爆炸),要么放太多(高危动作裸奔)。
❌ 意图列表没有「都不对」的出口
选项列表没覆盖所有输入时,模型被迫从错误的集合里挑一个最不差的——而且会自信地选一个(官方在 function calling cookbook 里对参数选择的原话:「没有 stated 问题,choice 就必须硬选一个 window,而且会自信地选一个」)。正确做法:列表可能不全就加 other / none of the above 选项,并在代码里给 other 一个明确出口(上面的 else → 转人工)。官方还提醒 none of the above 在很多场景效果一般,备选方案是同一请求里加一道伴随 Noul,问「这条消息是否提到了我们产品中的任何一个」,代码只在 Noul 为 yes 时读 Choice 答案。
进阶技巧(可选)
用 probabilities 做第二意图抄送。 门控之外,probabilities 还能当副路由信号。官方 Choice 文档的五题 triage 示例(一条同时涉及退换货和重复扣费的工单)里有这样一段:
# A second team with a real share of the probability gets a copy
for team, probability in answer.probabilities.items():
if team != answer.choice and probability > 0.25:
notify(ticket, team=team)主路由给 returns 团队,billing 因为拿到 0.35 的概率份额(超过 0.25 阈值)也收到抄送——比「confidence 低就全转人工」更精细的第三条路:主答案照常执行,次优答案通知到人。官方对该工单的记载:工单分给 returns(issue=wrong_size),billing 收到抄送,客户被追问想要什么(resolution 的 confidence 0.20 低于 0.5)——一次请求、五个答案,全部门控完成。
两个替代门控指标。 如果 confidence 的公式在你的场景里不合口味,官方推荐两个同样从 probabilities 算的指标:一是 top 概率 p_max(读作「选中项多可能」,阈值按选项数设——0.5 在两个选项里弱、十个选项里强);二是 top-to-second 比 p_max / p_second(衡量第一名对第二名的优势,专治「两强相争」的路由场景)。
门控思想的其他形态。 这个「答案 + 置信度不够就退一级」的模式不止一种退法:
- 75 类行业分类:SEC 年报分 75 个行业组,confidence 不够 0.9 就上报它所属的大类(division)——官方实测 60 份文件,确定的一半 90% 命中、不确定的一半只有 40%,上报大类后拉到 70%
- Agent 决策:从 194 个技能里给 agent 挑技能——两次请求 + 两个阈值,错误加载直接减半
- 官方 how-to 手册还有个最小版门控(
confidence < 0.8就转人工复核),四行代码,适合当你的第一版
本课小结
✅ 智能路由 = Jev 意图路由 + 置信度门控:Choice 分类意图、Score 评复杂度,一次请求并行拿回,confidence 决定放不放行。
✅ 三类处理方各得其所:确定性代码处理机械意图(零 LLM)、专家 LLM 处理领域问题、人工处理低置信与高危。
✅ 门控结构是「兜底线 + 风险阈值」:任何动作先过兜底线(示例:0.5 / 0.6),过了线再按动作代价各自设阈值(查余额 0.6、转账 0.85)。
✅ 意图列表可能不全时加 other / none of the above,并给它明确出口;模型被迫硬选时是「自信地选错」。
✅ 只挑最优不需要阈值;排序、抄送这类需求用 probabilities。
关键事实速查:
| 事项 | 值 | 出处 |
|---|---|---|
| 意图题 instructions | The primary intent of this customer message | 官方 Playground(patterns/intent-routing) |
| 意图选项 | order_status / product_question / return_exchange / complaint | 同上 |
| 分类兜底 | intent.confidence < 0.5 → 人工 | docs/patterns/intent-routing.md |
| complaint 二次门控 | complexity.score > 1 或 complexity.confidence < 0.5 → 人工 | 同上 |
| 银行兜底线 | confidence < 0.6 → 人工 | docs/patterns/confidence-routing.md |
| 转账阈值 | > 0.85 自动执行,0.6–0.85 先确认 | 同上 |
路由系统是典型的「关键路径上跑 Jev」,上线前请配合 Jev 1.13 踩坑手册过一遍九种失败模式(尤其字面理解和大 state 无关细节两条)。
下一课预告
第 4 步用一道 Score 评复杂度,已经能裁剪人工量。下一课把这个思路推满——组合打分:把复杂判断拆成原子分数再加权。
你会学到:
- 把「这单投诉多严重」这类复杂判断拆成多个原子 Score 问题
- 归一化(除以最高级号)把不同量表的分数拉到同一尺度
- 在代码里加权合并出总分,权重即风险表态
- 排序不合预期时只改权重常量,不动任何 prompt