vLLM生产栈同时承载对话与向量模型的部署实践
很多做RAG、智能客服、知识库问答的团队迟早都会撞上同一个问题对话模型和向量模型不能一直分家过了。线上生产环境里一边是负责生成回答的对话大模型另一边是负责召回语义片段的embedding模型两者明明都是Transformer架构、都跑在GPU上、都由同一套推理框架承载却往往被拆成两套完全独立的服务。最近我在折腾vllm部署DeepSeek这类对话模型的同时还要用vllm/vllm-openai:v0.27.1这个镜像去加载qwen3-embedding-0.6b做向量化这才意识到一个问题vllm production stack在同时迎接这两类模型时远没有我想象中那么顺理成章。这个场景不是个例。现在只要涉及RAG、文档问答、内容语义检索几乎都绕不开对话模型向量模型的组合需求。而vllm作为目前最主流的LLM推理生产框架大家默认它应该天然支持这两种模型毕竟OpenAI兼容协议里既包含/chat/completions也包含/embeddings接口。实际用下来才发现从镜像选择、模型加载入口、调度机制到资源隔离每一步都藏着和生产环境直接相关的取舍问题。这篇文章就把我踩过的坑、做过的对比、最终落地的方案一次性讲清楚。1. 为什么生产栈必须同时扛起对话与向量两类模型先说业务层面的驱动力。你做一个企业知识库助手用户问上个季度的销售政策有哪些调整系统要先把这个自然语言问题转成向量去向量数据库里把最相关的几段政策文档捞回来再把问题和文档片段一起喂给对话模型生成最终答案。这段链路里向量模型处理的是入库和召回环节对话模型处理的是理解和生成环节两者缺一不可。但如果你把这两者拆成两套独立服务问题就会冒出来。1.1 拆两套服务带来的运维难题我见过不少团队的初始架构是对话模型用vllm跑在A组GPU机器上向量模型用另一个框架跑在B组机器上中间再加一层网关做路由。表面看逻辑清晰实际运维时非常痛苦。首先是资源碎片化向量模型通常是个0.6B级别的参数模型一张GPU卡绰绰有余但为了高可用你可能得给它单独留几台机器这些机器的利用率通常不会太高。对话模型那边同样需要独立资源池当某些时段向量请求暴增而对话请求平稳时两边无法互相借调算力。其次是部署和监控的分裂。两个框架的镜像体系不一样健康检查机制不一样日志格式不一样prometheus指标也不一样。出了问题你得在两套系统里来回跳排障效率很低。还有一个更隐性的问题——模型版本管理。当你升级向量模型时需要同步保证已入库的旧向量和新向量的一致性不然召回质量会滑坡。如果两套服务完全独立这个版本同步依赖就只能靠人来保障。1.2 统一推理栈的实际收益把两类模型放到同一个推理栈里最直接的好处是运维心智的统一。vllm本身已经同时实现了/completions、/chat/completions和/embeddings接口这意味着你的API网关可以用完全一致的方式对接两类模型日志采集和监控告警也复用同一套模板。我在实际项目里把embedding服务切到vllm之后最直观的感受是不用再维护两套镜像仓库了原本专门负责部署向量服务的同学也可以把时间挪去做更值得投入的事情。当然把两类模型放到一个生产栈里不等于物理上必须塞进同一个GPU进程。vllm也支持通过不同的entrypoint和不同的服务实例来分别承载它们但上层保持统一调度。这个统一但不混跑的架构会在后面详细展开。2. vLLM对向量模型的支持边界与正确加载方式坦白说vLLM对embedding模型的支持并不是最近才有的但有一个非常容易踩坑的点不同版本的vllm加载embedding模型的入口完全不一样。尤其是网络热词里提到的docker vllm/vllm-openai:v0.27.1加载qwen3-embedding-0.6b这里必须说清楚v0.27.1这个版本属于相对早期的版本它的标准启动方式是python3 -m vllm.entrypoints.openai.api_server而不是后来新版本里的vllm serve命令。2.1 从qwen3-embedding-0.6b看模型加载参数以qwen3-embedding-0.6b为例这个模型是Qwen3系列里一个专门做向量化的embedding小模型参数量约6亿实际上比很多对话模型都小得多。它的特点是输出维度可控、支持多任务指令做中文语义召回的效果在同量级模型里非常能打。在生产里把它跑在vllm里时我最常用的启动命令长这样docker run --runtime nvidia --gpus all --ipchost \ -v /data/models:/models \ -p 8001:8000 \ vllm/vllm-openai:v0.27.1 \ python3 -m vllm.entrypoints.openai.api_server \ --model /models/qwen3-embedding-0.6b \ --task embed \ --dtype float32 \ --max-model-len 8192 \ --trust-remote-code \ --port 8000这里有几个参数需要重点解释。第一是--task embed这是告诉vllm你加载的是embedding类的模型它会自动选择对应的模型架构并在OpenAI协议里暴露/embeddings端点。如果你不指定这个参数vllm会按默认的对话模型去加载qwen3-embedding-0.6b结果往往是加载时报错或者输出不是预期的向量。第二是--dtype float32qwen3-embedding这类模型在默认的float16下有时会出现精度问题实测下来embedding向量用float32计算是更稳妥的选择。第三是--trust-remote-codeqwen系列模型普遍依赖远程代码文件不加上这个参数会直接拒绝执行模型自定义代码。2.2 vllm serve还是api_server版本差异的坑如果你用的是vllm比较新的版本比如0.6.x之后的入口命令通常是vllm serve或者是python -m vllm.entrypoints.openai.api_server这两种。但在v0.27.1这个版本里官方推荐的方式还是api_server。我自己一开始图省事写了vllm serve结果容器直接报command not found。这个排查过程说难不难但如果你没有意识到v0.27.1这个镜像内部的入口差异很容易绕弯路。另外提醒一个细节qwen3-embedding-0.6b的max-model-len不能盲目调大。embedding模型和对话模型不一样它会直接把输入序列编码成一个向量序列过长会导致显存占用非线性增长。我的建议是8192足够覆盖绝大多数文档切块的embedding需求不必追求过长。2.3 Ollama装了向量模型之后怎么用一个常见对比热搜词里有一个ollama安装了向量模型后如何使用这个我也特意做过对比。Ollama支持拉取embedding模型并用ollama run或者API调用来做向量化这对个人电脑上做原型验证确实很方便。但放到生产环境我们会发现vllm有一个Ollama短期比不了的硬优势吞吐量。在连续高并发的embedding请求下vllm的continuous batching调度能让GPU一直满载而Ollama更倾向于单请求独占资源。另外一个现实问题是很多团队已经在vllm上维护了一套模型版本管理、镜像发布、压测流程为了embedding再引入Ollama等于在技术栈上开了一个新的口子。3. Docker镜像里的模型从哪来镜像与权重的边界网络热词里有一条vllm docker镜像中带模型吗这个问题看着基础但在实际部署中我被问过太多次而且它对生产栈设计有直接影响。答案是不带。3.1 vllm镜像里有什么vllm/vllm-openai镜像本质上是把Python环境、CUDA运行库、vllm框架本体、OpenAI兼容的API服务代码全部打好了包。它的体积通常有好几个GB但这些全部是运行时代码和依赖不包含任何模型权重文件。模型权重是你自己通过HuggingFace下载或者从内部模型仓库拉取然后在启动容器时通过-v挂载进去的。前面给的启动命令里就用到了-v /data/models:/models把宿主机上的模型目录挂载进容器再通过--model参数指向容器内的路径。3.2 镜像版本与vLLM版本的对应关系另一个容易让人混乱的点是镜像tag和vllm版本之间的关系。vllm/vllm-openai:v0.27.1里的0.27.1指的是vllm的版本号。镜像tag通常和vllm发行版版本对齐但你不一定总能找到完全对应你本地环境的tag。建议的做法是先确定你要用的vllm版本再到镜像仓库确认那个tag是否存在如果不存在可以退而求其次选择一个临近的tag然后到容器内部用pip show vllm确认实际版本。我实测下来v0.27.1这个镜像里面对应的vllm内核其实已经是比较成熟的入口逻辑了用来跑0.6B的小embedding模型绰绰有余。注意不要在镜像里强求带模型。模型的更新频率远高于推理框架把权重和镜像解耦你才能实现模型单独升级、回滚而不用重建镜像。3.3 模型目录的组织方式我建议在宿主机上把模型统一放到一个独立的目录结构下例如/data/models/qwen3-embedding-0.6b和/data/models/deepseek-chat。每个模型目录包含完整的权重文件、config.json、tokenizer文件。启动容器时只需要挂载一层目录不同容器各自指定不同的--model路径即可。这样做的好处是你可以同时跑一个对话模型容器和一个embedding模型容器但两者只是同一个镜像的不同实例不会产生镜像层面的冗余。4. Scheduler视角对话模型与向量模型在资源调度上的根本差异如果说前面两节讲的是怎么把模型跑起来那这一节就是跑起来之后如何保证生产可靠。理解vllm的scheduler逻辑是回答vllm production stack如何同时应对两类模型这个问题最核心的一环。网络热搜词里专门有人搜vllm scheduler逻辑说明大家已经意识到光是能加载模型不代表能扛住线上流量。4.1 continuous batching与KV cache的运作机制vllm的调度核心是continuous batching。传统的推理方式是batch内所有序列同步推进一个批次生成完所有token后整个batch结束。vllm会实时追踪每个序列的生成进度当一个序列完成时马上把新到达的请求插入到这个batch里让GPU始终处于忙碌状态。在这个过程中显存里的KV cache是核心资源。每个对话序列自回归生成时每生成一个token都要缓存这个token对应的Key和Value序列越长KV cache占用越大。embedding模型的情况则完全不同。它不需要自回归生成输入通过Transformer编码后直接池化成一个向量整个计算过程只有一次前向传播。这个过程完全没有KV cache增长的问题用完即走。所以两类请求混在同一批次的GPU计算时它们的资源消耗特征是完全不同的对话请求占用显存并持续加压embedding请求则是短促的一次性脉冲。4.2 混跑时的互相影响与应对策略我在实验环境里尝试过把对话模型和embedding模型同时挂到同一个vllm进程里结果发现调度器的行为并不像预期那样完美融合。由于vllm在启动时就会根据--max-model-len和--gpu-memory-utilization预分配KV cache池embedding请求虽然不需要KV cache但对话模型留下的显存预留并不会自动让位。更具体的表现是并发embedding请求扎堆的时候它们会在对话序列的批次间隙穿插执行如果对话序列过长embedding请求可能会被压到后面导致P99延迟波动明显。所以我对生产环境的建议是vllm进程层面不要硬混跑。所谓一个production stack同时应对两类模型更合理的含义是同一个镜像、同一个调度框架、同一套API协议但用两个不同的vllm实例分别服务对话模型和向量模型。这样既保持运维统一又让scheduler各自专注在自己的请求特征上互不干扰。下面这个表格可以直观说明差异维度对话模型实例向量模型实例请求特征长连接、多轮生成、持续占用KV cache短请求、单次前向、无KV cache增长显存分配需要预留充足KV cache池主要是权重和中间激活值调度关键参数--max-model-len、--gpu-memory-utilization、--max-num-seqs--max-model-len、--max-num-seqs、并发上限核心监控指标TTFT、TPOT、吞吐量请求延迟、QPS、向量维度一致性4.3 Prefix Cache对两类模型的不同价值还有一个容易被忽略的细节是prefix caching。vllm的自动前缀缓存会复用相同前缀的KV cache对话模型场景中多轮对话的system prompt和历史记录高度重复命中率往往很高。但对embedding模型来说请求之间几乎不存在共享前缀每次向量化请求都是全新的输入序列prefix cache基本起不到作用。因此如果你的对话模型容器开启了--enable-prefix-caching而embedding模型容器也照抄了这个参数实际并不会带来收益反而会白白占用一部分显存去管理缓存结构。5. 生产部署拓扑三种常见方案与选型思路到了真正落地的阶段你会发现选择哪种拓扑本质上取决于你的业务对延迟和资源利用率的敏感度。我梳理了三种在真实项目中验证过的方案你可以根据自己的规模来做取舍。5.1 方案A单GPU多进程统一入口反向代理如果你手头的GPU数量不多但又要同时提供对话和embedding两类能力最务实的做法是在一台GPU机器上同时跑两个vllm容器一个服务对话模型一个服务embedding模型外部用一个Nginx或者自研网关统一路由。机器上的多张GPU可以按需分配比如对话模型用两张卡embedding模型用一张卡。这个方案的优点是完全不用改动vllm本身两个容器天然隔离故障域升级某一类模型时不会影响另一类。缺点是单机故障会同时击穿两类服务需要注意在网关层做健康检查和重试。我在实际项目中有一段非常有效的配置思路网关层按照路径前缀路由/chat开头的请求走对话模型容器/embeddings开头的请求走向量模型容器。只要后端容器各自暴露8000端口前端完全无感。5.2 方案B部分混跑利用模型异构实现资源复用这个方案适合资源很紧、但请求量又还没大到需要弹性伸缩的团队。具体做法是在同一个vllm进程里先加载对话模型embedding模型则用一个小型进程单独跑在同一张GPU上。为了让两者互不挤占你可以给embedding模型容器设置--gpu-memory-utilization为0.2对话模型容器设置为0.75同时用CUDA_VISIBLE_DEVICES把两个容器指向不同的物理卡或者借用MIG把一张卡切分成多个实例。但正如前面scheduler那节所说的如果两类请求都要达到极高的并发混跑带来的调度不确定性会显著增加排障成本。我推荐这个方案只在资源受限、业务峰值明显的场景下过渡使用。5.3 方案C独立集群vllm生产栈标准化当业务体量发展到一定程度最清晰的方式还是独立集群。对话模型走一批A100/H800节点embedding模型走一批相对低配的节点。听起来又回到了开头说的拆两套服务但核心区别是这次两端都跑在vllm上镜像一致、监控一致、发布工具链一致、API协议一致。所谓production stack的统一并不是物理层面的完全合并而是管理口径的一致化。这种逻辑统一、物理可分离的设计让团队既保住了运维效率又保留了资源调度的灵活性。5.4 接入Chatbox等前端工具的注意事项热搜词里出现vllm部署大模型chatbox我也顺便说一下。Chatbox这类第三方对话前端通常只支持OpenAI兼容API接入vllm时只需要把API地址填成vllm的/v1端点就行。但如果你在同一个网关后面同时挂了对话模型和embedding模型Chatbox只会用到对话接口这和embedding服务互不干扰。反过来如果你在调试过程中发现Chatbox里无法调用embedding能力那不是vllm的问题而是这类前端工具本身就没有暴露embedding入口。embedding接口通常是通过代码方式调用的比如from openai import OpenAI client OpenAI(base_urlhttp://your-gateway:8000/v1, api_keyEMPTY) resp client.embeddings.create( modelqwen3-embedding-0.6b, input[生产环境中的向量模型部署方案] ) print(resp.data[0].embedding)6. 实测中的几个拦路坑与排查链路最后把这几天折腾过程中真正让我卡住过的几个问题列出来每个都附带排查思路希望你能少走几步弯路。6.1 模型加载直接报错Unsupported architecture这个问题一度最让我费解。明明qwen3-embedding-0.6b是官方支持的模型vllm却报不支持。后来排查发现是因为我忽略了--task embed参数。不要在命令里省掉这个参数它实际上是告诉vllm为该模型选择embedding专属的模型类如果省掉vllm会按默认的ModelType.LLM去处理进而认为这是不支持的架构。6.2 Embedding向量维度不一致我加载完模型后用脚本测试发现输出的向量维度跟模型设计值对不上。这个问题的根因通常是被--dtype影响了精度进而导致输出异常。换成float32之后维度终于稳定在了我所期望的数值上。在embedding场景里向量维度是一个必须稳定的契约下游向量数据库的索引结构依赖于这个固定维度一旦线上模型输出的向量维度发生漂移写入向量表时会直接失败或者检索结果错乱。6.3 镜像拉取缓慢导致的部署超时vllm/vllm-openai镜像体积不小在海外源不稳定时很容易拉取超时。这个问题虽然技术含量不高但在生产部署里它带来的是实实在在的时间成本。我的做法是在本地提前把镜像拉到私有仓库部署时统一从内网拉取。尤其是在你有多台GPU机器需要部署同一个生产栈时这个预拉策略能省下很多等待时间。6.4 Chatbox访问报404我遇到过前端配置好了地址但请求始终返回404的情况。排查发现是base_url填写时漏掉了/v1后缀。vllm的OpenAI兼容接口统一挂在/v1路径下比如http://host:8000/v1/chat/completions。这一点在接入任何OpenAI兼容客户端时都要留意不只是Chatbox。最后再分享一个判断准则当对话模型遇上向量模型vllm production stack的应对方式我最终归纳成一句话逻辑上统一物理上隔离。统一的是镜像、协议、监控和发布流程隔离的是GPU实例、调度参数和故障域。你不需要为了embedding再去引入第二套推理框架但也不要天真地把两类请求硬塞进同一个scheduler。基于这个准则无论使用哪个版本的vllm镜像无论你加载的是DeepSeek还是qwen3-embedding-0.6b都能很快找到一个既清楚又经得起生产考验的部署方案。