第 4 章

第 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(多少分),处理二元判断与有序评分这两类最常见、也最容易被做坏的决策。

📑 Jev 实战:用类型化决策重构软件

1 第 1 章:为什么需要决策层——从"生成一切"到"只做判定" 2 第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约 3 第 3 章:十个决策形态——什么时候该把判断交给 Jev 4 第 4 章:Classification 与 Routing——把分诊做成一等公民 5 第 5 章:Detection 与 Scoring——二元判断与有序评分 6 第 6 章:Search 与 Retrieval——在候选集里找相关 7 第 7 章:Ranking 与 Verification——排序与校验 8 第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据 9 第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs 10 第 10 章:模型路由与 LLM 护栏——Model routing / LLM guardrails 11 第 11 章:语义代码检查——Semantic code linting 12 第 12 章:特征提取与需求预测——Feature extraction / Demand forecasting 13 第 13 章:招聘与线索生成——Recruiting / Lead generation 14 第 14 章:客户支持——Customer support 15 第 15 章:理赔、金融犯罪与风险评估——Insurance claims / Financial crime / Risk assessment 16 第 16 章:法律与合规——Legal and compliance 17 第 17 章:电商市场与广告——E-commerce marketplaces / Advertising 18 第 18 章:内容审核与信任安全——Moderation and trust and safety 19 第 19 章:游戏——Gaming 20 第 20 章:实时应用——150 毫秒预算 21 第 21 章:科学发现——Scientific discovery 22 第 22 章:大数据上的 AI Map Reduce——为什么便宜 100 倍 23 第 23 章:通用验证——Universal Verification 24 第 24 章:Harness Engineering——把决策织进智能体骨架
← 返回本书大纲