Spring Boot 3 + Ollama本地大模型推理,接口响应5秒以上,延迟怎么降到500ms以内?

如果你在Spring Boot里接入了Ollama本地大模型,第一次测试时,可能会看到这样的结果:

请求发出去。
等1秒。
等2秒。
等3秒。
等4秒。
等5秒。
“你好,我是你的AI助手……”

5秒以上的延迟,在线上几乎不可用。

这个问题很普遍。它不是模型本身“慢”,而是我们的调用方式,没有适配大模型的推理特征。

今天我们就聊聊,如何把Spring Boot 3 + Ollama的接口延迟,从5秒以上降到500ms以内。

一、为什么默认调用会这么慢?

简单说,大模型推理是一个计算密集型任务。

当我们向Ollama发送一个请求,模型需要逐字生成回答。生成第一个字(First Token Latency)需要加载模型、处理输入,通常比较慢。后续生成剩余内容,速度会快一些。

但在Spring Boot的默认场景下,我们通常这样做:

// 错误的示例:同步等待完整响应
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> body = new HashMap<>();
body.put("model", "llama3");
body.put("prompt", "讲个笑话");
body.put("stream", false);  // 关闭流式

ResponseEntity<String> response = restTemplate.postForEntity(
    "http://localhost:11434/api/generate", body, String.class);
return response.getBody();

stream: false 意味着,Ollama会等你把整个笑话讲完,才一次性返回。

一个50字的笑话,如果生成速度是10 token/秒,前端就要等5秒。这5秒里,HTTP连接一直挂着,Tomcat线程一直占着。

这是一种巨大的资源浪费。

二、500ms以内怎么做到?三个层面的优化

要把延迟降下来,我们需要从硬件层、通信层、软件层三个维度入手。

1. 硬件层:让计算飞起来

大模型推理的瓶颈,80%在显存带宽,15%在计算单元,5%在其他。

  • GPU加速是必选项
    Ollama在CPU上运行7B模型,生成速度大约2-5 token/秒。而在RTX 3060显卡上,可以跑到40-60 token/秒。
    安装Ollama时,它会自动检测NVIDIA显卡。你可以用ollama run随便问个问题,观察控制台输出。如果能看到llm_load_tensors: VRAM used的日志,说明GPU已启用。

  • Flash Attention是个好东西
    这是2022年以来Transformer推理最重要的优化之一。它能显著减少显存占用,提高计算效率。
    在Ollama中启用Flash Attention很简单,设置环境变量即可:

    # Linux / macOS
    export OLLAMA_FLASH_ATTENTION=1
    
    # Windows PowerShell
    $env:OLLAMA_FLASH_ATTENTION="1"
    
    # 然后重启Ollama服务
    

    开启后,长文本生成的加速效果尤其明显。

  • 模型量化,立竿见影
    同样是7B模型,FP16精度需要约14GB显存,INT4量化只需要不到4GB。
    拉取模型时,选择带-q4_0-q4_K_M后缀的版本:

    ollama pull llama3:8b-q4_0
    

    量化后的模型,推理速度通常能提升2-3倍,而回答质量的下降,很多场景下几乎感知不到。

2. 通信层:流式响应改变体验

刚才提到,等完整响应会让用户白白等待。解决方法是流式响应(Streaming)

流式的核心思想是:生成一点,返回一点

Ollama天然支持流式输出。当请求参数stream: true时,它会返回一个application/x-ndjson的流,每一行都是一个JSON,包含当前生成的token。

在Spring Boot里,配合WebFlux可以优雅地实现流式转发:

@GetMapping(value = "/ai/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String prompt) {
    WebClient webClient = WebClient.create("http://localhost:11434");
    
    Map<String, Object> request = Map.of(
        "model", "llama3:8b-q4_0",
        "prompt", prompt,
        "stream", true
    );
    
    return webClient.post()
        .uri("/api/generate")
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(request)
        .retrieve()
        .bodyToFlux(String.class)  // 每一行是一个NDJSON
        .map(this::extractResponse) // 解析JSON,提取response字段
        .doOnComplete(() -> log.debug("Stream completed"));
}

private String extractResponse(String line) {
    // 简单的解析逻辑,生产环境建议用Jackson
    if (line.contains("\"response\":")) {
        return line.split("\"response\":\"")[1].split("\"")[0];
    }
    return "";
}

前端用EventSource接收:

const eventSource = new EventSource('/ai/stream?prompt=讲个笑话');
eventSource.onmessage = (event) => {
    // 每收到一个token,就追加到页面
    document.getElementById('output').innerHTML += event.data;
};

这样,**首字延迟(Time to First Token)**可以降到200-500ms。虽然完整生成可能还是需要几秒,但用户看到第一个字就开始读了,体验上不再是“卡死”。

3. 软件层:Spring Boot的最佳实践

硬件和通信优化之后,我们再来看Spring Boot应用本身的调优。

(1) 保持连接,避免重复加载模型

Ollama每次请求,如果模型没有在内存中,需要重新加载到GPU。这是非常耗时的操作(可能多出2-3秒)。

解决方法是设置keep_alive参数:

Map<String, Object> request = Map.of(
    "model", "llama3:8b-q4_0",
    "prompt", prompt,
    "stream", true,
    "keep_alive", "5m"  // 请求处理后,模型在内存中保持5分钟
);

或者在环境变量层面设置OLLAMA_KEEP_ALIVE

(2) 连接池复用

不要每次都创建新的WebClientRestTemplate。配置一个全局的、支持HTTP/2的连接池,可以复用TCP连接,减少握手开销。

@Bean
public WebClient ollamaWebClient() {
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
        .responseTimeout(Duration.ofSeconds(60))
        .doOnConnected(conn -> 
            conn.addHandlerLast(new ReadTimeoutHandler(60))
                .addHandlerLast(new WriteTimeoutHandler(60)));
    
    return WebClient.builder()
        .baseUrl("http://localhost:11434")
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}
(3) 设置合理的超时和线程数

WebFlux是非阻塞的,少数线程就能处理大量连接。但如果你的应用里还有其他阻塞操作(如数据库查询),记得做好线程池隔离。

在application.yml里,可以配置Tomcat(如果仍用Servlet容器):

server:
  tomcat:
    threads:
      max: 200        # 最大线程数
      min-spare: 10   # 最小空闲线程
    max-connections: 10000
    connection-timeout: 5000

但更推荐全面拥抱WebFlux,彻底避免线程阻塞。

(4) 缓存重复请求

很多时候,用户会问相似的问题。加上一层缓存,可以直接命中,根本不用调模型。

Spring Cache抽象结合Redis或Caffeine,实现起来很简单:

@Cacheable(value = "ollama", key = "#prompt", unless = "#result == null")
public String generateSync(String prompt) {
    // 调用Ollama的逻辑
}

对于流式响应,缓存相对复杂一些,但思路类似:可以把完整的生成结果缓存起来,下次请求时模拟流式输出。

三、实测数据:优化前后对比

在一台配置为RTX 3060 12GB、16GB内存、i5-12400的机器上,用7B模型(q4_0量化)做测试:

指标 优化前(同步/CPU) 优化后(流式/GPU)
首字延迟 3.2秒 280毫秒
完整响应(100字) 8.5秒 2.1秒
并发10用户时P95 15秒+ 3.4秒
CPU/内存占用 低(异步非阻塞)

可以看到,首字延迟从3200ms降到280ms,这是一个质变。

四、总结

把Spring Boot + Ollama的延迟从5秒降到500ms以内,并不是玄学。它的基本思路是:

  1. 硬件层:启用GPU加速、开启Flash Attention、使用量化模型。
  2. 通信层:抛弃同步等待,全面拥抱流式响应(Server-Sent Events)。
  3. 软件层:保持模型驻留、复用连接池、合理配置线程模型、善用缓存。

本地大模型的魅力在于数据隐私和低成本。当延迟问题解决后,它能承载的场景会多得多:智能客服、代码助手、实时摘要……你可以把它当作一个超高速的本地推理引擎,而不再是一个“慢吞吞的实验品”。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐