ARTICLE · 1144140
推理引擎默认拒绝请求里的聊天模板
自建推理服务升级到 SGLang 0.5.21 之后,如果调用方习惯在请求体里带 chat_template_kwargs,并且其中带上了 chat_template 这个键,最可能先看到的会是一个 400。升级到 vLLM 0.31.0 的服务也会遇到同类拒收,只是被拒的字段换成了多模态处理参数。这不是故障,而是引擎把一类字段从「默认接受」改成了「默认拒绝」。
被拒的到底是哪几个字段
SGLang 0.5.21 发布于 2026 年 10 月 2 日。它的 breaking changes 里写着「请求自带的聊天模板会被拒绝」:在 /v1/chat/completions 和 /v1/tokenize 的聊天分支上,只要请求的 chat_template_kwargs 里出现 chat_template 这个键,就返回 400,除非服务端启动时带了 --trust-request-chat-template。
维护者在合入说明里给了理由。OpenAI 兼容路径会把客户端传来的 chat_template_kwargs 合并进 apply_chat_template 的参数里,调用方因此可以用自己写的 Jinja 模板替换掉服务端的模型模板,等于控制了 system prompt。
拦截范围要说清楚:只拦 chat_template 这一个精确键名。enable_thinking、reasoning_effort、tools 这些同样常被请求带上的 kwarg 不在拦截范围内,写成近名键 chat_templates 也不拦。维护者用 Qwen3-0.6B 在 A800 上做了对照,注入模板的请求在改动前返回 200,正文变成了模板指定的内容;改成默认拒绝后同一个请求返回 400;显式打开信任开关后又回到 200。同一台服务上的普通对照请求照常给出正常答复。
vLLM 0.31.0 发布于 2026 年 10 月 5 日,拦的是另外两个字段:请求级的 mm_processor_kwargs 和 media_io_kwargs,非空就报错,开关是 --trust-request-mm-kwargs。它的 breaking changes 和安全文档都写明,服务端的 --mm-processor-kwargs、--media-io-kwargs 以及离线 LLM 用法不受影响。
引擎给出的理由
vLLM 对多模态 kwargs 的说法是,这些值会走到多模态媒体的加载,以及处理器的构造和尺寸计算上;不受信任的覆盖可以把图像、视频的加载和采样推到极端设置,把内存耗光。对应改动是 PR #58830,2026 年 9 月 26 日合入,实现上由 validate_request_mm_kwargs 在请求校验阶段抛错。
聊天模板那边,理由更早有定论。GHSA-6fvq-23cw-5628 对应的 CVE-2025-61620,把问题定性为资源耗尽型拒绝服务。Jinja 模板本身允许循环和嵌套这类编程结构,一个构造过的模板就能吃掉大量 CPU 和内存,让服务对正常请求失去响应。这里有一个容易被说错的地方:官方公告给的是资源耗尽,不是任意代码执行。
同一份公告还指出了修复的难点。只挡住请求顶层的 chat_template 字段并不够,因为代码会先用 dict.update 把用户的 chat_template_kwargs 合并进参数字典,把 chat_template 塞进 kwargs 里就能绕开顶层检查。这就解释了为什么今天的实现是拦「kwargs 里的那个键」,而不是拦一个顶层字段。
两个引擎合上这道口子的时间差值得记一下。vLLM 的修复 PR 在 2025 年 9 月 27 日合入,随 0.11.0 发布,公告在 2025 年 10 月 7 日公开;SGLang 的对应改动 2026 年 9 月 25 日才合入,随 0.5.21 发布。同一类问题在两个引擎里的处置节奏并不同步,一个修了不代表另一个也修了。
它和提示注入不是一回事
模板这件事容易被并到提示注入里一起讲,但两者的作用面不同。提示注入发生在内容层,影响的是这一轮请求的对话走向,护栏、隔离和评测都在这一层做。请求自带的聊天模板发生在渲染层,它不改变某一段文字,而是改变服务端把消息拼成提示词的方式。模板一旦生效,经过它的所有请求都按调用方定义的格式走,资源开销也由模板本身的结构决定。这也是为什么引擎的处置方式是拒绝字段,而不是过滤内容。
升级之后怎么改
最省事的动作是打开信任开关,但它不该是默认选择。vLLM 在安全文档里对 --trust-request-mm-kwargs 的措辞是,只有 API 客户端本身可信的部署才应该开,并且明确写了不要在面向不可信客户端的端点上启用。SGLang 的 trust_request_chat_template 字段注释同样写着默认关闭是为了安全。
如果服务前面站着一个自建网关,判断的关键在于开关是给网关本身开的,还是等于对外部流量开放了透传这些字段的能力。在第二种情况下,开关只是把整块面重新暴露出来。
更好的方向是把配置收回服务端。SGLang 的服务端参数 chat_template 接受内置模板名或模板文件路径,只作用于 OpenAI 兼容 API;vLLM 同理有服务端的 --chat-template。模板落到服务端之后,请求体里不再需要这个字段:
{ "model": "qwen3-32b", "messages": [{"role": "user", "content": "..."}], "chat_template_kwargs": { "chat_template": "<自定义 Jinja 模板>" } } 上面这段请求在 0.5.21 上会被拒。把 chat_template 删掉、模板挪到启动参数里,其余字段不动,调用方就不需要任何额外权限。对于本来只想给模型设个缺省的场景,两个引擎都提供了服务端默认值:SGLang 的 default_chat_template_kwargs 与 vLLM 的 --default-chat-template-kwargs,可以在服务端写死 enable_thinking、reasoning_effort 这类默认值,请求级的值仍然优先。这次改动的意图就落在这里:让「服务端定默认、请求微调」成为主路径,把「请求改写模板」单独挑出来管。
这不是一次封堵完成
把这条改动读成「注入问题解决了」会高估它。两个引擎的门禁都只覆盖一部分字段:SGLang 只拦 chat_template,其余 chat_template_kwargs 仍会进入渲染路径;vLLM 在 0.31.0 里新增的门禁只覆盖请求级的两个多模态字段(请求自带的 chat_template 早在 0.11.0 就被同类门禁挡住),服务端参数照旧生效。也就是说,「哪些请求字段能影响服务端行为」这件事仍然需要各个部署自己维护一张清单,而不是指望引擎一次给全。
好在错误形态是可排查的。这是请求校验阶段同步抛出的错误,SGLang 返回 400,vLLM 抛校验异常,不会变成静默降级。代价是升级窗口里会集中出现,凡是网关或客户端库里固化了下发这些字段的地方,会在同一时刻一起暴露出来。
要做出的判断其实只有一件:这些字段是谁在写。模板和多模态参数由平台自己定的,就搬到服务端启动参数里;确实需要请求级透传的,只留给内部链路,并确认这条链路的入口不面向不可信调用方。