首页
/ vLLM /generative_scoring 端点实战:用 CausalLM 的下一词元概率对候选项进行打分

vLLM /generative_scoring 端点实战:用 CausalLM 的下一词元概率对候选项进行打分

2026-09-06 11:08:33作者:蔡怀权

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)。

工作机制:五步流水线

源码中的 ServingGenerativeScoringserving.py)完整实现了如下流程:

  1. 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)。
  2. 前向推理:对每个 prompt 各发起一次引擎 generate 调用,得到下一词元 logits。源码中为每个 item 生成独立的 request_id(形如 generative-scoring-{base_id}-{i}),并通过 merge_async_iterators 并行汇聚结果(serving.py#L268-L300)。
  3. 概率提取:从返回的 output.logprobs[0] 中取出 label_token_ids 对应的 logprob。
  4. Softmax 归一化:当 apply_softmax=true 时,仅在标签词元子集上做 softmax 归一化。
  5. 出分:返回第一个标签词元的归一化概率作为该 item 的 score。

其中一步值得展开:源码用 max_tokens=1SamplingParams,并通过 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

queryitems 均支持直接传 token ID 列表(混合类型会在 Pydantic 解析阶段被拒绝),方便复用已分词结果或构造特殊 prompt。

打分公式:子集 Softmax

设标签词元的 logprob 集合为 {lp₁, ..., lpₙ}(对应 label_token_ids),源码 _compute_probabilitiesserving.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" 分别对应 94542753。务必使用与服务端相同的 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 中每个元素与输入 itemsindex 一一对应;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 兼容客户端代码中。

登录后查看全文
热门项目推荐
相关项目推荐