云计算百科
云计算领域专业知识百科平台

[AI工程]Jev 决策模型第二篇:三原语、一次多问与置信度路由,从 Playground 到能上线的代码

💡 上一篇讲的是它是什么,这一篇只讲怎么用它。官方给的入口非常窄:一个 POST https://api.typesafe.ai/v1/systemone,请求体你只要管三样——state、model、questions;返回体只要管两样——answers 和 usage。剩下的全是工程问题。

但真要把它放进服务里,问题会一串接一串冒出来:一次能问几道题?题目之间会不会互相影响?Choice 的选项能写多少个,Score 的等级有上限吗?confidence 0.78 到底算高还是算低?只读操作和放款操作用同一条阈值行不行?jev-latest 哪天换成新版本,我调好的阈值还在不在?

这些官方文档都答了,只是散在六个地方:api 讲请求体,models 讲价格和限额,primitives 讲三种原语,confidence 讲阈值,patterns 讲套路,cookbooks 讲重排、function calling 和护栏。

麻烦的地方不在于文档写得少,而在于这些答案互相牵制:只按输入计费这一条会逼你改写 state 的形态,state 塞了什么又直接决定概率分布长什么样,分布形状最后决定你的阈值敢不敢让代码自动执行。所以单看任何一页都得不出"能不能上线"的结论。

所以这一篇纯讲实操:一次调用的骨架长什么样、三原语怎么选、一次多问到底省在哪、置信度怎么切成三档,以及意图路由、检索重排、function calling、LLM 护栏这四个官方 pattern 各自怎么落到代码里。收益数字全部标注来源,不掺我自己的实验结果。 jev-in-practice-cover 第二篇

1. 动手之前必须先记住的 5 个数

这一章回答:调用前要钉住哪几个数,以及从拿到 key 到第一次调通有哪三条路径。

这 5 个数决定了后面所有设计判断,先摊成一张表。

维度官方给的数它管住了什么上线时怎么处理
价格 $42/Btok,即 $0.042/Mtok;只按输入计费,输出 token 免费 成本只跟"喂进去多少"有关,跟答案长短无关 按 usage.input_tokens 记账,不要按调用次数估成本
上下文预算 每请求 64k(state + 所有问题合计);另有 32k(state + 最长的那一个问题) 两把尺子:总预算和单题预算 长 state 加多题时两条都算,超哪条砍哪边
速率 250,000 tokens/秒、1,200 requests/分钟;超限返回 429 一个卡体量,一个卡频次 退避 + 队列上限,别让重试把自己打穿
输入类型 string / object / array,仅文本,不接受图片、音频、视频 多模态这条线现在不存在 图片先转成结构化描述,或者留在你自己的链路里
语言 英语为主,CJK 能用但精度更低 中文场景的准确率上限 上中文前先用自家数据测,路由时格外关注 confidence

价格那行值得单独说一句:输出免费不只是便宜,它改变了写法——你不必再压缩答案长度、不必强制精简返回结构,甚至可以让每道题多带一档兜底选项而不花钱。第 4 章的 fan-out 收益和第 6 章的重排打分都建立在这条上。还有两个数很容易被忽略。限额本身不稳:官方明确警告当前需求极大、限额可能随时调整,更高限额走 custom / enterprise,所以别把 1,200 rpm 当成架构容量写进扩缩容公式,要按"随时可能变小"来设计退避和回落。不做 per-tenant 微调:不 fine-tune、不 LoRA,所有账户同一份权重,想让模型按你的口径判,只有 state、instructions、criteria 三处塑形入口——这也是后面几章反复回到 criteria 措辞的原因。

那 250k tokens/秒 和 1,200 rpm 谁会先撞上?简单换算一下就知道(前提只有官方这两个数和官方示例的 392 input_tokens):400 token 量级的短请求跑满 1,200 rpm,折算只有约 8,000 tokens/秒,离 250,000 差两个数量级,先撞的是 rpm;反过来 state 很大(文档级负载)时,64k 上限意味着同一秒最多约 3.9 个满载请求就能顶到 250k tokens/秒,先撞的是 token 线。所以短请求场景要做的是频控,长文档场景要做的是并发闸。

接入的三条路径分工是:Playground 试措辞、看返回长什么样 ──> curl 确认鉴权与请求体形状 ──> SDK 或裸 HTTP 进服务。Playground 在 https://console.typesafe.ai/playground,登录就能用:把文本粘进去当 state,加问题,多个结果一次混着看;API key 在 https://console.typesafe.ai/keys。Playground 省掉的是措辞迭代那一轮成本——criteria 怎么写,在没看到返回的 probabilities 之前很难凭空调。curl 这条建议每个人先走一遍,它能把"官方示例"和"我的服务"之间那层壳剥掉(key 一律走环境变量):

curl -X POST https://api.typesafe.ai/v1/systemone \\
-H "Authorization: Bearer $TYPESAFE_API_KEY" \\
-H "Content-Type: application/json" \\
-d @- <<'EOF'
{ "state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.",
"model": "jev-latest",
"questions": { "urgency": { "type": "noul", "instructions": "Does this message express urgency?" } } }
EOF

要看当前有哪些模型,就 GET https://api.typesafe.ai/v1/models,返回 name / description / release_date 三个字段——这三个字段就是版本 pin 的数据来源,第 7 章会回到这里。SDK 这条路径:pip install typesafe-sdk(或 uv add typesafe-sdk),要求 Python >= 3.10,客户端自动读环境变量 TYPESAFE_API_KEY,默认模型 jev-latest,提供 TypeSafeClient 和 AsyncTypeSafeClient,支持 context manager;JavaScript 侧包名是 @typesafe-ai/sdk,new TypeSafeClient(),列模型是 await client.models.list()。

Q1:官方 SDK 只有 Python 和 JavaScript,我的 Go / Java 服务怎么办?

直接调 HTTP。官方 Client SDKs 页只列了这两种语言,但同一页原文写明也可以直接调用 HTTP API 从任意语言接入。SDK 的增值点其实就两条:默认处理重试(指数退避),以及遵守 retry-after 头。自己实现的话这两条得补上,别只写一个 RestTemplate 就上线。


2. 一次调用的骨架:state + questions 进,answers + usage 出

这一章回答:请求体和响应体到底长什么样,问题 key 和答案是怎么对齐的。

请求 响应
state(string | object | array) ──┐ ┌── answers.<你自己定的 id>:type + 答案字段 + probabilities + confidence
questions(map<id, Question>) ──┘ └── model(真实版本 ID,如 jev-1.13.0) + usage(input_tokens / output_tokens)

请求体顶层三个字段全部必填:state(string / object / array,要被判断的内容)、model(如 jev-latest)、questions(map<你自定的 id, Question>,id 只用来对齐答案、不参与推理)。每道题里 type 和 instructions 必填,criteria 看原语:noul 可选、choice 必填 map、score 必填有序数组(第 3 章细讲)。官方 quickstart 的完整例子就是这一形状的最好说明(下面整段是官方示例,字段、取值、注释一律原样,只压掉了多余折行):

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

client = TypeSafeClient()
ticket = "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP."

response = client.system_one(
state=ticket,
questions={
"department": Choice(
instructions="Which team should handle this",
criteria={"billing": "Payment or subscription issues",
"technical": "Bugs or integration problems",
"sales": "Pricing or account questions"}),
"frustration": Score(
instructions="How frustrated the customer appears",
criteria=["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"]),
"is_urgent": Noul(instructions="The message conveys urgency or time-sensitivity"),
},
)

print(response.answers["department"].choice) # "technical"
print(response.answers["frustration"].score) # 1.0
print(response.answers["is_urgent"].noul) # 1.0

官方同一例子的响应体:

{
"model": "jev-1.13.0",
"answers": {
"department": { "type": "choice", "choice": "technical", "confidence": 0.78,
"probabilities": { "technical": 0.85, "sales": 0.0, "billing": 0.15 } },
"frustration": { "type": "score", "score": 1.0, "confidence": 1.0,
"legend": { "0": "Calm, just stating facts", "1": "Frustrated but civil", "2": "Very angry, strong language" },
"probabilities": { "0": 0.0, "1": 1.0, "2": 0.0 } },
"is_urgent": { "type": "noul", "noul": 1.0 }
},
"usage": { "input_tokens": 392, "output_tokens": 65 }
}

三件事一眼能读出来:answers 的键就是你写的 id;每条答案自带 type,方便代码按类型分派;noul 那条没有 confidence,只有 noul 数值(第 3 章讲为什么)。usage 里那两个数是后面所有成本推导的锚点:一次三题请求,输入 392、输出 65,而输出这一半不收费。

state 的三种形态各有分工,官方 concepts/state 页的原文口径:string 放单条消息、文章、段落("My card was charged twice.");object 放命名字段、相关记录、应用当前状态({"message": "My card was charged twice.", "order_id": "A-104"});array 放消息序列、多条记录(["Hi", "My customer number is TS1337.", "My card was charged twice."])。官方建议大多数请求用 object,让每一部分有描述性名字、关系清晰;简单场景才用 string。这条建议的价值不在排版,在于把"这段文本是什么角色"显式交给判断——同一个 order_id 字段名,比在自然语言里插一句"订单号是 A-104"更稳(这是我的读法,官方只给了"关系清晰"这个理由)。

字段访问方式,SDK 例子给的是 response.answers["department"].choice;官方另一处计数例子里的写法是 result.nouls["item_0"].noul,也就是说批量同种问题时用原语名的复数容器,键还是你的 id。客户端初始化还能直接指定版本:TypeSafeClient(model="jev-1.13")。

Q1:问题的 key 会不会影响判断结果?一次请求能问几道题、能放图片吗?

key 不会影响。官方明确写了:问题的 id 不会送进模型、不参与推理,只用于把回答对齐回你的 key。所以想在题目里表达的东西只能写进 instructions 和 criteria,把业务含义藏在 id 名字里是无效的——这是个很容易犯的错,尤其是习惯用字段名自解释的后端同学。题数没有上限,官方给的是 token 预算:每请求 64k(state + 所有问题合计)、另外 32k(state + 最长的那一个问题),说白了题数是被预算管的,不是被配额管的,第 4 章会把这个换算讲实。图片、音频、视频都不在接受范围里,三种形态全是文本——多模态的活要留在你自己的链路里做完,再把结论作为文本放进 state。


3. 三原语怎么选:Noul / Choice / Score

这一章回答:三种原语返回的字段差在哪、谁带 confidence、选项和等级各有多少上限、instructions 的结构化写法怎么用。

一张表钉死差异(这张表就是选型的全部依据):

原语它在回答什么criteriacriteria 形态上限返回字段confidence
noul 某件事成不成立(是 / 否) 可选 {"true": "…", "false": "…"},定义"是"和"否"各意味着什么 文档未给上限 只有 noul 一个数值 不带
choice 给定选项里哪一个最贴(选项之间比较) 必填 "选项 → 判据描述"的 map,某选项可以给 null 每个 Choice 最多 255 个选项 choice + probabilities(和为 1) 带
score 落在有序档位的第几级 必填 有序等级描述数组 至少 2 级,API 最多接受 10 级 score + legend + probabilities 带

noul 的 criteria 字段名就是 "true" / "false",官方给的形态是 {"true": "Explicitly time-sensitive", "false": "No urgency expressed"}。不写也能跑,但"什么算否"就只能靠模型猜——写清楚是把口径从模型侧搬到你代码侧的主要手段。score 的返回里那个 legend 把数字档位映射回你写的描述(官方示例响应里就是 {"0": "Calm, just stating facts", "1": …, "2": …}),这个字段该跟着答案一起落库,否则半年后你只知道某条记录 score 是 1.0,不知道 1.0 当时是哪句描述——而 criteria 是会改的。

instructions 除了字符串,还能给对象,用来把"问题"和"它要引用的数据"分开。官方原文示例:

"instructions": {
"potential_duplicate": { "name": "John Smith", "location": "Oakland, California", "last_employer": "Google" },
"question": "Is the resume for the same person as `potential_duplicate`?"
}

关键是问题里那个反引号包住的字段名:potential_duplicate 在对象里是数据,在 question 里是一个引用点。说白了,这是把"待比较的另一条记录"从正文里拎出来单独命名,state 放主对象,instructions 放对照对象,两者不混成一坨文本。

Choice 和 Noul 答的不是同一个问题,这是选型里最容易糊的一点:Choice 是候选之间互相比较、必出一个——哪怕所有选项都不合适,它也一定会选一个;Noul 是每个候选各自判、可以全部为否——“一个都不推荐"是一个合法结果。官方 skill suggestion cookbook 的做法把这两者同时用了,理由很清楚:同一份候选集上,Choice 负责"选哪个技能”(相对),Noul 负责"到底要不要推荐"(绝对,可能全低)。只要你的场景里存在"宁可不答"这个选项,就不要只写 Choice。官方四个 pattern 里还有一条 composite scoring,原文是 “Break a complex judgment into atomic scores, combine with weights you control in code.”——复杂判断拆成原子 Score,权重与合成逻辑留在代码里。这条和上一句是同一个意思的两面:排序、加权、阈值这些算术,官方一律交给你的代码,它只负责每题的分。

Q1:255 个选项、10 个等级,是建议值还是硬限制?

按 API 校验算硬限制:官方 api 页写的是"每个 Choice 最多 255 个选项"、“Score 至少 2 级、API 最多接受 10 级”,超出会走 422,而且响应 body 会指出是哪个字段不合格。所以别指望"多塞几个模型会自己忽略"。

Q2:Score 用 Choice 加个排序不行吗?

语义不一样。Choice 的 probabilities 是互斥选项上的分布(和为 1),Score 的 score 是概率加权后的档位值,可以落在两级之间(官方口径),也就是"介于’平静’和’恼火’之间"这种中间态是能被表达的,而 Choice 里没有这个位置。反过来,Score 的档位必须有序,你要是拿它装"部门 / 类型"这种无序标签,就用错了。至于 noul 为什么不带 confidence:官方的 confidence 是从概率分布形状算出来的统计量,只在 Choice / Score 上提供(公式在第 5 章),而 noul 那个 0~1 数值本身就带强弱含义——官方护栏 cookbook 就把它描述成"每类危害的是 / 否概率",要路由直接对它设阈值就行。


4. 一次请求问多个问题:省钱省时的主战场

这一章回答:fan-out 为什么能省这么多、省的钱具体从哪来、以及什么时候不该 fan-out。

官方的性质描述是整个成本结构的地基,原文是:“Jev ingests the state once and evaluates every question against it in parallel”——state 只吞一次,所有问题并行、相互独立地对它评分。省在哪一目了然:拆成 13 次请求就要付 13 份 state 的输入 token 和 13 次往返,合成 1 次请求只付 1 份 state、1 次往返,13 个答案并行返回。

官方 Parallel questions cookbook 的实测结果(这是官方 cookbook 自述的实测结论,不是我跑出来的):负载是一篇约 54,000 字符的 GDPR 维基长文加 13 道题(8 noul / 2 choice / 3 score),批量对单题分请求,12.2x 更便宜、10.0x 更快,答案不变。

"答案不变"官方说得很具体,值得抄成你的回归判据:2 个 Choice、3 个 Score 和 8 个 Noul 里的 6 个,在 5 次重复中数值完全一致,run-to-run std dev 正好 0.0;剩下 breach_72h 和 criminal_penalties 两道题带一点采样噪声,但两种批处理策略下噪声同量级、均值落在噪声内。官方结论是噪声属于题目本身,不是批处理引入的。还有一个口径必须一起读:10.0x 那个时延,是把 13 次单题请求的耗时串起来加总得到的(官方原话点明了这点)。如果你并发打这 13 个请求,时延差距会明显缩小,但 13x 的 token 节省不会跟着缩。所以"快 10 倍"能不能算进你的 SLO,取决于你现在那 13 次调用是串行还是并发——成本这条是硬收益,时延那条要看你原来的写法。

下面这几条是我按官方计费结构做的推算,前提都写出来,读者请按自己的 state 规模重算:

  • 单次成本量级。前提:$0.042/Mtok 且只计输入;官方 quickstart 例单次 input_tokens = 392。推导:392 / 1e6 × 0.042 ≈ 1.6e-5 美元 / 次。
  • 跑满限额一小时多少钱。前提:同上,且每次输入规模接近 400 token。推导:1,200 rpm 跑满一小时是 7.2 万次请求,输入约 2.9e7 token,成本量级约 1.2 美元 / 小时。这个量级意味着:在这个模型上,工程成本(重试、限流、人工复核)大概率比 token 账单贵。
  • 12.2x 为什么不是 13x。前提:官方 cookbook 是文档主导负载(GDPR 文章约 54,000 字符)加 13 个问题。推导:拆成 13 次请求要把文档的 token 付 13 次,所以理论上限接近 13x;官方实测 12.2x,差的约 0.8x 可以归因于"问题自身也要付输入 token"——批量请求里 13 道题的文本是同时进输入的。这是我按官方计费结构的推算,不是官方给出的结论;而且它只在"输出免费"这个结构下成立。

官方 Speculative fan-out pattern 的原文正好给了 fan-out 的正确用法:“Send many questions in a single call, including speculative ones, and let your code decide what’s relevant.”——把可能用到的判断一次问完,再由代码决定读哪条答案。接着上面那个官方例子的形状加两道投机题(angry 的口径写进 criteria、compensation 是一道四档 Score、needs_human 是升级判断),裁决侧就成了纯代码:

answers = client.system_one(state=ticket, questions=questions).answers # 4 道题一次问完
if answers["angry"].noul > 0.5:
tier = answers["compensation"].score # 投机题:只有触发这条分支它才产生业务意义
issue_refund() if tier >= 2.0 else issue_coupon()
elif answers["department"].choice == "technical":
enqueue_specialist(queue="integration") # needs_human 的答案躺着不动,钱已经付过了

关键点在最后一行注释:compensation 的 token 已经在请求里付掉了,但读不读由代码决定。投机题的成本模型是"花几行题目文本的钱,换掉一次额外往返",不是免费的。而题目之间不会互相干扰——所有问题是并行、相互独立评分的,加题主要加的是输入 token 而不是串起来的时延(这是"并行、相互独立"那句的直接推论);真正会互相影响的只有一件事,它们和 state 一起挤在同一条 64k 预算里。也别按顺序取答案:answers 是按你自定的 id 对齐的容器,id 不参与推理,也就没有任何"先后"语义,官方示例里全是 answers["department"] 这种按键取;要按题序处理,请自己维护一个 id 列表。真出现变慢,先怀疑 250k tokens/秒 那条线被顶到,或者 429 在悄悄重试——把 usage.input_tokens 和状态码一起落库就能区分。

场景一次多问合不合适原因写法建议
工单分诊(同一封信要 5~8 个判断) 合适 state 只吞一次,多题摊薄到同一份输入 主判断 + 投机题放同一请求,代码按分支取答案
检索重排(N 个候选) 合适,但要算钱 每个候选一道 Noul,候选数直接进输入 token 先用快召回把候选缩到几十个(官方 cookbook 用的就是每查询 30 条)
后一题依赖前一题的答案 不合适 问题相互独立、并行评分,一次请求里做不到链式 拆成两次调用,第一次的分当第二次的 state 字段
候选集要在运行时才知道 不合适 请求发出前 questions 这个 map 就得定下来 先召回再发第二请求,本质就是上一行
超长文档 + 只要一个判断 一般 省的是重复 state,不是问题本身 先看 64k / 32k 够不够,再谈 fan-out

5. 置信度路由:一条地板线,两条执行线

这一章回答:confidence 是怎么算出来的、三档行为怎么落地、为什么同一系统里只读和破坏性操作要用不同阈值。

官方的三选项 demo 给的近似公式是 (3 × 最大概率 − 1) / 2,一般式写成 (n × maxP − 1) / (n − 1):全部压力集中在一个选项上得 1.0,分布越平均越低。它是从 probabilities 分布形状算出的 0~1 统计量,只存在于 Choice / Score 上。拿官方 quickstart 的响应体做纯算术核对(不涉及任何调用):department 的 probabilities 是 {technical: 0.85, sales: 0.0, billing: 0.15},代进公式 (3 × 0.85 − 1) / 2 = 0.775,四舍五入正好是响应里的 0.78;frustration 的概率全压在一档上,(3 × 1 − 1) / 2 = 1.0,和响应里的 1.0 一致。也就是说:confidence 不是模型自报的"心情",是分布的函数,你能自己复算。

官方给的三档建议:

档位官方建议的行为落到代码里常见错法
高置信 自动执行 直接进分支 只读操作也加二次确认,把时延花在不需要风险控制的地方
中置信 谨慎推进:要用户确认 / 标记复核 / 补信息 走确认链路或打 needs_review 标 当成高置信直接执行,或者当成低置信全部转人工
低置信 不执行,转人工或换系统 route_to_human(),并记录原始答案 拿"再问一次模型"当降级

主例就用官方 confidence 页的原文代码(下面这段是官方示例,逐行讲):

response = client.system_one(
state=user_message,
questions={"action": Choice(
instructions="What is the user trying to do?",
criteria={"check_balance": "View account balance",
"approve_transfer": "Approve the pending withdrawal request",
"support": "Get help with an issue"})},
)

action = response.answers["action"]
confidence = action.confidence

if confidence < 0.5:
# Model is genuinely unsure. Don't guess.
route_to_human(user_message)
elif action.choice == "check_balance":
# Low stakes. Showing the wrong screen is recoverable.
show_balance(account_id)
elif action.choice == "approve_transfer":
if confidence > 0.9:
# High stakes, high confidence. Proceed with confirmation.
confirm_then_execute(account_id)
else:
# High stakes, moderate confidence. Verify first.
ask_user_to_confirm(account_id)

这段的结构值得抄,注释值得逐条读:confidence < 0.5 是一条与业务无关的地板线,兜住"模型自报不确定",不管什么操作都不许越过它;地板线之上,check_balance 直接执行(错了就是看错页面,代价可恢复),approve_transfer 却还要再看一次 0.9。同一个 confidence、同一个答案,两条不同的门槛——官方那句话点得很直白:风险容忍度由你的代码编码。摊成矩阵看更清楚(这是我对上面那段官方代码的读法,不是官方图表):

confidence < 0.5 0.5 <= confidence <= 0.9 confidence > 0.9
只读(查余额) 转人工 直接执行 直接执行
破坏性(放款) 转人工 要用户确认 带确认执行

官方也提醒了:正确阈值取决于你的域和模型在你用例上的表现,保守起步、用自己数据测、观察后再调。所以 0.5 和 0.9 这两个数是示例值,别当推荐值抄进配置中心。

Q1:官方那两个数(0.5 / 0.9)能不能直接用?confidence 一直偏低又是怎么回事?

阈值:能用来自结构,不能用来自数值。可以照搬的是"一条地板线 + 按操作风险分档"这个形状;数字必须自己定——官方自己就写了阈值取决于域和表现。我会用官方示例值起步,然后按 confidence 分布拉开分位点看误判落在哪。偏低则有两种可能,看 probabilities 而不是只看 confidence 一个数就能分清:分布接近平坦,说明题目本身在你的判据下无法区分,这时候改 criteria 的描述比调阈值值钱(塑形入口只有 state / instructions / criteria);分布已经很尖但业务上确实不该执行,那是阈值该抬高。这个调试回路的前提,同样是把 probabilities 跟着答案落库。

Q2:noul 没有 confidence,这类题怎么路由?

直接用 noul 那个 0~1 数值设阈值。官方护栏 cookbook 就是这么做的:一整套 Noul(每类危害的是 / 否概率)加一道 Score(照做的危害有多重),然后由你设阈值决定放行、送审、拦截还是转客服。二值题没有"分布形状"这一层,但分本身已经可用。


6. 四个能直接进生产的 pattern

这一章回答:意图路由、检索重排、function calling、LLM 护栏各自的输入是什么、问题怎么设计、代码怎么裁决。

每个 pattern 都按同三段走:输入 → 问题设计 → 代码怎么裁决。这也是我核对官方 patterns / cookbooks 时的读法顺序。

6.1 意图路由:三条出路,不是只有"交给模型"

官方 patterns 页原文:“Classify incoming requests and route each to the optimal handler: deterministic logic, a specialist LLM, or a human.”——注意"optimal handler"是三类,不是只把请求交给另一个大模型。链路就是:请求进来 ──> Choice 选哪条路 ──> confidence 分档 ──> 确定性逻辑 / 专家 LLM / 人工。

问题设计三条要点(依据官方示例的写法):criteria 里每个选项写的是判据描述,不是标签复读(官方给的是 "billing": "Payment or subscription issues",而不是 "billing": "billing");必须留一个非业务出口(官方示例里就是 support);只读分支和破坏性分支要能在同一份 probabilities 上被区分开,第 5 章那个双阈值就是给这个用的。裁决侧就是第 5 章那段官方代码,不重复。补一条工程约束:路由结果要连 probabilities 一起落库,否则线上出现"该走人工却走了 LLM"这类事故时,你无法回溯当时是分布平还是阈值松。

6.2 检索重排:快召回负责缩小集合,Jev 负责选对那一条

官方《Re-ranking》cookbook 的设定和结果(全部为官方 cookbook 的实测结果):

环节官方设定数字
数据集 CLERC 法律数据集,法院意见书段 3,565 段
快召回 BM25,为每个查询取候选 40 个查询 × 各 30 条候选
重排 每候选一道 Noul,一次请求问完 每查询 30 道题
top-1 命中 重排前 → 重排后 5% → 18%
top-10 命中 重排前 → 重排后 38% → 62%

要点是官方这个分工:快召回负责缩小集合,Jev 负责在候选里判对的那一条。这里有个设计选择值得单独看:官方明确说为什么用 Noul 而不是 Choice——“要在 30 个候选里排序,一个非黑即白的答案不够用”,而 Noul 返回的是 0~1 的数值,这个数本身就是代码拿来排序的分数,你不需要为通用模型另造一套打分标尺。问题模板官方给的是原文一句 Could this candidate passage be from the cited precedent?,靠 criteria 定义什么算真、什么算假,对每个 query-候选对套同一句。那为什么不干脆用 Jev 做召回、省掉 BM25?因为 3,565 段全塞进一个请求就顶穿 64k 预算了,就算切成多次,输入 token 也是按段落总量付费,比 BM25 便宜得多的召回先跑一次才合理——这不是偏好,是预算和单价算出来的(前提:每候选一道 Noul + 只按输入计费)。也正因为每候选一道题,候选数直接进输入 token 账单:把 30 变成 300,花的钱和两条预算线都跟着线性变。

6.3 Function calling:从类型标注里抽闭集

官方 cookbook 的比喻很好懂:咖啡师不会把你的话原样记下来,而是在杯子上勾四个选项;Jev 做同样的事。做法是从函数签名的类型标注里找闭集,映射关系(官方口径):

类型标注生成什么官方叫法说明
Literal[…] 一道 choice 题 One-of 成员就是选项,判据写在 criteria 的描述里
list[Literal[…]] 一道 set 题 Any-number 从成员里挑任意个
bool 一道 flag 题 on / off 开关语义
int、自由文本、数字、日期 不生成问题 —— 沿用函数默认值

官方例子规模:10 个函数共 28 个可填参数。另外要准备一份 spec:每个参数一句问题、每个选项一行描述、每个函数一句描述,再加一道**“在函数之间选哪个”**的题;官方明确说这份 spec 可以由 LLM 从签名替你生成。按上面的映射,book_meal(dish: Literal["pasta", "risotto", "grilled"], is_spicy: bool, guests: int) 这个签名拆出来就是两道题:dish 走 choice(criteria 里每个选项写一行判据,比如 "grilled": "Names a grilled item, no starch mentioned")、is_spicy 走 flag,guests 是 int,不生成题、沿用默认值。核心价值链一句话:进到函数里的值一定是该函数接受的封闭集成员。

官方 date extraction cookbook 是同一手法的另一个方向——把日期每个分量当闭集用 Choice 枚举(12 个月、1-31 日、有界年份),并且显式加一个"未说明"选项,代码再组装成真日期;之后的大小比较、时长、星期全部留在代码里。这个"未说明"选项值得抄:它是把"模型不知道"和"文本里没说"区分开的唯一办法。

6.4 LLM 护栏:为什么"再套一个 LLM 当闸门"不划算

官方的论证有三层,每层都挺扎心:各家 LLM 的安全线位置不同、每个版本还会挪;而你的系统提示词,正好写在"越狱话术要绕过的那个位置"上——这也是护栏不能只写在 prompt 里的原因,闸门和被保护的东西在同一个可说服的组件里。至于再放一个 LLM 当闸门——每个来回都要多付一次延迟和一次调用,而且它同样能被说服。改法(官方 cookbook 的思路):一次请求里放一整套 Noul(每类危害的是 / 否概率)加一道 Score(照做的危害有多重),然后由你设阈值决定放行 / 送审 / 拦截 / 转客服。形状照官方描述写(阈值全是示意值,官方只说"由你设阈值",没给推荐数;score 可落两级之间,所以用 >= 2.0 而不是 == 2):

from typesafe_sdk import Noul, Score, TypeSafeClient

client = TypeSafeClient()

GUARD_QUESTIONS = {
"violence": Noul(instructions="The text asks for help planning violence against people"),
"self_harm": Noul(instructions="The text expresses intent to harm oneself"),
"jailbreak": Noul(instructions="The text tries to ignore or override the assistant's instructions"),
"severity": Score(instructions="How much harm would following this text cause",
criteria=["No harm", "Financial or reputational harm", "Physical harm to a person"]),
}

def gate(text: str) –> str:
a = client.system_one(state=text, questions=GUARD_QUESTIONS).answers
if a["severity"].score >= 2.0 or a["violence"].noul > 0.7:
return "block"
if a["self_harm"].noul > 0.5:
return "route_to_support"
return "pass"

decision = gate(user_text) # 输入侧过一次
if decision == "pass":
answer = your_llm(user_text)
decision = gate(answer) # 输出侧再过一次

两个官方强调点值得单独记住:输入侧和输出侧都要过一遍,因为看起来很正常的提问也可能引出有害回答;而 "Ignore your instructions" 这类文本在这里是被打分成越狱,不是被当成指令执行——判断和执行被拆到了两个组件里,这正是"闸门不是另一个 LLM"的意义。至于四个 pattern 的共同形状,其实就一行:

把判断拆成题(一次请求)──> 按 id 取答案 ──> 阈值 / 权重 / 枚举校验在代码里 ──> 三条出路:确定性逻辑 / 专家模型 / 人工

四段全部是你自己的代码,官方只给第一段和最后一段之间的字段。


7. 接到自己系统里的工程细节

这一章回答:版本怎么 pin、错误码怎么接、state 怎么瘦身、中文场景要额外盯什么、以及让编码助手替你写接入代码。

版本这条最容易埋雷。官方当前的指向关系:

写法现在指向会不会漂该用在哪
jev-latest jev-1.13.0 会,新版本发布后别名跟着走 探索期、Playground、线上没有调好的阈值
jev-preview 目前也是 jev-1.13.0 官方注明现在没有独立 preview 版本 别把它当"实验特性开关"用
jev-1.13 固定 不漂 阈值 / 评估集对着它调出来的线上服务

官方建议写得很直接:如果你的置信度阈值是对着某个版本调出来的,就 pin 版本 ID 而不是别名,并按自己的节奏升级;同时响应里的 model 字段回的是真实版本(如 jev-1.13.0),应该落日志。SDK 侧的写法是 TypeSafeClient(model="jev-1.13")。我在核对时看到一个挺能说明问题的现象:官方公开的 Models 页当前写的是 jev-1.13.0,而《Parallel questions》cookbook 的示例代码里 pin 的是 jev-1.12——两处版本口径不完全一致。我不判断哪边是对的,但它恰好是"为什么要 pin、为什么要把响应的 model 落库"最现实的理由:文档口径都不完全同步,你线上按别名走,出事时凭什么确定当时跑的是哪个版本。判断标准我按"阈值有没有对着它调过"来分:调过就 pin 版本 ID,没调过(探索期、内部工具)用 jev-latest 无妨;灰度时按 model 字段分组对比 confidence 分布,比看总准确率更早发现问题(设计判断)。

错误码四件套:

状态码含义(官方口径)该怎么接我的处理顺序
401 key 缺失或无效 配置问题,重试无意义 启动时就校验环境变量,失败别等首个流量
422 请求体校验失败,body 会指出哪个字段 修代码 日志里存整个 body,不要只存状态码
429 超过速率限制 退避重试 队列要有上限,超了就走确定性路径
529 TypeSafe 临时过载 稍后重试 与 429 分开计数,一个是你的量,一个是它的量

SDK 默认处理重试(指数退避)并遵守 retry-after 头;自己写 HTTP 的这两条要补。401 / 422 属于请求本身错,重试只会放大问题(这是我的判断,官方只说了 SDK 会重试与遵守 retry-after,没有按码区分)。429 意味着你撞的是自己那 1,200 rpm(或 250k tokens/秒)的线,而官方还警告限额可能随时调整,所以重试队列必须有上限;上限之外按官方 intent routing 的第一条出路回落确定性逻辑,而不是无限退避把工单堆到超时。

state 瘦身的几条实操(依据官方 state 形态建议 + 预算结构):优先 object 且只放判断需要的字段,用户历史、附件摘要、内部标签留在自己库里;两把尺子都要算,64k 管 state 加所有问题的总量、32k 管 state 加最长单题,长文档负载撞的第二条更隐蔽,因为单题写长了同样会超;投机题也算 state 之外的输入,一次 fan-out 里十几道 instructions 会明显推高 input_tokens,记进日志才能归因;重排场景里候选段落是绝对主成本,先压候选数再压题长。

中文(CJK)场景要额外盯的:官方口径是英语为主、CJK 能用但精度更低,上非英语负载前先用自家数据测,路由时格外关注 confidence。我会这么落(以下是我的设计判断,不是官方建议):中文阈值整体比英文保守起步;Choice 里给"其他 / 信息不足"这类兜底选项(官方 date extraction 那个"未说明"选项的手法与语言无关);线上按语言分组看 confidence 分布,别让一个平均值把两类混在一起盖住。官方文档没有规定 instructions 该用哪种语言,所有官方示例都是英文,中文题里要不要写成中文我拿不到文档答案,只能你自己测——这条我明确标为未核实。

最后一件省事的事:官方给了 agent skill,让编码助手替你写接入代码。Claude Code 走插件市场 claude plugin marketplace add typesafe-ai/skills 然后 claude plugin install typesafe@typesafe-ai;其他 agent 用 npx skills add typesafe-ai/skills –skill typesafe-ai,默认装到项目级,加 -g 装全局。官方说明是:装了这个 skill,编码助手就能拿到 API、primitives、patterns 的完整上下文来生成正确代码(SKILL.md 是公开文件,在官方 skills 仓库里)。这里要说清楚避免误会:本文所有示例都是我从官方文档与 cookbook 原文改写来的,没有借助这个 skill 生成,也没有实测过它生成的代码。但按它的定位,让编码助手写接入代码时挂上它,比让它凭记忆猜字段名安全得多。

Q1:能不能让它学我的业务口径?

不能微调,官方明确不 fine-tune、不 LoRA、所有账户同一份权重。业务口径只有三个落点:state 里放什么字段、instructions 怎么问、criteria 每条描述怎么写。第 4 章那个 criteria={"true": …, "false": …} 和第 3 章的反引号结构化引用,就是全部的可控面。


最后总结

  • 接入这件事本身很快:Playground 试措辞、curl 定形状、SDK 或裸 HTTP 进服务;官方 SDK 只有 Python(>= 3.10)和 JavaScript,其他语言直接调 POST https://api.typesafe.ai/v1/systemone,SDK 的增值是重试指数退避和遵守 retry-after。
  • 5 个数决定架构:$0.042/Mtok 且输出免费、64k 与 32k 两把预算尺、250k tokens/秒 与 1,200 rpm、只接文本、英语为主 CJK 精度更低;而且官方自己写了限额可能随时调整,所以容量按"会变小"设计。
  • 一次多问是主战场:官方口径是 state 只吞一次、所有问题并行且相互独立,cookbook 实测 12.2x 更便宜、10.0x 更快、答案不变;12.2x 和 13x 之间那 0.8x,是问题自身也要付输入 token 的结果(我按官方计费结构的推算)。但那个 10.0x 的前提是"单题请求串行发",你原来就并发的话时延收益会缩水,无条件成立的是成本那条。
  • 三原语的差别在返回字段,不在语义包装:choice 给相对选择、score 给绝对档位且可落两级之间,两者带 confidence;noul 只有数值、不带 confidence,路由时直接对它设阈值。只要场景里存在"宁可不答",就不要只写 Choice。
  • 置信度是可复算的分布函数:(n × maxP − 1) / (n − 1) 能对上官方响应里的 0.78 和 1.0;要抄的是"一条地板线 + 按操作风险分档"这个形状,0.5 / 0.9 这两个数得自己重定。
  • 四个 pattern 的共同形状:把判断拆成题(一次请求)→ 按 id 取答案 → 阈值 / 权重 / 枚举校验全在你代码里 → 出口保留确定性逻辑、专家模型、人工三条。护栏这条官方讲得最狠:再套一个 LLM 当闸门要多付一次延迟一次调用,而且它同样能被说服。
  • 工程细节里最容易被跳过的是版本,也最不该跳过:别名会漂、jev-preview 现在并没有独立版本、pin 版本 ID 并把响应 model 落日志;至于对后端 / 架构同学,这个接口真正省下来的不是"少写 prompt",是把一段非结构化判断换成一组可落库、可索引、可回归的字段——你要建的还是那三样老东西:answers 落库表、usage.input_tokens 的成本归因、429 / 529 之后的确定性回落。

一句话结论:把 Jev 当一个"返回字段的接口"设计,它会很听话;把它当一个"会替你决定的服务"接进主干,你缺的不是模型能力,是阈值、回落和日志这三件老基本功。

参考资料 & 致谢

[1] Quick start-官方文档 [2] API reference-官方文档 [3] Models、价格与限额-官方文档 [4] 三种原语-官方文档 [5] State 写法-官方文档 [6] SDK-官方文档 [7] Parallel questions-官方 cookbook [8] Speculative fan-out-官方 patterns [9] Confidence-官方文档 [10] Intent routing-官方 patterns [11] Re-ranking-官方 cookbook [12] Function calling-官方 cookbook [13] Guardrails for LLMs-官方 cookbook [14] Agent skill-官方文档

赞(0)
未经允许不得转载:网硕互联帮助中心 » [AI工程]Jev 决策模型第二篇:三原语、一次多问与置信度路由,从 Playground 到能上线的代码
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!