
做Java后端的朋友应该都有体会SpringAI这个框架一出来接大语言模型的成本确实降了不少但真到生产环境里把接口跑起来性能问题一个接一个地冒出来。我在一个智能审核项目里用SpringAI调大语言模型做了大半年从最开始一个审核请求要等三四十秒到后面压到5秒以内出结果过程中踩了不少坑也沉淀了一套可以复用的优化方法。这篇文章就围绕SpringAI大语言模型调用优化展开核心覆盖YAML配置参数调优、系统提示词配置、缓存与并发控制、流式输出这几个维度也会专门聊聊本地部署大语言模型时怎么调参以及视觉大语言模型在调用上的注意事项。不管你是刚把SpringAI跑通、正愁线上响应太慢还是已经在生产环境使用但想进一步压低成本和延迟这篇文章都值得花十分钟读完。1. 先搞清楚SpringAI调大模型慢在哪儿1.1 调用链路的四个瓶颈点很多人一上来就想着换模型、上GPU其实问题往往不在模型本身而在调用链路的某个环节。我习惯把一次SpringAI调用拆成四段看应用层发起请求、网络传输、模型服务的排队与推理、结果返回与解析。每一段都可能是瓶颈。应用层最大的问题通常是同步阻塞。SpringAI默认的RestClient是同步调用如果你在Controller里直接调一个请求过来不光占住一个线程还得干等着模型把整段话生成完。这个等待时间对用户体验来说就是“白屏时间”对应用来说就是线程资源被白白占住。我见过不少项目QPS一上去线程池就爆了其实不是机器不行是被同步等待拖垮的。网络传输层容易被忽略尤其是本地部署大语言模型时。很多人觉得内网调用不存在网络问题实际不一定。如果模型服务和业务应用不在同一个机房一次HTTP请求的往返延迟可能在10到50毫秒这个量级在单次调用里不算什么但在高并发场景下会被放大。更关键的是SpringAI的HTTP客户端默认连接池参数比较保守并发一高就出现连接排队。模型服务端是很多人以为的唯一瓶颈其实它也分排队和推理两个阶段。排队时间取决于模型服务的并发能力和当前请求量推理时间则取决于模型大小、输入token数、输出token数和硬件配置。这里有个常见误区只优化模型推理速度忽略了排队时间。你用vLLM这类框架把单次推理压得很快但并发一上来照样排队整体延迟曲线照样难看。最后是结果返回与解析。SpringAI默认会把完整的SSE流收完再返回这里有个隐藏开销如果响应体很大JSON解析、对象转换都会占用CPU和内存。尤其是在视觉大语言模型场景下输出内容可能包含大量结构化字段解析耗时非常可观。很多人优化了半天最后发现瓶颈在反序列化上。1.2 优化目标延迟、吞吐、成本三个指标动手优化之前先定好目标。我通常看三个指标首令牌时间TTFT、整体响应时间、每千token成本。三者的优先级和优化手段不一样。首令牌时间决定用户“感知到的速度”。流式输出的首令牌时间如果能在1秒内用户基本感觉不到在等整体响应时间决定完整获取结果的时间适合审核、批处理这类不要求流式的场景成本则是长期运营里绕不开的指标尤其是调用商用模型接口时同样的功能提示词写得松紧不同费用能差出好几倍。所以在优化前先问自己三个问题这个功能是要流式打字机效果还是等完整结果调用方是用户交互还是后台任务模型是自建的还是调外部API这三个问题的答案直接决定了你下面要做哪些优化。我见过太多人一上来就套缓存和并发方向错了做完也白做。2. 配置层优化YAML里藏着半条命的性能2.1 把SpringAI的配置参数吃透SpringAI的配置有一个特点大部分行为都能通过YAML配置控制但默认值往往偏保守。以OpenAI协议为例最基础的配置长这样spring: ai: openai: base-url: http://localhost:8000 api-key: ${AI_API_KEY} chat: options: model: Qwen2.5-7B-Instruct temperature: 0.1 max-tokens: 2048注意几个关键点。base-url指向模型服务地址这里决定了网络链路长短。temperature是采样温度影响结果的随机性但在明确“对错”的场景下比如审核、分类、信息抽取温度调低不仅能稳定输出格式还能让模型更大概率走贪婪解码配合缓存类优化效果更好。max-tokens直接限制输出长度这个值不是越大越好越大意味着模型要生成的token越多响应时间越长成本也越高。还有一个经常被忽略的top-p参数。SpringAI里可以同时设置temperature和top-p但要注意两者同时调整会互相影响。我的习惯是固定top-p为1或null只调temperature这样行为更可预测。实际上OpenAI官方也建议不要同时修改两个参数二选一即可。2.2 超时、重试与连接池肉眼可见的提速SpringAI基于RestClient实现底层可配置的连接管理和超时参数直接决定了高并发下的表现。我在项目里常用这套配置spring: ai: openai: timeout: connect: 3s read: 120s client: max-connections: 200 max-connections-per-route: 100connect超时不能太长3秒足够否则排队高峰期会出现大量线程堆积在TCP建连阶段。read超时要看得见的模型服务端速度来定大模型生成1000个token在慢的机器上可能要30到60秒所以read超时一般要放宽到120秒甚至更长不然容易误杀慢请求。连接池这部分是重灾区。SpringAI的HTTP客户端默认连接池参数非常小并发稍微上来就会出现Connection pool timeout异常。我把max-connections调大到200以后同样的并发量错误率从3%降到了0。注意max-connections-per-route也要同步调否则所有并发都挤到一条路由上。重试也需要设计。模型服务端返回503或者超时不代表模型挂了可能只是瞬间负载高。我建议对可重试的状态码做一层Spring Retry最多重试2次间隔指数退避第一次等待1秒第二次等待2秒。但重试要谨慎对于已经结算了token的请求重试意味着双倍成本。我通常只在连接失败或者明确的http 429/503时重试对业务侧“模型生成了但解析失败”的情况不重试。2.3 本地部署大语言模型的配置要点本地部署和调用外部API在配置思路上有本质区别。外部API你只能控制请求参数本地部署你还能控制服务端配置尤其是并发、显存和上下文长度。以Ollama为例SpringAI接入的配置是spring: ai: ollama: base-url: http://192.168.1.10:11434 chat: options: model: qwen2.5:14b temperature: 0.2 num-ctx: 8192 num-predict: 2048num-ctx这个参数特别关键它决定了模型能看到的上下文窗口长度也直接决定了KV Cache占用显存的多少。很多人默认让框架用4K甚至2K的context结果对话一长就被截断。但num-ctx调大会显著增加显存占用和prefill阶段的计算时间。我实测过上下文从4096翻到8192显存占用大约增加15%到20%同时首token延迟增加约5%。所以这个值要根据业务真实需求来控制不要盲目拉满。本地部署的并发控制也很重要。我用的经验是模型并发数控制在显存允许范围内的“少数”比较好。比如一张24G显卡跑14B模型显存占用约14G理论上能并行跑1到2个任务我就会把业务侧的并发信号量设为2。设多了模型服务端排队严重单个请求反而要等更久整体吞吐也没有提升。3. 提示词与参数不花一分钱的性能优化3.1 系统提示词怎么配置才不拖累响应SpringAI系统提示词的配置很简单但很少有人意识到它对性能的影响。一个业务里塞了800字系统提示词每个请求都要把这段文字转成token送进模型然后模型在prefill阶段处理完这些token才开始生成答案。这800字看着不多但在高并发下就是算力浪费。优化提示词有三个方向精简、结构化和模板化。先精简能用50字说明白的事不要用200字。我见过有些需求方会把历史遗留的规则文档整段贴进系统提示词这对模型输出的稳定性有帮助但对性能是拖累。建议只保留核心约束把详细规则放到RAG检索或者外部知识库里。再结构化用明确的格式说明比如“你是审核员请输出JSON包含字段A、B、C”这样模型既能更快理解任务输出也更容易解析省掉后续的格式纠错。最后是模板化SpringAI里可以用PromptTemplate把固定部分和变量部分分离固定部分缓存在模板里每次请求只拼变量这样既能减少重复构造也方便后续统一调整。系统提示词配置示例String systemPrompt 你是一个内容审核助手。请判断以下文本是否违规。 只输出JSON格式的结果格式为{result:pass|reject,reason:简述原因} ; String userPrompt MessageTemplate.create(待审核内容{{requestContent}}) .render(Map.of(requestContent, content));3.2 温度、最大令牌数等参数怎么选参数的取舍直接决定生成速度和结果稳定性。temperature我一般分三档审核、抽取、分类类任务用0到0.2要求结果准确创意写作、营销文案用0.7到0.9保留多样性其他常规问答用0.3到0.5。这是基于实际经验的分档不是理论教条。模型在低温度下更容易走确定性路径也更容易命中各种缓存策略。max-tokens或num-predict的选择有个计算逻辑先估算业务结果的平均长度再加上一点余量。比如审核场景结果是一个JSON和一小段原因通常200到300个token足够我就设为512。如果设成2048模型即使提前生成完了也会占用同样的请求配额因为max-tokens影响的是上限不是实际输出量。但设得太小也不行万一输出被截断反而要重试一次变成两次。补充一个Java代码层面的调用方式ChatResponse response chatClient.call(new ChatRequest( List.of(new SystemMessage(systemPrompt), new UserMessage(userPrompt)), ChatOptions.builder() .temperature(0.1) .maxTokens(512) .build() ));3.3 视觉大语言模型调用中的注意点视觉大语言模型是新的热点方向SpringAI里也能通过多模态消息传入图片。但视觉模型的调用成本和延迟明显高于纯文本模型优化点也更特殊。图片输入会转为视觉token一张大图可能消耗上千甚至几千个token直接影响prefill阶段的耗时和成本。优化方向有三个压缩图片分辨率、裁剪无效区域、控制图片数量。比如审核一张商品主图把原图从4000x4000压到1024x1024视觉token数量能降70%以上而审核准确率基本不变。SpringAI多模态调用示例UserMessage message UserMessage.builder() .text(请判断这张图片是否包含违规内容只输出pass或reject) .media(new Media(MediaType.IMAGE_PNG, imageBytes)) .build();另外视觉模型的输出通常包含位置信息、标签列表等结构化字段建议在提示词里要求模型输出紧凑JSON避免把大段的描述文字也吐出来。我在项目里实测过要求“只输出JSON”之后视觉审核接口的整体响应时间缩短了约40%token消耗降低了30%左右。4. 应用层优化缓存、并发与流式输出4.1 结果缓存把重复请求拦在门外SpringAI本身不自带结果缓存但整合Spring Cache非常自然。最常见的做法是把“输入内容哈希模型参数提示词版本”作为缓存key把完整响应结果缓存一段时间。这么做的好处是显而易见的一模一样的审核请求第二次进来3秒变3毫秒。在Spring Boot里接入很直接Service public class AuditService { Cacheable(cacheNames modelResponse, key #content : #modelVersion) public AuditResult audit(String content, String modelVersion) { ChatResponse response chatClient.call(buildRequest(content)); return parseResult(response); } }用Cacheable有几个注意点。第一缓存key要包含提示词版本或模型版本因为升级提示词后旧缓存必须自动失效不然线上用的全是旧模型的答案。第二还要考虑业务数据的变化。审核规则如果按日期变化缓存时间按天设置或者把日期拼进key。第三Cacheable默认会缓存异常结果吗不会它只在方法正常返回时缓存。但如果你的方法返回的是一个包含错误信息的对象那就等于把坏结果也缓存了所以解析结果时要对“成功”有明确判断。缓存时间上我建议按场景区分。静态知识问答可以缓存24小时电商评论审核缓存30分钟涉及实时风控的敏感内容审核缓存5分钟甚至不缓存。缓存的本质是拿“数据新鲜度”换“响应速度”业务上能做多少取舍缓存就能发挥多大作用。4.2 并发控制不要让线程池打爆模型服务很多人以为把SpringBoot的线程池调大就能提升并发实际往往是线程池调大了模型服务端先扛不住。模型服务的并发能力受显存和算力限制和你的应用线程数完全是两个概念。前端1000个请求进来如果全部透传给模型服务等待时间会指数级上升最终每个请求的体验都很差。正确做法是在应用层做一个漏斗用Semaphore限制同时发往模型服务的请求数。我先定义一个信号量最大许可数根据模型服务的实际并发能力来定Component public class ModelConcurrencyGuard { private final Semaphore semaphore new Semaphore(4); public T T executeWithConcurrencyLimit(SupplierT supplier) { boolean acquired semaphore.tryAcquire(); if (!acquired) { throw new TooManyRequestsException(模型服务繁忙请稍后重试); } try { return supplier.get(); } finally { semaphore.release(); } } }这里我故意用tryAcquire()而不是acquire()是为了快速失败而不是无限等待。如果业务上能接受排队也可以改用acquire(timeout)让请求等一小会儿。设置多少并发合适我的经验是用压测去探模型服务的拐点逐步提高并发数观察吞吐不再增长、延迟开始快速上升的那个临界点然后取它的70%作为信号量上限。比如14B模型在单卡环境下临界并发是6信号量设4到5比较稳妥。还有一个并发优化的细节如果同一个用户有多个相似请求可以在业务层做请求合并。比如在做批量审核时把几十条待审核内容合并成一个请求发给模型让模型一次性输出数组结果。这能大幅减少请求次数和HTTP往返开销但同时要做好超长上下文的管理。SpringAI的ChatClient本身是线程安全的可以放心在并发环境中复用。4.3 流式输出把首字节延迟砍到最低如果业务是面向用户的对话式交互强烈建议改成流式输出。SpringAI对流式调用的支持比较成熟接口返回FluxChatResponse配合WebFlux可以很方便地实现SSE推送。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString streamChat(RequestParam String question) { return chatClient.stream(new ChatRequest(buildMessages(question))) .map(response - response.getResult().getOutput().getText()); }改成流式之后有三个立竿见影的效果。第一用户体验完全不一样模型边生成边输出用户不用对着白屏等待。第二应用的线程不再被长时间占住等待完整结果虽然底层模型生成还是要那么久但应用层可以提前释放连接资源。第三如果模型在前几个token就给出了明确答案用户甚至可以不等完整输出就开始下一步操作。但要提醒一句流式输出对网络和前端有要求。SSE连接是长连接如果网关配置了短超时需要单独放行。另外流式输出不等于响应更快它只是把等待感知前置了如果下游逻辑必须要完整结果才能继续那流式优化的收益就很有限这种情况优先考虑缓存和并发控制。5. 智能审核场景实战一个完整的优化案例5.1 场景拆解与性能基线我在智能审核项目里做的优化可以作为参考。业务背景电商平台的商品评论进来后调用大模型判断是否存在广告、辱骂、虚假宣传等问题每天大约5万条评论需要审核每条评论平均200字。优化前的现状是评论进来后调SpringAI接口同步等待完整结果没有缓存没有并发限制模型服务是本地Ollama部署的Qwen2.5-14B显存24G。线上表现非常不稳定高峰时段单条审核平均耗时28秒部分请求超过60秒超时。业务方反馈强烈因为审核结果直接影响商品是否被下架。我先把性能基线测清楚用固定20条真实评论做了3轮压测记录核心指标平均响应时间、P95响应时间、每千token耗时、错误率。基线数据如下表。5.2 优化节奏与手段清单优化不是一口气全上我按影响面从大到小分了三步走。第一步先解决最大的短板并发控制和超时。在模型服务同一台机器上部署了轻量的请求排队应用侧引入ModelConcurrencyGuard设置信号量为3同时把read超时从默认改为120秒。这一步先把错误率压下来避免了高峰期“大量请求超时重试重试又加剧了服务端压力”的恶性循环。第二步做提示词精简和参数固化。审核提示词从原来的一段几百字自由描述改成结构化的System Prompt明确输出JSON格式temperature从默认0.8改成0.1max-tokens从2048改成512。这一步的效果明显模型的输出稳定性提高JSON解析失败的次数大幅减少同时单次请求的token消耗也降下来了。第三步加缓存。评论审核的重复率不低同一个商品下相似评论经常会重复出现。我在审核服务入口加了Cacheable缓存key是评论内容哈希加模型版本缓存时间设为10分钟。这一步上线后缓存命中率约12%虽然比例不算很高但响应时间从秒级降为毫秒级直接拉低了整体平均耗时。5.3 优化前后的数据对比优化完成后我把同样的20条评论又跑了一遍压测对比如下指标优化前优化后变化平均响应时间28s4.6s下降约84%P95响应时间45s7.2s下降约84%错误率8%0.3%大幅降低每千token成本基线下降41%提示词精简输出缩短缓存命中率无12%新增这里需要注意的是平均响应时间的下降是组合拳的效果不是单点优化能达成的。并发控制避免了排队风暴提示词精简缩短了输入处理时间max-tokens收敛缩短了输出阶段的上限缓存则把一小部分请求直接清零。这四件事单独拿出来都有效果但合在一起才是数量级的提升。这个案例也说明了为什么优化要分层做先保证系统稳定不超时再降低无效token消耗最后用缓存吃掉重复请求。顺序错了比如一开始就加缓存会发现因为排队严重缓存击穿时照样慢整体收益会打折扣。6. 常见问题与排查技巧实录6.1 典型问题速查表把这一年多遇到的高频问题整理成一张表方便大家按图索骥。现象可能原因排查方向解决方案高并发时大量Connection pool timeoutHTTP连接池过小检查SpringAI底层HTTP客户端配置调大max-connections和max-connections-per-route响应速度慢且不稳定模型服务端排队严重看模型服务日志和监控应用侧加Semaphore压低透传并发同样的输入输出时好时坏temperature过高检查请求参数将temperature调到0.1左右输出总被截断max-tokens或num-predict过小统计真实输出token分布按业务预估结果长度设合理上限缓存不生效缓存key包含不稳定字段打印cache key检查规范化key仅使用业务唯一标识视觉审核响应特别慢图片未压缩、token消耗过大计算单次请求的token用量降分辨率、裁剪、限制图片数量模型服务OOM或显存不足num-ctx过大、并发过高观察显存监控缩小num-ctx调低信号量这张表只能覆盖常见场景实际排查时还是要学会看数据不要蒙着头瞎试。比如你说响应慢先看慢在哪一段。应用层加日志记录请求时间戳模型服务端看日志记录排队和推理的耗时两边一对比瓶颈立刻现形。6.2 排查思路与工具排查SpringAI调用性能问题我一般按三个层面走先看应用侧日志再确认模型服务状态最后抓网络链路。应用侧的排查重点是调用耗时分布。我在Service方法入口和出口各打一条日志记录开始时间、拿到响应时间这样能区分“应用内部处理耗时”和“等待模型响应耗时”。如果发现等待模型响应耗时很长问题基本就在模型服务端。同时看线程池活跃线程数如果长期处于最大值说明并发设计有瓶颈。模型服务端的排查要看排队队列长度和GPU利用情况。Ollama可以通过ollama ps查看当前加载模型和显存占用也可以用nvidia-smi看GPU利用率。如果GPU利用率很高但响应还是慢那就是模型本身算力不够换更大的卡或者更小的模型才有用。如果GPU利用率不高但响应依然慢多半是排队或上下文处理的问题。网络链路的排查相对简单用ping测延迟用curl -w测HTTP请求各阶段耗时。我曾经遇到过一个玄学问题某个区域访问模型服务总是慢2秒查了发现是DNS解析到旧IP问题并不在模型本身。这类问题在分布式部署时比较容易出现。6.3 我的经验与建议最后分享三个我认为最重要的经验。第一性能优化不是一锤子买卖要建立监控和回归机制。我在项目里给SpringAI调用加了简单的Metrics埋点记录调用量、平均耗时、错误率、token用量用PrometheusGrafana做每日巡检。这样每次改动上线后都能用数据说话而不是靠感觉评判。第二不要盲目追求大模型和高端配置。在同一个业务场景下先做提示词和参数优化再考虑换模型。我在项目中试过从14B换成32B模型准确率提升了不到1%但响应时间和显存开销上涨明显最后又换回了14B。很多场景是提示词没写清楚不是模型不够聪明。第三SpringAI本身还比较年轻版本迭代很快遇到问题时先查新版有没有修复或新增配置项。我遇到过的一个超时配置不生效的bug就是在升级到新版后通过新增的client配置解决的。生产环境升级框架版本一定要谨慎做好回归测试但也不能因为怕麻烦就停在旧版不跟进。优化没有终点每个新模型、新框架版本都会带来新的可能性。你手头的场景就是最好的试验场从一次压测开始从一条日志开始慢慢就会形成适合自己项目的优化方法论。