第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约
第 2 章:三种类型化输出——Choice、Score、Noul 与调用契约
上一章说 Jev 输出"类型化决策",但类型化到什么程度?答案是:只有三种——多选、有序评分、布尔。听起来少得可疑,但正是这种克制让判别变得可组合、可测试。本章拆开调用契约,把三种输出的语义、概率和边界用法讲透。
一次调用长什么样
Jev 的核心接口是 POST /v1/systemone,模型名通常是 jev-latest(也可以锁定到具体版本如 jev-1.13)。一次调用由三部分组成:
- state:非结构化的上下文。可以是用户输入、文档片段、代码、检索结果、工具返回值——任何"需要被判断的东西"。
- questions:一组类型化的提问。这是关键:每个问题都要声明它的类型(
choice/score/noul),可选地给出选项或打分范围。 - instructions:系统层面的指令,描述角色、判断标准、边界。相当于 system prompt,但服务的对象是决策而不是生成。
返回值就是与 questions 一一对应的类型化决策,每个决策都带概率。用官方 Python SDK 写出来大致是这样:
from typesafe import TypesafeClient
client = TypesafeClient() # 读取 TYPESAFE_API_KEY
decision = client.systemone(
state="订单号 88371,用户申请退款,理由:收到时包装破损,商品本身完好。",
questions={
"intent": {
"type": "choice",
"question": "这条工单的核心诉求是什么?",
"choices": ["退款", "换货", "咨询", "投诉"]
},
"urgency": {
"type": "score",
"question": "这条工单的紧急程度如何?",
"labels": ["低", "中", "高"]
},
"needs_human": {
"type": "noul",
"question": "是否必须人工介入才能处理?"
}
},
instructions="你是电商客服分诊助手,按工单的实际诉求判断,不要被情绪词带偏。"
)返回值里,intent 给的是带概率的选项,urgency 给的是有序评分,needs_human 给的是布尔概率。三种类型,覆盖了绝大多数判别任务。下面逐个说清。
Noul:布尔判断
Noul 回答的是"是/否",返回一个 P(true)。官方之所以不叫它 Boolean,是想强调它返回的不是一个干巴巴的 true/false,而是带概率的布尔。
needs_human = decision["needs_human"]
print(needs_human.value) # True / False
print(needs_human.probability) # 例如 0.87用法上,Noul 是十种决策形态里 Detection 和 Verification 的底层形态:这条评论是不是垃圾、这段代码有没有 SQL 注入、这个回复有没有捏造事实——凡是能归约成"是/否"的,都用 Noul。
实际工程里,Noul 最重要的不是那个布尔值,而是概率。你可以据此设阈值:
p = decision["needs_human"].probability
if p >= 0.9:
escalate_now(ticket) # 高置信,直接人工
elif p >= 0.5:
queue_for_review(ticket) # 中等,进入待复核队列
else:
auto_reply(ticket) # 低置信,自动处理阈值不是拍脑袋定的,而是对着你的业务代价调出来的——漏判的成本高就压低阈值,误判的成本高就抬高阈值。第 5 章会专门讲校准。
Choice:多选
Choice 回答的是"从 N 个选项里选一个"。返回每个选项的概率,所有概率之和为 1。选项数量支持 2 到 255 个。
intent = decision["intent"]
print(intent.choice) # "换货"
print(intent.probabilities) # {"退款": 0.12, "换货": 0.79, "咨询": 0.05, "投诉": 0.04}三个工程要点:
第一,选项是封闭的。 模型只能在给定的选项里选,不可能给你一个"其他"。这消灭了"解析自由文本"的全部麻烦。代价是你要提前穷举可能的类别——这也是分类任务真正的设计工作量所在。
第二,概率分布是连续的可用信息。 0.79 对 0.12 说明这次判断很笃定;但如果分布是 0.34 / 0.31 / 0.20 / 0.15,你就知道这条工单本身就模糊,适合转人工或者追问。不要只看 choice 字段,要看整个分布。
第三,多选不等于多标签。 这里"多选"指的是从多个候选中选一个。如果你要打多个标签,用多个 Noul 问题(每个标签一个"是不是"),而不是一个 Choice。这个区别在 Classification 形态里很重要。
Choice 是十种形态里 Classification 和 Routing 的底层形态。
Score:有序评分
Score 回答的是"在某个有序尺度上打几分"。它的精妙之处在于评分不是一个数字,而是一个概率分布。
urgency = decision["urgency"]
print(urgency.probabilities) # {"低": 0.10, "中": 0.25, "高": 0.65}
print(urgency.score) # 计算得到:1*0.10 + 2*0.25 + 3*0.65 = 2.55score 的计算方式是按序号加权求和:设尺度的第 i 档对应数值 i,则 score = Σ i · p_i。上面例子里 低/中/高 对应 1/2/3,得到 2.55——一个落在 1 到 3 之间的连续值,比单纯取"最高概率的那一档"信息量更大。
为什么要用分布而不是直接要一个数?因为直接让模型给数字("打 1-10 分")是最不可靠的——它对"7 分"和"8 分"的边界没有共识,不同调用之间抖动很大。而让它在少数几个有序档位上给分布,每个档位都有明确语义,稳定性高得多,还能顺带拿到不确定性。
sev = decision["severity"].score
if sev >= 2.5:
pager.oncall()Score 是 Scoring 和 Ranking(用 score 排序)的底层形态。
三种类型与十种形态的对应
十种决策形态不是十种新 API,而是三种类型化输出在具体任务上的组合玩法。先建立这张对应关系,后面的章节会顺次展开:
flowchart LR
subgraph 三种类型化输出
C["Choice
多选,Σp=1"]
S["Score
有序评分,Σi·p_i"]
N["Noul
布尔,P(true)"]
end
subgraph 十种决策形态
CL["Classification"] --> C
RT["Routing"] --> C
DT["Detection"] --> N
SC["Scoring"] --> S
RK["Ranking"] --> S
VF["Verification"] --> N
SE["Search"] --> N
RE["Retrieval"] --> N
FE["ML Feature Extraction"] --> S
SD["Structured Data Extraction"] --> C
end一句话记住:Choice 管"选哪个",Score 管"多少分",Noul 管"是不是"。 十种形态无非是在这三种之上叠加了任务语境和后处理。
调用契约里的几个细节
关于成本。 决策调用的成本远低于生成调用,因为它不生成 token 序列,只在一个封闭的选择空间上出分布。社区里大量项目的实测都指向同一个量级:单次判定成本常常低到"每千次几美分",某些场景下比用生成模型省两个数量级。第 22 章会用具体数字展开。
关于延迟。 决策调用的延迟也显著低于生成,官方把 150 毫秒作为实时应用的预算线。第 20 章会讲怎么在 150 毫秒内完成一次决策乃至一串决策。
关于批量。 大规模判定不要一次一个地调用,而是把多条 state 打成一批(batch)送进去。第 22 章的 AI Map Reduce 就是围绕批处理展开的。
关于版本。 生产环境建议锁定具体版本(如 jev-1.13)而不是 jev-latest,这样行为可复现。升级时单独刷一遍回归测试——第 23 章会讲怎么做决策层的回归验证。
关于确定性。 决策层给出的是概率分布,不是"永远同一个答案"。同一个 state 调两次,choice 可能一致但概率有细微浮动。如果你的下游逻辑对确定性敏感,要么设阈值把区间切断,要么在测试里断言"概率落在某个范围"而不是"等于某个值"。
一个最小可运行的例子
把三种类型凑在一次调用里,做一个"邮件分诊":
def triage(email_text: str) -> dict:
d = client.systemone(
state=email_text,
questions={
"label": {
"type": "choice",
"question": "这封邮件的类型?",
"choices": ["工作", "账单", "推广", "社交", "其他"]
},
"importance": {
"type": "score",
"question": "重要程度?",
"labels": ["可忽略", "一般", "重要", "紧急"]
},
"has_deadline": {
"type": "noul",
"question": "邮件的正文里是否包含一个明确的截止时间?"
}
},
instructions="你是邮箱助理。判断依据只看邮件正文,不要脑补。"
)
return {
"label": d["label"].choice,
"label_conf": max(d["label"].probabilities.values()),
"importance": d["importance"].score,
"deadline": d["has_deadline"].value,
"deadline_p": d["has_deadline"].probability,
}一次调用同时拿到了分类、评分、检测三种判断,各有各的概率。你的代码拿着这三个结果去做任何它想做的控制流——这就是"代码拥有控制流,模型只做判定"落到手边的样子。
小结
- 一次调用由 state + questions + instructions 组成,返回与 questions 对应的类型化决策。
- 三种输出:Noul(布尔 + P(true))、Choice(多选,Σp=1,2–255 个选项)、Score(有序评分,
score = Σ i·p_i)。 - 看决策要看整个概率分布,不只看选中的那一项;概率是设阈值、判不确定性、做升级策略的依据。
- 十种决策形态都是这三种输出在具体任务上的组合:Choice 管"选哪个",Score 管"多少分",Noul 管"是不是"。
- 生产建议:批处理、锁版本、对概率做区间断言而非等值断言。
下一章把镜头拉远,站在选型的高度看十种形态各自的适用边界,回答"什么判断该交给 Jev"。