莱客AI商学院/文章/Jev 实操:一次请求问完五个问题,把路由、打分、抽取、校验一起做掉
深度文章免费

Jev 实操:一次请求问完五个问题,把路由、打分、抽取、校验一起做掉

从 curl 到 SDK 到投机扇出,全部按官方文档的真实接口形状写。附带五个会让你拿到错答案的坑

莱客编辑部2026-09-24预计 9 分钟
Jev 实操:一次请求问完五个问题,把路由、打分、抽取、校验一起做掉 封面

文章目录

共 6 节
01一、五分钟跑通第一个请求02二、三种题型的正确写法03三、投机扇出:把还用不上的问题也一起问了04四、confidence 是分布的形状,不是准确率05五、抽取:先用代码找候选,再让它选06六、五个会让你拿到错答案的坑

这篇不讲理念,只讲怎么调。所有请求体、响应体、SDK 调用的形状都来自 docs.typesafe.ai 的当前文档,不是我想象出来的 SDK。

前置条件:在 console.typesafe.ai 拿一个 API key(目前是早期访问 + 候补名单)。想先不写代码看看效果,Playground 在 console.typesafe.ai/playground,粘一段文本当 state,加一个问题就能跑。

一、五分钟跑通第一个请求

一个端点,一个 Bearer token。

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "state": "付款连续失败三天了,急。",
  "model": "jev-latest",
  "questions": {
    "is_urgent": {
      "type": "noul",
      "instructions": "这条消息表达了紧急或时间敏感"
    }
  }
}
EOF

返回:

{
  "model": "jev-1.13.0",
  "answers": {
    "is_urgent": { "type": "noul", "noul": 0.95 }
  },
  "usage": { "input_tokens": 296, "output_tokens": 20 }
}

注意 model 字段返回的是带版本号的真实模型 ID,不是你传进去的别名。jev-latest 指向 jev-1.13.0,jev-preview 目前指向同一个。别名会随发布移动,所以:如果你的 confidence 阈值是在某个版本上调出来的,就把版本号钉死,别用别名。

Python SDK:pip install typesafe-sdk(要 Python ≥ 3.10),客户端默认读环境变量 TYPESAFE_API_KEY、默认走 jev-latest。JS/TS:npm install @typesafe-ai/sdk(Node ≥ 20)。

二、三种题型的正确写法

三种题型共享 type 和 instructions,各自再加自己的 criteria。

Choice 的 criteria 是「选项 → 说明」的字典,说明可以是 null。Python 写法:

from typesafe_sdk import (
    Choice, Noul, Score, TypeSafeClient)

client = TypeSafeClient()

resp = client.system_one(
    state=ticket,
    questions={
        "department": Choice(
            instructions="这条该谁处理",
            criteria={
                "billing": "付款或订阅问题",
                "technical": "Bug 或集成问题",
                "sales": "价格或开户问题",
            },
        ),
        "frustration": Score(
            instructions="客户表现出的不满程度",
            criteria=[
                "平静,只是在陈述事实",
                "不满但克制",
                "非常愤怒,用词激烈",
            ],
        ),
        "is_urgent": Noul(
            instructions="这条消息表达了紧急或时间敏感",
        ),
    },
)

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

JS 版更紧凑,答案类型由问题推断出来:

import { choice, TypeSafeClient }
  from "@typesafe-ai/sdk";

const client = new TypeSafeClient();
const res = await client.systemOne({
  state: { document: "被重复扣款了,尽快处理。" },
  questions: {
    category: choice("这条工单是关于什么的", {
      billing: null, technical: null, other: null,
    }),
  },
});
console.log(res.answers.category.choice);

三件容易写错的事:

  • →问题的 key 不会发给模型。官方明说 question id 只是给你的代码用的,不参与推理。所以别指望把语义藏在 key 里——完整的意思必须写进 instructions。
  • →Score 的等级描述必须能独立成立。「中等」这种词是废的,每一级要描述一个具体情形。等级最少 2 个、最多 10 个。
  • →Noul 的 criteria 里 true/false 不要写反。官方点名说,true 映射到「否」、false 映射到「是」这种写法会显著变差——instructions 和 criteria 要求的东西不一致时,模型会乱。

还有一个很实用的语法:instructions 可以是对象。把问题放一个字段、把它要引用的数据放别的字段,然后在问题里用反引号指名字:

"instructions": {
  "potential_duplicate": {
    "name": "John Smith",
    "location": "Oakland, California",
    "last_employer": "Google"
  },
  "question": "这份简历和 `potential_duplicate` 是同一个人吗?"
}

指 state 里的嵌套字段是同一套写法,比如 `ticket.messages[0].text`。

三、投机扇出:把还用不上的问题也一起问了

这是 Jev 和 LLM 在工程上差别最大的地方,也是最省钱的一招。

传统做法是先问类别,拿到「是 bug」之后再发第二次请求问严重度。Jev 不用:所有问题在同一次请求里对同一份 state 并行、隔离地评估,彼此看不见对方的答案。多问几个问题几乎不增加响应时间,而 state 只计费一次。

所以正确姿势是一次把决策树需要的全部判断都问掉,用代码决定哪些答案是相关的:

category = resp.answers["category"]
severity = resp.answers["bug_severity"]
repro = resp.answers["has_reproducible_steps"]
refund = resp.answers["refund_requested"]
frustration = resp.answers["frustration"]

if category.choice == "bug_report":
    if severity.score > 1.5 and repro.noul > 0.6:
        escalate_to_engineering(tid, severity="high")
    else:
        add_to_bug_backlog(tid)
elif category.choice == "billing":
    if refund.noul > 0.7:
        route_to_billing_with_flag(tid, refund_likely=True)
    else:
        route_to_billing(tid)

# 不管哪一类,不满度都有用
if frustration.score > 1.5:
    flag_for_priority_response(tid)

如果工单最后是个功能请求,bug_severity 的答案就直接扔掉,没关系——它省下的是一次往返。

官方有一份 cookbook 把这件事量化过:拿 GDPR 的维基百科条目(约 53,777 字符)当文档,问 13 个问题(8 个 Noul、2 个 Choice、3 个 Score)。一次请求打包问完,比拆成 13 次请求便宜 12.2 倍、快 10.0 倍,答案没有变化。每种方式各重复 5 次比对标准差,结论是打包不引入额外噪声,大部分答案 5 次完全一致、标准差就是 0.0。

文档主导了每一次请求的体积。13 次单问题调用要为这份文档付 13 次钱、走 13 个往返;打包只付一次。文档越大,省下的越接近 N 倍。

什么时候必须发第二次请求?只有三种情况:需要先拿到第一个答案才能去取证据;需要用它构造新的 state;或者下一步的选项集合取决于上一步的结果。

四、confidence 是分布的形状,不是准确率

这是最容易用错的一个字段。

Choice 和 Score 的答案都带 confidence(Noul 不带)。它是从 probabilities 的形状算出来的:概率全压在一个选项上 → 接近 1;摊得越平 → 越低。它描述的是「这次分布集中不集中」,不是「你这条流水线的正确率」,更不是「可以动手了」的许可。

官方给的用法是按风险分档,同一个系统里不同动作用不同阈值:

action = resp.answers["action"]

if action.confidence < 0.5:
    # 模型确实拿不准,别猜
    route_to_human(user_message)

elif action.choice == "check_balance":
    # 低风险,看错一屏是可恢复的
    show_balance(account_id)

elif action.choice == "approve_transfer":
    if action.confidence > 0.9:
        # 高风险 + 高置信,可以走,但仍然要确认
        confirm_then_execute(account_id)
    else:
        # 高风险 + 中等置信,先问一句
        ask_user_to_confirm(account_id)

两个要记住的反直觉点:

  • →Noul 落在 0.5 附近,意思是「是」和「否」概率接近,不是「中等强度」。别把它当成一个程度刻度。
  • →低 confidence 不一定代表答案没用。如果好几个选项本来就都可以接受(比如给用户推荐一个无害的偏好),概率摊平是正常的。用不到的那些分支上的不确定性,直接忽略。

阈值定多少?官方的回答是:在你自己的数据上测,按后果定。别抄 cookbook 里的数字。

五、抽取:先用代码找候选,再让它选

「从这段文本里把金额抽出来」是个很自然的需求,但直接这么问是错的——Jev 不做生成。

正确的拆法是:代码负责找出候选,模型负责选出对的那一个,代码负责归一化。官方 cookbook 的做法是用正则先把所有邮箱、电话、金额的候选片段找出来,编上号,然后用一个 Choice 让模型在这些编号里挑。这样拿回来的一定是原文里真实存在的一段,不是模型「写」出来的。

同一个思路可以推得很远。有一份 cookbook 把 GitHub 服务条款切成 218 行、每行编号,用一个 Choice 对 218 个行号打分做语义搜索;同一次请求里再挂一个 Noul 问「这份文档里到底有没有答案」——因为 Choice 的概率恒定加起来等于 1,哪怕全都不相关也会有一行排第一,所以必须有一个独立的 Noul 来回答「有没有」。

日期也一样。别问「哪个日期更早」,改成用 Choice 把年、月、日各自挑出来(月份 12 个、日 31 个,都是封闭集合,还能加一个「未提及」选项),然后在代码里拼成真正的 date,排序、算差、算星期几全归代码。

六、五个会让你拿到错答案的坑

  • →state 塞太多。无关内容是干扰项,准确率会掉,而且出了错你分不清是哪一段导致的。先检索、先过滤,只发这个问题要用的字段。实在没法在代码里过滤,就先用一个 Noul 判相关性,再决定要不要送进去。
  • →一个问题里藏了好几个判断。「这条工单要不要升级」里面其实压着严重度、可复现性、客户等级三件事。拆开问,用代码合。
  • →把 Noul 上调好的阈值搬到 Choice 上。两者回答的不是同一个问题:Choice 是相对的(哪一个赢),Noul 是绝对的(这条成不成立),所有选项的 Noul 可以同时很低。
  • →用 Score 的期望值去反推一个精确数值。官方明确说等级之间的数值标定很弱,能用来判「过没过某个阈值」,不能用来在两级之间插值还原出原始数字。
  • →忘了限速。jev-1.13 目前是每秒 250,000 token、每分钟 1,200 请求,超了返 429;服务端过载返 529。官方 SDK 默认带指数退避重试并尊重 retry-after;自己写 HTTP 的记得补上。上下文预算是单次请求 64k token(state 加所有问题),其中 state 加最长的那一个问题不超过 32k。

最后补一句纪律:API key 放服务端。这在 Web 应用里是常识,但 Jev 因为「延迟低到可以边打字边判断」,很多人第一反应是直接在前端调——官方文档专门写了这一句。