Skip to content
⚡ 高效工作流第 2 / 5 节

并行提问与投机扇出:一次请求问 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 遍
  • 维护「先问哪题、拿到答案再问哪题」的状态机
  • 猜「加题会不会把其他答案带偏」

🎒 开始前的准备 ​

核心思路 ​

先记官方 primitives 文档的三句底牌:

  • 并行:请求里的每道题对同一个 state 并行评估——加题几乎不改变响应时间,只多花那几题的问题 token
  • 独立:一个问题的答案不是另一个问题的隐藏上下文——加减问题,其他答案不变
  • state 是大头:材料占据每次请求的绝大部分——N 次单题请求把材料重发 N 遍,批问只发一遍

投机扇出的官方定义(TypeSafe fan-out 模式页):

把系统需要的所有问题放进单个请求——包括投机的问题——事后再由代码决定哪些相关。

为什么敢把「可能用不上」的问题也提前问?官方解释:所有问题并行评估,加题对响应时间的影响通常很小;问一个可能不需要的问题,接近免费。

一条工单的完整分诊流程长这样(官方示例的结构):

text
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:

python
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 示例原文,一字未改。

运行:

bash
.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 原样;处理函数是官方占位名,换成你自己的业务逻辑):

python
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_reportseverity > 1.5 且 repro > 0.6升级工程组
bug_report(其余)—进 bug 积压
billingrefund > 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_72hNoul个人数据泄露是否必须在 72 小时内上报监管机构?
instrument_typeChoiceGDPR 属于哪种欧盟法律工具?(Regulation / Directive / Treaty / Recommendation)
compliance_burdenScoreGDPR 给组织施加的合规负担有多重?(5 档量表)

结果一:批不批,答案不变。 13 道题里 11 道(2 Choice + 3 Score + 8 Noul 里的 6 道)在 5 次重复里逐次完全一致,两种发法标准差都是 0.0;剩下 breach_72h 和 criminal_penalties 带一点运行间噪声,两种发法的均值在噪声范围内一致。官方结论:噪声是问题自身的属性,不是批处理加的——批处理既不偏移答案,也不增加方差。

结果二:成本与时间差 12.2 倍 / 10.0 倍。

发法调用次数成本总耗时
一次请求装 13 题1$0.0004970.27s
13 次请求,一题一次13$0.0060902.71s

官方的解释:单题发法把 53,777 字符的条目重发 13 遍,批问只发一遍。表里的总耗时把 13 次单题延迟相加(假设一次接一次跑);并发发出去能缩小耗时差距,但 13 倍的 token 成本不变。文档越大,节省越接近全额 13x。

复跑(不用 API key):

bash
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 并行提问有一条硬边界:同一请求里的问题彼此独立,一个答案不能当另一个问题的上下文。那「先问类别、拿到答案再决定下一题问什么」的代码,到底该不该拆成两次请求?

官方判据只有一条:代码不拿到第一个答案,就无法构造第二个请求。具体三种情况:

  1. 要用答案去抓更多数据进 state(答案决定去哪抓什么)
  2. 要用答案决定 state 里装哪些东西
  3. 要用答案挑下一题的选项

三种之外:并进第一个请求,答案回来在代码里组合。官方原话:两个请求是例外,不是规则。

三个官方「真依赖」实例(都来自 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 以上
搞懂AI关于我们怎么写隐私说明

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