Skip to content
📚 日常使用第 3 / 7 节

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:

python
# 运行前: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)

运行它:

bash
.venv/bin/python choice_first.py

你应该看到:

text
returns

鞋码不对、想换一双——returns 组的事,官方示例录制时模型全押 returns。

写 criteria 的三条要点:

  • 选项名就是答案值。 代码里拿到的 choice 是选项名字符串,用稳定的英文标识符(returns),别用会变的展示文本

  • 描述的作用是分离。 官方原话:选项名和描述都会发给模型,「所以要把描述写得让选项彼此分开」。名字本身就说清楚了的时候,描述可以给 null(第 3 步有现成例子)

  • 给全列表,别给短名单。 一道 Choice 最多 255 个选项,加选项只多花几 token——把全部团队、类目、商品给模型,而不是先筛一轮

  • 我跑出了 returns,知道这个值来自 criteria 里的选项名

第 2 步:读懂返回的 choice、probabilities 与 confidence ​

为什么:choice 只是分布的峰值;真实项目里你大概率要拿 probabilities 和 confidence 写分支,而不只是取一个名字。

第 1 步那个请求的完整返回(官方示例):

json
{
  "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 还有三个值,逐个拆:

字段本例的值怎么读
choicereturns概率最高的选项
probabilitiesreturns 1.0,其余 0.0全部选项的分布,加起来约为 1;键是选项名
confidence1.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:

python
# 运行前: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):

问题choiceconfidence概率要点
departmentreturns0.42returns 0.61、billing 0.35、shipping 0.04
return_reasonwrong_size1.0wrong_size 1.0,其余 0.0
shipping_issuedelayed0.67delayed 0.74、other 0.26
requested_resolutionrefund0.2refund 0.40、replacement 0.34、exchange 0.24
tonefrustrated0.76frustrated 0.84、angry 0.16

官方对每个答案的解读,值得逐条看:

  • department 分裂了:returns 0.61、billing 0.35——重复扣款给了 billing 三成多的概率。这张工单横跨两个组,0.42 的 confidence 说的就是这件事
  • return_reason 干净:「尺码不对」写得明明白白,全押 wrong_size,confidence 1.0
  • shipping_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 步之后的完整路由代码:

python
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 步那条工单,官方给出的行为是:

text
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,判断「有没有选项沾边」,例如:

text
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)
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 长这样:

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
搞懂AI关于我们怎么写隐私说明

独立的第三方教程,与文中提到的任何产品及其公司没有隶属或合作关系;产品名称和商标归各自所有者。价格和政策以官方页面为准。