0%

解决低版本SpringBoot使用langchain4j Azure 冲突问题

新公司使用的Java技术栈,我们有部分新业务需要调用 OpenAI 的接口进行交互,之前我找了一个比较轻量的SDK来调用OpenAI的接口,地址是:https://github.com/Lambdua/openai4j ,这个库作为日常使用足够了,但是一些高阶能力无法满足,而这些也是我们未来会用到的,比如:

  • 对接微软 Azure 上部署的 GPT 模型
  • Function Calling
  • RAG

把第一版功能完成后,这几天工作不是那么多,于是我从Github上找到了这个库https://github.com/langchain4j/langchain4j ,从名字就能看出来,这个项目是参考的 Python 的LangChain,Java 库的命名很有意思,很喜欢叫 xxxx4j,4j 的意思是 for Java,比如 log4j。

我大致看了一下介绍,功能还算完备,给出的demo来看使用方式上可读性也很高,更重要的一点是支持古老的Java8。于是我在项目中进行了引入,将已有代码进行了改造,在跑直接调用 OpenAI 的例子时很顺利,当我切换为 Azure 后问题出现了,报错堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception in thread "main" java.lang.NoClassDefFoundError: reactor/core/Disposable
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at com.azure.core.http.netty.NettyAsyncHttpClientProvider.createInstance(NettyAsyncHttpClientProvider.java:81)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupOpenAIClientBuilder(InternalAzureOpenAiHelper.java:71)
at dev.langchain4j.model.azure.InternalAzureOpenAiHelper.setupSyncClient(InternalAzureOpenAiHelper.java:51)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.<init>(AzureOpenAiChatModel.java:123)
at dev.langchain4j.model.azure.AzureOpenAiChatModel$Builder.build(AzureOpenAiChatModel.java:536)

我按照堆栈的引导,一步一步去看代码,发现是在创建 HttpClient 对象时挂了,我进到 ConnectionProvider 源码中查看,确实找不到上边说的 Disposable 类,这个类来自 reactor-core 包。通过IDE跳转进的路径看到,目前项目中所使用的 reactor-core 版本是 2.0.8.RELEASE,我找到最新 3.6.7 版本的 reactor-core 源码看了下是有Disposable 这个类的。

一开始我认为是 langchain4j 的这个项目有问题,去 Github 的 Issue 中搜了下并没有相关的提问,于是我自己开始尝试动手解决,尝试了以下几种方式都不行:

  1. 直接在项目中引入最新版本的 reactor-core
  2. 排除(exclusions) langchain4j-azure-open-ai 下的 reactor-core 依赖,保证我自己引入的最新版本生效
  3. 引入 reactor-netty-core 的最新版
  4. 引入全部 langchain4j 的依赖
  5. 重启IDE
  6. 重启电脑

在做上边的第2步时,启动调试后可以看到,IDE在进入ConnectionProvider 后确实可以正常跳转进Disposable 了,但最终还是报错。通过依赖分析也没有发现和 reactor 的任何冲突,一直搞到晚上下班也没解决。

今天早上上班后我换了个思路来排查这个项目,创建了一个新项目,只引入 langchain4j 的依赖,可以正常执行,接下来我把我们项目中其他依赖项引进来,发现还是没问题,当我把 parent 引入后问题出现了。虽然 parent 的 pom 文件在远端,但IDEA提供了一个功能,可以修改本地的文件来进行调试,我用二分法删除 parent 中的依赖,最终将问题定位在了:

1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

parent 中 spring.boot.version 的值是 1.5.7.RELEASE ,我在上上家公司写Java时就有这个版本了,是个非常老的版本,但升级 SpringBoot 关联的问题会更多。我继续深入进去看,在 spring-boot-dependencies 的 pom 文件中 properties 指定了reactor.version2.0.8.RELEASE,这下破案了。之前我无法通过依赖分析找到冲突,也是因为依赖是在 parent 指定的,且这个依赖版本无法在后续进行修改。

有种覆盖 parent 版本号的方式是在自己项目的父 pom 中的dependencyManagement 下进行声明,我尝试在 dependencyManagement 加上如下片段:

1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>

此时报了另一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
java.lang.VerifyError: class io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe overrides final method close.(Lio/netty/channel/ChannelPromise;)V

at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:473)
at java.net.URLClassLoader.access$100(URLClassLoader.java:74)
at java.net.URLClassLoader$1.run(URLClassLoader.java:369)
at java.net.URLClassLoader$1.run(URLClassLoader.java:363)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:362)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at reactor.netty.resources.DefaultLoopKQueue.getChannel(DefaultLoopKQueue.java:50)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:243)
at reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:251)
at reactor.netty.transport.TransportConfig.lambda$connectionFactory$1(TransportConfig.java:277)
at reactor.netty.transport.TransportConnector.doInitAndRegister(TransportConnector.java:277)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:164)
at reactor.netty.transport.TransportConnector.connect(TransportConnector.java:123)
at reactor.netty.resources.DefaultPooledConnectionProvider$PooledConnectionAllocator.lambda$connectChannel$0(DefaultPooledConnectionProvider.java:519)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.subscribeWith(Mono.java:4634)
at reactor.core.publisher.Mono.subscribe(Mono.java:4534)
at reactor.core.publisher.Mono.subscribe(Mono.java:4470)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.drainLoop(SimpleDequePool.java:437)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.pendingOffer(SimpleDequePool.java:600)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool.doAcquire(SimpleDequePool.java:296)
at reactor.netty.internal.shaded.reactor.pool.AbstractPool$Borrower.request(AbstractPool.java:430)
at reactor.netty.resources.DefaultPooledConnectionProvider$DisposableAcquire.onSubscribe(DefaultPooledConnectionProvider.java:204)
at reactor.netty.internal.shaded.reactor.pool.SimpleDequePool$QueueBorrowerMono.subscribe(SimpleDequePool.java:720)
at reactor.netty.resources.PooledConnectionProvider.lambda$acquire$2(PooledConnectionProvider.java:170)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.lambda$subscribe$0(HttpClientConnect.java:273)
at reactor.core.publisher.MonoCreate.subscribe(MonoCreate.java:61)
at reactor.core.publisher.FluxRetryWhen.subscribe(FluxRetryWhen.java:81)
at reactor.core.publisher.MonoRetryWhen.subscribeOrReturn(MonoRetryWhen.java:46)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:63)
at reactor.netty.http.client.HttpClientConnect$MonoHttpConnect.subscribe(HttpClientConnect.java:276)
at reactor.core.publisher.Mono.subscribe(Mono.java:4568)
at reactor.core.publisher.Mono.block(Mono.java:1778)
at com.azure.core.http.netty.NettyAsyncHttpClient.sendSync(NettyAsyncHttpClient.java:199)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:51)
at com.azure.core.http.policy.HttpLoggingPolicy.processSync(HttpLoggingPolicy.java:183)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.implementation.http.policy.InstrumentationPolicy.processSync(InstrumentationPolicy.java:101)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.KeyCredentialPolicy.processSync(KeyCredentialPolicy.java:115)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.CookiePolicy.processSync(CookiePolicy.java:73)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddDatePolicy.processSync(AddDatePolicy.java:50)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RetryPolicy.attemptSync(RetryPolicy.java:211)
at com.azure.core.http.policy.RetryPolicy.processSync(RetryPolicy.java:161)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersPolicy.processSync(AddHeadersPolicy.java:66)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.AddHeadersFromContextPolicy.processSync(AddHeadersFromContextPolicy.java:67)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.RequestIdPolicy.processSync(RequestIdPolicy.java:77)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.policy.HttpPipelineSyncPolicy.processSync(HttpPipelineSyncPolicy.java:51)
at com.azure.core.http.policy.UserAgentPolicy.processSync(UserAgentPolicy.java:174)
at com.azure.core.http.HttpPipelineNextSyncPolicy.processSync(HttpPipelineNextSyncPolicy.java:53)
at com.azure.core.http.HttpPipeline.sendSync(HttpPipeline.java:138)
at com.azure.core.implementation.http.rest.SyncRestProxy.send(SyncRestProxy.java:62)
at com.azure.core.implementation.http.rest.SyncRestProxy.invoke(SyncRestProxy.java:83)
at com.azure.core.implementation.http.rest.RestProxyBase.invoke(RestProxyBase.java:124)
at com.azure.core.http.rest.RestProxy.invoke(RestProxy.java:95)
at com.sun.proxy.$Proxy24.getChatCompletionsSync(Unknown Source)
at com.azure.ai.openai.implementation.OpenAIClientImpl.getChatCompletionsWithResponse(OpenAIClientImpl.java:1444)
at com.azure.ai.openai.OpenAIClient.getChatCompletionsWithResponse(OpenAIClient.java:318)
at com.azure.ai.openai.OpenAIClient.getChatCompletions(OpenAIClient.java:685)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:257)
at dev.langchain4j.model.azure.AzureOpenAiChatModel.generate(AzureOpenAiChatModel.java:215)

回到最开始的问题,报错误的根本原因是,初始化 Azure模型时需要构造一个 HttpClient,默认情况下会使用ConnectionProvider 来构造。看了下 AzureOpenAiChatModel 的 builder 方法,支持自己传入 OpenAIClient,而 OpenAIClient 可以自己构造 HttpClient,通过这个文档看到 https://learn.microsoft.com/en-us/azure/developer/java/sdk/http-client-pipeline HttpClient 有多种实现,其中可以用 OkHttpClient 来实现,于是我进行了以下魔改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private static OpenAIClient setupSyncClient(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
OpenAIClientBuilder openAIClientBuilder = setupOpenAIClientBuilder(endpoint, serviceVersion, credential, timeout, maxRetries, proxyOptions, logRequestsAndResponses);
return openAIClientBuilder.buildClient();
}

private static OpenAIClientBuilder setupOpenAIClientBuilder(String endpoint, String serviceVersion, Object credential, Duration timeout, Integer maxRetries, ProxyOptions proxyOptions, boolean logRequestsAndResponses) {
timeout = getOrDefault(timeout, ofSeconds(60));
HttpClientOptions clientOptions = new HttpClientOptions();
clientOptions.setConnectTimeout(timeout);
clientOptions.setResponseTimeout(timeout);
clientOptions.setReadTimeout(timeout);
clientOptions.setWriteTimeout(timeout);
clientOptions.setProxyOptions(proxyOptions);

Header header = new Header("User-Agent", "langchain4j-azure-openai");
clientOptions.setHeaders(Collections.singletonList(header));
// HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

HttpLogOptions httpLogOptions = new HttpLogOptions();
if (logRequestsAndResponses) {
httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS);
}

maxRetries = getOrDefault(maxRetries, 3);
ExponentialBackoffOptions exponentialBackoffOptions = new ExponentialBackoffOptions();
exponentialBackoffOptions.setMaxRetries(maxRetries);
RetryOptions retryOptions = new RetryOptions(exponentialBackoffOptions);

OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder()
.endpoint(ensureNotBlank(endpoint, "endpoint"))
.serviceVersion(getOpenAIServiceVersion(serviceVersion))
.httpClient(httpClient)
.clientOptions(clientOptions)
.httpLogOptions(httpLogOptions)
.retryOptions(retryOptions);

if (credential instanceof String) {
openAIClientBuilder.credential(new AzureKeyCredential((String) credential));
} else if (credential instanceof KeyCredential) {
openAIClientBuilder.credential((KeyCredential) credential);
} else if (credential instanceof TokenCredential) {
openAIClientBuilder.credential((TokenCredential) credential);
} else {
throw new IllegalArgumentException("Unsupported credential type: " + credential.getClass());
}

return openAIClientBuilder;
}

private static OpenAIServiceVersion getOpenAIServiceVersion(String serviceVersion) {
for (OpenAIServiceVersion version : OpenAIServiceVersion.values()) {
if (version.getVersion().equals(serviceVersion)) {
return version;
}
}
return OpenAIServiceVersion.getLatest();
}

从开源代码中拷贝出 setupSyncClientsetupOpenAIClientBuilder 方法,并对setupOpenAIClientBuilder 中的HttpClient httpClient 的创建逻辑进行了调整

1
2
3
4
// before
HttpClient httpClient = new NettyAsyncHttpClientProvider().createInstance(clientOptions);
// after
HttpClient httpClient = new OkHttpAsyncClientProvider().createInstance(clientOptions);

初始化Azure模型时传入我自己的 client:

1
2
3
4
5
6
7
8
9
// 默认生成的client使用NettyAsyncHttpClientProvider和SpringBoot所依赖的版本不兼容,改用OkHttpAsyncClientProvider进行重写
OpenAIClient client = setupSyncClient(System.getenv("AZURE_OPENAI_ENDPOINT"), "",
System.getenv("AZURE_OPENAI_API_KEY"), ofSeconds(30), 2, null, true);

model = AzureOpenAiChatModel.builder()
.openAIClient(client)
.deploymentName(modelName)
.temperature(0.0)
.build();

并在工程中引入 azure-core-http-okhttp 的依赖

1
2
3
4
5
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-okhttp</artifactId>
<version>1.12.0</version>
</dependency>

再次执行还是报错了,不过这次的错误变为:

1
2
3
4
5
6
7
java.lang.NoClassDefFoundError: reactor/util/context/ContextView

at com.azure.core.http.rest.RestProxy.<init>(RestProxy.java:56)
at com.azure.core.http.rest.RestProxy.create(RestProxy.java:140)
at com.azure.ai.openai.implementation.OpenAIClientImpl.<init>(OpenAIClientImpl.java:144)
at com.azure.ai.openai.OpenAIClientBuilder.buildInnerClient(OpenAIClientBuilder.java:283)
at com.azure.ai.openai.OpenAIClientBuilder.buildClient(OpenAIClientBuilder.java:351)

还是 reactor 的问题,但可以看到,现在已经不再使用 reactor.core.Disposable 了,也许升级一下 reactor-core 可以解决,我再次在项目的 parent 的dependencyManagement 下引入

1
2
3
4
5
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.6.7</version>
</dependency>

再次尝试,问题解决。