Jev 是什么
怎么用,怎么跟大模型配合

左边是代码,右边是注释和图。同一行对着看,不用上下找。

第一部分

Jev 到底是什么

一句话:让大模型从「写作文」变成「打勾」。

普通用法问它问题,它写一段话
你:这张工单该给哪个部门?
    财务、客服,还是销售?

AI:根据工单内容,客户提到被
    重复扣款并要求退款,这属于
    账单和退款范畴,因此应该由
    财务部门处理。

(8 秒)

你得再写个程序从这段话里抠出「财务」。
它还可能写「都有可能」「建议人工确认」。

Jev 用法给它选项,它直接给数字
你:选项 = 财务 / 客服 / 销售

Jev:财务  99.97%
     客服   0.03%
     销售   0.00%

(1 秒)

它给的本来就是数字,不用抠。
它也没法含糊,不能说「都有可能」。

为什么 Jev 没法含糊?因为它根本不写字。

普通大模型是一个字一个字往外蹦的,所以要好几秒,也可能蹦废话。
Jev 只算一遍,然后只看那几个选项对应的字母(A、B、C)各有多大分量,别的地方压根不看。

像让老师写段评语会跑题;给他一张只有三个格子的打分表,他跑不了题——表上没有别处可填。
普通用法Jev
给你什么一段文字一串数字
花多久几秒(逐字生成)约 1 秒(一次算完)
能直接用吗不能,要解析文字,本来就是数字
能说「都可能」吗不能,必须选一个
适合干什么:一切「从固定几个选项里挑一个」的事:工单分流、是不是垃圾邮件、该打几分、是不是差评。
不适合:写文章、回答问题、闲聊。那些还得用普通大模型。
第二部分

三组问题,写法完全一样

用 demo.py 里的三处来说:第 62–66、68–79、81–88 段,写法几乎一样,只有「问题」那个参数在变。

# 第 62–66 行 t = time.perf_counter() r = jev.decide({"ticket": ticket}, Choice("Which queue...?", QUEUES)) show("", r, time.perf_counter() - t)
Choice · 三选一 问题参数是 Choice(...)
从三个部门里挑一个
billing99.97%
access0.03%
sales0.00%
# 第 74–79 行 t = time.perf_counter() r = jev.decide({"ticket": ticket}, Boolean("Is the customer...?", ...)) show("", r, time.perf_counter() - t)
Boolean · 是 / 否 问题参数换成 Boolean(...)
只有两个选项
True99.98%
False0.02%
# 第 81–85 行 t = time.perf_counter() r = jev.decide({"ticket": ticket}, URGENCY) show("", r, time.perf_counter() - t)
Score · 打分 问题参数换成 URGENCY(第 32–38 行定义的)
从五个档位里挑,再按倾向平均
5 分59%
4 分32%
1 分5%
3 分3%
2 分1%
模型根本不知道你在问哪种。在它眼里三种问法收到的都是同一句话:「这里有一堆选项,挑一个。」
区别只在拿到结果之后怎么翻译
整份文件其实就是:
第 1–38 行备料 → 第 41–48 行做工具 → 第 51–93 行主流程(加载 → 预热 → 问三组 → 总结)→ 第 96–97 行发令。

真正「问模型」的动作全篇只出现 8 次,全是同一个写法 jev.decide(材料, 问题)

选项里没有正确答案的时候

第 4 张工单:三个部门都不对口,模型给了个低分。

# TICKETS[3] 是这一句: "The dashboard takes about eight seconds to load..." # 但 QUEUES 里只有三个部门: Option("access", ...) Option("billing", ...) Option("sales", ...) # 没有「技术支持」这个选项
「网站变慢」三个部门都不对口 模型只能硬选一个最不差的,于是概率摊开了:
access67.88%
sales24.97%
billing7.15%
😎
98–100%
前三张
模型很确定
🤔
67.88%
这张没把握
于是举了手
答不上来的时候,模型会告诉你答不上来。把它当阈值用:低于多少就转人工,这个数你自己定。
⚠️ 别把百分比读成「答对的概率」
这些百分比只是模型心里各选项的分量。第 92 行打印的那句 uncalibrated as decision confidence 就是库自己在提醒这件事。
看这些百分比是集中还是散开,比看数字本身靠谱。
第三部分

Jev 能用来干什么

全部就四种写法。左边是代码,右边是解释。

不管你想让 Jev 干什么,代码永远是同一个形状:jev.decide(材料, 问题)
变的只有「问题」那一个参数,而问题一共只有三种: Choice(挑一个)、Boolean(是不是)、 Score(打几分)。
想一次问好几个,外面套一层 decide_many。四种,没了。
① Choice · 从固定选项里挑一个 分类 · 分流 · 路由
from fastjev import Choice, Option QUEUES = ( Option("access", "Login and account access."), Option("billing", "Billing, payments, and refunds."), Option("sales", "Pricing and new contracts."), ) r = jev.decide( {"ticket": "I was charged twice, need a refund."}, Choice("Which queue should handle this?", QUEUES), ) r.value # "billing" r.probabilities # {"billing": 0.9997, ...}
每个选项 = 一个 id + 一句说明 Option(id, 说明) 里,id 是代码里拿到的值, 说明是写给模型看的,所以要写成人话,别写缩写。

拿到 r.value 直接做分支就行:
if r.value == "billing": ...
elif r.value == "sales": ...
billing99.97%
access0.03%
sales0.00%
只能选一个。如果有几件事可能同时成立,别硬塞进 Choice,改用下面那个 Boolean。
② Boolean · 判断一个条件成不成立 检测 · 校验 · 打标
from fastjev import Boolean r = jev.decide( {"ticket": ticket}, Boolean( "Is the customer asking for money back?", true_description="They ask for a refund.", false_description="They do not ask for money.", ), ) r.value # True r.probabilities # {True: 0.9998, False: 0.0002}
一个 Boolean 只问一件事 true_description / false_description 是给模型看的 「两边各是什么情况」,写清楚它才判得准。不写也能跑(有默认值),但准确率会差。

要分三个标签,就写三个 Boolean:
True99.98%
False0.02%
为什么不用一个 Choice 搞定?因为 Choice 只能选一个。而「辱骂」和「广告」经常同时成立,必须能同时为真。
③ Score · 按有序档位打分 打分 · 排序 · 分级
from fastjev import Level, Score URGENCY = Score("How urgently does this need a human?", ( Level(1.0, "No urgency; can wait."), Level(2.0, "Low; within a few days."), Level(3.0, "Normal; within one business day."), Level(4.0, "High; needs attention today."), Level(5.0, "Critical; service is down."), )) r = jev.decide({"ticket": ticket}, URGENCY) r.value # 4.13 ← 概率加权后的数值,可以是小数 r.selected # 5.0 ← 概率最高的那一档
每一档要写成「具体什么情况」 模型看的是文字说明,不是数字。写「3.0」它不知道是什么意思;写「一个工作日内回复」它才懂。

两个结果,别搞混:
r.value加权平均——4.13 表示「4 分和 5 分之间,偏 5 分」。
r.selected 才是它挑中的那一档,5.0。
5 分59%
4 分32%
1 分5%
3 分3%
2 分1%
因为返回的是数值,Score 能排序,Choice 不能。需要排出「谁更紧急」时用 Score。
④ decide_many · 同一份材料,一次问好几个 省时间的那一个
results = jev.decide_many( {"ticket": ticket}, { "queue": Choice("Which queue handles this?", QUEUES), "refund": Boolean("Do they want money back?"), "urgency": URGENCY, }, ) results["queue"].value # "billing" results["refund"].value # True results["urgency"].value # 4.13
传一个字典:名字 → 问题 回来的也是字典:名字 → 结果。名字是你自己起的,不会被模型看到,所以随便起。

decide 其实就是 decide_many 只问一个:
def decide(state, question):
    return decide_many(state, {"decision": question})["decision"]
关键区别:循环调 3 次 decide = 跑 3 遍模型;decide_many 把 3 个问题 塞进同一次调用算完,快得多。

但这些问题互相看不见。不能拿「queue」的答案去问「refund」。要接力的,只能分两次调用。
为什么三种问题能塞进同一次调用?因为它们编译完是一样的。 不管你写的是哪种,到了模型面前都只剩一句话:「这里有一堆带说明的选项,挑一个。」
你写的模型实际收到的选项
Choice(QUEUES) ("access", "Login and account access.")
("billing", "Billing, payments, and refunds.")
("sales", "Pricing and new contracts.")
Boolean("Is…?") ("true", "They ask for a refund.")
("false", "They do not ask for money.")
Score(URGENCY) ("0", "Level 0: No urgency; can wait.")
("1", "Level 1: Low; within a few days.")
所以差别只在拿到结果之后怎么翻译。
Choice 的 id 直接就是答案;Boolean 翻译成真/假;Score 把各档数字按概率加权,再还你一个数。
模型不知道你在问哪一种。它是「挑选项」,翻译是你的代码干的活
第四部分

真实场景里怎么拼

上面四种是零件。下面是把它们拼成能用的东西,每段都能直接抄。

场景 1 · 内容审核:几条规则可能同时踩 Boolean × N
RULES = { "spam": Boolean("Is this spam or an ad?"), "abuse": Boolean("Does this attack a person?"), "unsafe": Boolean("Is this dangerous medical advice?"), } flags = jev.decide_many({"comment": c}, RULES) hit = [n for n, d in flags.items() if d.value] if hit: send_to_review(c, hit) # 哪几条踩了就一起报
一条规则一个 Boolean 这是最常用的形状。decide_many 一次全问完,返回的字典里 哪条是真、哪条是假一目了然

为什么不用一个 Choice 分四类?因为「垃圾广告」和「辱骂」会同时成立, 而 Choice 只能选一个,会丢信息
场景 2 · 检索 / 重排:从候选里挑最相关的 代码粗筛 → 模型精挑
docs = search(query) # 先粗筛出 10 条 cands = {f"d{i}": d for i, d in enumerate(docs)} opts = tuple(Option(k, v.text) for k, v in cands.items()) r = jev.decide( {"query": query}, Choice("Which passage best answers it?", opts), ) best = cands[r.value] # r.value 是 "d3" 这种 id
候选由代码找,选择由模型做 这是个反复出现的分工:代码负责「找出来」,模型负责「挑一个」。 模型看不到你的整个数据库,它只在你给的候选里选。

候选没给对,它不可能选对。漏掉的那条,它再聪明也选不出来。这是这类写法最常见的坑。
cands 这个字典把 id 映射回原对象,就不必去 int(r.value[1:]) 这种解析了。
场景 3 · 护栏:检查大模型的输出 用便宜的判贵的
r = jev.decide( {"q": user_q, "a": llm_output}, Boolean( "Does the answer address what was asked?", true_description="It responds to the question.", false_description="It dodges the question.", ), ) if not r.value or r.probabilities[False] > 0.2: escalate(llm_output) # 拿不准就别直接给用户
大模型写完,Jev 检查一遍 大模型一次调用又贵又慢,Jev 便宜得多,可以用它来审前者

注意这里看了概率,不只看结论。r.value 是它选的; r.probabilities[False] 是它有多倾向「没答上」。 八成把握说是、两成把握说否,也是该转人工的

同样的写法可以检查:有没有编造引用、有没有越权承诺、工具调用参数对不对。
场景 4 · 模型路由:该用贵模型还是便宜模型 先判难度,再选模型
DIFFICULTY = Choice("How hard is this request?", ( Option("simple", "Short and factual; one step."), Option("hard", "Multi-step reasoning or math."), )) r = jev.decide({"prompt": prompt}, DIFFICULTY) model = "small" if r.value == "simple" else "large" answer = call_llm(model, prompt) # 难才用贵的
先花 1/100 的钱判断难度 大量请求其实很简单,用大模型是浪费。先让 Jev 判一下, 简单的大部分就不用调贵模型了

这条和场景 3 是一对:场景 3 是事后检查,场景 4 是事前分流。 一个管「答得好不好」,一个管「该谁来答」。

判断结果可以直接用概率兜底:r.probabilities["hard"] 超过某个阈值就走贵模型, 不必非等它明确选 hard。
场景 5 · 排序:谁更该排前面 Score 能做,Choice 不能
RELEVANCE = Score("How relevant is this to the query?", ( Level(0.0, "Unrelated to the query."), Level(1.0, "Same topic, but does not answer it."), Level(2.0, "Partially answers it."), Level(3.0, "Fully answers it."), )) def score_of(item): state = {"query": q, "text": item} return jev.decide(state, RELEVANCE).value ranked = sorted(items, key=score_of, reverse=True)
要排序,就得用 Score r.value 是个,所以能比大小、能排序、能设阈值。 Choice 返回的是个名字,没法比大小——「billing 比 sales 大」没有意义。

挑一个 vs 排一排,是两种需求:
  • 只要最好的那一条 → 用 Choice,一次调用
  • 要整个顺序 → 用 Score,每条打一次分
条目多的时候,sorted(key=score_of) 是逐条串行调用的。 想快,就用 decide_many 把一批问题打包成一次调用。
反过来,这些事别找 Jev:写文章、生成代码、回答问题、闲聊。
Jev 不写字,只挑选项,所以你没法让它「解释一下」或者「写一段」。 那类事还得用普通大模型。Jev 管的是那些 「从几个可能里确定一个」的判断。
第五部分

跟大模型配合:大模型负责想,Jev 负责定

大模型能生成,但它的输出是一段文字,没法直接进 if。Jev 补的正是这一步。

所以在跟大模型结合时,Jev 永远出现在同一个缝里: 大模型输出之后、代码拿它做决定之前。
一条 agent 链路上有三个这样的缝。
材料 → ① 前置判断 → 挑模型 / 拦下来 → 大模型生成 → ② 后置校验 → 通过 / 重来 agent 循环里,每一步之前再问一次 ③
① 前置 · 该用哪个模型 难度路由
ROUTE = Choice("Which model can handle this?", ( Option("small", "Rename, format, simple lookup."), Option("large", "Design, debugging, multi-file change."), )) r = jev.decide({"task": task}, ROUTE) model = "haiku" if r.value == "small" else "sonnet" answer = call_llm(model, task) # 你的大模型调用
先判难度,再决定调谁 这就是第四部分场景 4,换到 agent 链路的视角看:它是第一个岔口。

这里 Jev 在大模型前面它的输出决定了后面调谁。这一步不能用大模型自己做: 用大模型来决定用哪个模型,等于为了省钱先花钱。

call_llm 是占位,换成你自己的调用(Anthropic / OpenAI / 本地模型都行)。 后面几段里的 readretryask_human 同理, 这一段演示的是「接在哪」,不是「怎么调模型」
② 循环中 · 该读哪个文件 上下文选择
files = list_files() # 代码先把候选列出来 cands = {f"f{i}": p for i, p in enumerate(files)} r = jev.decide( {"task": task}, Choice("Which file must be read to do this?", tuple(Option(k, v) for k, v in cands.items())), ) read(cands[r.value]) # 只把挑中的那个喂给大模型
别把整个仓库塞给大模型 又贵又慢,还容易淹掉真正相关的那部分。让 Jev 先从候选里挑出来, 只把挑中的读进来

这跟第四部分场景 2 是同一个形状,只是候选从文档换成了文件。

agent 每一步都这么挑一次,context 就不会越滚越大。 不这么做的话,跑到第十步上下文就满了。
③ 后置 · 大模型真的干了我要求的事吗 输出校验
r = jev.decide( {"task": task, "diff": diff}, Boolean( "Does this diff do what was asked?", true_description="It implements the request.", false_description="It misses it or edits other code.", ), ) if not r.value or r.probabilities[False] > 0.2: retry(task) # 拿不准就重来,别直接交
agent 说「改好了」,不一定真改好了 它经常会改错文件、顺手动了不相干的代码、或者只改了一半。 让 Jev 拿原始要求对一遍 diff,比让大模型自己检查自己可靠。

为什么不让大模型自检?因为它会为自己的输出找理由。 Jev 只看「要求」和「改动」两段文字,不知道是谁写的,没有护短动机。

这就是第四部分场景 3 那个护栏,用在写代码上。
④ 贯穿 · 危险动作要不要拦 工具调用把关
DANGER = Boolean( "Could this command destroy data?", true_description="Deletes, overwrites, or hits production.", false_description="Read-only, or scoped to this repo.", ) r = jev.decide({"command": cmd}, DANGER) if r.value or r.probabilities[True] > 0.05: ask_human(cmd) # 宁可多问一句
跑之前先问一句 agent 拿着 shell 的时候,最贵的错误是不可逆的那一下rm -rf、覆盖配置、往生产库写。

注意这里的阈值方向是反的。前面几段是「有把握才做」, 这里是「有一点可疑就拦」,所以是 > 0.05 而不是 > 0.5。 拦错一个只多问一句,放过一个可能就回不来了。

这是策略选择,不是模型能力问题。阈值多少永远取决于犯错的代价, 得拿你自己的数据去调。

怎么接进去

两条路:agent 本身就是 Python,就进程内直连;要接编排工具,就走 HTTP。

接法 A · 进程内直连 agent 是 Python 程序
from fastjev import FastJev # 模型只加载一次,整个 agent 生命周期共用 with FastJev.from_pretrained(MODEL_DIR) as jev: r = jev.decide({"task": task}, ROUTE) model = "haiku" if r.value == "small" else "sonnet"
最省事的一条路 你的 agent 本来就是 Python 的话,直接 import 就行,不用起服务、不用走网络。

唯一要记住的:模型加载很贵。 from_pretrained 要读几 GB 权重,所以要 在整个程序里只加载一次,别每次判断都来一遍。

注意它是 with 语句,退出时自动释放显存。写成全局变量也行, 但记得程序结束时 close()
接法 B · 起一个 HTTP 服务 给 n8n / Dify 这类编排工具
# 终端里起服务 fastjev-serve \ --model Qwen/Qwen3.5-4B \ --revision 851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a \ --served-model fastjev-qwen3.5-4b \ --served-model-description 'fastjev on Qwen3.5-4B' \ --served-model-release-date 2026-09-18 # 然后任何能发 HTTP 的节点都能调它 POST http://127.0.0.1:8000/v1/systemone { "model": "fastjev-qwen3.5-4b", "state": "帮我重命名这个函数", "questions": { "danger": {"type": "noul", "instructions": "Is this destructive?"}, "route": {"type": "choice", "instructions": "Which model fits?", "criteria": { "small": "Rename, format, lookup.", "large": "Design or debugging." }} } }
编排工具只认 HTTP,这是唯一的路 n8n、Dify、扣子这类工具没法 import 你的 Python 库, 但它们都能发一个 POST。起服务之后,Jev 就变成了流程里的一个普通判断节点

注意名字变了:
Python 里叫 Boolean,HTTP 上叫 noul
Python 里叫 Choice,HTTP 上叫 choice(一样)。
同一个东西两个名字——从 SDK 换到 HTTP 时最容易卡在这。
state 是材料,questions 是问题字典, 和 decide_many 一一对应,只是翻译成了 JSON。
最后两个参数是必填的:它们只是给 GET /v1/models 做描述用, 但少一个服务就直接启动失败。
返回长什么样 三个类型,三种形状
{ "model": "fastjev-qwen3.5-4b", "answers": { "danger": {"type": "noul", "noul": 0.03}, "route": { "type": "choice", "choice": "small", "probabilities": {"small": 0.97, "large": 0.03}, "confidence": 0.88 } }, "usage": {"input_tokens": 214, "output_tokens": 0} }
三种类型,三个字段名
  • noul → 直接给一个数,是「成立的概率」,不是 true/false
  • choicechoice 是选中的那个,probabilities 是全部分布
  • scorescore 是加权后的数值
output_tokens 永远是 0。 Jev 不生成文字,只读那几个选项位置上的分量, 这就是它比大模型快和便宜的原因
想自己跑一下看完整字段,服务起来之后浏览器打开 http://127.0.0.1:8000/docs,有自动生成的接口文档。
三个一定会踩的坑:

1. 这个服务不是高并发服务。服务只跑一个 worker,所有请求在模型上排队串行执行。 实测:4 个并发请求的总耗时正好是单个的 4 倍,一点都没并行。 agent 里并发打十几个判断,不会更快,只会一起等。

2. 非本机访问必须配密钥。0.0.0.0 时要设 FASTJEV_API_KEY, 否则服务直接拒绝启动。这是好事,别把这个服务裸奔到公网。

3. 概率不是「答对的概率」。它只是各选项的分量,没校准过。 第 92 行打印的那句 uncalibrated as decision confidence 就是在说这件事。 阈值必须拿你自己的数据试出来,别抄别人的。
最后一句实话:不是每一步都值得判。
每问一次 Jev,就是一次前向传播。agent 每一步都判一遍,整体会被拖慢,而你多半感知不到收益。
真正值得插判断的地方通常只有两类: 错了很贵的(删库、发出去、花钱),和 选项本来就不多的(走哪条路、用哪个模型)。
剩下的交给普通代码就行。Jev 是给「代码判断不了、大模型又不确定」的那一小块用的。

FastJev · 是什么、怎么用、怎么跟大模型配合