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(...)
只有两个选项
# 第 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%
答不上来的时候,模型会告诉你答不上来。把它当阈值用:低于多少就转人工,这个数你自己定。
⚠️ 别把百分比读成「答对的概率」
这些百分比只是模型心里各选项的分量。第 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:
为什么不用一个 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 / 本地模型都行)。
后面几段里的 read、retry、ask_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
choice → choice 是选中的那个,probabilities 是全部分布
score → score 是加权后的数值
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 是给「代码判断不了、大模型又不确定」的那一小块用的。