
最近不是大家都在测多模态 Agent 嘛我也跟进了一轮把手上的 GPU 集群重新收拾了一遍跑通了DeepSeek-V4-Flash-Vision-Exp这套多模态模型的部署并且用GPUStack做了统一的算力编排最后在上面挂了一层简单的Agent 服务。整个过程不算复杂但是坑真的不少。尤其是 GPUStack 和 vLLM 的版本匹配、多模态输入的 service token 限制、还有 Agent 工具调用那块的超时控制随便一个都能让你折腾半宿。这篇就把我整个部署过程、踩坑记录、压测数据全部摊开写出来。如果你正打算在私有环境里搭一套多模态 Agent 服务或者手头有 GPU 但不知道该怎么把模型服务化、工具化这篇应该能让你少走很多弯路。整个方案是三层结构GPUStack 负责 GPU 资源管理和模型服务编排vLLM 负责跑 DeepSeek-V4-Flash-Vision-Exp 推理最后在服务之上写一个轻量 Agent 代理层来做图片理解、工具调用和对话循环。我会从选型理由说起再一步步落到配置和代码最后把最坑的几个问题拿出来单独讲。1. 为什么是这个组合GPUStack、DeepSeek-V4-Flash-Vision-Exp 与 Agent 的适配逻辑先说选型。很多人上来就问“部署多模态模型用哪个框架好”但其实最关键的不是框架本身而是你的使用场景决定了一切。我的需求很明确公司内部要做一批图片理解类的 Agent 应用需要并发推理、需要动态调度 GPU、需要对外暴露 OpenAI 兼容接口。在这个前提下GPUStack vLLM DeepSeek-V4-Flash-Vision-Exp就成了一个非常合理的组合。1.1 GPUStack 在整套链路里的位置GPUStack 本质上是一个 GPU 集群管理和模型服务平台它做的事情是把多台机器的 GPU 聚合起来然后统一跑各种推理引擎。你可以在上面创建模型部署、指定使用哪几张卡、自动做负载均衡、对外提供 API。它在整个链路里处于“底座”位置。模型跑在 vLLM 上而 vLLM 的实例是由 GPUStack 创建和管理的。这套架构的好处是如果你有 3 台机器、每台 2 张卡你不需要手动去每台机器上部署推理服务、自己写负载均衡。GPUStack 会把这些都处理掉。并且它原生支持 vLLM 作为推理引擎创建模型部署的时候可以直接选择 vLLM 模板不需要自己手写 Dockerfile 之类的东西。1.2 DeepSeek-V4-Flash-Vision-Exp 解决了什么问题标题里这个 DeepSeek-V4-Flash-Vision-Exp 是带视觉能力的实验版模型算力消耗比完整版小响应速度更快适合做高频的 Agent 服务。对多模态 Agent 来说响应延迟是直接影响体验的你让用户等 30 秒才看到第一段分析这个服务基本做不下去。Flash 版本在推理速度上有明显优势代价是复杂图片理解上偶尔会丢细节比如小字号文字识别有时候会漏字。我的判断是对于企业内部场景截图理解、流程自动化、表单识别准确率完全够用速度上的收益远大于偶尔丢细节的损失。1.3 Agent 层为什么要单独写一套模型部署好了只是一个“会看图的接口”离 Agent 还差一层工具调用、上下文管理、任务拆解。我在模型之上写了一个轻量级的 Agent 循环核心就是接收用户请求可能是图片加文字的组合决定是否需要调用工具比如拉取文件、查数据库、发通知调用模型进行推理整理结果返回这套逻辑不复杂但如果没有它你拿到的只是一个“看图说话”的 API而不是一个能完成任务的 Agent。2. 部署环境准备与 GPUStack 集群搭建的细节这一节讲环境。我的测试环境一共是三台机器每台挂了两张 RTX 4090。为什么要强调硬件因为多模态模型对显存的要求比纯文本模型高不少。2.1 硬件资源规划参考配置项本环境配置备注GPU 型号NVIDIA RTX 4090 24GB x 6也可以用 A800/H800显存建议不小于 24G系统Ubuntu 22.04 LTS内核干净驱动兼容性好GPU 驱动535.xx 及以上CUDA 12.x 版本对应内存单机 128GBvLLM 会做 KV cache内存太小容易 OOM存储NVMe SSD 2TB模型文件大且多模态数据读取频繁2.2 GPUStack 的安装流程GPUStack 的安装方式非常简单官方给了脚本。我是用的 worker server 模式第一台机器做 server其他两台作为 worker 加入。curl -sSfL https://get.gpustack.com | sh这个脚本会装好 GPUStack 的全部组件包括依赖的 Docker、CUDA runtime 等。安装完成之后访问http://server-ip:80就能看到管理界面。然后添加 worker 节点curl -sSfL https://get.gpustack.com | GPUSTACK_SERVER_URLhttp://server-ip:80 GPUSTACK_TOKENtoken shtoken 可以从管理界面的 API key 那里获取。这里有一个非常容易忽视的地方worker 节点必须与 server 在同一二层网络且能相互访问 80/443 端口否则节点状态会一直显示 offline。2.3 NVIDIA 容器工具链的坑如果 GPUStack 创建推理实例时报“no GPU available”之类的错大概率不是 GPUStack 的问题而是宿主机没有装好 NVIDIA Container Toolkit。distribution$(. /etc/os-release;echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit sudo systemctl restart docker装完之后记得在 GPUStack 的 worker 设置里检查 GPU 是否被正确识别。我遇到过驱动版本正常、但 worker 上报的 GPU 数量是 0 的情况最后发现是 docker 的 nvidia runtime 没有配置好。3. 模型注册与 vLLM 推理服务启动关键参数解读算力底座准备好之后下一步就是把 DeepSeek-V4-Flash-Vision-Exp 跑起来。GPUStack 支持从 HuggingFace 拉取模型也支持直接指定本地路径。我个人的建议是先用 GPUStack 的界面注册模型确认能跑通再去改 vLLM 参数。一上来就玩花的出了 bug 你根本分不清是哪一层的问题。3.1 在 GPUStack 中创建 vLLM 模型部署在 GPUStack 管理界面中进入 Models 页面点击 Deploy Model。模型来源可以选 Hugging Face 或者本地文件。我因为在内网环境提前把模型文件下载到了本地然后通过本地路径注册。关键配置项配置项取值说明推理引擎vLLM必须选这个模板里封装好了模型路径/models/deepseek-v4-flash-vision-exp本地路径需要是模型目录副本数1后期并发不够再往上加GPU 选择自动让 GPUStack 自己调度环境变量CUDA_VISIBLE_DEVICES一般不用手动设GPUStack 会为这个部署启动一个 vLLM 实例并在内部绑定好端口。等状态变为 Running 之后它会生成一个 OpenAI 兼容的 API 地址。3.2 vLLM 模板参数调整不只是改个模型名GPUStack 的 vLLM 模板默认参数对多模态模型不够优化。我改了几个关键参数这些参数的取舍直接决定了服务的吞吐量和延迟。--gpu-memory-utilization 0.9 --max-model-len 16384 --trust-remote-code --limit-mm-per-prompt image5gpu-memory-utilization 0.9让 vLLM 尽量多利用显存做 KV cache。4090 是 24G留 10% 给 CUDA context 和其他开销就够了。多模态模型的 KV cache 消耗比纯文本大得多这个参数不调并发一高很容易 OOM。max-model-len 16384多模态的输入 token 中图片会占掉一大块。如果你做的是文档理解建议直接给到 32768但要留意显存占用。limit-mm-per-prompt image5限制单次请求最多带 5 张图。这个限制很重要否则有客户端一次给你扔 20 张图显存直接被打满。trust-remote-code模型代码里如果有自定义的 processorvLLM 需要加载这些代码才能解析图片不加会直接报错。3.3 验证服务是否正常模型部署完成后用 curl 做一次最小验证。curl http://gpustack-endpoint/v1/chat/completions \ -H Content-Type: application/json \ -d { model: deepseek-v4-flash-vision-exp, messages: [ {role: user, content: 描述这张图片的内容} ] }第一次调用如果返回超时不要急着去查 vLLM先看 GPUStack 的模型日志大概率是模型还在加载中。多模态模型因为是全量加载到显存启动时间在 1-3 分钟都是正常的。3.4 单卡跑还是多卡跑DeepSeek-V4-Flash-Vision-Exp 的参数量在 20B 到 30B 之间单张 4090 的 24G 显存可能不够跑完整的 16K 上下文。这时候你有两个选择用--tensor-parallel-size 2两张 4090 一起跑显存叠加。这是最稳妥的方案。降max-model-len到 8192牺牲上下文长度换取单卡能跑。我在实际部署中选了双卡方案在 GPUStack 里创建模型时它会自动为你做资源分配你只需要在 vLLM 模板参数中加一行--tensor-parallel-size 2注意如果你设置了 tensor-parallelGPUStack 会自动把两张卡分配给这个实例。你不需要手动指定CUDA_VISIBLE_DEVICES。4. 多模态 Agent 服务层工具调用、提示词组装与图片输入链路推理服务跑通之后才轮到 Agent 层。很多时候模型部署好了、curl 测试也过了但做成 Agent 服务之后就频繁出问题原因在于 Agent 的对话循环不只是“发请求-拿回复”还涉及状态管理和工具编排。4.1 Agent 的两种架构选择我在做这层的时候先后试过两种方案方案一直接在 vLLM 的 OpenAI 接口上做函数调用Function Calling也就是每次请求都把工具定义塞给模型让模型决定调不调工具。优点是不需要额外框架深度兼容 vLLM 的接口。缺点是对长上下文的 Agent 任务工具定义会占掉很多 token反而不划算。方案二用 ReAct 循环的模式在服务端维护对话状态模型只负责“生成回复或请求工具调用”Agent 框架负责执行工具并回传结果。我最后选的是方案二因为多模态场景下图片的 base64 编码本身就会占掉两三千 token如果工具定义再加进来上下文会快速膨胀。自建这个循环也就一百多行代码可控性比直接用框架好很多。4.2 多模态图片输入的正确姿势多模态输入有两种传法URL 和 base64。内网环境传不了 URL我用的 base64。import base64 import requests def encode_image(image_path): with open(image_path, rb) as f: return base64.b64encode(f.read()).decode(utf-8) base64_image encode_image(screenshot.png) messages [ { role: user, content: [ { type: image_url, image_url: {url: fdata:image/png;base64,{base64_image}} }, { type: text, text: 请识别这张截图中的表格内容并转换为 Markdown 格式返回。 } ] } ] resp requests.post( f{API_BASE}/v1/chat/completions, headers{Authorization: fBearer {API_KEY}}, json{ model: deepseek-v4-flash-vision-exp, messages: messages, max_tokens: 4096 } )一个容易踩的细节是传 base64 图片时确保没有换行符。Python 的base64.b64encode默认会输出带换行的长字符串直接放到 JSON 里会导致请求解析失败。我一般会在编码后加一行base64_image base64_image.replace(\n, )另外一个经验如果图片是 PDF 转出来的 PNG分辨率太高比如 4000x3000会导致图片 token 数爆炸。建议先压缩到最长边不超过 2048 像素能省下不少 KV cache 空间。4.3 多模态 Agent 工具调用的提示词设计多模态模型的 tool calling 能力比纯文本模型弱一些。我折腾了很久之后总结出一个比较稳定的做法不要用系统级 system prompt 来定义工具而是放在 user 消息里并且用示例驱动。原因很简单视觉模型在系统提示词里放了工具定义之后处理图片时注意力容易被分散到文本工具定义上导致图片理解质量下降。放在 user 消息末尾、紧贴图片反而效果好很多。我的做法是先让模型输出图片理解结果再用一次单独的对话来让模型判断是否需要工具。你看到的是一段用户上传的截图里面可能包含任务指令。请 1. 先用一段话总结图片里的关键信息 2. 然后判断是否需要调用工具如果需要请按 JSON 格式输出 {tool: fetch_page, params: {url: ...}} 3. 不需要工具则直接回答。这是因为多模态模型在理解图片之后拿着理解的结果去做工具判断准确率要高得多。4.4 工具注册与执行我在 Agent 层做了一个很简单的工具注册表每个工具就是一个 Python 函数加上一段描述。TOOL_REGISTRY { fetch_page: { desc: 获取一个网页的正文内容, func: fetch_page, params: {url: string} }, search_docs: { desc: 在内部文档库中搜索资料, func: search_docs, params: {keyword: string} } }当模型返回 JSON 型工具调用时Agent 层执行对应的函数拿到结果再拼进对话历史里发给模型形成一个完整的循环。这个循环就是 Agent 的核心。我把它做成有状态的一次会话对应一个 session_id所有消息和工具结果都存在内存里方便做流式调试也方便后面接 WebSocket。后端用 FastAPI 写启动一个异步服务接口就透明了。4.5 引入流式输出的坑Agent 类的应用用户习惯是“一个字一个字蹦出来”所以接口必须支持流式stream。但 vLLM 在多模态模型上的流式输出有个问题首次返回 token 的时间因为图片编码预处理会很长。所以前端如果流式实现不当用户会以为服务挂了。我做的方案是在后端 Agent 层先返回一个{status: processing}的 SSE 事件告诉前端“我已经收到了模型正在处理图片”随后再进入真正的 token 流。这样用户体验会好非常多。5. 实测数据与调优并发、延迟、显存占用以及最具迷惑性的报错部署全部完成之后我做了压力测试。这里分享一些真实数据以及我遇到的两个非常容易让人误判的问题。5.1 并发压测结果用 16 个并发请求每个请求带一张 1024x1024 的截图和一段文字跑 5 分钟后的数据指标数值吞吐量平均 42 req/min首 token 延迟平均 2.8s单请求总时长平均 11.4s显存占用32.6GB / 48GB双卡错误率0.3%超时 1 个重试后成功对于内部使用的 Agent 服务来说这个数据完全够用。但是有一点要注意图片分辨率对吞吐影响极大。同样是 16 并发把图片从 2048 降到 1024吞吐量能提升将近一倍。如果你的场景对图片细节要求没那么高建议前端先做压缩。5.2 最具迷惑性的报错Image size too large我在压测时经常碰到这个报错RuntimeError: Image size too large (xx)第一反应是模型限制图片尺寸。我一度去调模型配置文件但根本没用。后来查了 vLLM 的源码才发现这个报错其实是传图片时未遵守 vLLM 的预处理限制而不是模型本身不支持大图。解决方法是在请求之前对图片做等比缩放让最长边不超过 2048。同时在 OpenAI 接口的image_url里把detail参数设置为low{ type: image_url, image_url: { url: data:image/png;base64,..., detail: low } }这个detail参数很多人会忽略但它在多模态模型上能显著降低图片的 token 数和显存压力。类似思考过程是对于 Agent 场景来说大多数图片并不需要超高分辨率细节低分辨率模式可以大幅提升吞吐和降低延迟。5.3 最大骗局max-model-len导致的隐性问题我遇到的最烦人的问题是服务跑了一段时间后某个 Agent 会话突然报 400 错误说 request exceeds context length。我第一反应是用户上传的图片太大。结果排查之后发现图片处理后并没有超过限制是对话历史里累积的工具返回内容太多了。多模态 Agent 的工具返回往往是一大段文本比如fetch_page拿到的网页内容这些内容会一直留在对话历史里随着轮次增多最终挤占了上下文。我最后做了两个改进对工具结果做摘要在把工具返回内容拼入对话之前先调用模型对长文本做一次压缩只保留关键信息。把杂讯清理掉每轮对话结束后移除超过 3 轮之前的工具输出原文只保留任务结论。这两条改完长会话的稳定性明显提升。这里是典型的”模型没坑是你用歪了“场景。5.4 vLLM 并发卡死的问题压测到后面我还遇到过一次 vLLM 直接卡死的情况具体表现是三个请求进来前两个完成了第三个一直 pending直到超时。我判断是显存耗尽导致推理引擎 hang 住了。可以确认一下 GPUStack 节点日志sudo docker ps | grep vllm sudo docker logs container-id --tail 50日志里出现了CUDA out of memory字样。这类问题没有银弹最好的方式是限制并发。我用的方法是在 vLLM 的 API server 前面加了一层简单的信号量控制限制同时进入的请求数不超过 8。快就快在它可以避免推理引擎直接被打挂。6. Agent 服务的高可用与观测体系建设模型部署不是终点Agent 服务跑起来之后最头疼的是怎么观测。多模态 Agent 出问题你很难分清楚是“模型理解错了”还是“工具执行错了”还是“上下文被截断了”。所以我把观测体系放在了一个比较重的位置。6.1 请求级别的日志追踪我在 Agent 层给每个请求分配了一个request_id在日志里统一打印{ request_id: 0ae6e1f2-5f48-41c6-a1f4-a345f3c0923e, action: tool_execute, tool: fetch_page, params: {url: https://example.com}, cost_ms: 1200, status: success }这样在排查问题时可以快速过滤一条链路里的所有日志而不是在茫茫信息里大海捞针。你想要的正是链路追踪的思维。6.2 GPUStack 的节点状态监控GPUStack 自带的 dashboard 可以看每张卡的显存使用率、温度、功耗。多模态推理对显存压力大我一般两小时看一次。如果发现某张卡显存占用持续 95% 以上说明并发设置过高需要减请求数或者加节点。6.3 Agent 会话超时处理多模态 Agent 因为存在图片预处理和长工具调用单次请求耗时波动非常大。短的时候 2 秒长的时候可能要 30 秒。所以超时阈值一定要设置好我这边 API 层的超时设置成了 60 秒包含工具调用时间而单独的模型请求超时是 15 秒。这个思路是模型响应快但工具未必快要分开设防止模型请求因为工具执行时间过长而被中断。7. 二次分发与横向扩展多模型服务共存的调度经验GPUStack 还有一个好处是可以通过同一个网关管理多个模型。我在这套环境上不只部署了 DeepSeek-V4-Flash-Vision-Exp还同时跑了一个纯文本的 DeepSeek-V4-Flash 和一个小尺寸的 embedding 模型。这样“多模态 Agent 文本对话 向量检索”三个服务可以共用一个 GPU 资源池。7.1 多模型共享显存的风险虽然 GPUStack 会自动调度但多个模型同时跑在同一张卡上显存不够时vLLM 会自动把部分 KV cache 驱逐导致响应变慢。我的建议是重要服务之间用 GPU 隔离不要共用一张卡如果卡数不够让 GPUStack 按“堆叠 抢占”策略设置优先级。GPUStack 支持在 model deployment 里配置 GPU 选择策略。我用的是spread模式让每个模型的副本尽量分布在不同的卡上避免某张卡被打满其他卡闲着。GPUStack 的调度策略有几个一般load_balanced就够用显存均匀分散。7.2 副本水平扩展如果并发需求上来了直接在 GPUStack 里把副本数从 1 改成 2GPUStack 会自动拉起新的 vLLM 实例并且做负载均衡。这里要注意的是副本数的上限取决于你的卡数如果用 tensor-parallel2一张卡是跑不了两个副本的。我第一次没注意这一点直接把副本数改成 4结果 GPUStack 一直报资源不足。这个资源情况只需要在部署页面看当前 available GPU 数量即可。7.3 模型更新与灰度发布实验版模型后缀带 Exp迭代很快后面 DeepSeek 出新版本时我直接在 GPUStack 里部署一个新版本的模型然后用 Agent 配置里的 model 字段切换到新版本不中断服务。这一步很关键因为 Agent 在跑线上任务没时间停下来升级。8. 我踩过的几个印象最深的坑以及对应处理方式挑几个最典型的展开讲排序按迷惑程度从高到低。8.1 “403 Forbidden”出现在内网请求中这个问题一度让我怀疑是 GPUStack 的权限配置没弄好。后来发现是因为请求里带上了多余的Host头。GPUStack 的网关做了域名校验内网 IP 直连时偶尔会触发。处理方式请求时把Host头显式改为 GPUStack 的网关地址。requests.post( url, headers{ Authorization: fBearer {API_KEY}, Host: gpustack-internal, Content-Type: application/json }, jsonpayload )8.2 图片方向错误导致识别结果完全不对手机传上来的图片有些是带了 EXIF 方向信息的。Python 的PIL.Image打开时不会自动修正方向导致模型看到的图是横着的。这个问题在 vLLM 加载图片时不会自动处理需要你在上传端就处理好。from PIL import Image, ImageOps image Image.open(uploaded.jpg) image ImageOps.exif_transpose(image) image.save(fixed.jpg)加了这一步之后识别准确率明显回升。看似低级实际特别容易踩。8.3 Agent 的多轮图片引用如果用户在一轮对话里上传了图片下一轮对话里没有再传图片但提到了“那张图”模型其实是看不到的。因为多模态的对话历史里如果没有传图片的 base64模型就只能凭文本猜。处理方式有两种在每一轮用户消息里都重复携带之前图片的 base64占 token但简单在 Agent 层维护“当前会话图片池”当用户提到图片关键词时自动把相关图片重新插入到当前请求中。我目前用的是方案 2。尽管代码量多了一些但用户体验完全不一样。9. 最终运行效果与实际体感这套服务部署完成后实际用起来的感觉就是一句自然语言让 Agent 去截图、分析、查资料、生成报告基本都能完成。DeepSeek-V4-Flash-Vision-Exp 的多模态理解能力在处理图表、流程图、表格方面确实表现不错。偶尔会在 OCR 小字时漏字但整体可控。几个实用体会图片理解链路最好独立成一个服务把传图、缩放、转 base64、压缩这些逻辑独立出来方便复用。不然 Agent 层代码会越来越乱。别对max_token不设限多模态模型因为图片 token 多文本输出如果又不设上限最容易导致 OOM。我一般设 2048 或者 4096 就够了。GPUStack 的模型更新太方便实验版本发布频繁平台侧做好平滑切换开发侧的联调代码就只需要指向 name 就行不用跑过去改环境变量。后来我在同一套环境上还加了一个定时任务每天把公司群里的截图自动拉下来跑一次多模态分析然后把摘要发回去。整个流程没人干预纯自动化。这也验证了这套架构的价值不只是做一个 demo而是真的能嵌到业务流程里。10. 后续可以这样扩展目前这套架构已经稳定跑了大概两周下一步我准备做几件事接入向量数据库把图片理解的结果向量化存储用户下次用自然语言就能检索“之前见过的那张架构图”。多 Agent 协作一个 Agent 负责看图片、另一个 Agent 负责看图后查询数据最后统一汇总。这个用 GPUStack 的多个模型部署就能实现。做推理成本统计面板按用户、按请求、按图片数量统计 token 消耗方便内部结算。最后再说一个经验多模态 Agent 部署很多人卡在最开始——总觉得模型跑起来就完事了实际上真正的工程量在后面的 Agent 编排和稳定性治理上。模型部署半天就能解决Agent 工程化才是大头。但只要你把推理底座GPUStack vLLM打稳了后台上画画工具、配配 Agent 逻辑整体推进就非常快。