Spring AI 与 vLLM 交互踩坑记:HTTP/2 协议引发的“消失”的请求体

背景

最近在开发一个基于 Spring AI 的 Agent 功能。在开发阶段,我用的是 硅基流动的云端 API 进行调试,一切顺风顺水。

功能验证完毕后,我按计划将模型切换为本地部署的 vLLM。本以为只是改个 base-urlapi-key 的事,结果却遇到了一个奇怪的bug。

问题初现

配置切换完成后,启动服务发起对话,控制台直接甩出了一行刺眼的 400 错误日志:

{
    "object": "error",
    "message": "[{'type': 'missing', 'loc': ('body',), 'msg': 'Field required', 'input': None}]",
    "type": "Bad Request",
    "param": null,
    "code": 400
}

错误信息非常直白:Field required,位置在 body。换句话说,vLLM 认为我发过去的请求是空的,没有 Body!

这让我百思不得其解。我的代码仅仅是改了配置文件的地址,逻辑并未变动,怎么 Body 就凭空消失了?

排查之路:谁动了我的请求体?

为了找出真凶,我开始了一场类似“控制变量法”的排查过程:

1. 怀疑代码逻辑

首先,切回 硅基流动 的 API 进行测试。

  • 结果:正常对话。
  • 结论:Spring AI 的代码逻辑构建请求体(Prompt、Message 等)没有问题。

2. 怀疑模型服务端

既然代码没问题,难道是本地 vLLM 服务挂了或者接口不一样?我直接在服务器使用 curl 命令,手动构造 JSON 请求访问本地 vLLM。

  • 结果:正常响应。
  • 结论:vLLM 服务端本身工作正常,能正确解析标准 HTTP 请求。

3. 怀疑传输丢失

会不会是 Spring AI 发出的请求在传输过程中 Body 丢了?
为了验证这一点,我用 VibeCoding 快速生成了一个兼容 OpenAI 协议的 Mock 接口。让 Spring AI 调用这个 Mock 接口,并打印接收到的所有参数。

  • 结果:Mock 接口完整打印出了 Body 内容。
  • 结论:Spring AI 确实发送了 Body,数据并没有在客户端构建阶段丢失。但奇怪的是,Mock 服务能收到,vLLM 却收不到

4. 怀疑框架兼容性

此时我怀疑是否是 Spring AI 底层封装的问题。

  • 尝试方案 A:抛弃 Spring AI,换成 LangChain4j
    • 结果:依然报错,Body 丢失。
  • 尝试方案 B:抛弃所有 AI 框架,直接用 Java 原生的 HttpClient手写请求。
    • 结果:成功了

这一步锁定了问题的范围:Spring AI 和 LangChain4j 的底层网络客户端与 vLLM 之间存在某种“沟通障碍”,而原生 HttpClient 在某种默认配置下避开了这个问题。

真相大白:HTTP/2 的锅

通过查阅 Spring AI 和 LangChain4j 的文档,发现它们在 Spring Boot 环境下通常默认使用 WebClient(基于 Reactor Netty 或 JDK Client 适配器)。

经过深入分析和向 AI 助手求证,线索指向了 HTTP/2

  • Spring 的 WebClient(尤其是配置了 JDK Connector 时)会默认尝试协商使用 HTTP/2 协议。
  • 原生 HttpClient 虽然也支持 HTTP/2,但如果不显式配置,有时在握手阶段的行为可能不同。
  • 最关键的是,我去 vLLM 和 Spring AI 的 GitHub Issues 里搜了一圈,果然发现了类似的反馈 https://github.com/spring-projects/spring-ai/issues/2042 :vLLM 的服务端(基于 Uvicorn/FastAPI)在处理某些 HTTP/2 的 Upgrade 请求或者 Frame 传输时,存在兼容性问题,导致无法正确读取 Request Body,从而被 Pydantic 校验拦截,报出 body missing 的错误。

解决方案

解决方案就很简单:强制客户端使用 HTTP/1.1

在 Spring AI 中,我们可以通过自定义 WebClient.Builder 并配置 ClientHttpConnector 来实现这一点。

以下是修复后的配置代码:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JdkClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import java.net.http.HttpClient;
import java.time.Duration;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient.Builder webClientBuilder() {
        // 强制指定 HTTP 版本为 HTTP_1_1
        HttpClient httpClient = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1) 
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        // 使用 JDK 原生 HttpClient 作为底层连接器
        ClientHttpConnector connector =
                new JdkClientHttpConnector(httpClient);

        return WebClient.builder()
                .clientConnector(connector);
    }
}

加上这个配置后,重启项目,再次调用本地 vLLM,久违的 AI 回复终于出现了

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐