第 4 章:Classification 与 Routing——把分诊做成一等公民
第 4 章:Classification 与 Routing——把分诊做成一等公民
大多数 AI 应用写下的第一段真实逻辑都是分诊:这条输入是什么,接下来交给谁。Classification 回答"是什么",Routing 回答"去哪",在 Jev 里都落在 Choice 上,却是两种不同的设计。本章讲清标签集怎么立、概率分布怎么用、路由的成本账怎么算,再用三个生产项目把它落到地上。
分诊为什么值得当成一等公民
在没有决策层的年代,分诊通常这样写:
resp = llm.chat(f"这段话属于[退款, 咨询, 投诉]里的哪一类?只回答类别名:{text}")
category = resp.text.strip()这段代码有两个隐患。一是类别名是自由文本,模型可能返回"退款投诉""售后服务"这种复合词,你的 if/elif 全部落空;二是它只给一个标签,不给分布,你无从判断这次分诊到底有多笃定。而分诊恰恰是系统里被调用次数最多、下游分叉最多的一次判断——错一个,整条处理链都走偏。
Jev 把它收窄成一次带分布的 Choice:
from typesafe import TypesafeClient
client = TypesafeClient()
d = client.systemone(
state=text,
questions={
"category": {
"type": "choice",
"question": "这条输入属于哪一类?",
"choices": ["退款", "咨询", "投诉"]
}
},
instructions="你是客服分诊员,只看诉求本身,不要被措辞里的情绪带偏。"
)
print(d["category"].choice) # "退款"
print(d["category"].probabilities) # {"退款": 0.82, "咨询": 0.11, "投诉": 0.07}选项封闭、概率归一,代码拿到的不再是一个字符串,而是一个能直接喂给阈值策略的分布。分诊从"提示词里的祈祷"变成了接口上的一个字段,这就是"一等公民"的含义。
Classification 与 Routing:是什么 vs 去哪
这两个形态长得几乎一样——都是 Choice,都给一组选项——但设计意图不同。
Classification 关心"它是什么"。 输入是一条待归纳的东西,输出是它的类别。类别的意义是描述性的:这条工单属于退款、这个文档属于合同、这封邮件属于工作。它的下游用法通常是"按类别做聚合或统计"。
Routing 关心"它该去哪"。 输入是一条待处理的请求,输出是它应该走的处理路径。选项的意义是指向性的:给小模型还是大模型、给人工还是自动、给哪个团队。它的下游用法通常是"按路径分派"。
用一句话记住:Classification 的选项是名词(它是什么),Routing 的选项往往对应动作或目的地(送去哪)。同一段代码写错形态最常见的情况,就是把一个路由目标硬塞进分类体系里——比如类别写成"退款组""咨询组",名字里其实混进了组织架构。
flowchart LR
S["同一条 state"] --> C{"Classification
它是什么?"}
S --> R{"Routing
它该去哪?"}
C --> C1["标签:退款 / 咨询 / 投诉"]
C1 --> C2["按类别聚合、统计、展示"]
R --> R1["路径:小模型 / 人工 / 退款组"]
R1 --> R2["分派到对应处理流程"]一个系统里两者经常串联:先 Classification 判明性质,再 Routing 决定去向。下面就用真实项目看这两种串法。
标签集怎么设计:MECE、覆盖与"其他"桶
Choice 的选项是封闭的,这意味着标签集的设计工作量全在你这边。三条准则:
其一,互斥且穷尽(MECE)。 每个输入应该恰好落进一个类别。如果两个类别会同时命中,说明你其实需要多标签(下一节处理);如果总有输入落不进任何类别,就该补一个"其他"。
其二,覆盖要留"其他"桶,但要盯住它的占比。 一个健康的分类体系的"其他"占比应该很低且稳定。"其他"占比突然上升,通常是你的类别体系该迭代了——业务变了,标签没跟上。把"其他"当一个监控指标,而不是一个垃圾桶。
其三,选项数量要克制。 Choice 支持 2–255 个选项,但选项越多,判定越难、分布越平。经验上超过十几类就该考虑分层:先分大类(Choice),再在大类内部细分(第二个 Choice)。分层既降低单次难度,也让标签集更可维护。
qs = {
"domain": {
"type": "choice",
"question": "这个请求首先属于哪个领域?",
"choices": ["账号", "账单", "功能", "技术", "其他"]
}
}
# 拿到 domain 后,若为"账单",再发一次更细的 Choice并发标签:用多个 Noul 拆开
Choice 是"从 N 个里选一个"。但现实里很多分类是多标签的:一篇文章既谈技术也谈商业,一个工单既是 Bug 也涉及体验。这时候不要勉强用 Choice,而是给每个标签问一个 Noul。
qs = {
"is_bug": {"type": "noul", "question": "这是否是一个缺陷报告?"},
"is_billing": {"type": "noul", "question": "这封邮件是否与账单有关?"},
"is_feature": {"type": "noul", "question": "这是否是一个功能建议?"}
}每个 Noul 独立给出 P(true),于是你得到的是一个并发的标签集合,而不是一个互斥的类别。工程上有两点好处:一是每个标签的阈值可以单独调("缺陷"阈值压低宁可多报,"功能建议"阈值抬高);二是标签之间可以自由组合,不用担心"选了 A 就没法选 B"。
反过来,只有当类别真的互斥时,才用 Choice。判断标准很简单:两个标签有可能同时为真吗?会,就用多个 Noul;不会,就用一个 Choice。这个选择在邮件分类里尤其常见:一封邮件既是"账单"又是"紧急",两个维度必须拆成两个问题。
概率分布怎么用:不只看 choice
这是新手最容易漏掉的一点。Choice 返回一整个分布,而你往往只看那个被选中的 choice 字段。分布本身就是可用信息。
p = d["category"].probabilities
top = max(p, key=p.get)
if p[top] >= 0.7:
auto_route(top) # 笃定,直接走
elif p[top] >= 0.4:
queue_for_review(top) # 模糊,进复核队列
else:
to_human(text) # 分布摊平,判不出来,交人看两个分布的对比:
{"退款": 0.88, "咨询": 0.08, "投诉": 0.04}——几乎确定是退款,自动处理。{"退款": 0.36, "咨询": 0.34, "投诉": 0.30}——三条腿几乎一样长,这条工单本身就模糊,任何自动分派都是赌。
Jev 在这两种情况下都可能把 choice 标成"退款",但概率分布不会骗你。"低置信转人工"这个策略的全部依据,就是这个分布。 没有分布,你无法区分"笃定的退款"和"三者难分时恰好排第一的退款"。
Routing 的成本—能力权衡
Routing 有一个 Classification 没有的负担:它的错误代价直接和钱挂钩。路由决定了下游花多少资源,所以两个方向的错误代价都很高:
把难题路由给小模型——省了钱,但答错。用户拿到一个敷衍的结果,甚至任务失败。这类错误的代价是质量。
把简单题路由给大模型——答对了,但花了不该花的钱。这类错误的代价是成本。
两个方向不对称,所以路由的阈值不该设在"正好一半",而要按你的成本结构去调。一个常见的做法是分档而不是二分:把请求分成"小模型 / 中模型 / 大模型"三档,让绝大多数请求落在便宜的那一档,只把少数真正复杂的送上去。
qs = {
"tier": {
"type": "choice",
"question": "这个请求需要哪一档模型才能可靠完成?",
"choices": ["小模型", "中模型", "大模型"]
}
}路由还有个容易被忽视的约束:稳定性。同一个会话里如果每次请求都重新路由,很可能这次走小模型、下次走大模型,把 prompt 缓存打碎,反而更贵。所以生产级路由器往往会把首次选定的路由"钉住"整个会话——这正是后面 Switchboard 做的事。
三个生产项目
inbox-zero(elie222/inbox-zero)——开源邮件助手的分诊。 inbox-zero 是一个成熟的开源 AI 邮箱助手,它用 TypeSafe Jev 的 System One 决策模型对收件邮件做意图分类和行动项分诊。它面对的正是本章开头的场景:每封进信都要判一次"这是什么类型的邮件、需要我做什么"。邮件分类天然的痛点是标签重叠——一封邮件既可能是账单也包含行动请求——所以这里的正确姿势是把"意图"和"是否需要行动"拆成两类问题,而不是塞进一个大 Choice。
Paca(Paca-AI/paca)——把分诊当成产品的一等能力。 Paca 是自托管的开源 Jira 替代品。它把 Jev 用在了三个地方:用 Choice 在读成员描述的基础上自动分配任务;用 Choice 和 Score 填充空白的任务字段;用 Choice/Score/Noul 组成的条件节点处理自动化工作流的走向。关键的一个设计细节是它的置信度门槛:只有达到 0.6 及以上的答案才被真正应用,否则任务保持未分配、或走 Else 分支。 这就是上一节说的"低置信不硬做"——Paca 宁愿让一条任务空着等人,也不肯把一个 0.5 置信度的错误指派写进系统。对一个项目管理工具来说,乱指派比不指派伤害更大。
jev-oncall(minglei/jev-oncall)——一次调用做完整个告警分诊。 值班告警是分诊的高压场景:一条告警来,要在几秒内决定"要不要叫醒人、叫谁、是不是重复"。jev-oncall 的做法是每条告警一次 Jev 调用,里面塞四个类型化问题:Noul 判断这事是否可行动、Score 判严重度、Choice 判归属团队、Choice 判是否重复。然后它在代码里做两段联动:P(SEV1) + P(SEV2) ≥ 0.80 才寻呼,只有在可行动性也同意时才降到 0.20 以下的直接丢弃;中间那段灰色地带交给人,人有 15 分钟 ack 时间,否则照样寻呼。它还用一次破损语义的调用把重复告警连成事件图,避免两条告警互相静音。实测数据:一次 300 条告警的运行里,p50 418 ms、p95 1477 ms、每千条告警 $0.04,最慢的一次调用比 2 秒超时线只差 151 ms。
这个项目几乎把本章所有要点演了一遍:多个类型化问题拆开并发判断(可行动性/严重度是不同维度,不能用一个大 Choice)、概率分布驱动阈值(0.80 与 0.20 两条线而不是一条)、以及路由到人的兜底。它同时也说明——分诊的正确设计从来不是"选一个标签",而是"几个正交问题 + 代码里的联动规则"。
其余项目一张表带过
| 项目 | 场景 | 关键设计 |
|---|---|---|
| DocJev(jerryjliu/docjev) | 文档流水线 | 按自然语言类别规则分类文档 / 找子文档边界;40 篇文档试点 40/40 正确,Jev 决策 p50 约 182 ms |
| Notra(usenotra/notra) | 营销分析 | NOTRA_JEV_CLASSIFIERS 开关把品牌可见度分类从 LLM 切到 Jev 布尔决策,阈值 0.5,目标 p50 300 ms |
| jev-logtriage | 值班运维 | 把折叠后的 Loki 日志打成一次 Jev 调用(Noul + Score + Choice),代码里映射为 suppress/watch/review/notify/page |
| jev-resume-disqualifier | 招聘 | 先问"淘汰性问题",25 ms 内把简历踢出流程,只有幸存者才进入完整评估 |
| Switchboard(ruban-24/switchboard) | 编码 Agent | 为每个 Claude Code / Codex 任务匹配模型与推理档位,并把选择固定整个会话以保住 prompt 缓存 |
| jev-table-import-mapper | 数据导入 | 先用确定性名称相等做一轮映射,剩下的每对(源列, 目标列)问一个 Jev Noul,外加每列一个守卫 Noul;23 列导出映射中 10/10 列命中,一次调用 253 个问题、915 ms、$0.0012,0.75 阈值以下的不猜、留可见 |
| AI-decision-maker | 数据清洗 | 用 Jev Choice 把 CSV 列分进 13 类类型词表、每个数据集分进 6 个场景之一;实测 Jev 的 token 成本是某个 LLM 的 6.6–12.7×(因为输出只有一个字符、判据可复用) |
注意 DocJev 与 jev-table-import-mapper 都体现了同一个原则:能用确定性代码做的事先做掉,只把剩下的模糊判断交给 Jev。 jev-table-import-mapper 的"名称完全相等"是免费的、百分之百准的,没必要送给模型;只有名称不匹配的列才值得问 Jev。这条原则会贯穿整本书。
小结
- Classification 回答"它是什么"(选项是名词),Routing 回答"它该去哪"(选项指向动作/目的地),两者都是 Choice,但设计意图不同,常串联使用。
- 标签集要 MECE、要留"其他"桶并监控其占比,选项太多就分层;多标签场景用多个 Noul 而不是一个 Choice。
- 不要只看
choice,要看整个概率分布——分布摊平往往意味着任务本身模糊,这是"低置信转人工"的唯一依据。 - Routing 的成本—能力权衡是两个方向的不对称代价(难题给小模型伤质量,简单题给大模型伤成本),阈值要按成本结构调,并注意会话内的路由稳定性(别打碎 prompt 缓存)。
- Paca 的 0.6 门槛、jev-oncall 的 0.80/0.20 双线与 300 条告警实测(p50 418 ms / $0.04 每千),都是"用概率驱动分诊"的落地样板。
- 能用确定性代码判定的部分先做掉,只把真正模糊的判断交给 Jev。
下一章转向另外两种底层输出:用 Noul 做 Detection(有没有),用 Score 做 Scoring(多少分),处理二元判断与有序评分这两类最常见、也最容易被做坏的决策。