← 返回首页
READING NOTE / 010完整原文 · 分块对照

Build your own Jev (100% local)

自己构建 Jev(100% 本地运行)

2,984 个英文词 · 11 个原文块 · 19 张保存图片

总结

文章作者介绍了一种将开源大语言模型转化为本地决策引擎的方法:当应用已预先知道所有可能答案时,不让模型生成句子或 JSON,而是对固定选项进行打分并返回概率分布。以将“同一订阅被重复扣费”的工单分流到三个团队为例,模型可以一次性比较 billing、technical support 和 account access,并由应用根据概率和阈值决定自动路由或转人工审核。

作者区分了固定答案评分与结构化输出。结构化输出保证生成合法对象,但模型仍会逐 token 生成字段名和值;固定答案评分则读取下一 token 位置上对应候选标签的 logits,只在声明的选项之间应用 softmax。文章强调,0.91 表示三项候选中的概率质量分配,不代表模型有 91% 的正确率,准确性仍需通过带标签样本评估和校准。

文章说明了如何用 SGLang 的 /v1/score 和 /tokenize 接口,在本地运行 Qwen 模型实现这一机制:用单 token 标签 A、B、C 代表语义选项,检查其分词结果,读取对应分数并映射回应用选项。示例使用 Qwen/Qwen2.5-0.5B-Instruct,返回的概率为 0.67776233、0.310878605 和 0.011359035;作者还将评分路径与同一服务器上的普通文本生成进行延迟比较。

作者指出,这篇文章复现的是 Jev 风格的推理路径,而不是完整的 Jev 系统;Jev 的训练、校准、权重、RLCD 流程和评估栈并未被复现。评分适用于输出集合有限且调用方只需要标签和概率分布的任务;当输出内容无法预先枚举时,应继续使用普通生成或结构化生成,并在候选不完整时加入 OTHER 或 ESCALATE 选项。

  • 大语言模型决策评分
  • SGLang
  • 固定答案概率分布
  • 本地推理
  • 结构化输出

英文原文 01

235 词 · 2 张图
Image

Everything you need to turn an open-source LLM into a fast, local decision engine without retraining it. It covers next-token scoring, fixed choices with probability distributions, SGLang, and a hands-on benchmark against standard text generation on the same model.

Many LLM calls do not need newly written text. The application already knows the possible answers, and it only needs the model to choose one.

Suppose a support ticket says, "I was charged twice for the same subscription."

The application needs to send it to one of three teams: billing, technical support, or account access.

A normal LLM call asks the model to write an answer. It might return a sentence, a label, or a JSON object. The application waits for that text, parses it, and extracts the selected team.

Image

That is unnecessary if every valid answer is already known.

In fact, a better and more efficient way to do this is to treat the same request as a decision (what Jev does). The application provides the ticket/query and the three allowed answers to Jev. The model then returns a score for each answer in one go (we'll discuss shortly how exactly you can do that):

plaintext
billing             0.91
technical support   0.06
account access      0.03

Billing is selected with the highest probability here, and the application code can actually see how strongly it won.

This is the behavior Jev possesses that we will reproduce locally.

中文译文 01

377 字
Image

将开源 LLM 转变为快速、本地决策引擎所需的一切,而无需重新训练它。本文涵盖下一个 token 评分、带概率分布的固定选项、SGLang,以及在同一模型上与标准文本生成进行的实践基准测试。

许多 LLM 调用并不需要新写出的文本。应用已经知道可能的答案,只需要模型选择其中一个。

假设一张支持工单写着:“同一个订阅我被收了两次费用。”

应用需要将它发送给三个团队中的一个:计费、技术支持或账户访问。

普通的 LLM 调用会要求模型写出一个答案。它可能返回一个句子、一个标签或一个 JSON 对象。应用等待这段文本,解析它,然后提取所选团队。

Image

如果每个有效答案都已经已知,这样做就是不必要的。

事实上,更好且更高效的做法是将同一个请求视为一次决策(这正是 Jev 所做的)。应用向 Jev 提供工单/查询以及三个允许的答案。随后,模型一次性返回每个答案的分数(稍后我们会讨论具体如何实现):

纯文本
billing             0.91
technical support   0.06
account access      0.03

这里会选择概率最高的“计费”,应用代码实际上可以看到它胜出的程度。

这就是我们将在本地复现的 Jev 所具备的行为。

英文原文 02

219 词 · 2 张图

We will provide the input query and allowed answers. In one scoring request, the model will return a decision and a probability distribution without generating a sentence or JSON object.

While Jev is closed-source, this inference pattern is already available in several open language models.

More specifically, we will implement it using SGLang (through /v1/score), test it with Qwen and DeepSeek models, and compare it against structured output and ordinary text generation.

Image

To set expectations upfront, this article recreates the inference path, not the complete Jev system. Jev also includes training and calibration work that a scoring endpoint does not provide.

Fixed-answer scoring is different from structured output

It is easy to confuse Jev's mechanism with structured output since both approaches restrict what the application receives, but they do different work inside the inference server.

For the same support ticket discussed above, a structured output might get the following output from the model:

json
{"team": "billing"}

The specified schema prevents an invalid object, so it does not select the team on its own.

However, this can be further improved since, under the hood, the model still generates the opening brace, the field name, the value, and the closing brace one token at a time. Once generation finishes, the application reads the team field. This video depicts this process:

中文译文 02

369 字

我们将提供输入查询和允许的答案。在一次评分请求中,模型会返回一个决策和一个概率分布,而不会生成句子或 JSON 对象。

虽然 Jev 是闭源的,但这种推理模式已经可以在几个开放语言模型中使用。

更具体地说,我们将通过 SGLang(使用 /v1/score)来实现它,用 Qwen 和 DeepSeek 模型进行测试,并将其与结构化输出和普通文本生成进行比较。

Image

先说明预期:本文复现的是推理路径,而不是完整的 Jev 系统。Jev 还包括训练和校准工作,而评分端点并不提供这些功能。

固定答案评分不同于结构化输出

由于这两种方法都会限制应用接收到的内容,所以很容易将 Jev 的机制与结构化输出混淆,但它们在推理服务器内部执行的是不同的工作。

对于上文讨论的同一张支持工单,结构化输出可能从模型得到如下输出:

JSON
{"team": "billing"}

指定的模式会阻止无效对象,因此它并不会自行选择团队。

然而,这还可以进一步改进,因为在底层,模型仍然会一次一个 token 地生成左花括号、字段名、值和右花括号。生成完成后,应用读取 team 字段。该视频展示了这一过程:

英文原文 03

246 词 · 2 张图

With scoring (which Jev does), the application can supply the three teams as the complete list of valid outcomes. The server can read one model score for each outcome and return the distribution shown earlier in this article. It does not generate a JSON object.

Image

We can then return all three values instead of billing so that the application code can treat these two results differently:

plaintext
Result 1                          Result 2
billing       0.91                billing       0.46
technical     0.06                technical     0.44
account       0.03                account       0.10

For instance, in the above situation, both will select billing.

But the first has a clear preference, while the second one is almost tied. The application can route the first ticket automatically and send the second one for review if needed.

Also, the model will only supply these scores, and the application code will set the rule for its downstream use.

For example, one might require the top answer to exceed 0.80 and lead the second answer by at least 0.20. Those thresholds live in code, where they can be tested and changed.

Image

A value of 0.91 means billing received 91 percent of the probability mass to these three choices. It does not prove that the model is correct 91 percent of the time. We need labeled examples to measure that. This is the calibration problem I will discuss later.

But in the meantime, remember that structured output generates a valid object. Fixed-answer scoring returns a distribution over answers the application already knows.

中文译文 03

403 字

使用评分(Jev 所采用的方式)时,应用可以将三个团队作为完整的有效结果列表提供给服务器。服务器可以读取模型对每个结果的一个分数,并返回本文前面展示的分布。它不会生成 JSON 对象。

Image

然后我们可以返回全部三个值,而不是只返回 billing,这样应用代码就能区别处理以下两种结果:

纯文本
Result 1                          Result 2
billing       0.91                billing       0.46
technical     0.06                technical     0.44
account       0.03                account       0.10

例如,在上述情况下,两者都会选择 billing。

但第一个结果有明确的偏好,而第二个结果几乎难分高下。应用程序可以自动路由第一个工单,并在需要时将第二个工单发送审核。

此外,模型只会提供这些评分,应用程序代码将设定它们在下游使用时所遵循的规则。

例如,可以要求最高答案超过 0.80,并且至少领先第二个答案 0.20。这些阈值位于代码中,因此可以进行测试和修改。

Image

数值 0.91 表示,在这三个选项中,billing 获得了 91% 的概率质量。这并不能证明模型有 91% 的概率判断正确。我们需要带标签的示例来测量这一点。这就是我稍后将讨论的校准问题。

但与此同时,请记住,结构化输出会生成一个有效对象。固定答案评分则会返回一个由应用程序已经知晓的答案构成的分布。

英文原文 04

284 词 · 2 张图

How an LLM generates the first output token

Before we discuss how a causal LLM can be turned into a Jev-style model, it would be better to first understand a regular generation step in LLMs.

Image
  • A tokenizer first converts the prompt into token IDs.
  • The model processes that sequence and produces a vector for the next position.
  • The vector has one number for every token in the model's vocabulary. For instance, Qwen's vocabulary contains tens of thousands of tokens, so the vector contains tens of thousands of numbers.

Those raw numbers are logits. A larger logit means the model prefers that token as the next continuation. The values are not probabilities yet.

During normal generation, the server applies the model's decoding rules (temperature, etc.) to this vector, selects one token, and appends it to the prompt.

The model then produces a new vocabulary-sized vector for the next position. Generation/decoding repeats this process until it reaches a stop token or the output limit.

Image

But when we want to make a bounded decision, we care about the first vector only.

Recall the support router example discussed above. The application accepts three answers:

plaintext
billing
technical support
account access

In the prompt, we can assign a short label to each answer:

plaintext
A = billing
B = technical support
C = account access

The prompt ends by asking for one label.

plaintext
Route the support ticket into exactly one category.

Ticket:
I was charged twice for the same subscription.

Allowed labels:
A = billing
B = technical support
C = account access

Return only the label.

Label:

"Label:" is the final text in the prompt.

So the next position is therefore where the model would normally generate either A, B, or C.

中文译文 04

443 字

LLM 如何生成第一个输出词元

在讨论如何将因果 LLM 转变为 Jev 风格模型之前,最好先理解 LLM 中一个常规的生成步骤。

Image
  • 分词器首先将提示转换为词元 ID。
  • 模型处理该序列,并为下一个位置生成一个向量。
  • 该向量中的每个数字对应模型词汇表中的一个词元。例如,Qwen 的词汇表包含数万个词元,因此该向量包含数万个数字。

这些原始数字称为 logits。logit 越大,表示模型越偏好将该词元作为下一个延续。此时这些数值还不是概率。

在正常生成过程中,服务器会将模型的解码规则(温度等)应用于这个向量,选择一个词元,并将其追加到提示中。

随后,模型会为下一个位置生成一个新的、大小等于词汇表的向量。这个生成与解码过程会反复进行,直到遇到停止词元或达到输出限制。

Image

但当我们想要做出有界决策时,我们只关心第一个向量。

回想一下上文讨论的支持路由器示例。应用程序接受三个答案:

纯文本
billing
technical support
account access

在提示中,我们可以为每个答案分配一个简短标签:

纯文本
A = billing
B = technical support
C = account access

提示以要求输出一个标签结束。

纯文本
Route the support ticket into exactly one category.

Ticket:
I was charged twice for the same subscription.

Allowed labels:
A = billing
B = technical support
C = account access

Return only the label.

Label:

“Label:”是提示中的最终文本。

因此,下一个位置就是模型通常会生成 A、B 或 C 之一的位置。

英文原文 05

285 词 · 2 张图

After processing this prompt, the model will produce its usual vocabulary-sized vector for that position. That vector will contain the logit for token A, the logits for B and C, and logits for every other token in the vocabulary.

The scoring path can then perform four operations:

Image
  1. Find the token IDs for A, B, and C.
  2. Read the three logits at those positions in the vocabulary vector.
  3. Ignore every other logit.
  4. Apply softmax across the three selected values.

If the selected logits are 8.2, 5.5, and 4.8, the restricted softmax produces approximately 0.91, 0.06, and 0.03. We can map those positions back to billing, technical support, and account access.

The normalization is restricted to the declared choices. We are not asking whether A has 91 percent probability across the entire vocabulary.

Instead, we are asking how the model divides its preference among A, B, and C after the application has ruled out every other response.

Image

This is an operation that SGLang already implements in /v1/score.

It runs the prompt through the model, reads the requested token positions, and returns their scores. It saves us from modifying the Qwen implementation and extracting the final tensor ourselves.

Btw, the reason why the answers use A, B, and C and not score the words "billing", "technical support", and "account access" directly is because a visible word is not necessarily one token.

For instance:

  • "billing" might be one token for one tokenizer and several tokens for another.
  • "technical support" will definitely span multiple positions.

Comparing those phrases requires sequence scoring. The model must score the first token, append it, score the next token, and combine the values for the complete phrase. Length also becomes part of the comparison.

中文译文 05

466 字

处理这个提示后,模型会为该位置生成一个与词汇表大小相同的向量。该向量会包含词元 A 的 logit、B 和 C 的 logits,以及词汇表中其他每个词元的 logits。

评分路径随后可以执行四项操作:

Image
  1. 找到 A、B 和 C 的词元 ID。
  2. 读取词汇表向量中这些位置的三个 logit。
  3. 忽略所有其他 logit。
  4. 对选出的三个值应用 softmax。

如果选出的 logits 是 8.2、5.5 和 4.8,受限 softmax 会分别产生约 0.91、0.06 和 0.03。我们可以将这些位置映射回账单、技术支持和账户访问。

归一化仅限于声明的选项。我们并不是在询问 A 在整个词汇表中是否有 91% 的概率。

相反,我们是在询问:在应用程序排除了所有其他响应之后,模型如何在 A、B 和 C 之间分配其偏好。

Image

这是 SGLang 已经在 /v1/score 中实现的一项操作。

它会将提示送入模型,读取请求的词元位置,并返回它们的分数。这样就不必修改 Qwen 的实现,也不必自行提取最终张量。

顺便说,答案使用 A、B 和 C,而不是直接为“billing”“technical support”和“account access”这些词评分,是因为可见的单词不一定对应一个词元。

例如:

  • “billing”对一种词元化器来说可能是一个词元,对另一种词元化器来说可能是多个词元。
  • “technical support”在所讨论的词元化下肯定会跨越多个位置。

比较这些短语需要进行序列评分。模型必须先对第一个词元评分,将其附加上去,再对下一个词元评分,并合并完整短语的数值。长度也会成为比较的一部分。

英文原文 06

264 词 · 1 张图

But single-token labels avoid that problem. Every option is represented by one vocabulary entry at the same output position while the semantic meaning still appears in the prompt:

plaintext
A = billing questions and payment problems
B = product errors and technical failures
C = login, password, and account access problems

The model reads those descriptions when it processes the prompt. The label is only the token whose logit we inspect afterward.

We still have to verify that each label is one token.

For instance, tokenizers often encode a leading space as part of the token. The strings "A" and " A" can therefore have different token IDs.

Image

A chat template may also place whitespace or control tokens immediately before the answer position.

To avoid this, render the complete prompt using the model's chat template. Determine the exact continuation expected at the answer position. Send that continuation to /tokenize. Reject the label if it produces anything other than one token.

This label mapping stays inside the scoring client. The application sends semantic choices such as billing and technical_support. It never sends token IDs and never receives A, B, or C. That is what it means for the public API to remain independent of the model labels.

Lastly, the answer list also needs an escape route when the listed choices are not exhaustive.

For instance, if a security incident reaches a router that offers only billing, technical support, and account access, restricted softmax still assigns all probability mass to those three wrong choices.

To avoid this, add OTHER or ESCALATE when none of the named options may be correct.

中文译文 06

432 字

但将每个选项设计为单个词元的标签可以避免这个问题。每个选项都在同一个输出位置由一个词汇表条目表示,同时语义仍然出现在提示中:

纯文本
A = billing questions and payment problems
B = product errors and technical failures
C = login, password, and account access problems

模型在处理提示时会读取这些描述。标签只是我们随后检查其 logit 的那个词元。

我们仍需验证每个标签是否是一个词元。

例如,分词器通常会把前导空格编码为词元的一部分。因此,字符串“A”和“ A”可能具有不同的词元 ID。

Image

聊天模板也可能在答案位置之前立即放置空白或控制词元。

为避免这种情况,请使用模型的聊天模板渲染完整提示。确定答案位置所需的精确后续内容。将该后续内容发送到“/tokenize”。如果结果不是恰好一个词元,则拒绝该标签。

这种标签映射保留在评分客户端内部。应用程序发送 billing 和 technical_support 等语义选择。它从不发送词元 ID,也从不接收 A、B 或 C。这就是公共 API 保持独立于模型标签的含义。

最后,当列出的选项并不穷尽所有可能性时,答案列表还需要提供一条退出路径。

例如,如果安全事件到达一个只提供 billing、technical support 和 account access 的路由器,受限 softmax 仍会把全部概率质量分配给这三个错误选项。

为避免这种情况,当列出的选项都可能不正确时,请添加 OTHER 或 ESCALATE。

英文原文 07

349 词 · 1 张图

Implementing a local scoring endpoint using SGLang

We only need one inference server for the local example. SGLang loads Qwen into GPU memory and exposes its native HTTP endpoints. Our Python script sends requests straight to that server.

This is the complete process:

  1. Start SGLang with a Qwen model.
  2. Write the decision as a prompt with letter labels.
  3. Ask SGLang to tokenize those labels.
  4. Send one request to /v1/score.
  5. Map the returned probabilities back to the choices.
Image

Step 1: Start Qwen with SGLang

Create a Python environment and install the two packages used here:

bash
python3 -m venv .venv
source .venv/bin/activate
pip install "sglang[all]==0.5.10.post1" "requests==2.34.2"

Then start the model server:

bash
python -m sglang.launch_server \
  --model-path Qwen/Qwen2.5-0.5B-Instruct \
  --host 127.0.0.1 \
  --port 30000

The first launch downloads the model from Hugging Face. Later launches reuse the local cache. Once loading finishes, Qwen remains in memory and SGLang listens on port 30000.

This SGLang process is the inference service. Keep it running while executing the client below.

Step 2: Define the choices and build the prompt

Create decide(.)py with the following code:

python
import json
import requests

BASE_URL = "http://127.0.0.1:30000"
MODEL = "Qwen/Qwen2.5-0.5B-Instruct"

choices = {
    "A": "billing and payments",
    "B": "technical support",
    "C": "account access",
}

ticket = "I was charged twice for the same subscription."
choice_lines = "\n".join(
    f"{label} = {meaning}" for label, meaning in choices.items()
)

prompt = f"""Ticket:
{ticket}

Question:
Which category matches the ticket?

Allowed labels:
{choice_lines}

Return only the label.
Label:
"""

print(prompt)

The dictionary records both representations of each answer.

  • A is the token we score.
  • And "billing" is the meaning returned to the application.
  • Their order must remain unchanged through tokenization, scoring, and result mapping.

This is the exact prompt produced here:

plaintext
Ticket:
I was charged twice for the same subscription.

Question:
Which category matches the ticket?

Allowed labels:
A = billing and payments
B = technical support
C = account access

Return only the label.
Label:

The prompt ends at "Label:".

We want the model's scores for the token that would appear next. We do not ask SGLang to generate that token.

中文译文 07

494 字

使用 SGLang 实现本地评分端点

对于本地示例,我们只需要一个推理服务器。SGLang 将 Qwen 加载到 GPU 内存中,并暴露其原生 HTTP 端点。我们的 Python 脚本直接向该服务器发送请求。

完整流程如下:

  1. 使用 Qwen 模型启动 SGLang。
  2. 将决策写成带字母标签的提示。
  3. 请求 SGLang 对这些标签进行分词。
  4. 向“/v1/score”发送一个请求。
  5. 将返回的概率映射回各个选项。
Image

步骤 1:使用 SGLang 启动 Qwen

创建 Python 环境,并安装这里使用的两个软件包:

bash
python3 -m venv .venv
source .venv/bin/activate
pip install "sglang[all]==0.5.10.post1" "requests==2.34.2"

然后启动模型服务器:

bash
python -m sglang.launch_server \
  --model-path Qwen/Qwen2.5-0.5B-Instruct \
  --host 127.0.0.1 \
  --port 30000

首次启动会从 Hugging Face 下载模型。之后的启动会复用本地缓存。加载完成后,Qwen 会保留在内存中,SGLang 会监听 30000 端口。

这个 SGLang 进程就是推理服务。在执行下面的客户端时,请让它持续运行。

步骤 2:定义选项并构建提示词

创建 decide(.)py,并填入以下代码:

python
import json
import requests

BASE_URL = "http://127.0.0.1:30000"
MODEL = "Qwen/Qwen2.5-0.5B-Instruct"

choices = {
    "A": "billing and payments",
    "B": "technical support",
    "C": "account access",
}

ticket = "I was charged twice for the same subscription."
choice_lines = "\n".join(
    f"{label} = {meaning}" for label, meaning in choices.items()
)

prompt = f"""Ticket:
{ticket}

Question:
Which category matches the ticket?

Allowed labels:
{choice_lines}

Return only the label.
Label:
"""

print(prompt)

这个字典记录了每个答案的两种表示形式。

  • A 是我们评分的标记。
  • 而“billing”是返回给应用程序的含义。
  • 在分词、评分和结果映射过程中,它们的顺序必须保持不变。

这里生成的确切提示词如下:

plaintext
Ticket:
I was charged twice for the same subscription.

Question:
Which category matches the ticket?

Allowed labels:
A = billing and payments
B = technical support
C = account access

Return only the label.
Label:

提示词以“Label:”结束。

我们需要模型对下一个将出现的标记的评分。我们不要求 SGLang 生成该标记。

英文原文 08

316 词

Step 3: Resolve the label token IDs

Add this code below the prompt:

python
label_token_ids = []

for label in choices:
    response = requests.post(
        f"{BASE_URL}/tokenize",
        json={
            "model": MODEL,
            "prompt": label,
            "add_special_tokens": False,
        },
        timeout=30,
    )
    response.raise_for_status()
    token_ids = response.json()["tokens"]

    if len(token_ids) != 1:
        raise ValueError(
            f"{label!r} is not a single token: {token_ids}"
        )

    print(f"{label!r} -> {token_ids}")
    label_token_ids.append(token_ids[0])

SGLang's /tokenize endpoint returns the integer IDs produced by Qwen's tokenizer. We reject any label that becomes several tokens. The scoring request needs one vocabulary position for each answer.

For Qwen/Qwen2.5-0.5B-Instruct, this prints:

plaintext
'A' -> [32]
'B' -> [33]
'C' -> [34]

Each list contains one integer. We therefore know that every label occupies one token. The later scoring request will read vocabulary positions 32, 33, and 34.

This check also prevents us from assuming that tokenization is identical across models. A label that works with Qwen may split under another tokenizer.

Step 4: Ask SGLang for the three probabilities

Add the scoring request next:

python
response = requests.post(
    f"{BASE_URL}/v1/score",
    json={
        "model": MODEL,
        "query": prompt,
        "items": [""],
        "label_token_ids": label_token_ids,
        "apply_softmax": True,
    },
    timeout=120,
)
response.raise_for_status()
score_response = response.json()

print(json.dumps(score_response, indent=2))
scores = score_response["scores"][0]
  • query contains the complete prompt.
  • The empty "items" means we score the position immediately after it.
  • "label_token_ids" tells SGLang which three entries to read from Qwen's vocabulary-sized output.
  • "apply_softmax" normalizes those entries into probabilities.

The response has one score list because items contains one entry. We ran this exact prompt against the same Qwen checkpoint and obtained:

json
{
  "scores": [
    [
      0.67776233,
      0.310878605,
      0.011359035
    ]
  ]
}

The three positions correspond to A, B, and C. The selected logits were 25.277620, 24.498226, and 21.188837 before softmax. Small numeric differences may appear across hardware and precision settings.

The official endpoint reference states that each returned list follows the order of label_token_ids. Our first score therefore belongs to A, the second to B, and the third to C.

中文译文 08

493 字

步骤 3:解析标签标记 ID

将以下代码添加到提示词代码下面:

python
label_token_ids = []

for label in choices:
    response = requests.post(
        f"{BASE_URL}/tokenize",
        json={
            "model": MODEL,
            "prompt": label,
            "add_special_tokens": False,
        },
        timeout=30,
    )
    response.raise_for_status()
    token_ids = response.json()["tokens"]

    if len(token_ids) != 1:
        raise ValueError(
            f"{label!r} is not a single token: {token_ids}"
        )

    print(f"{label!r} -> {token_ids}")
    label_token_ids.append(token_ids[0])

SGLang 的 /tokenize 端点返回 Qwen 分词器生成的整数 ID。我们拒绝任何会变成多个标记的标签。评分请求需要为每个答案提供一个词汇表位置。

对于 Qwen/Qwen2.5-0.5B-Instruct,它会打印:

plaintext
'A' -> [32]
'B' -> [33]
'C' -> [34]

每个列表都包含一个整数。因此,我们知道每个标签都占用一个标记。之后的评分请求将读取词汇表位置 32、33 和 34。

这项检查还避免我们假定不同模型的分词方式完全相同。对 Qwen 有效的标签,在另一个分词器下可能会被拆分。

第 4 步:让 SGLang 返回三个概率

接下来添加评分请求:

python
response = requests.post(
    f"{BASE_URL}/v1/score",
    json={
        "model": MODEL,
        "query": prompt,
        "items": [""],
        "label_token_ids": label_token_ids,
        "apply_softmax": True,
    },
    timeout=120,
)
response.raise_for_status()
score_response = response.json()

print(json.dumps(score_response, indent=2))
scores = score_response["scores"][0]
  • query 包含完整的提示词。
  • 空的 "items" 表示对其紧接着的位置进行评分。
  • "label_token_ids" 告诉 SGLang,要从 Qwen 词汇表大小的输出中读取哪三个条目。
  • "apply_softmax" 将这些条目归一化为概率。

由于 items 包含一个条目,响应中有一个分数列表。我们将这个确切的提示词发送给同一个 Qwen 检查点后,得到:

json
{
  "scores": [
    [
      0.67776233,
      0.310878605,
      0.011359035
    ]
  ]
}

这三个位置分别对应 A、B 和 C。在应用 softmax 之前,选定的 logits 分别为 25.277620、24.498226 和 21.188837。不同硬件和精度设置下可能会出现细微的数值差异。

官方端点参考文档说明,每个返回的列表都遵循 label_token_ids 的顺序。因此,我们的第一个分数对应 A,第二个对应 B,第三个对应 C。

英文原文 09

277 词 · 2 张图

Step 5: Convert model labels back into decisions

Finish the script with the mapping code:

python
probabilities = {
    choices[label]: float(score)
    for label, score in zip(choices, scores, strict=True)
}

decision = max(probabilities, key=probabilities.get)

print(
    json.dumps(
        {
            "decision": decision,
            "probabilities": probabilities,
        },
        indent=2,
    )
)

Now run the script in a second terminal:

json
// Output after running python decide.py

{
  "decision": "billing and payments",
  "probabilities": {
    "billing and payments": 0.6777623295783997,
    "technical support": 0.31087860465049744,
    "account access": 0.011359035037457943
  }
}

SGLang performs that calculation behind /v1/score. One SGLang process tokenizes the labels, runs Qwen once, and returns the selected probabilities.

In this case, billing wins, but only with 0.678 probability. A policy requiring 0.70 would send this ticket for review instead of automating the route. That threshold should come from evaluation on labeled examples.

Measuring scoring latency against autoregressive generation

The single-request example above shows the mechanism. I also built a small application to test how that mechanism behaves across many decisions.

Image

The deployed demo supports several open models, like Qwen 3 4B, Qwen 2.5 0.5B and 1.5B, SmolLM2 1.7B, TinyLlama 1.1B, and DeepSeek-R1-Distill-Qwen 1.5B.

Image

The Jev-style lane calls the decision method:

python
result = engine.decide(request)

answer = result["answers"]["decision"]

choice = answer["choice"]

probabilities = answer["probabilities"]

Inside the decide() method, SGLang receives the prompt through /v1/score. The request includes the token IDs for A, B, and C.

SGLang runs the prompt, reads those three next-token scores, normalizes them, and stops. The response contains no generated tokens.

The standard lane calls the generation method on that same engine:

python
result = engine.generate_response(
    case["state"],
    case["question"],
    list(case["criteria"].items()),
    max_tokens=32,
)

This method sends the same state, question, and choices to /v1/chat/completions.

中文译文 09

420 字

第 5 步:将模型标签转换回决策

用映射代码完成脚本:

python
probabilities = {
    choices[label]: float(score)
    for label, score in zip(choices, scores, strict=True)
}

decision = max(probabilities, key=probabilities.get)

print(
    json.dumps(
        {
            "decision": decision,
            "probabilities": probabilities,
        },
        indent=2,
    )
)

现在在第二个终端中运行脚本:

json
// Output after running python decide.py

{
  "decision": "billing and payments",
  "probabilities": {
    "billing and payments": 0.6777623295783997,
    "technical support": 0.31087860465049744,
    "account access": 0.011359035037457943
  }
}

SGLang 会在 /v1/score 后台完成这项计算。一个 SGLang 进程会对这些标签进行分词,运行 Qwen 一次,然后返回选定的概率。

在这个例子中,billing 获胜,但概率只有 0.678。如果策略要求达到 0.70,就会将该工单送去审核,而不是自动路由。这个阈值应当根据带标签示例上的评估结果确定。

将评分延迟与自回归生成进行比较

上面的单请求示例展示了工作机制。我还构建了一个小型应用,用于测试该机制在许多决策中的表现。

Image

部署的演示支持多个开放模型,例如 Qwen 3 4B、Qwen 2.5 0.5B 和 1.5B、SmolLM2 1.7B、TinyLlama 1.1B,以及 DeepSeek-R1-Distill-Qwen 1.5B。

Image

Jev 风格通道调用如下的决策方法:

Python
result = engine.decide(request)

answer = result["answers"]["decision"]

choice = answer["choice"]

probabilities = answer["probabilities"]

在 decide() 方法内部,SGLang 通过 /v1/score 接收提示词。请求包含 A、B 和 C 的词元 ID。

SGLang 运行该提示词,读取这三个下一词元的分数,将其归一化,然后停止。响应不包含生成的词元。

标准通道在同一个引擎上调用生成方法:

Python
result = engine.generate_response(
    case["state"],
    case["question"],
    list(case["criteria"].items()),
    max_tokens=32,
)

该方法将相同的状态、问题和选项发送到 /v1/chat/completions。

英文原文 10

272 词 · 4 张图

Qwen generates an answer and short explanation, up to 32 tokens. The application searches the first 100 characters for one allowed choice name. It marks the case correct when that parsed choice matches the stored label.

The video compares our 100% local Jev with a 100% local LLM generator, across some examples:

The speed is immediately evident for the reasons we have already discussed above.

Along with this, I also built a simulation of 100 cases from a fixed local dataset.

Image

The current datasets cover support routing, candidate screening, and expense review. Their expected labels let the interface display both speed and correctness.

The remote SGLang path starts two workers behind one barrier:

python
starting_line = threading.Barrier(2)

def worker(lane, runner):
    starting_line.wait()
    for case in cases:
        updates.put(runner(case))

workers = [
    threading.Thread(target=worker, args=("jev", run_jev)),
    threading.Thread(target=worker, args=("llm", run_llm)),
]

for worker_thread in workers:
    worker_thread.start()

The barrier releases both workers together. Each lane processes its own cases in order. The Jev-style worker sends the next scoring request as soon as its previous request finishes. The standard worker does the same with generation requests.

Image

The two lanes therefore remain active at the same time, but we do not send all 200 requests at once. SGLang receives concurrent work from both lanes and schedules it through continuous batching. Both request types share the same GPU, memory bandwidth, and scheduler.

The video below depicts a side-by-side comparison of Jev and an LLM:

Note: The video was sped up after 8 seconds, which is why LLM requests appear to complete fast.

Both lanes started together and used Qwen/Qwen3-4B-Instruct-2507 on the same SGLang server.

中文译文 10

461 字

Qwen 生成回答和简短解释,最多 32 个词元。应用程序在前 100 个字符中搜索一个允许的选项名称。当解析出的选项与存储的标签一致时,应用程序将该案例标记为正确。

视频通过一些示例,将我们的 100% 本地 Jev 与 100% 本地 LLM 生成器进行了比较:

由于我们上文已经讨论过的原因,速度优势是立刻可以看出的。

此外,我还使用一个固定的本地数据集,构建了一个包含 100 个案例的模拟。

Image

当前数据集涵盖支持路由、候选人筛选和费用审核。其预期标签让界面能够同时展示速度和正确性。

远程 SGLang 路径在一个屏障后启动两个工作线程:

Python
starting_line = threading.Barrier(2)

def worker(lane, runner):
    starting_line.wait()
    for case in cases:
        updates.put(runner(case))

workers = [
    threading.Thread(target=worker, args=("jev", run_jev)),
    threading.Thread(target=worker, args=("llm", run_llm)),
]

for worker_thread in workers:
    worker_thread.start()

屏障会同时释放两个工作线程。每条通道按顺序处理自己的案例。Jev 风格工作线程会在上一个请求完成后立即发送下一个评分请求。标准工作线程对生成请求也采取同样做法。

Image

因此,两条通道会同时保持活动状态,但我们不会一次发送全部 200 个请求。SGLang 接收来自两条通道的并发工作,并通过连续批处理进行调度。两种请求类型共享同一个 GPU、内存带宽和调度器。

下面的视频展示了 Jev 与 LLM 的并排对比:

注意:视频在 8 秒后被加速,这就是 LLM 请求看起来完成得很快的原因。

两条通道同时启动,并在同一个 SGLang 服务器上使用 Qwen/Qwen3-4B-Instruct-2507。

英文原文 11

237 词 · 1 张图

How to choose the right approach

Scoring is not a replacement for generation, so Jev's approach only applies when the application defines the output space before inference.

A compatible workload ideally should have a finite set of meaningful labels. Each label should map to a distinct downstream action.

And the caller must only need a label and its probability distribution, not new text.

Structured output is different since it defines the syntax of a response, and the decoder still generates field names and values token by token.

Use structured generation when those values cannot be enumerated beforehand, since scoring removes decoding only when the candidate values are already known.

This graphic compares regular LLM decoding, structured output decoding, and Jev-style scoring:

One good way to proceed is to choose the inference path from the required output:

  • Generate when the output content is unknown before inference.
  • Score when the output set is known, and selection is sufficient. In that case, the first next-token vector already contains the ranking. Returning that ranking avoids an autoregressive loop the caller does not need.

To reiterate, this article project reproduces the Jev-style inference mechanism, but it does not reproduce Jev's weights, RLCD process, or evaluation stack.

I intend to cover the processes behind those pretty soon.

Stay tuned!

That's a wrap!

If you enjoyed this tutorial:

Find me → @_avichawla

Every day, I share tutorials and insights on DS, ML, LLMs, and RAGs.

中文译文 11

439 字

如何选择合适的方法

评分并不能替代生成,因此 Jev 的方法只适用于应用在推理前就定义了输出空间的情况。

理想情况下,兼容的工作负载应具有一组有限且有意义的标签。每个标签都应对应一个不同的下游操作。

并且,调用方只需要标签及其概率分布,而不需要新生成的文本。

结构化输出有所不同,因为它定义的是响应的语法,而解码器仍然会逐个 token 地生成字段名和值。

当这些值无法预先枚举时,应使用结构化生成,因为只有在候选值已经已知的情况下,评分才会移除解码过程。

这张图比较了常规 LLM 解码、结构化输出解码和 Jev 风格的评分:

一种有效的做法是根据所需的输出选择推理路径:

  • 当输出内容在推理前未知时,进行生成。
  • 当输出集合已知且进行选择就足够时,进行评分。在这种情况下,第一个下一 token 向量已经包含了排序结果。返回该排序可以避免调用方并不需要的自回归循环。

再次强调,这个文章项目复现的是 Jev 风格的推理机制,但并不复现 Jev 的权重、RLCD 流程或评估栈。

我打算很快介绍这些流程背后的过程。

敬请期待!

到这里就结束了!

如果你喜欢这篇教程:

找到我 → @_avichawla [1]

我每天都会分享有关数据科学、机器学习、大型语言模型和检索增强生成的教程与见解。

来源与阅读说明

正文取自用户提交的本地文件,原始文件与原图按字节归档。原网页含 4 段视频,本保存件未取得视频文件;保留静态预览(如有),完整视频请从原文链接查看。中文译文逐单元生成并由另一次模型请求审校;未进行人工全文审校、线上来源比对或代码执行。作者与日期缺失时保留未知。

阅读补充解释本文内容与限制,不冒充独立知识专题或外部事实核验。图片保持原样,图内说明单独保存。阅读器沿用 ReadLess 公共组件及中文约 400 字分块规则。