vLLM /generative_scoring 端点实战:用 CausalLM 的下一词元概率对候选项进行打分
vLLM 的 /generative_scoring 端点让你直接用任意因果语言模型(CausalLM,如 Llama、Qwen、Mistral)做“打分”任务:把 query 与每个候选 item 拼成 prompt,取模型预测的下一词元 logits,再提取你指定的标签词元(如 "Yes"/"No")的概率,即可对一批 item 逐一评分。读完本文,你将掌握该端点的请求参数、打分公式、标签词元 ID 的查找方法,以及它在 vLLM 源码中的实现链路(prompt 构建、logprob_token_ids 采样、子集 softmax 归一化),能够直接在自己的检索重排或分类场景中复制可用。
什么是 Generative Scoring,与 Score API 有何区别
/generative_scoring 端点使用生成式模型(task 为 "generate" 的 CausalLM)计算指定词元 ID 作为下一个词元出现的概率。每个 item(文档)与 query 拼接形成 prompt,模型预测该 prompt 之后每个标签词元作为下一词元的可能性。典型场景是检索排序:例如提问 "Is this the capital of France?",然后给每个城市按模型回答 "Yes" 的概率打分——Paris 得分高,London、Berlin 得分低。
需要特别注意它与 Score API 的区别:
| 对比项 | /generative_scoring(本文) |
/score(Score API) |
|---|---|---|
| 模型类型 | 生成式模型(CausalLM,task="generate") |
pooling 模型(cross-encoder、bi-encoder、late-interaction) |
| 启动条件 | 服务器以生成式模型启动时自动可用 | 需要加载支持 scoring 的 pooling 模型 |
| 打分依据 | 指定标签词元的下一词元概率 | 模型输出的相似度分数 |
从源码结构看,该 handler 仅在 generate 任务下挂载:/generative_scoring 路由在 register_generate_api_routers 中注册,ServingGenerativeScoring 实例也在 init_generate_state 中随其他生成式 handler 一起初始化(api_router.py#L237-L243)。若模型不支持生成任务,调用该端点会抛出 "The model does not support the Generative Scoring API"(见 api_router.py#L39-L44)。
工作机制:五步流水线
源码中的 ServingGenerativeScoring(serving.py)完整实现了如下流程:
- Prompt 构建:对每个 item 构建
prompt = query + item;当item_first=true时则拼接为item + query。query 按add_special_tokens参数决定是否加特殊词元,而 item 始终以add_special_tokens=False编码,避免 BOS/EOS 重复(serving.py#L404-L430)。 - 前向推理:对每个 prompt 各发起一次引擎
generate调用,得到下一词元 logits。源码中为每个 item 生成独立的request_id(形如generative-scoring-{base_id}-{i}),并通过merge_async_iterators并行汇聚结果(serving.py#L268-L300)。 - 概率提取:从返回的
output.logprobs[0]中取出label_token_ids对应的 logprob。 - Softmax 归一化:当
apply_softmax=true时,仅在标签词元子集上做 softmax 归一化。 - 出分:返回第一个标签词元的归一化概率作为该 item 的 score。
其中一步值得展开:源码用 max_tokens=1 的 SamplingParams,并通过 logprob_token_ids=request.label_token_ids 只请求指定标签词元的 logprob,而不是全词表 top-k(serving.py#L247-L258)。源码注释也澄清了一个常见误解:temperature/top_k/top_p 不影响 logprob——logprob 在任何采样变换之前由原始 logits 经 log_softmax 计算得出,因此打分结果只取决于模型前向输出,与采样参数无关。
请求参数完整说明
GenerativeScoringRequest 协议定义在 serving.py#L50-L103,字段及默认值如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
model |
str | None |
null |
使用的模型名,可选 |
query |
str | list[int] |
必填 | 查询文本,或预分词后的 query token ID 列表 |
items |
list[str] | list[list[int]] |
必填 | item 文本列表,或预分词的 token ID 列表;至少 1 个 item |
label_token_ids |
list[int] |
必填 | 要计算概率的标签词元 ID 列表;至少 1 个,且每个 ID 必须落在 [0, vocab_size) 内 |
apply_softmax |
bool |
true |
true 表示仅在 label_token_ids 子集上归一化;false 表示返回模型在全词表上的真实概率 |
item_first |
bool |
false |
true 表示 item 拼在 query 前面(item + query),否则拼在后面(query + item) |
add_special_tokens |
bool |
true |
tokenize query 文本时是否加特殊词元(item 文本恒不加) |
priority |
int |
0 |
请求优先级,数值越小处理越靠前 |
request_id |
str |
随机 UUID | 自定义请求 ID |
query 和 items 均支持直接传 token ID 列表(混合类型会在 Pydantic 解析阶段被拒绝),方便复用已分词结果或构造特殊 prompt。
打分公式:子集 Softmax
设标签词元的 logprob 集合为 {lp₁, ..., lpₙ}(对应 label_token_ids),源码 _compute_probabilities(serving.py#L442-L480)按 apply_softmax 分两种计算方式:
-
apply_softmax=true(默认):只在标签词元上做子集 softmax,并使用减最大值的技巧保证数值稳定:P(tᵢ) = exp(lpᵢ - max(lp)) / Σⱼ exp(lpⱼ - max(lp))因此当提供 2 个标签词元时,score 恰好等于
P(label[0]) / (P(label[0]) + P(label[1]));提供多个标签时,score 为第一个标签词元在所有标签词元上的归一化概率。 -
apply_softmax=false:直接取exp(lpᵢ),即模型在全词表 softmax 后对该词元的真实概率,各标签概率之和通常远小于 1(剩余概率质量分布在词表其他词元上)。适合需要绝对概率、而非标签间相对比例的场合。
最终返回的 score 恒为第一个 label_token_ids 的归一化概率(serving.py#L353-L355)。例如 label_token_ids=[yes_id, no_id] 时,score 即 "Yes" 的相对概率,值越接近 1 表示模型越肯定地回答 "Yes"。
如何找到标签词元的 Token ID
用 tokenizer 直接编码标签文本即可(注意关闭特殊词元):
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
yes_id = tokenizer.encode("Yes", add_special_tokens=False)[0]
no_id = tokenizer.encode("No", add_special_tokens=False)[0]
print(f"Yes: {yes_id}, No: {no_id}")
对 Qwen/Qwen3-0.6B,"Yes" 与 "No" 分别对应 9454 和 2753。务必使用与服务端相同的 tokenizer 查 ID——服务端会用 model_config.get_vocab_size() 校验每个 ID 是否落在词表范围内,超范围会直接返回错误(serving.py#L208-L214)。
完整调用示例
启动一个生成式模型服务器后(例如 vllm serve Qwen/Qwen3-0.6B),发送请求:
curl -X POST http://localhost:8000/generative_scoring \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-0.6B",
"query": "Is this city the capital of France?",
"items": ["Paris", "London", "Berlin"],
"label_token_ids": [9454, 2753]
}'
每个 item 会被追加到 query 之后,形成如 "Is this city the capital of France? Paris"、"... London" 的 prompt;模型预测下一词元,score 反映 "Yes"(9454)相对 "No"(2753)的归一化概率。
响应结构(GenerativeScoringResponse,定义于 serving.py#L120-L137):
{
"id": "generative-scoring-abc123",
"object": "list",
"created": 1234567890,
"model": "Qwen/Qwen3-0.6B",
"data": [
{"index": 0, "object": "score", "score": 0.95},
{"index": 1, "object": "score", "score": 0.12},
{"index": 2, "object": "score", "score": 0.08}
],
"usage": {"prompt_tokens": 45, "total_tokens": 48, "completion_tokens": 3}
}
data 中每个元素与输入 items 按 index 一一对应;usage 统计所有 item 的 prompt token 总数、completion token 总数(每个 item 生成 1 个 token,故 completion_tokens 等于 item 数量)及总 token 数。
校验规则与边界处理
源码在返回结果前做了多层校验,理解这些规则有助于排查 API 报错:
- 词表校验:任一
label_token_ids落在[0, vocab_size)之外 → 错误label_token_id X is out of vocabulary range。 - 非空校验:
label_token_ids为空、items为空 → 分别返回label_token_ids must contain at least one token ID./items must contain at least one item.(serving.py#L216-L223)。 - Token 截断:每个 prompt 会被截断到
max_model_len - 1,为 1 个输出 token 预留空间(serving.py#L432-L435),超长 query+item 不会报错而是静默截断。 - Logprob 缺失兜底:若返回的 logprobs 中缺少某个标签词元(理论上经过词表校验后不应发生),会返回明确的缺失 token 列表错误(serving.py#L332-L345)。
- LoRA 与优先级:请求支持通过
_maybe_get_adapters携带 LoRA 适配器,并可通过priority影响调度顺序(serving.py#L229-L286)。 - Tracing:请求头中携带 trace 头时,端点会透传给引擎做链路追踪(serving.py#L482-L494)。
测试侧的覆盖可参考 test_generative_scoring.py(协议模型、softmax 归一化、分数公式、prompt 构建与 item 排序的单元测试)与 test_generative_scoring_e2e.py(端到端验证),其中单元测试明确验证了 P(token[0]) / (P(token[0]) + P(token[1])) 这一两标签打分公式。
典型应用场景
- 生成式重排(Rerank):把 query 作为
query、候选文档摘要作为items,用 "Yes"/"No" 两个标签词元打分,即得到一个零训练的 cross-encoder 替身。 - 零样本分类:把类别名的首个 token ID 放入
label_token_ids,按首标签概率选出最可能类别。 - 绝对概率估计:设
apply_softmax=false可获得词元在全词表上的真实概率,适合需要与阈值比较(而非标签间相对比较)的场景。
使用时注意两个前提:服务端必须以生成式模型(task "generate")启动,且 label_token_ids 必须用该模型的 tokenizer 查出、落在词表范围内。其余调用方式与标准 HTTP 请求一致,可直接嵌入现有的 OpenAI 兼容客户端代码中。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00