是什么。 Jev 是 TypeSafe 的旗舰模型,也是第一个 System One 模型。一次请求里提交 state 和一组 typed questions,返回每道题的结构化答案、概率分布,以及 Choice/Score 的 confidence。它不生成给人类读的文本。
和 LLM 的差别。 常见 LLM 的后训练(RLHF)优化的是人更愿意读的回复。Jev 的后训练叫 RLCD(Reinforcement learning for calibrated decisions),优化的是决策与校准过的概率。代码拿得到的是 choice、score、noul 这些字段,不需要从一段话里再解析。
怎么用。 确定性的事放代码。需要常识判断的地方拆成原子问题,一次请求并行问完,再用阈值或加权在代码里组合。不确定就升级给人,或走一条保守回退。
一个可运行的例子。 我用它驱动了一条自动贪吃蛇:代码算碰撞和空间,Jev 判断危险度和方向,代码再按门控选出下一跳怎么走。仓库在 ximing/jev-snake-game,文章结尾放了一个运行演示视频。
看 TypeSafe 的文档时,有一句:LLM 是为了给人读的文本而训练的,但是实际上大部分的Agent应用里用的很多的经常是各种意图识别:这封邮件是不是催得急、这张工单该分给哪一组,可能是用workflow来固化,也可能用一个模型做意图路识别,让模型按 JSON 格式输出,再写解析和重试, 又慢又费钱。Jev 把这件事做成了模型的标准化输出。应用场景上给我的感觉类似之前的rerank模型,将大模型做相关性检测变成使用rerank模型做排序。
System One 和 Jev
System One 是一类模型:读自然语言和结构化 state,返回 typed 判断和概率。名字借自 Kahneman 的 《思考,快与慢》“System 1 / System 2”是这本书里的核心框架:
- System 1(系统1):快、自动、直觉、情绪化、省力,容易产生偏见。
- System 2(系统2):慢、费力、逻辑、分析、需要专注,但常常很懒。
System 1的JEV模型一次调用的流程:
极简的代码示例如下:
import { TypeSafeClient, choice, noul, score } from "@typesafe-ai/sdk";
const client = new TypeSafeClient(); // 读 TYPESAFE_API_KEY
const { answers } = await client.systemOne({
model: "jev-latest",
state: { message: "我被扣了两次款,请退款。" },
questions: {
refund: noul("客户是否在要求退款?"),
topic: choice("这张工单该分给哪一组?", {
billing: "扣款、发票、退款",
technical: "故障或对接失败",
account: "登录或资料",
}),
},
});
answers.refund.noul; // 0..1
answers.topic.choice; // "billing" | "technical" | "account"
answers.topic.confidence; // 分布有多集中当前 Jev 1.13 只接受文本:字符串、JSON 对象、数组。没有图像和音频。主训练语言是英语,其它语言能跑,准确率更低。文档写大多数请求大约 100 ms;按输入 token 计费,输出 token 免费,定价页上是每百万输入 token 0.042 美元。
请求参数分析
请求体只有三个顶层字段:
| 字段 | 类型 | 含义 |
|---|---|---|
state | string、object 或 array | 要被判断的内容。工单、对话、当前应用状态都可以。 |
model | string | 用哪一个模型。文档和 SDK 默认 "jev-latest",现在指向 jev-1.13.0。 |
questions | 对象 | 一组题。你给每道题起一个 id,答案按同样的 id 回来。 |
questions 不是一段自然语言,而是一张表:key 是题的 id,value 是一道 Question 对象。展开后,请求的嵌套关系是:
{
"state": {},
"model": "jev-latest",
"questions": {
"<题 id>": {
"type": "noul | choice | score",
"instructions": "真正送给模型的问题",
"criteria": {}
}
}
}上面 SDK 示例里的 noul("客户是否在要求退款?")、choice("这张工单该分给哪一组?", { ... }),发出去就是这样的 Question 对象。HTTP JSON 要自己把字段写全。
Question 对象里,每道题共用两个字段:type(choice / score / noul)和 instructions(字符串、对象或数组)。Choice 和 Score 还要带 criteria,划定可选答案;Noul 的 criteria 可选,用来说明 yes / no 各自指什么。
questions 的 key 只给你的代码用,不会送给模型做推理。模型看到的是 instructions(以及 criteria),不能指望它去猜 id 叫 refund 是什么意思。
instructions 写成对象时,把问题和它要对照的数据分开放,再用反引号路径去引用,和引用 state 里的嵌套字段是同一套规则:
"instructions": {
"potential_duplicate": {
"name": "张伟",
"location": "杭州",
"last_employer": "阿里巴巴"
},
"question": "这份简历和 `potential_duplicate` 是同一个人吗?"
}指向 state 里某一段时,同样用带反引号的路径,例如 `ticket.messages[0].text`。state 里只放这一组题需要的事实,不要把整份应用状态塞进去。
三种题的 criteria 结构不同:
| type | criteria | 限制 |
|---|---|---|
noul | 可选对象 { "true": "...", "false": "..." } | yes/no 各自含义 |
choice | 必填对象,选项名 → 描述(描述可以是 string、object、array,或 null) | 最多 255 个选项 |
score | 必填数组,从下标 0 起的有序档位 | 至少 2 档,最多 10 档 |
下面这个请求把三种题放在同一次调用里,场景和文档 Quick start 相同,文案改成中文:
{
"state": "你好,支付账号对接已经三天了,接口一直失败,订单收不了款。请尽快处理。",
"model": "jev-latest",
"questions": {
"department": {
"type": "choice",
"instructions": "这张工单该分给哪一组",
"criteria": {
"billing": "付款、订阅、退款",
"technical": "故障或对接问题",
"sales": "报价或开户"
}
},
"frustration": {
"type": "score",
"instructions": "客户看起来有多焦躁",
"criteria": [
"平静,只陈述事实",
"不满但还克制",
"非常生气,措辞强硬"
]
},
"is_urgent": {
"type": "noul",
"instructions": "这封消息是否在催、是否有时限"
}
}
}Choice 适合互斥标签(工单划分、这一步走哪个方向),选项之间没有顺序。列表可能盖不全时,自己加一个 other 或 none。Score 适合程度(有多紧急、有多受压),档位由你定义。Noul 适合要概率本身的 yes/no。Noul 的 0.5 表示 yes 和 no 的概率接近。要测程度就用 Score。
返回参数分析
响应同样是固定结构,API 里是这三个顶层字段:
| 字段 | 含义 |
|---|---|
model | 实际答了这道请求的版本号,例如 jev-1.13.0。请求里写别名时,用这个字段记账。 |
answers | 每道题一个答案,key 和请求里的 question id 对齐。 |
usage | input_tokens、output_tokens。计费按输入 token。 |
每个 answer 都带 type,和对应的 question 一致。三种答案的字段如下。
Noul 只有 noul(0 到 1),没有单独的 confidence。靠近 1 是强 yes,靠近 0 是强 no,靠近 0.5 是不确定。
"is_urgent": {
"type": "noul",
"noul": 1.0
}Choice 返回选中项、每个选项的概率(加起来为 1)、以及由该分布算出来的 confidence(0 到 1)。choice 是概率最高的那个选项。
"department": {
"type": "choice",
"choice": "technical",
"confidence": 0.78,
"probabilities": {
"technical": 0.85,
"sales": 0.0,
"billing": 0.15
}
}Score 返回加权分 score、档位图例 legend、各档概率、confidence。score 可以落在两档之间,例如档位是 0 / 1 / 2 时得到 1.05。
"frustration": {
"type": "score",
"score": 1.0,
"confidence": 1.0,
"legend": {
"0": "平静,只陈述事实",
"1": "不满但还克制",
"2": "非常生气,措辞强硬"
},
"probabilities": {
"0": 0.0,
"1": 1.0,
"2": 0.0
}
}模型不会给出你没写进 criteria 的选项。概率只在你划定的空间上分配,代码不用像传统LLM方案一样自己解析响应体。同一请求里可以有多个题目并发提问,而且互相不改对方的上下文,增删一道题不会影响别的题答案。
Confidence 描述分布。probabilities 里某一个选项的值才是「选它的质量」。三个选项里 90 / 6 / 4,confidence 高;33 / 33 / 34,confidence 接近 0。低 confidence 时应该升级给人,或走保守分支,不要把 choice 字段直接当真理。文档里的常用切法是:低于约 0.5 不自动执行;读操作可以低一些,写操作、转账这类把阈值抬高。具体数字要用自己的数据调,0.5、0.6、0.85 只是示例。
文档反复强调的用法是:一道题只问一件能在几秒内拍板的事。要把「这个 startup 好不好」拆成市场规模、技术可行性、差异化,再在代码里加权。需要第二轮请求的情况很少,多数时候把可能用到的题一次问完,用不到的答案丢掉。这就是 speculative fan-out。
和生成文本的 LLM 差在哪
AI primer 把预训练之后的路分成三条:
| 路线 | 优化目标 | 典型产物 |
|---|---|---|
| RLHF | 人更愿意读的回复 | 聊天模型 |
| RLVR | 可验证奖励(数学等) | 更慢、更贵的推理模型 |
| RLCD | 决策和校准过的概率 | System One / Jev |
RLHF 适合对话。它也会奖励听起来很有把握的胡说,以及把输出挤进某一种说话方式(文档里叫 mode dropping)。人觉得一段话写得好,并不等于这段输出能直接在生产环境使用。
RLCD 的范式是另一套:
- 不生成文本,不写解释,不写代码。
- 返回决策和概率。
- 校准是对一组预测说的:标成 0.8 的那一批,大约八成是对的。单次答案仍然可能错。
所以和「让 GPT 输出 JSON」的差别,不只是 schema 严不严。JSON mode 仍然是在生成文本,再希望文本恰好是合法结构。Jev 的答案空间由你在 questions 里划定,模型在这个空间上给分布。代码可以直接 if、排序、加权。
How to build 把三种架构分开写:传统软件是确定性原语组成的决策树;Agent 自己选下一步,人盯着还行,循环一多就容易跑偏;AI-powered software 是代码掌握控制流,只在需要常识判断的节点插入 System One。Jev 明确站在第三种。
贪吃蛇:把判断嵌进控制循环
贪吃蛇每一步的动作空间很小:上、右、下、左,还不能 180° 掉头。碰撞、合法方向、到食物的曼哈顿距离、从下一格出发的连通空间,这些都能用代码算死。真正模糊的是:这一步该追食物,还是该先留空间。这是 System One 可以使用的场景。
我没有把「下一步怎么走最好」丢给模型当一道大题,也没有把棋盘截图发出去(Jev 目前不识图)。流程是:
TypeSafe 文档把用法写成 atomic questions, composed in code:题拆成原子判断,答案在代码里组合。仓库里的 compose() 就是这段组合函数。它读 Jev 的独立答案和本地事实,用阈值走出一个方向。模型不负责合成,也不生成「因为所以」的说明。
- 浏览器持有 16×16 棋盘。每局开场随机铺 12 到 20 格静态路障。
analyze()算出非反向方向、是否立刻撞死、曼哈顿、邻格、flood-fill 可达格数,做成 JSON state。- 只有一个安全方向,或方向键覆盖时,不调 Jev。
- 否则一次
systemOne并行问:危险度 Score、是否先保命 Noul、建议方向 Choice,以及每个合法方向的「安全」「进度」Noul。 compose()用写死的阈值组合:危险度 ≥ 1.5 或保命 Noul ≥ 0.7 走空间最大的方向;否则 Choice 的 confidence ≥ 0.55 且落在安全集里就听 Jev;再不行按进度 Noul 回退。- 右侧决策台把事实、各题概率条、哪些题被采用/未使用、以及
compose()走了哪条分支画出来。
compose() 里是两道 if,然后才决定听不听 Choice:
如果 danger.score ≥ 1.5 或 prioritize_survival.noul ≥ 0.7
→ 保命:在安全方向里选可达空间最大的
否则如果 move.confidence ≥ 0.55 且该方向安全
→ 听 Choice
否则
→ 按各方向的进度 Noul 回退所以要问三道主问题:Score 给第一道 if 左边,Noul 给第一道 if 右边,Choice 给第二道 if。题型是跟着代码要读的字段来的:score、noul、choice + confidence。
危险度用 Score。局面是程度上的:开阔、受压、快被困。曼哈顿和可达格数代码已经算出来了,Jev 要判断的是这些数字合在一起算不算挤。Score 的加权分可以直接拿去和 1.5 比。
是否先保命用 Noul。这是一个开关:这一拍还追不追食物。Noul 接近 1 就进保命分支,接近 0 就继续听 Choice。它和危险度问的不是同一件事:格子多也可能食物在死胡同里,格子少也可能顺着空道就能吃到。
建议方向用 Choice。动作集合事先写死了(当前合法方向),需要从里面挑一个,并带上整份 probabilities 和 confidence。confidence 不够,或选出的方向不在安全集里,代码就丢掉这个 Choice,改走回退。
每个合法方向上的「安全」「进度」Noul 是投机题。保命分支用 _safe 在空间并列时排序,回退分支用 _progress。Choice 被采纳时这两道可以忽略,所以放在同一次请求里一起问。
没有做成一道「下一步怎么走最好」。那样生存和吃食物会混在一个标签里,决策台上也看不出上面哪条分支生效了。
这个游戏我想要验证的是:快速的判断能不能嵌进真实控制循环,答案能不能被代码和 UI 直接消费。录了一段运行过程:
代码在 github.com/ximing/jev-snake-game。pnpm workspace:apps/web 是 Vite 棋盘,apps/server 调 @typesafe-ai/sdk,packages/game 放规则和 compose。
适用边界
Jev 适合动作或标签集合事先能写出来、延迟要压在一次请求路径里、还需要把不确定显式交给代码的地方。工单分流、策略是否覆盖当前请求、工具调用参数对不对,文档里的例子都是这一类。目前实验下来的一些内部场景上甚至有一个暴论,所有路由+workflow的Agent都值得使用JEV重做一下。
但是也有局限性,它不适合需要长推理、自己创造新选项的任务。state 必须是文本。目前看英文题比中文题准,可能前置要过一个翻译模型。校准过的概率仍然会错,所以阈值判断和回退要写在你自己的代码里。阈值得用自己的数据调,文档给的 0.5、0.6、0.85 只是示例。
