从 vLLM ChatCompletionRequest 看懂大模型请求参数

把 vLLM 的 ChatCompletionRequest 当成推理控制面地图,讲清 messages、chat template、token 预算、采样参数、结构化输出、工具调用、streaming 和服务调度分别作用在大模型推理的哪一层。

从 vLLM ChatCompletionRequest 看懂大模型请求参数

这篇只解决一个问题:

OpenAI 风格的 chat completion 请求参数,最后到底在控制大模型推理里的哪一层?

我读的是 vLLM 当前工作区的 ChatCompletionRequest,源码位置是 vllm/entrypoints/openai/chat_completion/protocol.py,commit 是 752a3a504485790a2e8491cacbb35c137339ad34。这不是一张参数速查表,而是把这组字段当成一张推理控制面地图。

先给结论:

请求参数大致分三类:

1. 控制模型看到什么:messages、chat_template、documents、截断、特殊 token
2. 控制模型怎么选下一个 token:temperature、top_p、top_k、penalty、stop、structured outputs
3. 控制服务怎么跑和怎么返回:stream、priority、cache_salt、KV transfer、logprobs、metrics

ChatCompletionRequest 值得读,是因为它正好站在 API 协议和推理引擎之间。它自己不做 GPU forward,但它把一次请求翻译成后面 renderer、tokenizer、sampler、scheduler 能理解的配置。

1. 先看这条请求链路

可以把一次 chat completion 请求想成这条链:

flowchart TD
  A[OpenAI style JSON request] --> B[Pydantic validation]
  B --> C[ChatCompletionRequest]
  C --> D[build_chat_params]
  D --> E[chat template renders messages]
  E --> F[tokenizer turns prompt into token ids]
  C --> G[build_tok_params]
  G --> H[token budget and truncation]
  C --> I[to_sampling_params]
  I --> J[decode and sampling config]
  J --> K[prefill, decode, stop, stream]

几个源码锚点:

所以这个类最有价值的地方,不是告诉你 temperature 是 float,而是告诉你:

参数不是都作用在同一层。
有些在 prompt 渲染前生效,有些在 tokenization 时生效,有些在每一步 decode 采样时生效,有些只影响服务返回。

2. messages 不是模型真正看到的东西

API 用户发来的是:

{
  "messages": [
    {"role": "system", "content": "你是一个严谨的助手"},
    {"role": "user", "content": "解释 KV cache"}
  ]
}

但 Transformer 不认识 JSON,也不认识 role 这个字段。模型真正看到的是一串 token ids。中间必须经过 chat template:

messages -> chat template -> prompt text -> tokenizer -> token ids

这就是 build_chat_params() 管的事情。它会把这些字段塞进 ChatParams

chat_template
chat_template_content_format
chat_template_kwargs
add_generation_prompt
continue_final_message
documents
reasoning_effort
media_io_kwargs
return_assistant_tokens_mask

这些字段背后的原理是:所谓 chat 模型,本质还是 next-token prediction。system、user、assistant 的区别,是模板用特殊格式告诉模型“现在是谁在说话”。

add_generation_prompt=True 的意思是,模板要在最后补一个“轮到 assistant 输出”的提示。不同模型的模板长得不一样,但意图类似:

前面是对话历史
下面开始 assistant 的回复

continue_final_message=True 则是另一种模式:不新开 assistant 回复,而是继续最后一条消息。这适合“预填一半,让模型接着写”的场景。所以源码里会禁止它和 add_generation_prompt=True 同时打开。

documents 也不是神奇的外部知识库。它只是把 RAG 文档交给模板。如果模板没有把这些文档渲染进 prompt,它对模型就没有效果。

3. token 预算:真正的硬约束不是字符数

build_tok_params() 关注的是另一层:

max_model_len
max_completion_tokens 或 max_tokens
truncate_prompt_tokens
truncation_side
add_special_tokens
return_token_offsets

这里的关键认知是:

大模型请求的硬资源单位是 token,不是字符。

一次请求要满足:

prompt tokens + generated tokens <= max_model_len

max_completion_tokens 或旧字段 max_tokens 控制的是生成预算。它不是“回答质量”参数,而是 decode loop 最多能往后走多少步。每多生成一个 token,都意味着模型要再做一次下一 token 预测。

truncate_prompt_tokenstruncation_side 控制 prompt 太长时怎么截断。源码注释里写得很直接:

right keeps the first N tokens
left keeps the last N tokens

这不是小事。左截断保留最近上下文,可能丢掉开头的系统约束;右截断保留开头,可能丢掉用户最后的问题。请求参数在这里改变的是“模型看见的历史”,不是采样策略。

add_special_tokens 也要谨慎。大多数 chat template 已经负责插入 BOS、EOS 或角色标记。额外再加特殊 token,可能让 prompt 格式偏离模型训练时习惯的格式。

4. 最小数学地基:logits、softmax 和概率

接下来进入最核心的采样参数。先不要背 temperaturetop_p 的口号,先看模型每一步到底输出什么。

LLM 生成文本时,每一步只做一件事:

根据已有 token,给词表里的每个 token 打一个分。

这些分叫 logits。logit 可以先理解成:

softmax 之前,模型对每个候选 token 的原始偏好分。

假设词表里只剩三个候选:

秋:4
金:2
猫:1

这些还不是概率,因为它们不会加起来等于 1。softmax 做三步:

第一步:把每个分数变成正数。
第二步:把所有正数加起来。
第三步:每个正数除以总和。

用小数字看:

exp(4) = 54.6
exp(2) = 7.4
exp(1) = 2.7

sum = 54.6 + 7.4 + 2.7 = 64.7

所以概率大约是:

P(秋) = 54.6 / 64.7 = 0.84
P(金) =  7.4 / 64.7 = 0.11
P(猫) =  2.7 / 64.7 = 0.04

写成公式是:

P(token_i) = exp(logit_i) / sum(exp(logit_j))

你只要记住一件事:采样参数大多不改模型权重,它们改的是这一步的候选 token 分布。

5. temperature、top-p、top-k、min-p 在控制什么

to_sampling_params() 里会给这些字段补默认值:

repetition_penalty = 1.0
temperature = 1.0
top_p = 1.0
top_k = 0
min_p = 0.0

这组参数控制的是 decode 阶段每一步怎么选下一个 token。

概念上可以这样看:

hidden state
-> LM head
-> logits
-> penalties / logit_bias / bad_words / allowed_token_ids / grammar masks
-> candidate filtering
-> probability distribution
-> sample one token

不同实现里处理顺序会有细节差异,但读请求参数时先抓住这个事实:这些字段都在输出分布上做文章。

temperature 是分数差距缩放器。常见形式是:

softmax(logits / T)

T < 1 会放大分数差距,让高分 token 更容易赢。T > 1 会压小分数差距,让低分候选也有机会。

top_k 是只保留最高分的 k 个 token。top_k=40 就是每步最多从前 40 个候选里选。

top_p 是 nucleus sampling。它不是固定保留几个 token,而是按概率从高到低累计,直到累计概率超过 p。分布很尖时,候选可能很少;分布很平时,候选会变多。

min_p 是按相对概率过滤太弱的候选。可以把它理解成:如果一个 token 的概率低到离最高概率太远,就不要再参与抽样。

这解释了一个实践问题:不要同时乱调 temperaturetop_ptop_kmin_p。它们都在改候选分布,同时调太多,结果变了也很难知道是哪一个参数造成的。

6. penalty 和 bias:不是让模型变聪明,而是改 logits

to_sampling_params() 还把这些字段送进 SamplingParams

presence_penalty
frequency_penalty
repetition_penalty
logit_bias
bad_words
allowed_token_ids
repetition_detection

它们容易被误解成“增强模型能力”。更准确的理解是:

模型还是那个模型,只是下一 token 的分数被后处理改了。

这几个参数可以逐个拆开:

  • presence_penalty 惩罚“出现过”的 token 或内容倾向。
  • frequency_penalty 惩罚“出现次数多”的 token 或内容倾向。
  • repetition_penalty 通常是更直接的重复 token 惩罚。
  • logit_bias 是显式给某些 token 加分或扣分。
  • bad_words 是不希望出现的词。
  • allowed_token_ids 是只允许某些 token 参与候选。
  • repetition_detection 则是检测到重复 n-gram 后提前结束生成。

所以反重复参数不是提升推理能力,而是在采样层修正坏分布。它适合治“循环输出”,不适合治“不会推理”。

7. stop 条件:模型想停和服务让它停是两回事

这组字段控制结束条件:

stop
stop_token_ids
ignore_eos
min_tokens
include_stop_str_in_output
max_tokens

LLM 自己会学到 EOS token,表示“我觉得该结束了”。但服务端还可以加额外停止规则。

stop_token_ids 是 token 级停止。模型一旦生成这些 token id,服务可以停。

stop 是字符串级停止。它通常要等 token 被 detokenize 成文本后才能判断,所以它和 token 级 EOS 不是同一层。

ignore_eos=True 会无视模型自己的 EOS。这个参数很危险,因为模型已经想停了,你却要求它继续,结果可能进入重复或低质量输出。

min_tokens 则反过来阻止太早停止。比如你至少要模型生成 50 个 token,那么即使它很早想停,也会被继续推着往后生成。

很多线上问题都能归到这一层:

为什么回答被截断?可能碰到 max_tokens 或 stop。
为什么 stop 标记出现在输出里?看 include_stop_str_in_output。
为什么模型停不下来?检查 ignore_eos、stop_token_ids 和重复检测。

8. structured outputs:从软提示变成受约束解码

response_formatstructured_outputs 是这段代码里非常值得挖的一组。

to_sampling_params() 里,response_format 会被转成 StructuredOutputsParams

json_object -> json_object=True
json_schema -> json=<schema>
structural_tag -> structural_tag=<serialized tag object>

这背后是一个重要区别:

“请输出 JSON”是软提示。
structured outputs 是解码约束。

软提示只是告诉模型你想要什么,模型仍然可能漏逗号、漏括号、输出解释文字。受约束解码则是在每一步候选 token 上做限制,让非法 token 不能被选中,或者很难被选中。

可以这样理解:

模型给出 logits
结构化约束计算当前状态下哪些 token 合法
非法 token 被屏蔽
采样只能在合法 token 里发生

这就是为什么结构化输出和工具调用会有兼容性检查。源码里 check_structured_outputs_count() 要求 jsonregexchoice 这些约束不要同时开多个;当结构化约束和具名工具选择冲突时,也会提前报错。

因为这不是多加一句 prompt,而是在控制 decode 搜索空间。

9. tools:模型没有执行函数,它只是生成调用协议

toolstool_choiceparallel_tool_calls 经常被误读。

模型本身不会真的执行 Python 函数、查数据库或调用 HTTP API。模型做的是生成一段符合工具调用协议的文本或结构:

我要调用哪个 function
arguments 是什么 JSON

真正执行工具的是外部系统。

check_tool_usage() 很能体现这层边界:

  • tools 数组会被拒绝。
  • 如果传了 tools 但没传 tool_choice,会默认改成 auto
  • 如果指定了具名工具,源码会检查这个名字必须出现在 tools 里。
  • 如果设置 tool_choice,但没有提供 tools,会直接报错。

这说明 tool calling 是协议约束,不是魔法能力。模型负责生成“调用意图”,服务端和客户端负责解释、执行和把结果再喂回模型。

10. reasoning_effort:通常先影响模板和预算

这段类里有几个 reasoning 相关字段:

reasoning_effort
thinking_token_budget
include_reasoning
reasoning

build_chat_params() 有一个细节:当传了 reasoning_effort,而用户没有显式设置 enable_thinking,它会自动把 enable_thinking 放进模板 kwargs:

reasoning_effort != "none" -> enable_thinking=True
reasoning_effort == "none" -> enable_thinking=False

这说明 reasoning 参数常常不是直接“打开模型内部某个脑区”。在很多模型里,它先通过 chat template、特殊标记、推理 token 预算来改变生成格式和可用空间。

thinking_token_budget 进入 SamplingParams,说明它更接近 decode 预算控制。include_reasoning 则更偏响应层,决定是否把 reasoning 内容带出来。

更准确的说法是:

reasoning 参数控制模型被允许如何展开中间过程,不是直接注入逻辑能力。

11. stream、logprobs、token_ids:这是返回方式和可观测性

stream=True 不会改变模型的基本 next-token prediction。它改变的是服务端什么时候把结果发给你。

to_sampling_params() 里有一个清楚的映射:

stream=True  -> RequestOutputKind.DELTA
stream=False -> RequestOutputKind.FINAL_ONLY

也就是说,streaming 是 decode 过程中边生成边返回 delta;非 streaming 是等最终结果聚合好再返回。

logprobstop_logprobsprompt_logprobs 是可观测性参数。它们让你看到模型对 token 的概率判断,但代价是返回更多数据,也可能触发不支持的组合。例如源码里限制了某些 prompt_logprobs 不能在 stream 模式下使用。

return_token_idsreturn_tokens_as_token_idsreturn_token_offsetsreturn_prompt_text 也属于调试和观测:

我想知道 prompt 被模板渲染成了什么
我想知道每个 token id 是什么
我想把文本位置和 token 对齐
我想排查为什么 stop 或采样表现不对

这些字段不提升模型能力,但能显著提升你排查问题的能力。

12. 服务工程参数:priority、cache_salt、KV transfer

最后还有一组容易被忽略,但很 vLLM 的字段:

priority
cache_salt
kv_transfer_params
vllm_xargs
request_id

priority 是调度层参数。它影响请求被服务的先后,不改变模型对下一个 token 的概率判断。

request_id 是追踪 ID。它贯穿推理过程,方便日志、metrics 和返回结果关联。

cache_salt 和 prefix cache 有关。prefix cache 的直觉是:如果两个请求有相同前缀,就复用已经算过的 KV cache,减少 prefill 成本。但多租户场景里,前缀缓存也可能暴露“别人是否问过类似 prompt”的侧信道。salt 的作用是把缓存命名空间隔开,用安全性换一部分缓存命中率。

kv_transfer_params 面向 disaggregated serving。它不是采样参数,而是把 KV cache 在不同组件或节点间转移时需要的扩展参数。to_sampling_params() 会把它塞进 extra_args

这些字段提醒我们:vLLM 不只是模型 wrapper,它是推理服务系统。请求参数既有模型语义,也有系统调度和资源隔离语义。

13. validator 是一张“不能这样调”的地图

这段类后半部分的 validators 很值得看,因为它们告诉你哪些参数组合在原理上或协议上不成立:

stream_options 只能在 stream=True 时使用
top_logprobs > 0 时必须打开 logprobs
structured_outputs 里 json / regex / choice 不能同时开多个
continue_final_message 和 add_generation_prompt 不能同时为 True
tools 不能为空数组
具名 tool_choice 必须能在 tools 里找到
cache_salt 必须是非空字符串

这些不是普通表单校验。很多校验背后都有推理链路原因:

  • stream 是返回协议,stream_options 离开 stream 没意义。
  • top_logprobs 是 logprobs 的展开项,没有 logprobs 就没有 top_logprobs。
  • 多种结构化约束同时开,会让解码约束不清晰。
  • 继续最后一条消息和新增 assistant 生成提示,是两种互斥的 prompt 渲染策略。
  • tool_choice 指向不存在的工具,模型即使生成调用也没有可执行目标。

读 validator,比读字段注释更容易建立系统边界感。

14. BatchChatCompletionRequest:吞吐优化会牺牲自由度

文件后面还有 BatchChatCompletionRequest。它把 messages 从单个 conversation 变成 conversation 列表:

list[conversation]

但它也明确限制:

不支持 streaming
不支持 tool use
不支持 beam search
n 必须是 1

这符合推理服务里的常见取舍:batch 接口追求吞吐和结构简单,所以会砍掉一些难以批量对齐的交互能力。

streaming 强调低延迟返回;batch 强调一次吃多条请求。两者优化目标不同。

15. 最后把参数重新归类

如果只背字段,很快会乱。更好的记法是按推理链路归类。

控制模型看到什么:

messages
chat_template
chat_template_kwargs
documents
add_generation_prompt
continue_final_message
add_special_tokens
truncate_prompt_tokens
truncation_side

控制 token 预算:

max_completion_tokens
max_tokens
min_tokens
max_model_len

控制下一个 token 的候选分布:

temperature
top_p
top_k
min_p
presence_penalty
frequency_penalty
repetition_penalty
logit_bias
bad_words
allowed_token_ids
seed

控制停止:

stop
stop_token_ids
ignore_eos
include_stop_str_in_output
repetition_detection

控制结构化解码和工具协议:

response_format
structured_outputs
tools
tool_choice
parallel_tool_calls

控制 reasoning 形态和预算:

reasoning_effort
thinking_token_budget
include_reasoning

控制返回和观测:

stream
stream_options
logprobs
top_logprobs
prompt_logprobs
return_token_ids
return_prompt_text
return_token_offsets
metrics

控制服务调度和扩展:

priority
request_id
cache_salt
kv_transfer_params
vllm_xargs

16. 自检问题

读完这个类,可以用几个问题检查自己是不是真的懂了:

  1. messages 到底什么时候变成 token ids?
  2. max_completion_tokens 控制的是 prompt 长度,还是生成长度?
  3. temperature 改的是模型权重,还是 logits 到概率的过程?
  4. stopstop_token_ids 为什么不是同一层?
  5. 为什么“请输出 JSON”和 response_format=json_schema 不是一回事?
  6. tool calling 里,模型执行了函数吗?
  7. stream=True 改变了模型生成过程,还是改变了返回方式?
  8. cache_salt 为什么属于安全和缓存隔离问题,而不是采样问题?

如果这些问题能答出来,ChatCompletionRequest 就不再是一堆参数,而是一张从 API 到 LLM serving 内部机制的地图。