并行提问与投机扇出:一次请求问 13 个问题
上一阶段日常使用里学了五种提问方式,但例子要么单题请求,要么三五道题浅尝辄止。这一课把它们全装进一个请求——Jev 并行提问:官方实测,13 道题装进一次请求,比一题一请求发 13 次便宜 12.2 倍、快 10.0 倍,答案一模一样。
第二个要讲的是投机扇出(Speculative fan-out):「可能需要」的问题也提前问上,答案回来由代码决定用哪个。官方 Patterns 文档把它列为四种架构模式的第一种,也是本阶段其余几课反复要用到的基础。
学完你能做什么
- 用一次 Jev 请求并行问多个问题:Choice / Score / Noul 三种题型混装,官方示例 5 题、13 题都跑得动
- 写出投机扇出的两半:模型一次全答,代码按分支决定读哪些答案、忽略哪些
- 复现官方 13 问实测:12.2 倍便宜、10.0 倍快、答案不变(附不用 API key 的复跑方法)
- 判断真依赖:说出「必须发第二次请求」的三个官方判据,不再靠感觉拆请求
你现在的困境
- 我的工单分诊是一串串行请求:先问类别,等答案回来,是 bug 再问严重度,是退款再问有没有明说……一条工单三四次往返,延迟和账单一起涨
- 想过把问题一次问全,又怕「题多了模型晕」——好不容易调稳的答案,多塞两题会不会漂?
- coding agent 帮我生成的调用代码永远一题一请求,我还以为是我自己的 prompt 写得不对
什么时候用这一招
当你需要:
- 对同一份材料问多个问题:工单分诊、文档合规检查、一条消息打多个标签
- 其中一些问题「可能用不上」——要等分类结果出来,才知道哪些答案相关
- 控制成本和延迟:材料越大,一题一请求越是白交钱
而且不想:
- 为省几题 token 把请求拆成一串,材料重发 N 遍
- 维护「先问哪题、拿到答案再问哪题」的状态机
- 猜「加题会不会把其他答案带偏」
🎒 开始前的准备
- 完成了 高效工作流:四种架构模式与 coding agent 配合:知道本课的投机扇出是四种模式的第一种,后面几课都踩在它上面
- 跑通过第一个请求:
.venv里有typesafe-sdk,TYPESAFE_API_KEY在环境变量里 - 大致记得 Choice、Score、Noul 的写法(忘了也没关系,第 1 步给全代码)
核心思路
先记官方 primitives 文档的三句底牌:
- 并行:请求里的每道题对同一个 state 并行评估——加题几乎不改变响应时间,只多花那几题的问题 token
- 独立:一个问题的答案不是另一个问题的隐藏上下文——加减问题,其他答案不变
- state 是大头:材料占据每次请求的绝大部分——N 次单题请求把材料重发 N 遍,批问只发一遍
投机扇出的官方定义(TypeSafe fan-out 模式页):
把系统需要的所有问题放进单个请求——包括投机的问题——事后再由代码决定哪些相关。
为什么敢把「可能用不上」的问题也提前问?官方解释:所有问题并行评估,加题对响应时间的影响通常很小;问一个可能不需要的问题,接近免费。
一条工单的完整分诊流程长这样(官方示例的结构):
support ticket
│ 一次请求:工单 + 5 个问题
▼
┌──────────────────────────────────────┐
│ category(Choice) │
│ bug_severity(Score) │ 并行评估
│ has_reproducible_steps(Noul) │ 互相独立
│ refund_requested(Noul) │
│ frustration(Score) │
└──────────────────────────────────────┘
│ 一次响应:5 个答案
▼
你的代码:按 category 走分支
读要读的,忽略用不上的两种发法的差别:
| 对比维度 | 一题一请求 × 13 | 一次请求装 13 题 |
|---|---|---|
| 往返次数 | 13 次 | 1 次 |
| 材料 token | 付 13 遍 | 付 1 遍 |
| 响应时间 | 13 段延迟相加 | 加题几乎不变 |
| 「可能不需要」的问题 | 先等前面的答案才知道要不要问 | 提前问上,不用就忽略 |
| 答案 | 一样(官方实测) | 一样(官方实测) |
材料怎么给、state 装什么,看 State 状态那一课;本课所有问题都对着同一个 state 问。
跟我做
第 1 步:Jev 并行提问初体验——5 个问题装进一个请求
为什么:工单分诊是官方 fan-out 模式页的示例场景。真实工单经常一条里混着好几类事,先分类再「按需追问」就要好几次往返;5 道题一次问上,一次请求全拿回。
这是官方 Playground 示例用的工单——故意混装:重复扣费 + 无法登录 + 想要 Apple Pay + 不耐烦。新建 fanout_triage.py:
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
ticket = (
"Hi, I placed an order (#98423) last Thursday and was charged twice. "
"I also can't log in after the site update, and adding Apple Pay "
"would be really helpful. This is getting frustrating."
)
questions = {
"category": Choice(
instructions="Determine the broad category of this support ticket",
criteria={
"bug_report": "The user is reporting something that is broken or producing errors",
"billing": "Charges, invoices, refunds, subscriptions",
"feature_request": "The user is requesting new functionality",
"account": "Login, permissions, profile, security",
},
),
"bug_severity": Score(
instructions="How severe is the reported issue",
criteria=[
"Cosmetic; no impact to functionality",
"Broken or degraded feature; workaround exists",
"Blocking issue; no workaround exists",
],
),
"has_reproducible_steps": Noul(
instructions="The user describes specific steps to reproduce the issue"
),
"refund_requested": Noul(
instructions="The user is explicitly asking for a refund or credit"
),
"frustration": Score(
instructions="How frustrated the user appears",
criteria=[
"Calm, matter-of-fact",
"Frustrated but civil",
"Very angry",
],
),
}
with TypeSafeClient() as client:
response = client.system_one(state=ticket, questions=questions)
print(response.answers["category"].choice)
print(response.answers["bug_severity"].score)
print(response.answers["has_reproducible_steps"].noul)
print(response.answers["refund_requested"].noul)
print(response.answers["frustration"].score)工单原文、5 道题、每个选项的描述都是官方 Playground 示例原文,一字未改。
运行:
.venv/bin/python fanout_triage.py你应该看到:五行答案。第一行是四个类别之一;第二、五行是 0–2 的分数(可以落在两档之间);第三、四行是 0–1 的概率。官方没有发布这条混装工单的预期数值——具体数字以你的实际返回为准,但 5 个答案一定同时出现在一次响应里。
注意其中的投机问题:bug_severity、has_reproducible_steps 只在 category 是 bug_report 时才有用;refund_requested 只在 billing 时才有用。官方 NOTE 的解释:全都提前问上,因为加题对响应时间的影响很小;如果工单最后是 feature request,bug severity 的结果就不相关,代码路径直接忽略它。
- 一次请求拿到 5 个答案,代码里没有循环、没有第二次调用
第 2 步:用代码路由——投机答案该忽略就忽略
为什么:投机扇出的另一半在代码里。模型把 5 个答案全给回来;「这条路径上哪些答案算数」是你的 if/else 说了算。
官方 fan-out 模式页的路由代码(triage.py 原样;处理函数是官方占位名,换成你自己的业务逻辑):
category = response.answers["category"]
bug_severity = response.answers["bug_severity"]
bug_repro = response.answers["has_reproducible_steps"]
refund = response.answers["refund_requested"]
frustration = response.answers["frustration"]
if category.choice == "bug_report":
if bug_severity.score > 1.5 and bug_repro.noul > 0.6:
escalate_to_engineering(ticket_id, severity="high")
else:
add_to_bug_backlog(ticket_id)
elif category.choice == "billing":
if refund.noul > 0.7:
route_to_billing_with_flag(ticket_id, refund_likely=True)
else:
route_to_billing(ticket_id)
elif category.choice == "feature_request":
log_feature_request(ticket_id)
# Frustration is useful regardless of category
if frustration.score > 1.5:
flag_for_priority_response(ticket_id)阈值表:
| 分支 | 门控条件 | 动作(占位函数) |
|---|---|---|
| bug_report | severity > 1.5 且 repro > 0.6 | 升级工程组 |
| bug_report(其余) | — | 进 bug 积压 |
| billing | refund > 0.7 | 转账单组并打标 |
| billing(其余) | — | 转账单组 |
| feature_request | — | 记录功能请求 |
| 任何类别 | frustration > 1.5 | 优先响应 |
读三点:
- 有些答案注定不被读。 category 落在 billing 时,
bug_severity的答案没人看——问了、不用、也不亏,这就是投机 - frustration 不进任何 if/elif。 官方注释原话:Frustration is useful regardless of category(不管哪类都有用)——它在所有分支外面单独一道闸
- 整棵决策树只要一次调用。 官方总结:投机问题在无关时被忽略,在相关时省掉一次往返
你应该看到:category 落在哪个选项,代码就走哪条分支,其余分支不执行。只想看流程的话,把占位函数换成 print("escalate") 之类即可。官方同样没有发布这条工单的预期路由结果。
- 我能说出哪两道题的答案在 billing 分支里不被读(答案:bug_severity、has_reproducible_steps)
第 3 步:复现官方 13 问实测——12.2 倍便宜、10.0 倍快
为什么:「加题几乎不增响应时间、答案互不影响」不能只停在说法。官方 Parallel questions cookbook 做了一次对照实验,这一步看它的设置与结果,并给出不用 API key 的复跑方法。
实验设置:
- 材料:GDPR 的维基百科条目,固定修订版 1363040264,实测 53,777 字符(约 5.4 万)
- 13 道题:8 Noul + 2 Choice + 3 Score,覆盖合规团队要查的 13 件事
- 两种发法各跑 5 遍:一次请求装 13 题 vs 一题一请求发 13 次;比均值(有没有偏移)和标准差(有没有加噪声)
- 模型 jev-1.12,价格 $0.042/Mtok 输入、输出 $0(2026-09 时点)
13 道题长什么样?抽三道:
| 问题 ID | 题型 | 问什么 |
|---|---|---|
| breach_72h | Noul | 个人数据泄露是否必须在 72 小时内上报监管机构? |
| instrument_type | Choice | GDPR 属于哪种欧盟法律工具?(Regulation / Directive / Treaty / Recommendation) |
| compliance_burden | Score | GDPR 给组织施加的合规负担有多重?(5 档量表) |
结果一:批不批,答案不变。 13 道题里 11 道(2 Choice + 3 Score + 8 Noul 里的 6 道)在 5 次重复里逐次完全一致,两种发法标准差都是 0.0;剩下 breach_72h 和 criminal_penalties 带一点运行间噪声,两种发法的均值在噪声范围内一致。官方结论:噪声是问题自身的属性,不是批处理加的——批处理既不偏移答案,也不增加方差。
结果二:成本与时间差 12.2 倍 / 10.0 倍。
| 发法 | 调用次数 | 成本 | 总耗时 |
|---|---|---|---|
| 一次请求装 13 题 | 1 | $0.000497 | 0.27s |
| 13 次请求,一题一次 | 13 | $0.006090 | 2.71s |
官方的解释:单题发法把 53,777 字符的条目重发 13 遍,批问只发一遍。表里的总耗时把 13 次单题延迟相加(假设一次接一次跑);并发发出去能缩小耗时差距,但 13 倍的 token 成本不变。文档越大,节省越接近全额 13x。
复跑(不用 API key):
pip install ipython 'cooksafe>=0.2.0,<0.3.0'然后从官方 examples 仓库下载 json_cache.json(github.com/typesafe-ai/typesafe-public-examples/tree/main/cookbooks/parallel_questions),放到运行目录,照 cookbook 页的脚本跑。
你应该看到:结果一、结果二里的数字原样复现——缓存回放的就是官方实测那批调用,删掉 json_cache.json 才会重新实跑。
一个小坑:脚本启动时仍会读 TYPESAFE_API_KEY 环境变量,客户端没 key 会直接报错。回放前设个占位值即可(如 export TYPESAFE_API_KEY=dummy)——缓存命中的调用不会真发请求,占位 key 派不上用场。
💡 两个文档数字打架时,以 cookbook 为准
primitives.md 引用同一实验时写的是 11.5x 便宜、9.6x 快;cookbook 原文是 12.2x / 10.0x。本站一律采用 cookbook 原文(12.2x / 10.0x)。
- 我能说出官方实测三个结论:答案不变、12.2 倍便宜、10.0 倍快
第 4 步:判断真依赖——什么时候才发第二次请求
为什么:Jev 并行提问有一条硬边界:同一请求里的问题彼此独立,一个答案不能当另一个问题的上下文。那「先问类别、拿到答案再决定下一题问什么」的代码,到底该不该拆成两次请求?
官方判据只有一条:代码不拿到第一个答案,就无法构造第二个请求。具体三种情况:
- 要用答案去抓更多数据进 state(答案决定去哪抓什么)
- 要用答案决定 state 里装哪些东西
- 要用答案挑下一题的选项
三种之外:并进第一个请求,答案回来在代码里组合。官方原话:两个请求是例外,不是规则。
三个官方「真依赖」实例(都来自 cookbook):
| Cookbook | 第二次请求为什么躲不掉 |
|---|---|
| Skill suggestion | 先给技能目录排序,取前三名抓全文,再对着更好的证据重评——不拿到排名就不知道抓哪三篇 |
| Structure recovery | 先问每个换行是否切断句子,按答案把行拼成块——块在第一次请求回答之前根本不存在 |
| Hierarchical classification | 每层 Choice 的答案决定下一层请求给哪些选项 |
最后一个数量问题——一次到底能装多少题?
问题数量没有上限。限制在体积:每请求 64k、每题 32k(都含 state),详见模型与价格
问题通常比 state 短,两个限制的大头都是 state
实在装不下:拆成几个请求共用同一份 state
我的每个「第二次请求」都能对上三判据之一,而不是「感觉后面再问比较稳」
检查点 ✅
- 一次请求混装三种题型跑通,5 个答案按 ID 各自取回
- 我的路由代码里有「答案拿到了但不读」的分支
- 能说出官方实测:13 题、12.2 倍便宜、10.0 倍快、答案不变
- 能说出真依赖三判据:抓数据进 state / 定 state 组成 / 挑下一题选项
- 知道问题数量无上限,限制是 64k / 32k 两个上下文
踩坑提醒
❌ 一题一请求(coding agent 重灾区)
官方 primitives 文档的 TIP 原话:coding agents 比人更容易掉进「一题一请求」的习惯。TypeSafe Agent Skill 会专门纠正——让 agent 在每个请求里放很多问题,包括只对部分输入有意义的问题。
正确做法:同一 state 的所有问题并进一个请求。你审 agent 写的代码时,看到 for 循环里一次发一题,就是这个毛病。
❌ 把「可能需要」的问题留到第二次请求
症状:先问 category,等答案回来,再发 severity / refund 那次请求。官方 smart-home demo 把这条路直接点名为「错误做法」(the wrong way: sequential API calls)——它优化的是问题数最少,结果比一次批问慢得多、贵得多。
正确做法:投机的题提前问上。多问两道几乎免费(只多两题 token),省掉的一次往返是真金白银。
❌ 一个 instructions 塞一整串判断
「分析这条工单并决定最佳处理方式」不是好问题——System One 模型为快而专注的判断优化,好的问题是一个内行一秒钟就能下的判断。判断依赖多个因素时:一个因素一题,答案在代码里加权合并——这正是 组合打分 那课的主菜。
❌ 不敢加题,怕「把调稳的答案带偏」
官方实测专门回答了这点:13 道题两种发法答案一致(第 3 步);每道题独立评估,加减题不改其他答案。该怕的是问题本身写得含糊,不是问题的数量。
进阶技巧(可选)
看投机扇出的完整产品形态。 官方 smart-home demo(带视频)把整套玩法跑给你看:一句「关掉全屋的灯」,背后是一长串问题——请求类别、目标域、设备类型、对设备的动作……其中「对灯做什么动作」这道题,在你还不知道用户是不是在指挥灯的时候就已经问上了。demo 还展示 TypeSafe 和 LLM 的搭配:检测到复合请求时用 LLM 拆成原子命令、闲聊时退回对话 LLM——官方说法,TypeSafe 这一步相对 LLM 快得多,给整体系统增加的延迟可以忽略。
写问题前翻一眼 how-to 手册第 6 条。 官方 how-to-build-with-system-one 手册把「问很多问题」(Ask a lot of questions)列为最大化「每美元智能」的做法:问题并行评估、代码组合信号、不加串行往返。
让 coding agent 自己改。 你的 agent 如果总写一题一请求,装 TypeSafe Agent Skill(与 Claude Code 配合开发那课详细讲):装好之后,agent 生成的调用代码就会主动把问题批量装进请求,包括投机的那些。
本课小结
✅ Jev 并行提问 = 同一 state 的所有问题装一个请求:三种题型混装、并行独立评估、加题几乎不增响应时间。
✅ 投机扇出 = 「可能需要」的问题也提前问上;模型全答,代码按分支决定读哪些、忽略哪些。
✅ 官方实测背书:13 题 GDPR 监管简报,一次请求 vs 13 次请求——12.2 倍便宜、10.0 倍快、答案不变。
✅ 真依赖才发第二次请求:抓数据进 state / 定 state 组成 / 挑下一题选项,三判据之外都是借口。
✅ 数量无上限,体积有上限:64k per-request / 32k per-question;装不下就几个请求共用同一 state。
关键事实速查:
| 事项 | 值 | 出处 |
|---|---|---|
| 13 问实测 | 12.2x 便宜、10.0x 快、答案不变 | docs/cookbooks/parallel_questions.md |
| 成本 / 耗时 | $0.000497 / 0.27s(批)vs $0.006090 / 2.71s(13 次) | 同上 |
| 13 问构成 | 8 Noul + 2 Choice + 3 Score,各发法跑 5 遍 | 同上 |
| GDPR 条目 | 53,777 字符,固定修订版 1363040264 | 同上 |
| 工单分诊 5 题 | category(Choice)、bug_severity(Score)、has_reproducible_steps(Noul)、refund_requested(Noul)、frustration(Score) | docs/patterns/fan-out.md |
| 路由阈值 | severity > 1.5 且 repro > 0.6 升级;refund > 0.7 打标;frustration > 1.5 优先响应 | 同上 |
| 上下文限制 | 64k per-request / 32k per-question,问题数无上限 | docs/models.md、docs/primitives.md |
注:primitives.md 引用实测时写 11.5x / 9.6x,以 cookbook 原文 12.2x / 10.0x 为准。
下一课预告
一次请求拿回一批答案之后,接下来是「答案怎么用」:下一课 智能路由:意图分类加置信度门控。
你会学到:
- 用 Choice 分类意图、Score 评复杂度,一次请求并行拿回
- 给自动化装刹车:confidence 低于阈值先转人工,过了阈值再分发
- 阈值随风险缩放:查余额 0.6 就放行,批准转账要 0.85 以上