从 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]
几个源码锚点:
ChatCompletionRequest定义请求字段。build_chat_params()把请求变成 chat template 渲染参数。build_tok_params()把请求变成 tokenization 和长度预算参数。to_sampling_params()把请求变成采样和解码参数。- 下面一组 validator 负责在请求进入引擎前拦住不兼容组合。
所以这个类最有价值的地方,不是告诉你 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_tokens 和 truncation_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 和概率
接下来进入最核心的采样参数。先不要背 temperature、top_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 的概率低到离最高概率太远,就不要再参与抽样。
这解释了一个实践问题:不要同时乱调 temperature、top_p、top_k、min_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_format 和 structured_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() 要求 json、regex、choice 这些约束不要同时开多个;当结构化约束和具名工具选择冲突时,也会提前报错。
因为这不是多加一句 prompt,而是在控制 decode 搜索空间。
9. tools:模型没有执行函数,它只是生成调用协议
tools、tool_choice、parallel_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 是等最终结果聚合好再返回。
logprobs、top_logprobs、prompt_logprobs 是可观测性参数。它们让你看到模型对 token 的概率判断,但代价是返回更多数据,也可能触发不支持的组合。例如源码里限制了某些 prompt_logprobs 不能在 stream 模式下使用。
return_token_ids、return_tokens_as_token_ids、return_token_offsets、return_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. 自检问题
读完这个类,可以用几个问题检查自己是不是真的懂了:
messages到底什么时候变成 token ids?max_completion_tokens控制的是 prompt 长度,还是生成长度?temperature改的是模型权重,还是 logits 到概率的过程?stop和stop_token_ids为什么不是同一层?- 为什么“请输出 JSON”和
response_format=json_schema不是一回事? - tool calling 里,模型执行了函数吗?
stream=True改变了模型生成过程,还是改变了返回方式?cache_salt为什么属于安全和缓存隔离问题,而不是采样问题?
如果这些问题能答出来,ChatCompletionRequest 就不再是一堆参数,而是一张从 API 到 LLM serving 内部机制的地图。