在vLLM上跑多轮对话,history超限是常见且可预期的错误,不建议把它当作偶发故障来排查。vLLM侧通常会直接返回带长度说明的错误,报错里会出现请求的token数和模型上限值;真正的处理重点不是看懂报错,而是把长度控制放到请求构造阶段,并对并发场景下的重试和参数调整保持谨慎。
多轮对话history超限,核心处理顺序是:先确认错误来自模型上限还是并发资源不足;再把长度预算和截断逻辑放到请求构造阶段,优先保留system prompt和最近对话;最后在并发场景中避免盲目重试和盲目调大max_model_len。截断会丢上下文,需要业务侧接受这个代价。
先判断错误发生在哪一层
遇到超限错误时,先确认vLLM服务是否设置了max_model_len。vLLM启动时如果不显式设置这个参数,通常会读取模型config里的max_position_embeddings,或者使用服务默认值。排查时做三件事:查模型配置里位置编码上限、查vLLM服务启动参数里是否覆盖了max_model_len、查报错请求实际传入的token数。这三者对应上,才是真正的输入超限。如果有些请求长度远低于上限也失败,并且集中在并发高峰,那更可能是排队超时或显存不足,不一定要走截断逻辑。
把长度预算放到请求构造阶段
多轮对话输入由system prompt、历史轮次、当前用户问题三部分组成。组装请求时先估算这段messages的token数,估算不一定要精确tokenize,中英文混合场景下按字符数除3或4估算,再留出输出token的余量即可。超限时按优先级截断:先保留system prompt,再保留最后一到两轮对话,从中间丢弃或压缩。不要丢最近的对话,也不要只按轮数固定截断——不同轮次长短差异很大,固定轮数容易要么浪费空间要么仍然超限。
def truncate_history(messages, max_input_tokens, reserve_for_output=1024):
# messages: 按 OpenAI 格式组织的 dict 列表
budget = max_input_tokens - reserve_for_output
# 先估算整段 messages 的 token 数,若在预算内直接返回
# 超限时:先固定保留 system 消息,再从尾部向前保留
# 保留最近 2-3 轮后仍超限,就压缩中间轮次的内容
# 压缩仍不够时,只能丢弃更早的轮次
return truncated_messages这段逻辑放在业务侧调用vLLM之前执行,max_input_tokens需要结合模型上限和服务端配置来设,不能直接取模型理论最大值。截断之后请求长度会明显下降,但需要注意:丢弃中间轮次后,模型可能会丢失早期约定或背景信息,业务上如果依赖这些信息,应该提示用户或由应用层做摘要压缩,而不是无提示硬截断。
并发场景下两个常见误区
第一个误区是把超限错误当作临时故障自动重试。长度超限是确定性错误,相同输入重试结果不会变化;并发场景下大量超限请求同时重试,反而可能加剧服务排队。正确的做法是捕获错误后先截断,再重新构造请求发送,或者直接返回给调用方说明上下文过长。第二个误区是简单调大max_model_len。调大上限只是放宽了长度约束,模型占用的KV显存会随之增长,并发量会下降,结果可能从长度错误变成OOM错误。真的需要更长上下文时,要考虑换支持更长窗口的模型,而不是强行改配置。
并发场景下还需要注意请求长度分布。即使单请求都没超限,大量长请求同时到达时,prefill计算量和显存占用都会明显上涨,服务吞吐会受影响。建议在接入层统计各自请求的估算长度,设一个长度告警阈值;如果长请求占比持续偏高,需要回到截断策略或换模型这两个方向上解决,而不是只在报错时处理。
验证方式与边界
先在单路请求下验证截断逻辑:构造一个明显超长的多轮对话,确认截断后请求成功、模型回答不报错,并观察中间内容丢失后回答质量是否可接受。再在并发场景下验证:保持原有并发压力,确认不再出现长度超限错误,同时观察显存和排队情况是否正常。截断策略没有一劳永逸的方案,需要根据模型窗口、业务对话轮数、用户对上下文的敏感度做取舍。建议把长度估算和截断动作做成独立模块,方便后续调整预算比例和保留轮数。