news 2026/9/18 20:16:24

Spring AI 对接 vLLM 部署的 DeepSeek 报 400?一次 HTTP 分块传输的排查实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 对接 vLLM 部署的 DeepSeek 报 400?一次 HTTP 分块传输的排查实录

Spring AI 对接 vLLM 部署的 DeepSeek 报 400?一次 HTTP 分块传输的排查实录

【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai

本文记录在 Spring AI 中使用 OpenAiChatModel 组件对接 vLLM 部署的 DeepSeek 大模型时,遇到的 "body 字段缺失" 400 报错的完整排查过程,并给出通过切换 Jetty HTTP 客户端彻底解决该问题的操作方法。

一句话结论:请求被"分段发货",服务端拒收

先把答案放在前面:这个 400 报错和你的 baseUrl、API Key、model 参数统统无关,根因在 HTTP 请求体的传输方式上——vLLM 对分块传输编码(chunked transfer encoding)的请求解析有问题。

分块传输编码是什么白话概念?正常发 HTTP POST 时,你相当于寄一个封好的整箱货物,箱子上贴着"总件数"(Content-Length 头),收件方开箱即知全貌。而分块传输则是"先说一声我要分几批寄,具体几批不告诉你",边写边发、发完为止。vLLM 这边只认整箱货,见到分批发来的请求就当成"没带货"处理,于是回你一个"缺少必填字段"的报错。

这个坑最折磨人的地方在于:同样的服务,用 curl 直接打是通的,只有走 Spring AI 才会挂。下面按我实际的排查顺序走一遍。

场景还原:curl 能通,OpenAiChatModel 就报 400

当时我手上有三样东西:一台跑着 vLLM 的机器(上面加载了 DeepSeek 系列模型)、一份能正常对话的 curl 命令、以及一个标准的 Spring AI 工程。

vLLM 服务健康、模型能推理,这些我都先用 curl 验证过,/v1/chat/completions端点返回完全正常。然后切到 Java 侧:

ChatModel chatModel = OpenAiChatModel.builder() .openAiApi(openAiApi) // baseUrl 指向 vLLM 的 /v1,apiKey 任意占位 .defaultOptions(OpenAiChatOptions.builder().model("deepseek").build()) .build();

一调用,直接炸出这段报错:

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

注意看报错里的'input': None:vLLM 认为它收到的 body 是空的。可我们明明把 prompt 塞进去了。这就是整个案子唯一的"矛盾点"——货物明明发了,对方却说没收到。

排查路线:从外到内,三层排除法

第一层,排除配置问题。逐项核对spring.ai.openai.base-urlapi-keymodel三个配置,并且用 Postman 复现了一遍 Spring AI 发出去的请求(header、body 完全一致)——Postman 通了。这说明"配置错了"和"vLLM 挂了"两个嫌疑人当场出局。

第二层,排除框架序列化问题。我一度怀疑是 Jackson 把 body 序列化空了,于是把请求体打到日志里检查,JSON 完整、字段齐全。这条线也断了。

第三层,抓包看传输层。既然"内容"没问题,问题只能出在"内容的发送方式"上。用mitmproxy抓了 Spring AI 发出的请求头,关键差异出现了:

Transfer-Encoding: chunked

curl 和 Postman 发出的请求是带Content-Length的整包传输,而 Spring 默认 HTTP 栈在某些场景(尤其是响应式链路或 body 由流式构造时)会退化为分块传输。对比抓包结果的那一刻,元凶就锁定了:vLLM 无法正确解析 chunked 请求体。

解决操作:把默认 HTTP 客户端换成 Jetty

确认是"发货方式"的问题之后,解法就不多了:要么让服务端学会收分块(vLLM 侧的事),要么换一家"只发整箱货"的快递公司。我们可控的是前者不可行、后者可行,于是把 Spring AI 底层用的 HTTP 客户端换成 Jetty 实现。

分两步走。

第 1 步,补上 Jetty 的两个依赖:

<dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-client</artifactId> </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-reactive-httpclient</artifactId> </dependency>

一个是 Jetty 的阻塞式客户端,对应RestClient;另一个提供响应式连接器,对应WebClient(流式对话走的就是这条线)。两个都要,缺一个就会出现"非流式好了、流式还是坏"的半残状态。

第 2 步,构造 OpenAiApi 时显式指定这两套客户端:

OpenAiApi openAiApi = OpenAiApi.builder() .baseUrl(chatConfig.getBaseUrl()) .apiKey(chatConfig.getApiKey()) .restClientBuilder(RestClient.builder() .requestFactory(new JettyClientHttpRequestFactory())) .webClientBuilder(WebClient.builder() .clientConnector(new JettyClientHttpConnector())) .build();

这里restClientBuilder管的是同步调用(chatModel.call()),webClientBuilder管的是流式调用(chatModel.stream())。改完再跑一遍,Transfer-Encoding: chunked消失了,400 报错随之消失,DeepSeek 正常出词。

避坑备忘:一张表对齐检查点

检查项预期踩坑表现
curl 直连 vLLM/v1/chat/completions200通了却报 400,说明问题在客户端侧
报错 body 里input字段非 nullNone说明服务端根本没收到 body
抓包看请求头Content-Length出现Transfer-Encoding: chunked即命中本坑
Jetty 依赖两个都加只加jetty-client时流式调用仍会失败
Spring AI 版本1.x 可用OpenAiApi.builder()2.0 已重构,见下方提示

最后这条值得展开一句:当前仓库主分支(2.0 线)的 OpenAI 模块已经换了底座,models/spring-ai-openai/ 下的OpenAiChatModel直接构建在 OpenAI 官方 Java SDK 之上,HTTP 层是自带 OkHttp 实现的SpringAiOpenAiHttpClient(见 models/spring-ai-openai/src/main/java/org/springframework/ai/openai/http/okhttp/),不再有OpenAiApi.builder()这个入口。也就是说本文的 Jetty 换法适用于 1.x 版本;如果你在 2.0 上复现类似症状,思路仍然成立(先抓包确认传输编码),只是替换客户端的代码路径不同。

给同坑的三句话

  1. 400 加 "body missing" 不等于你没传 body,先抓包看Transfer-Encoding,再看服务端日志,顺序反了会浪费一整天。
  2. curl 通、框架不通,优先怀疑"发送方式"而非"发送内容"——序列化、参数绑定这些"内容"问题用日志就能查掉,传输层问题只能靠抓包。
  3. 临时方案(换 Jetty)救急没问题,但记得盯一下 vLLM 侧对分块传输的支持进展;上游修复后,这套绕行的客户端替换就可以摘掉了。

【免费下载链接】spring-aiAn Application Framework for AI Engineering项目地址: https://gitcode.com/GitHub_Trending/spr/spring-ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 20:09:05

非标设备物联网改造:从传统运维困局到数据驱动的设备管理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 20:06:49

小熊派HarmonyOS设备MQTT接入EMQX实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 20:05:12

串口通讯标准解析:RS232/RS422/RS485与Modbus协议实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 20:04:19

scrcpy安卓投屏教程:三步在电脑上掌控手机屏幕

scrcpy安卓投屏教程&#xff1a;三步在电脑上掌控手机屏幕 【免费下载链接】scrcpy Display and control your Android device 项目地址: https://gitcode.com/GitHub_Trending/sc/scrcpy scrcpy 是一款免费开源的安卓投屏与控制工具。连上一根数据线&#xff0c;手机画…

作者头像 李华
网站建设 2026/9/18 20:02:28

盖尔圆定理与特征值估计:从范数到相似变换的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华