Choice 选择题:从固定选项里挑一个
材料在上一课备好了,这一课开始学提问。Jev Choice 是五种提问方式的第一种,也是最直给的一种:你把选项列清楚,Jev 从里面挑一个,连同每个选项的概率和整体置信度一起返回——答案永远是你的选项之一,代码里一行解析文本的 if 都不用写。
其实你在第一个请求里已经用过它:route_ticket.py 的 department 就是道 Choice,挑出了 technical。这节课把它讲透,全程 10 分钟。
学完你能做什么
- 用
criteria选项 map(选项名 → 一行描述)写出一道完整的 Jev Choice 题,知道选项名和描述都会发给模型 - 读懂返回的
choice、probabilities、confidence三个字段,把最高概率选项直接映射成代码分支 - 一次请求并行问多道 Choice 题(一道最多 255 个选项),投机题的答案不用就忽略
- 列表可能不全时加
other选项,或按官方建议配一道伴生 Noul 兜底 - 用「重排选项再跑一遍」自查答案稳定性——选项顺序可能影响答案
你现在的困境
- 让 GPT 给工单分类,它每次回的类别名都不一样——「退款」「退货退款」「要求退钱」,下游的
if永远追不上它自创的词表 - 想知道它有多确定,只能追问「你有多确定」,得到的是一段「我认为相当有可能」的散文,没法写进代码
- 一条工单要判断好几件事(归哪个组、什么原因、客户想要什么),串三次请求又慢又贵;挤在一个 prompt 里问,答案又互相搅
什么时候用这一招
当你需要:
- 答案是一组你自己定的选项:路由到哪个组、商品属于哪个类目、代码是什么语言写的
- 答案直接变成代码分支——一个选项一条路径,不从生成文本里解析
- 同一份材料连着问好几道选择题,还想只发一次请求
而且不想:
- 模型临时发明你列表之外的新答案
- 为「这工单归谁」这种一秒判断写一大段 prompt
- 在模型没把握的时候也放它自动执行
🎒 开始前的准备
- 完成了 State 状态:知道材料放
state(字符串、JSON 对象、文本数组都行),判断写在 questions 里 - 跑通过第一个请求:
.venv里装好了typesafe-sdk,环境变量TYPESAFE_API_KEY已设置 - 记得
route_ticket.py里department那题的大致样子——忘了就回去扫一眼
核心思路
官方一句话定义:答案是一组固定选项之一时,用 Choice。文档给的典型问题:哪个团队处理这张工单、这个商品属于哪个类目、这段代码用什么语言写的。
Choice 好用的根源在一条硬保证——答案被约束在你给的选项里:
每个答案都被约束在你提供的选项上。模型返回的是你的选项上的概率分布,永远不会是选项之外的值。你的代码永远不用从生成的散文里捞答案。
一道 Choice 题长这样:
| 字段 | 写什么 | 说明 |
|---|---|---|
| ID(问题的键) | 自取,如 department | 只在你的代码里用,不会发给模型;答案按它取回 |
type | 固定 "choice" | — |
instructions | 完整的问题 | 问题的全部内容写在这里,别指望 ID 帮你传话 |
criteria | 选项 map:选项名 → 描述 | 选项名和描述都会发给模型;一题最多 255 个选项 |
三种题型怎么选(官方判据):
| 题型 | 回答什么 | 代码里的用法 |
|---|---|---|
| Choice(本课) | 固定选项挑哪个,选项之间没有顺序 | 一个选项一条代码路径 |
| Score | 光谱上到什么程度,每级你描述得出 | 过阈值、排序 |
| Noul | 干净的是非,概率本身有用 | 一个 if |
答案是光谱(多严重、多愤怒)→ Score;是干净是非(有没有提到退款)→ Noul。两个都像时,官方建议选答案能被代码直接行动的那个:refund / rebook / information 三个选项直接映射三条代码路径,这就是 Choice 的形状。
跟我做
第 1 步:定义选项 map,写出你的第一道 Jev Choice 题
为什么:一道 Choice 题只有三个字段:type、instructions、criteria——选项名和描述都会发给模型,所以描述要写得能把选项区分开。
用官方文档的例子:一家在线鞋店的客服工单,问该哪个组处理。新建 choice_first.py:
# 运行前:export TYPESAFE_API_KEY="<你的API Key>"
from typesafe_sdk import Choice, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state="My running shoes arrived in the wrong size. Can I swap them for a size 10?",
questions={
"department": Choice(
instructions="Which team should handle this?",
criteria={
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems",
},
),
},
)
print(response.answers["department"].choice)运行它:
.venv/bin/python choice_first.py你应该看到:
returns鞋码不对、想换一双——returns 组的事,官方示例录制时模型全押 returns。
写 criteria 的三条要点:
选项名就是答案值。 代码里拿到的
choice是选项名字符串,用稳定的英文标识符(returns),别用会变的展示文本描述的作用是分离。 官方原话:选项名和描述都会发给模型,「所以要把描述写得让选项彼此分开」。名字本身就说清楚了的时候,描述可以给
null(第 3 步有现成例子)给全列表,别给短名单。 一道 Choice 最多 255 个选项,加选项只多花几 token——把全部团队、类目、商品给模型,而不是先筛一轮
我跑出了
returns,知道这个值来自criteria里的选项名
第 2 步:读懂返回的 choice、probabilities 与 confidence
为什么:choice 只是分布的峰值;真实项目里你大概率要拿 probabilities 和 confidence 写分支,而不只是取一个名字。
第 1 步那个请求的完整返回(官方示例):
{
"model": "jev-1.13.0",
"answers": {
"department": {
"type": "choice",
"choice": "returns",
"confidence": 1.0,
"probabilities": {
"shipping": 0.0,
"returns": 1.0,
"billing": 0.0
}
}
},
"usage": {
"input_tokens": 328,
"output_tokens": 34
}
}你应该看到:答案对象里除了 type 还有三个值,逐个拆:
| 字段 | 本例的值 | 怎么读 |
|---|---|---|
choice | returns | 概率最高的选项 |
probabilities | returns 1.0,其余 0.0 | 全部选项的分布,加起来约为 1;键是选项名 |
confidence | 1.0 | 把分布形状压成一个 0–1 的数:全在一个选项上 = 1,均匀摊开 = 0 |
三个值得记住的认识:
- 答案必在选项之内。 三个选项的题,
probabilities就只有这三个键——不存在「模型自己加了第四个」 - confidence 只看最高概率。 三个选项的公式是
(3 × 最大概率 − 1) / 2,所以(0.6, 0.3, 0.1)和(0.6, 0.2, 0.2)的 confidence 都是 0.4。分布形状怎么算成这个数、三档怎么用,Confidence 置信度课细讲 - confidence 是分布的描述,不是答案正确的保证。 这条工单简单,概率全押
returns,confidence 自然是 1.0;一条又提错码又提重复扣款的工单会把概率劈给returns和billing,confidence 会掉下来——第 3 步就有这么一条
示例里的 "model": "jev-1.13.0" 是官方录制示例时的版本;你现在用 jev-latest 跑,这个字段显示当前模型版本。
- 我能指着返回说出:
choice是峰值、probabilities是分布(总和约 1)、confidence是分布形状
第 3 步:一次请求问五道 Choice 题(并行评估)
为什么:官方建议把可能用到的 Choice 题一次问完:所有题对同一份 state 并行、独立评估——加题几乎不增加响应时间,代码忽略用不到的答案就行。
这次换一条更含糊的工单:迟到了两周、尺码错了、还被扣了两笔钱——三个组都沾边,客户又没说要什么。官方对这条工单一次问了五道 Choice:
# 运行前:export TYPESAFE_API_KEY="<你的API Key>"
from typesafe_sdk import Choice, TypeSafeClient
TRIAGE_QUESTIONS = {
"department": Choice(
instructions="Which team should handle this?",
criteria={
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems",
},
),
"return_reason": Choice(
instructions="If the customer wants to return something, why?",
criteria={
"wrong_size": "The item doesn't fit",
"wrong_item": "A different product was delivered",
"damaged": "The item arrived broken or faulty",
"changed_mind": "The item is fine, the customer no longer wants it",
"other": "A return reason that fits none of the above",
},
),
"shipping_issue": Choice(
instructions="If this is a shipping problem, which kind is it?",
criteria={
"not_delivered": "The package never arrived",
"delayed": "The package is late but still on the way",
"wrong_address": "The package went to the wrong place",
"damaged_in_transit": "The package arrived damaged",
"other": "A shipping problem that fits none of the above",
},
),
"requested_resolution": Choice(
instructions="What does the customer want to happen?",
criteria={
"exchange": "Swap the item for a different one",
"refund": "Money back",
"replacement": "The same item sent again",
"information": "Just an answer, no action needed",
},
),
"tone": Choice(
instructions="What is the customer's tone?",
criteria={"calm": None, "frustrated": None, "angry": None},
),
}
ticket = "Shoes arrived two weeks late and in the wrong size. Also I see two charges of $120 on my card. What are you going to do about this?"
with TypeSafeClient() as client:
response = client.system_one(state=ticket, questions=TRIAGE_QUESTIONS)你应该看到(官方示例返回,五个答案一次到齐;这次请求共 589 输入 / 212 输出 token):
| 问题 | choice | confidence | 概率要点 |
|---|---|---|---|
department | returns | 0.42 | returns 0.61、billing 0.35、shipping 0.04 |
return_reason | wrong_size | 1.0 | wrong_size 1.0,其余 0.0 |
shipping_issue | delayed | 0.67 | delayed 0.74、other 0.26 |
requested_resolution | refund | 0.2 | refund 0.40、replacement 0.34、exchange 0.24 |
tone | frustrated | 0.76 | frustrated 0.84、angry 0.16 |
官方对每个答案的解读,值得逐条看:
department分裂了:returns 0.61、billing 0.35——重复扣款给了 billing 三成多的概率。这张工单横跨两个组,0.42 的 confidence 说的就是这件事return_reason干净:「尺码不对」写得明明白白,全押wrong_size,confidence 1.0shipping_issue是投机题:答案分裂在 delayed 和 other 之间,但department压根不是 shipping——代码忽略它就行(第 4 步就这么做)requested_resolution低置信:refund 0.40、replacement 0.34、exchange 0.24——重复扣款像要退钱、尺码不对像要换货,客户从头到尾没说要哪个tone的描述全是null:calm / frustrated / angry 名字自明,不需要描述——这正是第 1 步说的「写不出区分度就给 null」的场合
注意观察这五道题里有两道投机题(speculative):return_reason 只在归 returns 组时有用,shipping_issue 只在归 shipping 时有用。先问了再说、按需取用,是 Jev 的标准做法——官方 13 题 vs 13 次请求的实测是 12.2 倍便宜、10.0 倍快,答案不变(展开在并行提问课)。官方的智能家居 demo 更极端:每个请求都对一长串 Choice 题(类别、房间、设备、动作)整体评估,多数答案无用,代码只取需要的那几个。
第 4 步:用 choice、probabilities 和 confidence 写路由分支
为什么:答案都是类型化值——choice 直接映射代码路径,低置信转人工,第二名选项的概率过阈值就抄送。一次请求,五个答案,剩下的全是普通 if。
官方接在第 3 步之后的完整路由代码:
def triage(ticket: str) -> None:
with TypeSafeClient() as client:
response = client.system_one(
state=ticket,
questions=TRIAGE_QUESTIONS,
)
answers = response.answers
department = answers["department"]
if department.confidence < 0.3:
# Not clear which team to send to. Let a person decide.
send_to_manual_triage(ticket)
return
if department.choice == "returns":
# return_reason answer is only used here
assign(ticket, team="returns", issue=answers["return_reason"].choice)
elif department.choice == "shipping":
# shipping_issue answer is only used here
assign(ticket, team="shipping", issue=answers["shipping_issue"].choice)
else:
assign(ticket, team="billing")
# A second team with a real share of the probability gets a copy
for team, probability in department.probabilities.items():
if team != department.choice and probability > 0.25:
notify(ticket, team=team)
resolution = answers["requested_resolution"]
if resolution.confidence < 0.5:
# The customer hasn't said what they want. Ask, don't guess.
ask_customer_what_they_want(ticket)
elif resolution.choice == "refund":
flag_for_refund_approval(ticket)
if answers["tone"].choice == "angry":
flag_for_senior_agent(ticket)(assign / notify / send_to_manual_triage 等是示意函数,换成你自己的路由逻辑。)
你应该看到——对第 3 步那条工单,官方给出的行为是:
assigned to returns team with issue wrong_size
billing team notified (0.35 > 0.25)
customer asked what they want (confidence 0.20 < 0.5)三个门控值都来自官方示例:0.3(低于它连组都定不下来,转人工)、0.25(第二名选项的概率份额,过线就抄送)、0.5(客户诉求的把握,低于它就问而不是猜)。shipping_issue 的答案全程没被读——投机题的意义就在这。
「意图分类 + 置信度门控」做成完整路由系统(确定性逻辑 / 专家 LLM / 人工三级分流),在智能路由课展开。
- 我能说清对这条工单:为什么 billing 被抄送、为什么去问客户、为什么 shipping_issue 没用上
第 5 步:列表不全时的两个兜底:other 选项与伴生 Noul
为什么:选项列表可能盖不住所有输入——客户提了个你没想到的诉求,模型只能在你给的选项里硬挑一个。官方给了两个兜底。
兜底一:other 选项。 官方建议:列表可能覆盖不了全部输入时,加一个 other 或 none of the above 选项,让模型能说「其他都不沾」。第 3 步的 return_reason、shipping_issue 里都有 other 选项,描述里写明它是「都不沾时的出口」。
兜底二:伴生 Noul(官方更推荐)。 官方提示 none of the above 在很多场景不一定好用;替代方案是同一请求里配一道 Noul,判断「有没有选项沾边」,例如:
Choice:这个诉求属于哪个类目?
Noul :`message` 里提到了 `our_products` 中的任何一个吗?代码只在 Noul 为 yes 时才读 Choice 的答案——没沾边就整组跳过。这正是直播聊天过滤的玩法:每条消息一次请求、三道题,不沾就直接放行。Noul 本身在下一课讲。
你应该看到:两个兜底都不需要新请求——other 是 criteria 里多一个选项,伴生 Noul 是同一份 questions 里多一道题,加起来都还在「一次请求」的框框里。
- 我能说出两个兜底各自什么时候用:列表偶有漏项加
other;要「完全不沾也拦得住」就配伴生 Noul
检查点 ✅
- 我的
criteria是选项 map(名字 → 描述),知道两个都会发给模型、问题 ID 不会 - 我知道答案必在所给选项之内,
probabilities覆盖全部选项、总和约为 1 - 我知道一请求多题并行、独立评估;投机题的答案代码不用就行
- 我知道 confidence 只看最高概率(三选项
(3×最大概率−1)/2),是分布的描述不是正确保证 - 我记住了两个自检:重排选项验一致性;只挑最优就直接取最高概率
踩坑提醒
❌ 两个选项的描述分不开
现象:return_policy / return_status 这类近义选项,工单里都提 return、refund,模型老在两者之间摇摆,confidence 一直上不去。
为什么错:选项名和描述都会发给模型。两条描述都指向同一类输入时,模型没有对照依据,概率只能分开押。
正确做法:描述里把边界写死——这个选项管什么、不管什么(隔壁选项的事)。官方对 return_policy / return_status 就是这么写的:每条描述明确说「whether and how an item can be returned」的边界,配对照例子后,官方示例拿到 return_status、confidence 1.0。要写得更多时,选项值直接用对象(见进阶技巧第一条)。
❌ 换个顺序,答案就变了
现象:把 criteria 里三个选项的顺序换一换,答案跟着变了。
为什么错:这是官方踩坑手册(适用于 jev-1.13)列出的已知失败模式——Choice 的选项顺序会影响答案,且模型偏向排在前面的选项。
正确做法:官方就一句话——「重排选项,再跑一遍,检查答案一致」。把它当上线前的固定自检,尤其选项少、描述短的题。完整失败模式清单见 Jev 1.13 踩坑手册。
❌ 拿 Choice 让模型「自己写答案」
现象:instructions 写「这条消息里提到的日期是什么」,指望模型把日期填出来。
为什么错:Jev 不是生成模型,为生成文本而训练的是 LLM 的活。开放值问法两头不讨好:模型既不能编(答案被约束在选项内),你又没给它可挑的选项。
正确做法:答案空间有界就枚举成选项。日期就是典型:年、月、日各一道 Choice——月只有十二个、日最多三十一个——再各加一个「没有提到」选项,缺失的部件会被如实报告而不是被猜。代码把部件拼成真正的日期、负责所有比较和运算。值提取的完整套路(regex 找候选、Jev 挑对的那个)见值提取。
❌ 到处设 confidence 阈值
现象:每个答案、每条分支都放一个 confidence 阈值,越设越多,调不过来。
为什么错:官方 Agent Skill 的常见问题清单点名了这个习惯——只关心挑出最好的选项时,直接取概率最高的那个就行,不必处处设 confidence 阈值;确有统计算法需求时,该用的是 probabilities。
正确做法:路由分支用 choice;分布分析用 probabilities;只在「不确定要不要自动执行」这类关键门控上设 confidence 阈值(第 4 步的 0.3 / 0.5 就是),并让它随风险缩放——完整方法在 Confidence 置信度课。
进阶技巧(可选)
🔧 选项写不进一行时,用对象。 两三个选项反复混淆、一行描述说不清边界时,把描述换成对象:一个字段说这选项覆盖什么、一个说它不管什么、一个给几个例子。官方对 return_policy / return_status 的写法(Playground 直发的原始请求,响应是 return_status、confidence 1.0):
官方结构化 criteria 示例(原始 JSON)
{
"state": "I sent the shoes back a week ago. When do I get my money?",
"questions": {
"return_topic": {
"type": "choice",
"instructions": {
"question": "Which returns topic is the customer asking about?",
"focus": "Classify the information the customer wants."
},
"criteria": {
"return_policy": {
"what": "Whether and how an item can be returned",
"not_for": "Progress of a return already sent",
"examples": [
"Can I return shoes I've worn once?",
"How long do I have to return an order?"
]
},
"return_status": {
"what": "Progress of a return already sent",
"not_for": "Whether and how an item can be returned",
"examples": [
"Has my return arrived yet?",
"When will my refund be paid?"
]
}
}
}
}
}两个要点:instructions 也能这样结构化(question + focus);字段名 what / not_for / examples 都不是 API 保留字,你自己起——模型看得见字段名和值,字段名起成「标注下文是什么」的短词就行。
🔧 深层级类目树:一层一层问。 类目很深(专利分类、零售目录、源码库的目录树)时,把 Choice 串成链:每层的答案决定下一层请求给哪些选项。官方的层级分类 cookbook 在这之上跑 beam search——用 Choice 的 probabilities 保留每层最好的 K 条候选路径,而不是每层只押一个。完整玩法见深层级分类。
🔧 绕过 SDK 直连 API。 POST https://api.typesafe.ai/v1/systemone,请求体顶层三个字段 state / model / questions,department 那道题的原始 JSON 长这样:
{
"model": "jev-latest",
"state": "My running shoes arrived in the wrong size. Can I swap them for a size 10?",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"returns": "Exchanges, wrong or damaged items",
"shipping": "Delivery status, delays, lost packages",
"billing": "Charges, invoices, payment problems"
}
}
}
}字段约束的完整定义(criteria 最少 1 个、最多 255 个选项,答案四字段)见 API 速查。
本课小结
✅ Jev Choice 的写法:criteria 选项 map(名字 → 一行描述;最多 255 个),答案永远落在你的选项里,代码不用解析文本。
✅ 三个返回字段连着读:choice 是最高概率选项、probabilities 是全部选项的分布、confidence 是分布形状的单一数字。
✅ 一请求多题并行、独立评估;投机题先问再说,代码按需取用——13 题合成一次请求实测 12.2 倍便宜、10.0 倍快。
✅ 列表可能不全:加 other 出口,或配伴生 Noul 判断「有没有沾边的」。
关键配置字段回顾:
| 字段 | 推荐写法 | 说明 |
|---|---|---|
type | "choice" | 固定值 |
instructions | 完整的问题 | ID 不发给模型,问题要写全 |
criteria | 选项 map:名字 → 一行区分度描述 | 名字自明时描述给 null;给全列表不给短名单 |
| 答案访问 | response.answers["id"].choice | 连 .probabilities、.confidence 一起读 |
想看 Choice 在真实场景里怎么用:深层级分类(Choice 概率跑 beam search 穿类目树)、直播聊天过滤(一条消息一次请求三道题)、智能路由(Choice 做意图分类、confidence 做门控)。
下一课预告
固定选项的另一头是「到什么程度」:Score 打分题:按你定义的量表打分。
你会学到:
criteria从选项 map 换成有序等级列表,级号就是数组下标score可以落在两级之间(比如 1.43),要连着概率分布一起读- 多维度判断拆成多道 Score,代码里归一化加权合并
- 什么时候该把 Choice 换成 Score