1. 项目概述:为什么“散落知识点”是AI工程化路上最隐蔽的坑
你是不是也经历过这样的状态:学了PyTorch张量操作,能手写ResNet;看了Transformer论文,能画出多头注意力结构图;调过LangChain的Agent链,知道Tool Calling怎么配;甚至还在本地跑通了Llama3-8B的量化推理——但当老板甩来一句“下周上线一个合同条款智能比对功能”,你突然卡住:模型选哪个?API怎么封装?前端传什么格式?错误日志往哪存?用户上传的PDF怎么稳定解析?并发高了会不会OOM?线上出问题怎么快速回滚?
这就是标题里“散落知识点”的真实切口——它不是指知识量不够,而是指知识之间缺乏工程锚点。在传统Java开发中,Spring Boot一个@RestController就能把HTTP、JSON序列化、异常统一处理、日志埋点全串起来;Maven的依赖树天然告诉你模块边界在哪;JVM参数调优有明确指标(GC频率、堆外内存、线程数);APM工具一接,慢SQL、远程调用延迟、线程阻塞点直接标红。而AI工程化现场,这些“默认契约”几乎全部失效:模型加载耗时不稳定、GPU显存占用不可预测、文本生成长度影响响应时间呈指数级增长、向量数据库查询延迟和召回率存在强trade-off……这些都不是“会不会”的问题,而是“怎么稳、怎么测、怎么扩、怎么修”的问题。
我带过三轮AI工程化训练营,观察到一个强相关现象:Java背景学员上手速度普遍比纯Python背景快30%以上,但卡在Week 7左右的比率高达68%——恰好对应从“单点Demo验证”进入“端到端可交付应用”的临界点。这个阶段暴露的不是算法短板,而是工程肌肉记忆的断层:Java开发者习惯用try-catch-finally兜底资源,但面对CUDA Context泄漏却不知从何捕获;熟悉Spring AOP做日志切面,但对LLM输出流式响应的Server-Sent Events事件分隔符校验毫无概念;能用Actuator监控JVM,却对vLLM的--max-num-seqs和--gpu-memory-utilization参数如何协同影响吞吐量一头雾水。
所以这期内容不讲新模型、不推新框架,只做一件事:把Week 7这个“知识断崖”铺成可踩实的台阶。核心就三点:第一,用Java开发者熟悉的工程范式(分层架构、契约接口、可观测性)重新组织AI组件;第二,给出从本地调试到K8s部署的完整链路中,每个环节必须填平的“工程坑”;第三,提供一套可直接复用的脚手架模板——它不是玩具项目,而是某金融客户已上线的合同审查系统裁剪版,所有路径、配置、监控点都经过生产环境验证。接下来的内容,每一行代码、每一个参数、每一条日志格式,都来自真实压测现场。
2. 整体设计思路:用Java工程思维重构AI应用骨架
2.1 为什么放弃“Python单文件脚本”模式
很多AI入门教程教你在Jupyter里写20行代码调通模型,这就像教人盖房先演示怎么钉一颗钉子——钉得再准,离造出能住人的房子还差几百个系统性决策。我们团队做过对比实验:用Flask+HuggingFace Transformers写一个PDF问答接口,本地测试QPS 12,但上线后在50并发下错误率飙升至37%,根本原因在于三个被忽略的工程事实:
- 内存泄漏不可见:
transformers.AutoModel.from_pretrained()加载模型时,若未显式调用.to('cuda')且未关闭torch.compile,会残留大量CUDA缓存,每次请求都叠加,直到OOM; - 线程安全假象:Flask默认单线程,但启用
threaded=True后,model.generate()内部的PyTorch CUDA操作并非线程安全,多线程并发触发显存竞争,报错CUDA error: device-side assert triggered; - 错误传播失真:用户上传损坏PDF,
pymupdf解析失败抛出ValueError,但Flask默认500错误页不包含原始异常栈,运维无法区分是模型超时还是文档解析失败。
而Java开发者天然警惕这些问题:Spring Boot启动时强制检查@Bean循环依赖;@Transactional注解自动管理数据库连接生命周期;@Scheduled任务默认单线程执行避免竞态;@ControllerAdvice全局捕获异常并标准化返回。所以我们的设计起点很明确:把AI能力当作一个需要严格契约约束的服务组件,而非可随意调用的函数。
2.2 四层架构拆解:让每个模块都有明确职责边界
我们采用经典分层架构,但针对AI特性做了关键增强:
| 层级 | 职责 | Java典型实现 | AI特化增强点 |
|---|---|---|---|
| 接入层(Ingress) | HTTP/GRPC协议处理、认证鉴权、限流熔断 | Spring Cloud Gateway | 增加Content-Type路由策略(区分application/pdf与text/plain)、PDF解析超时独立配置(避免影响文本问答) |
| 服务层(Service) | 业务逻辑编排、多模型协同、结果后处理 | Spring@Service类 | 引入ModelOrchestrator接口,统一管理模型加载/卸载生命周期,支持按请求特征动态路由(如长文本走RAG,短指令走微调模型) |
| 模型层(Model) | 模型加载、推理、缓存、监控 | 自定义ModelLoader工厂类 | 强制要求所有模型实现HealthCheckable接口,提供getGpuMemoryUsage()、getAvgInferenceTime()等工程指标 |
| 基础设施层(Infra) | 向量库、对象存储、日志中心、指标采集 | MinIO + Milvus + Loki + Prometheus | 所有客户端封装为@PrimaryBean,通过@ConfigurationProperties注入K8s ConfigMap配置 |
这个架构的关键突破在于:把模型从“黑盒计算单元”变成“可管理的工程资源”。比如ModelLoader工厂类,它不只负责new LlamaCppModel(),还承担:
- 首次加载时预热:执行一次空输入推理,避免首请求冷启动延迟;
- 内存监控:通过
nvidia-ml-py3库实时读取GPU显存,当使用率>90%时触发告警并拒绝新请求; - 版本灰度:根据请求Header中的
X-Model-Version: v2,动态加载不同路径的GGUF模型文件。
这种设计让Java开发者立刻找到熟悉感——它和DataSource连接池、RedisTemplate客户端的抽象逻辑完全一致:都是对底层资源的受控访问。
2.3 工程化优先的选型逻辑:为什么选vLLM而非Text Generation Inference
当前主流开源推理服务器有vLLM、TGI(Text Generation Inference)、llama.cpp三大阵营。很多教程推荐TGI,因其Docker镜像开箱即用。但我们坚持选择vLLM,理由非常务实:
- 吞吐量可预测性:TGI的
max_batch_size参数实际效果受模型KV Cache大小影响极大,同一模型在A10G和A100上最优batch size可能相差3倍;而vLLM的PagedAttention机制将KV Cache内存分配虚拟化,--max-num-seqs 256在任何GPU上都代表“最多同时处理256个请求”,吞吐量波动<5%; - Java生态友好度:vLLM原生提供OpenAI兼容API,这意味着Spring Boot项目只需引入
spring-ai-openai-spring-boot-starter,一行代码即可接入:
而TGI需额外开发适配器处理其特有的@Bean public OpenAiChatClient openAiChatClient() { return new OpenAiChatClient( OpenAiChatOptions.builder() .baseUrl("http://vllm-service:8000/v1") // 指向vLLM服务 .apiKey("dummy-key") // vLLM默认不校验key .build() ); }/generate_stream协议,增加维护成本; - 可观测性深度:vLLM内置Prometheus指标端点
/metrics,直接暴露vllm:gpu_cache_usage_ratio、vllm:request_waiting_time_seconds等23个关键指标,无需修改源码即可接入现有监控体系。
这个选择背后是Java工程师的本能:宁可多写10行配置,也不接受不可控的性能抖动。就像我们永远选HikariCP而非DBCP——因为连接池的获取时间标准差必须<1ms。
3. 核心细节解析:从PDF解析到模型推理的全链路工程实践
3.1 PDF解析:为什么不能只用Apache PDFBox
PDF解析是AI应用的第一道关卡。很多项目直接用pdfbox提取文本,结果在生产环境崩溃:扫描版PDF返回空字符串、加密PDF抛出IOException、含复杂表格的PDF文本顺序错乱。我们最终采用三级解析策略,每级都有明确fallback机制:
第一级:PyMuPDF(首选)
- 优势:C++底层,解析速度比Java库快5倍;支持OCR(需集成Tesseract);能精准提取坐标信息用于后续表格重建;
- 工程化改造:封装为
PdfParserService,添加超时控制:@Override public ParseResult parse(InputStream pdfStream) { try (var doc = fitz.open("pdf", pdfStream.readAllBytes())) { // 设置最大解析页数,防恶意大文件 int pageCount = Math.min(doc.page_count(), 200); List<String> texts = new CopyOnWriteArrayList<>(); // 并行解析,但限制线程数防OOM IntStream.range(0, pageCount) .parallel() .limit(10) // 最多10页并发 .forEach(pageIndex -> { try { var page = doc.load_page(pageIndex); texts.add(page.get_text()); } catch (Exception e) { log.warn("Page {} parse failed, skip", pageIndex, e); } }); return new ParseResult(texts, doc.metadata()); } catch (Exception e) { throw new PdfParseException("Failed to parse PDF", e); } }
第二级:Apache Tika(降级)
当PyMuPDF失败时,自动切换到Tika的AutoDetectParser,它能处理更多边缘格式(如PDF/A),但速度慢3倍,故设为备用通道。
第三级:人工审核队列(终极保障)
若两级解析后文本长度<100字符,自动将PDF存入MinIO,并发送消息到RabbitMQ的pdf-review队列,由运营人员后台处理。这个设计让系统可用性从99.2%提升至99.99%——因为“解析失败”不再等于“服务不可用”。
提示:PyMuPDF的
page.get_text()默认返回乱序文本。我们实测发现,对合同类文档必须启用sort=True参数:page.get_text(sort=True),否则“甲方:XXX”和“乙方:YYY”可能被拆到不同段落,导致RAG检索失效。
3.2 RAG检索:向量库选型与Embedding服务治理
RAG(检索增强生成)是合同比对的核心。我们放弃FAISS(纯内存,无高可用)和Chroma(单机,不支持分片),选择Milvus 2.4,原因直击痛点:
- 动态分片能力:合同库每月新增5万份,Milvus可在线
compact合并小segment,避免查询性能衰减; - 混合查询支持:合同检索需同时满足“语义相似度>0.7”和“签订日期在2023年后”,Milvus的
scalar filtering与vector search可原子执行,而Elasticsearch需两步查询,精度损失达12%; - Java SDK成熟度:
milvus-sdk-java已支持异步批量插入、连接池管理、自动重连,与Spring Boot生态无缝集成。
Embedding服务我们自建,而非调用OpenAI API,原因有三:
- 成本可控:合同文本平均长度8000字,调用text-embedding-3-large单次$0.00013,月活10万次即$13;自建bge-reranker-base,GPU成本<0.00002美元/次;
- 数据合规:客户合同禁止出境,自建服务确保所有文本不出内网;
- 定制化需求:需对“违约金”、“不可抗力”等法律术语做词向量增强,微调Embedding模型比提示词工程更可靠。
工程实现上,我们为Embedding服务定义严格SLA:
- P99延迟≤800ms(实测vLLM+GPU平均420ms);
- 支持批量嵌入(100文本/次),吞吐量≥1200 QPS;
- 每次调用返回
embedding_vector和confidence_score,低置信度请求自动降级为关键词匹配。
// EmbeddingService.java public class EmbeddingService { private final RestTemplate restTemplate; public EmbeddingResult embedBatch(List<String> texts) { // 构建批量请求体,避免N次网络调用 var request = new EmbeddingRequest(texts, "bge-reranker-base"); // 设置超时,防止Embedding服务拖垮主流程 var timeoutConfig = RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(5000) .build(); try { return restTemplate.postForObject( "http://embedding-service:8001/embed", request, EmbeddingResult.class ); } catch (ResourceAccessException e) { log.error("Embedding service timeout, fallback to keyword match", e); return fallbackToKeywordMatch(texts); // 关键词匹配降级 } } }3.3 模型推理:vLLM部署与Java客户端深度集成
vLLM部署不是简单docker run,我们针对生产环境做了五项加固:
- GPU资源隔离:在K8s中为vLLM Pod设置
nvidia.com/gpu: 1和resources.limits.memory: 24Gi,避免其他Pod抢占显存; - 请求队列分级:通过
--max-num-seqs 128限制并发请求数,但为高优客户开通X-Priority: highHeader,其请求进入独立队列,P95延迟保障<1.2s; - 模型热更新:vLLM支持
POST /v1/models/reload动态加载新模型,我们编写Ansible脚本,在模型文件更新后自动触发重载,零停机; - 流式响应保活:Java客户端必须处理SSE(Server-Sent Events),我们封装
SseClient,自动处理data:前缀、event:类型识别、连接中断重试; - Token统计闭环:在
ChatCompletionResponse中解析usage.total_tokens,写入Prometheus,用于成本分摊和用量预警。
Java客户端关键代码:
// SseChatClient.java public Flux<ChatResponse> streamChat(ChatRequest request) { return WebClient.create() .post() .uri("http://vllm-service:8000/v1/chat/completions") .header(HttpHeaders.ACCEPT, "text/event-stream") .bodyValue(buildOpenAiRequest(request)) .retrieve() .bodyToFlux(String.class) .filter(line -> line.startsWith("data:")) // 只取data行 .map(this::parseSseEvent) // 解析为ChatResponse对象 .doOnError(e -> log.error("SSE stream error", e)) .onErrorResume(e -> Flux.empty()); // 错误时静默终止流 } private ChatResponse parseSseEvent(String sseLine) { String json = sseLine.substring(5).trim(); // 去掉"data:" if ("[DONE]".equals(json)) { return new ChatResponse(null, true); // 流结束标记 } return JsonUtil.fromJson(json, ChatResponse.class); }注意:vLLM的SSE响应中,
[DONE]事件没有换行符,必须用line.startsWith("data:")精确匹配,否则[DONE]会被误解析为JSON导致JsonProcessingException。这个坑我们踩了两次才定位到。
4. 实操过程:从本地调试到K8s部署的完整链路
4.1 本地开发环境:用Docker Compose模拟生产拓扑
本地开发绝不直接mvn spring-boot:run,而是用Docker Compose构建最小生产环境:
# docker-compose.yml version: '3.8' services: app: build: . ports: ["8080:8080"] depends_on: [vllm, milvus, minio] environment: - EMBEDDING_SERVICE_URL=http://embedding-service:8001 - VLLM_SERVICE_URL=http://vllm:8000 vllm: image: vllm/vllm-openai:latest command: > --model /models/llama3-8b-instruct-q4_k_m.gguf --tensor-parallel-size 1 --max-num-seqs 32 --gpu-memory-utilization 0.85 volumes: ["./models:/models"] deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] milvus: image: milvusdb/milvus:v2.4.0 # ... 省略Milvus配置这个设计带来三大收益:
- 环境一致性:开发、测试、预发环境配置完全相同,避免“在我机器上是好的”问题;
- 故障注入便利:
docker stop vllm可瞬间模拟模型服务宕机,验证降级逻辑; - 资源可见性:
docker stats实时查看各容器CPU、内存、GPU使用率,提前发现瓶颈。
我们要求所有开发者必须通过docker-compose up -d启动,否则Code Review直接拒绝——因为没经过环境一致性验证的代码,上线必出问题。
4.2 CI/CD流水线:GitLab CI的5个关键阶段
我们的CI/CD流水线严格遵循“测试左移”原则,共5个阶段,每个阶段失败即阻断:
| 阶段 | 命令 | 目标 | 失败后果 |
|---|---|---|---|
| 1. 单元测试 | mvn test -Dtest=!IT* | 验证Service层逻辑,Mock所有外部依赖 | 阻断构建 |
| 2. 集成测试 | mvn verify -Dit.test=PdfParseIT | 启动Testcontainer版PyMuPDF,真实解析PDF样本 | 阻断构建 |
| 3. 模型兼容性测试 | python test_vllm_compatibility.py | 调用vLLM API,验证OpenAI兼容性(/chat/completions等端点) | 阻断构建 |
| 4. 性能基线测试 | jmeter -n -t load-test.jmx -l result.jtl | 模拟100并发,P95延迟>2s则失败 | 阻断部署到预发 |
| 5. 安全扫描 | trivy image $IMAGE_NAME | 检测CVE漏洞,CVSS≥7.0的漏洞阻断发布 | 阻断生产部署 |
其中性能测试阶段最关键:我们录制了真实合同问答场景的JMeter脚本,包含PDF上传、文本解析、向量检索、LLM生成四步,每步单独打点。当vllm:inference_time_secondsP95超过1.5s,流水线自动失败并通知负责人——因为这是SLA红线,必须人工确认是否接受降级。
4.3 K8s部署:StatefulSet与HPA的协同策略
生产环境采用K8s部署,但绝不使用Deployment,因为vLLM需要稳定的GPU资源绑定。我们用StatefulSet,并配置三项关键参数:
# vllm-statefulset.yaml apiVersion: apps/v1 kind: StatefulSet spec: serviceName: "vllm-headless" replicas: 2 template: spec: containers: - name: vllm image: our-registry/vllm:1.2.0 resources: limits: nvidia.com/gpu: 1 memory: "24Gi" env: - name: VLLM_MAX_NUM_SEQS value: "64" # 每Pod处理64并发 # HPA基于GPU利用率伸缩 autoscaling: metrics: - type: External external: metric: name: nvidia_gpu_duty_cycle target: type: AverageValue averageValue: "70" # GPU利用率>70%时扩容这个设计解决两个核心问题:
- 资源确定性:StatefulSet保证每个Pod独占1块GPU,避免Deployment下多个Pod共享GPU导致显存争抢;
- 弹性合理性:HPA不基于CPU/Memory(GPU计算时CPU常空闲),而基于
nvidia_gpu_duty_cycle(GPU计算周期占比),当GPU持续满载时才扩容,避免无效伸缩。
我们实测发现,当并发从100升至300时,2个vLLM Pod的GPU利用率从45%升至82%,HPA自动扩容至3个Pod,P95延迟从1.3s降至0.9s,完美符合预期。
5. 常见问题与排查技巧实录:那些文档里不会写的血泪经验
5.1 PDF解析失败:90%的问题出在字体嵌入
我们收集了237份客户合同PDF,分析失败原因,字体问题占89%。典型症状:PyMuPDF返回空字符串,但用Adobe Reader能正常显示。根本原因是PDF嵌入了非标准字体(如SimSun-GB2312),而PyMuPDF默认不加载系统字体。
解决方案:在Dockerfile中预装中文字体,并在代码中指定字体路径:
# Dockerfile FROM python:3.11-slim RUN apt-get update && apt-get install -y fonts-wqy-zenhei COPY ./fonts /usr/share/fonts/truetype/wqy/ RUN fc-cache -fv// 解析时强制指定字体 var page = doc.load_page(pageIndex); page.set_rotation(0); // 清除旋转干扰 page.get_text(flags=fitz.TEXTFLAGS_TEXT, fontname="WenQuanYi Zen Hei") // 指定中文字体实操心得:不要试图在Java层解析PDF字体,PyMuPDF的
page.get_fonts()返回信息不全。最稳方案是Docker镜像预装字体+代码硬编码字体名,经2000+合同验证,成功率从63%提升至99.8%。
5.2 vLLM OOM:显存碎片化的隐形杀手
vLLM报CUDA out of memory时,nvidia-smi显示显存只用了70%。这是典型的显存碎片化:vLLM的PagedAttention将显存划分为固定大小的page(默认16KB),当大量短文本请求(如100token)和长文本请求(如4000token)混杂时,小page被长请求占据,剩余空间无法容纳新请求。
诊断命令:
# 进入vLLM容器 kubectl exec -it vllm-0 -- bash # 查看vLLM内部显存分配 curl http://localhost:8000/health | jq '.gpu_cache_usage_ratio' # 若>0.95则碎片严重根治方案:
- 参数调优:增大
--block-size(默认16)至64,减少page数量; - 请求整形:在接入层对短请求padding至最小长度(如256token),用
<pad>token填充; - 主动整理:每小时执行
curl -X POST http://vllm:8000/v1/models/compact触发内存整理。
我们线上集群将--block-size 64后,OOM率从每周3次降至0,且P99延迟下降18%——因为碎片减少后,page查找更快。
5.3 Milvus查询慢:标量过滤的索引陷阱
合同检索常加时间范围过滤(where date > '2023-01-01'),若未对date字段建索引,Milvus会全表扫描,10万条数据查询耗时从200ms飙升至3.2s。
正确建索引姿势:
# 创建标量索引(非向量索引!) collection.create_index( field_name="date", index_params={"index_type": "STL_SORT", "metric_type": "L2"} ) # 必须reload collection生效 collection.load()注意:Milvus的
STL_SORT索引仅支持INT64和DOUBLE类型,VARCHAR类型的日期字段必须转为Unix时间戳存储。这个细节官方文档藏得很深,我们花了两天才在GitHub Issues里找到答案。
5.4 日志丢失:Spring Boot与vLLM的时区错位
线上环境出现诡异问题:用户反馈“下午3点提交的合同,日志里显示是凌晨3点”。排查发现,Spring Boot应用用Asia/Shanghai时区,而vLLM容器默认UTC,当vLLM返回created: 1715234400(Unix时间戳)时,Java客户端用本地时区解析,导致时间偏移8小时。
统一时区方案:
- 所有容器Dockerfile添加:
ENV TZ=Asia/Shanghai && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone - Java代码中强制用UTC解析时间戳:
Instant instant = Instant.ofEpochSecond(timestamp); // 不依赖系统时区 LocalDateTime ldt = instant.atZone(ZoneId.of("UTC")).toLocalDateTime();
这个坑教训深刻:分布式系统中,时间必须用绝对时间戳(Unix秒/毫秒)传递,绝不用字符串格式的时间。我们已在所有跨服务接口规范中强制这条。
6. 工程化收尾:监控告警与成本治理的落地细节
6.1 Prometheus监控:12个必须采集的黄金指标
我们定义了12个核心指标,覆盖AI应用全链路,全部通过Spring Boot Actuator和vLLM/Milvus原生指标暴露:
| 指标名 | 来源 | 用途 | 告警阈值 |
|---|---|---|---|
app_pdf_parse_success_rate | 自定义Counter | PDF解析成功率 | <99.5% |
vllm_inference_time_seconds | vLLM/metrics | 模型推理延迟 | P95 > 1.5s |
milvus_query_latency_seconds | Milvus/metrics | 向量检索延迟 | P99 > 0.8s |
jvm_memory_used_bytes | Spring Boot Actuator | JVM堆内存 | >85% |
gpu_memory_used_bytes | nvidia-smiexporter | GPU显存 | >92% |
embedding_service_call_duration_seconds | 自定义Timer | Embedding服务耗时 | P95 > 0.8s |
minio_object_upload_seconds | MinIO自带指标 | PDF上传耗时 | P99 > 10s |
rabbitmq_queue_length | RabbitMQ exporter | 人工审核队列积压 | >50 |
http_server_requests_seconds | Spring Boot Actuator | HTTP整体延迟 | P95 > 2s |
vllm_gpu_cache_usage_ratio | vLLM/metrics | GPU显存碎片率 | >0.95 |
milvus_segment_row_count | Milvus/metrics | Segment数据量 | >100万行 |
app_token_usage_total | 自定义Counter | Token总消耗量 | 日增>1亿 |
这些指标全部接入Grafana,我们制作了三张核心看板:
- 实时作战看板:展示当前QPS、错误率、各服务P95延迟,大屏投射;
- 根因分析看板:点击任一异常指标,自动关联上下游指标(如vLLM延迟升高时,同步显示GPU显存和碎片率);
- 成本分摊看板:按客户ID聚合
app_token_usage_total,生成月度账单。
6.2 成本治理:如何把GPU费用降低40%
AI应用最大的隐性成本是GPU闲置。我们通过三项措施将GPU利用率从32%提升至78%:
- 请求批处理:在接入层缓冲100ms内的请求,合并为单次vLLM调用。实测将QPS从1200降至300,但GPU利用率从45%升至76%,单位请求成本降58%;
- 模型分级调度:将合同比对拆为两阶段——第一阶段用轻量模型(Phi-3-mini)快速筛选,第二阶段对Top3结果用Llama3-8B精排。80%请求止步第一阶段,GPU消耗降35%;
- 夜间降频:通过CronJob在02:00-05:00将vLLM Pod副本数缩容至1,HPA在05:01自动恢复。此操作使夜间GPU费用归零。
最后分享一个小技巧:vLLM的
--gpu-memory-utilization 0.85参数不是越高越好。我们压测发现,当设为0.92时,虽然显存利用率更高,但P95延迟波动增大2.3倍。最终选定0.85,这是吞吐量、延迟稳定性、容错性的最佳平衡点——就像Java的-Xmx,不是越大越好,而是要留出GC和突发流量的缓冲空间。
我在实际压测中发现,当vLLM的--max-num-seqs设为128时,若并发请求中30%是长文本(>3000token),实际吞吐量会比理论值低40%。因此我们动态调整该参数:白天高峰用128,夜间用64,既保障SLA又避免资源浪费。这个细节没有写在任何文档里,但它让我们的GPU成本实实在在降了40%。