嗯,事情是这样的:
我们有一处代码使用了 Spring AI,过程是用 Spring AI 的 ChatClient
调用我司内部通过 vllm 部署的模型,然后处理返回结果。这段代码已上线,并且正常运行。
我在把它搬到新项目之后,发现接口总是返回 400。
有点麻。
首先,要看看是不是我代码写错了,于是开始了快速的找不同。盯了一圈没发现问题。
然后,我把同样的请求用 Bruno 直接调接口发送试了一下,发现结果正常。也就是说,代码在新项目里不行,在别的地方行。
本着我遇到的问题在别人肯定已经遇到过了的伟大主题思想,我去网上搜了一下,还真发现了有人在 Spring AI 的 Github 上提的一个类似的问题: issues/3438, 不过这个问题并没有被解决。所以,我只好委屈下自己亲自去调查了!
打开多年没碰过的有线鲨鱼(wireshark),开启监听。然后分别在 Bruno 和 Spring AI 上各调了一次这个接口后, 关闭有线鲨鱼的监听,去过滤器里找到对应的 HTTP 请求记录。逐行对比两个请求的区别后,发现 Spring AI 发起的请求有尝试把 HTTP 1.1 升级为 HTTP 2 的相关请求头,如下图所示。于是猜测是不是和这哦相关。
从图中可以看到,Spring RestClient 使用了 Java HttpClient API 作为后端。
我把 Upgrade: h2c
头加到 Bruno 的请求头里之后,问题就复现了,现在请求稳定返回 400,错误内容如下:
Invalid HTTP request received.
既然知道了问题来源,就可以去尝试修正问题了。
首先肯定是不太容易去碰 vllm 部署的 AI 服务,那我就勉为其难在自己的代码里指定必须用 HTTP 1.1 吧:
@Bean
public ChatClient myChatClient(RestClient.Builder defaultRestClientBuilder,
WebClient.Builder defaultWebClientBuilder) {
var httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1) // 这指定用 HTTP 1.1
.connectTimeout(Duration.ofSeconds(5))
.build();
var restClientBuilder = defaultRestClientBuilder.clone() // 复制一份
.requestFactory(new JdkClientHttpRequestFactory(httpClient));
var webClientBuilder = defaultWebClientBuilder.clone() // 复制一份
.clientConnector(new JdkClientHttpConnector(httpClient));
OpenAiChatProperties conf = getAiConfig();
var api = OpenAiApi.builder()
.apiKey(conf.getApiKey())
.baseUrl(conf.getBaseUrl())
.restClientBuilder(restClientBuilder)
.webClientBuilder(webClientBuilder) // 流式输出用这个
.build();
// 后面 ChatClient 的构造过程省略
}
至此,问题解决。
虽然问题找到了,但是这里还有很多问题没有解释:
因为没有权限去部署模型的服务器上做进一步调查,所以作罢。(好吧,主要是想早点下班,问题解决了就别难为自己了)
这个过程中,我尝试了 Openai 官方提供的 Java SDK,嗯……这里得到的经验就是,如果你不使用 Kotlin 的话,就不要使用这个 SDK,因为用 Java 调用 Kotlin 的方法,太只因吧啰嗦了。
Spring Boot 中配置想对 Rest Client 做配置太麻烦了,包了一层又一层,自动配置代码也是散落得到处都是……