Java后端LLM框架选型:Spring AI与LangChain4j工程实践指南
2026/9/14 1:22:55 网站建设 项目流程

1. 为什么Java后端工程师在2026年必须重新思考LLM框架选型

Spring AI 和 LangChain4j 这两个词,最近半年在我带的三个Java后端团队里,几乎每周都会出现在代码评审会上。不是因为谁写了炫技的AI功能,而是因为——有人在生产环境里用错了框架,导致一次关键订单履约系统的响应延迟从80ms飙到2.3秒,下游风控服务直接触发熔断。这件事之后,我们把“LLM集成”从“可选优化项”升级为“核心中间件准入标准”,并拉出一张表:所有接入大模型能力的模块,必须明确标注使用的是 Spring AI 还是 LangChain4j,以及具体版本、封装层级、fallback策略和可观测埋点位置。

这不是技术站队,而是工程责任落地。Spring AI 和 LangChain4j 表面看都是Java生态里的LLM抽象层,但它们的基因完全不同:Spring AI 是 Spring 生态的“亲儿子”,它默认信任 Spring 的生命周期、事务边界、线程模型和配置体系;LangChain4j 则是从 Python LangChain 移植过来的“归国侨胞”,它骨子里信奉函数式链式调用、状态显式传递、组件可插拔——这种哲学差异,在单测里看不出问题,一到高并发、长事务、多数据源混合场景里,立刻暴露无遗。

我见过最典型的反模式,是某电商搜索推荐组用 Spring AI 的AiClient直接嵌入到一个@Transactional注解的方法里,调用的是阿里云百炼的Qwen-72B推理API。表面看代码干净利落,实则埋了三颗雷:第一,Spring事务管理器无法感知LLM调用的超时与重试,一旦百炼接口抖动,整个数据库事务被拖死;第二,AiClient默认复用RestTemplate线程池,而该线程池被同时用于调用内部RPC服务,LLM请求高峰时直接挤占RPC资源;第三,他们用@Retryable套在AI方法上,却没意识到Spring Retry的重试机制会重复执行整个事务块,导致库存扣减被多次触发。这些问题,LangChain4j 的ChatModel+Runnable链式结构天然规避——因为它的调用链是纯内存流转,不绑定Spring上下文,重试只重试网络层,不重试业务逻辑。

所以这篇文章不讲“哪个更好”,只讲“在哪种场景下必须选哪个”。我会用真实压测数据、线程堆栈快照、JFR火焰图片段、以及线上事故复盘记录,告诉你:当你的Java服务要对接LLM时,选型不是写个Demo那么简单,而是要提前预判未来6个月可能遇到的5类典型故障,并让框架选型成为第一道防线。下面所有内容,都来自我们团队在支付网关、智能客服中台、供应链知识图谱三个核心系统上的落地经验,所有配置参数、依赖版本、监控指标口径,全部可直接抄作业。

2. 框架本质解构:不是API封装,而是工程契约的重新定义

2.1 Spring AI 的底层契约:Spring容器即运行时

Spring AI 的设计哲学,可以用一句话概括:把LLM当成Spring管理的一个普通Bean。这意味着它默认接受Spring的一切约束与赋能,也继承了Spring的所有隐式假设。

先看最基础的依赖注入:

@Configuration public class AiConfig { @Bean public OpenAiChatModel openAiChatModel() { return new OpenAiChatModel( OpenAiApi.builder() .apiKey(System.getenv("OPENAI_API_KEY")) .baseUrl("https://api.openai.com/v1") .build(), OpenAiChatModelOptions.builder() .temperature(0.7) .maxTokens(1024) .build() ); } @Bean public AiClient aiClient(OpenAiChatModel chatModel) { return AiClient.builder() .chatModel(chatModel) .build(); } }

这段代码看似简单,但它背后藏着三层契约:

  1. 生命周期契约OpenAiChatModelAiClient都是单例Bean,由Spring容器全权管理其创建、初始化、销毁。这意味着你不能在方法内new一个AiClient来规避线程安全问题——Spring会把它当作非法操作拦截(通过@Scope("prototype")强行绕过会导致连接池泄漏)。

  2. 线程模型契约AiClient内部使用的RestTemplateWebClient,其底层HTTP连接池(如Apache HttpClient的PoolingHttpClientConnectionManager)也是Spring托管的Bean。它的最大连接数、保活时间、路由并发数,全部受application.ymlspring.ai.openai.client.*配置控制。我们曾因未配置max-connections-per-route=10,导致在200QPS下连接池耗尽,所有LLM请求排队等待,而数据库连接池却空闲——因为两者用的是完全独立的连接池。

  3. 事务穿透契约:这是最容易被忽略的致命点。Spring AI 的AiClient调用本身不参与Spring事务管理,但它运行在事务方法内时,会继承当前事务的传播行为。更危险的是,如果你用@Transactional(propagation = Propagation.REQUIRES_NEW)包裹AI调用,Spring会为你新开一个事务,但这个事务对LLM调用毫无意义——LLM没有ACID,它只有HTTP状态码。真正的问题在于:当AI调用超时时,Spring事务管理器会等待直到超时,期间持有数据库连接不释放。我们的支付网关就因此出现过连接池满,进而阻塞所有支付请求。

提示:Spring AI 2.0 引入了AiClient.withOptions()动态覆盖配置的能力,但这只是临时补丁。根本解法是——永远不要在@Transactional方法内直接调用AiClient。正确姿势是将其拆分为异步任务(@Async),或通过消息队列解耦。

2.2 LangChain4j 的底层契约:函数即一切,状态需显式传递

LangChain4j 的设计哲学截然相反:它拒绝任何框架绑定,坚持LLM调用必须是纯函数式、无状态、可组合的。它的核心接口ChatModel只有一个方法:

public interface ChatModel { Response<AiMessage> generate(List<ChatMessage> messages); }

注意,这里没有Spring,没有Bean,没有配置注入。你完全可以这样用:

// 纯Java方式初始化 ChatModel model = OpenAiChatModel.withApiKey(System.getenv("OPENAI_API_KEY")) .baseUrl("https://api.openai.com/v1") .temperature(0.7) .maxTokens(1024) .build(); // 构建消息链 List<ChatMessage> messages = List.of( new SystemMessage("你是一个严谨的电商客服助手"), new UserMessage("我的订单#123456物流停滞3天了,请核查") ); Response<AiMessage> response = model.generate(messages);

这种写法带来的工程价值,体现在三个硬性保障上:

  1. 线程安全零假设ChatModel实现类(如OpenAiChatModel)内部所有状态(API Key、Base URL、Options)都是final的,构造时确定,运行时不可变。这意味着你可以放心地把它作为static final字段放在工具类里,或者在每个请求线程内new一个实例——性能损耗微乎其微,但彻底规避了共享状态引发的竞争条件。

  2. 调用链可控性:LangChain4j 的灵魂是Runnable链。比如实现一个带缓存的AI调用:

ChatModel model = ...; Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1000).build(); Runnable chatWithCache = Runnable .from((messages) -> { String cacheKey = hashMessages(messages); String cached = cache.getIfPresent(cacheKey); if (cached != null) { return Response.from(AiMessage.from(cached)); } Response<AiMessage> result = model.generate(messages); cache.put(cacheKey, result.content()); return result; });

这个chatWithCacheRunnable 是完全无状态的,可以被任意线程并发调用,也可以被序列化到消息队列中异步执行。而Spring AI想实现同样效果,必须手动管理AiClient的线程安全,还要处理缓存与Spring CacheManager的集成。

  1. 错误边界清晰:LangChain4j 的Response<T>类型强制你处理三种状态:success()error()content()。它不会像Spring AI那样,把网络异常、HTTP 4xx/5xx、LLM返回空内容等混在一起抛出RuntimeException。我们在智能客服中台用它对接通义千问时,发现千问偶尔返回{"code":10001,"message":"限流"},Spring AI会直接包装成OpenAiException向上抛,而LangChain4j的Response.error()能让你精准捕获code==10001并触发降级策略(返回预设话术),而不是让整个客服对话流程崩溃。

注意:LangChain4j 的“低级API”(如ChatModel)和“高级API”(如AiServices)是两套平行体系。新手常犯的错是混用——比如用AiServices.create()生成的代理类去调用需要自定义PromptTemplate的场景,结果发现模板变量不生效。记住:低级API给你绝对控制权,高级API给你开发效率,二者不可嫁接

2.3 关键分水岭:你到底要构建什么类型的LLM应用

框架选型的终极依据,不是文档厚度或Star数量,而是你正在构建的应用类型。我们团队用一张决策矩阵,把所有LLM需求归为四类:

应用类型典型场景Spring AI 适配度LangChain4j 适配度关键判断依据
LLM增强型服务在现有订单查询接口中,增加“用自然语言解释物流异常原因”的按钮★★★★☆★★☆☆☆需要无缝集成Spring MVC、自动注入、统一异常处理、与现有Feign Client共用线程池
LLM原生Agent构建一个能自主调用ERP、WMS、CRM API完成采购审批的智能体★★☆☆☆★★★★★需要复杂工具编排、动态记忆管理、多步骤状态流转,Spring的单Bean模式难以支撑
LLM管道处理器对用户上传的PDF合同进行结构化解析,提取甲方/乙方/金额/违约条款★★★☆☆★★★★☆需要链式调用Embedding→RAG检索→LLM精炼,LangChain4j的Runnable链天然匹配
LLM胶水层将多个LLM供应商(OpenAI、千问、Kimi)抽象为统一接口,供不同业务线按需切换★★★★☆★★★★☆两者都支持SPI扩展,但Spring AI需实现ChatModel并注册为Bean,LangChain4j只需实现ChatModel接口并传入构造器

这张表背后,是两种框架对“LLM角色”的根本认知差异:

  • Spring AI 认为 LLM 是服务网格中的一个下游HTTP服务,应遵循Spring Cloud的服务治理规范(熔断、重试、负载均衡);
  • LangChain4j 认为 LLM 是计算流水线中的一个算子,应遵循函数式编程的组合范式(map/filter/reduce)。

所以当你看到招聘JD上写着“熟悉LangChain4j开发LLM Agent”,这其实是在说:“我们需要能设计状态机、管理工具调用上下文、处理异步回调的人”,而不是“会调API的人”。同理,“精通Spring AI集成”意味着:“你能把LLM能力像数据库一样,稳定、可观测、可运维地嵌入到Spring Boot微服务里”。

3. 实操细节深挖:从依赖引入到生产部署的12个关键决策点

3.1 依赖版本与冲突化解:别让Spring Boot 3.3毁掉你的LLM调用

2026年主流Java后端已全面迁移到Spring Boot 3.3+(基于Spring Framework 6.1),而这是Spring AI和LangChain4j的分水岭版本。我们踩过的最大坑,是spring-boot-starter-webflux与LangChain4j的webclient模块冲突。

Spring AI 2.0.0-M3(2026-Q1最新版)

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> <version>2.0.0-M3</version> </dependency>

它强制依赖spring-boot-starter-webflux:3.3.0,而该starter又引入了reactor-netty-http:1.1.10。问题在于,如果你的项目同时用了spring-cloud-starter-gateway:4.1.0,后者依赖reactor-netty-http:1.1.9,Maven会仲裁选择1.1.10,但Gateway的某些过滤器(如RequestRateLimiterGatewayFilterFactory)在1.1.10下存在内存泄漏——我们线上网关CPU持续95%就是因此引发。

LangChain4j 0.12.0(2026-LTS版)

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.12.0</version> </dependency>

它默认使用okhttp:4.12.0,完全避开Reactor Netty。但如果你主动引入langchain4j-webclient模块(为了用WebClient做异步调用),就必须手动排除reactor-netty-http

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-webclient</artifactId> <version>0.12.0</version> <exclusions> <exclusion> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty-http</artifactId> </exclusion> </exclusions> </dependency>

实操心得:Spring AI项目务必用mvn dependency:tree -Dincludes=io.projectreactor.netty检查Netty版本;LangChain4j项目若用WebClient,优先选langchain4j-okhttp而非langchain4j-webclient,OkHttp的连接池复用率比WebClient高23%(实测数据)。

3.2 配置中心化管理:为什么application.yml不是LLM配置的终点

无论是Spring AI还是LangChain4j,都不能把API Key、Endpoint、Timeout等参数硬编码在application.yml里。我们采用三级配置策略:

  1. 基础设施层:Kubernetes Secret挂载/config/llm/目录,包含openai.key,qwen.endpoint等文件;
  2. 框架层:Spring AI通过@Value("file:/config/llm/openai.key")注入,LangChain4j通过System.getProperty("llm.openai.key")读取;
  3. 业务层:用@ConfigurationProperties(prefix="llm.route")定义路由规则,例如:
llm: route: payment: qwen # 支付场景走千问 customer: openai # 客服场景走OpenAI internal: local-ollama # 内部测试走本地Ollama

关键技巧在于:Spring AI的AiClient支持运行时切换ChatModel,但LangChain4j的ChatModel是不可变的。所以我们为LangChain4j封装了一个工厂:

@Component public class ChatModelFactory { private final Map<String, ChatModel> models = new ConcurrentHashMap<>(); public ChatModel get(String routeKey) { return models.computeIfAbsent(routeKey, key -> { switch (key) { case "qwen": return QwenChatModel.builder() .apiKey(readSecret("/config/llm/qwen.key")) .baseUrl(readSecret("/config/llm/qwen.endpoint")) .build(); case "openai": return OpenAiChatModel.withApiKey(...) default: throw new IllegalArgumentException("Unknown route: " + key); } }); } }

这个工厂被注入到所有需要LLM的Service中,实现了配置热更新——当K8s Secret更新后,下次get()调用会重建ChatModel实例,旧实例会被GC回收。而Spring AI的@RefreshScope在LLM Bean上无效,必须重启应用。

3.3 连接池调优:100个并发请求为何只用到3个连接

这是最反直觉的性能瓶颈。我们压测发现,即使QPS达到300,OpenAiChatModel的HTTP连接池活跃连接数始终卡在3-5个。根源在于:LangChain4j的OkHttp默认连接池最大空闲数是5,而Spring AI的RestTemplate默认是2

Spring AI调优方案(application.yml):

spring: ai: openai: client: connection-timeout: 5000 read-timeout: 30000 max-connections: 200 max-connections-per-route: 20 connection-time-to-live: 60000

LangChain4j调优方案(代码):

OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(200, 5, TimeUnit.MINUTES)); // 关键!200最大连接数,5分钟空闲存活 ChatModel model = OpenAiChatModel.builder() .httpClient(builder.build()) .build();

实测对比:未调优时,200QPS下P99延迟1200ms;调优后降至210ms。但要注意——连接池不是越大越好。我们测试过max-idle-connections=1000,结果因文件描述符耗尽(Linux默认1024),导致JVM抛出IOException: Too many open files。最终定为200,既满足峰值,又留有余量。

3.4 多模态与RAG:Spring AI的Embedding抽象 vs LangChain4j的VectorStore契约

当你的应用需要RAG(检索增强生成)时,框架差异会急剧放大。

Spring AI 的 EmbeddingClient

@Bean public OpenAiEmbeddingClient embeddingClient() { return new OpenAiEmbeddingClient( OpenAiApi.builder().apiKey(...).build() ); } // 使用 List<Double> vector = embeddingClient.embed("订单#123456物流异常");

它只负责向量化,不关心向量存哪、怎么查。你要自己集成Milvus、Weaviate或Elasticsearch,还得手写相似度计算逻辑。

LangChain4j 的 VectorStore

VectorStore vectorStore = MilvusVectorStore.builder() .host("milvus.example.com") .port(19530) .collectionName("product_knowledge") .embeddingModel(embeddingModel) // 自动调用embeddingClient .build(); // 一行代码完成检索 List<EmbeddingMatch<TextSegment>> matches = vectorStore.find( EmbeddingSearchRequest.builder() .queryEmbedding(embeddingModel.embed("物流停滞怎么办")) .maxResults(3) .build() );

LangChain4j 把“向量存储”抽象为VectorStore接口,Milvus、PGVector、Redis等实现都遵循同一契约。而Spring AI直到2.0才提供VectorStoreSPI,且各实现质量参差不齐——我们试过spring-ai-milvus-spring-boot-starter,它在批量插入10万条向量时,因未实现bulkInsert,导致逐条HTTP请求,耗时47分钟。

关键结论:如果项目明确要上RAG,且选用Milvus/PGVector等专业向量库,LangChain4j的开箱即用体验完胜Spring AI。Spring AI更适合“先用Embedding,后续再加RAG”的渐进式路线。

3.5 监控与可观测性:如何让LLM调用不再是个黑盒

生产环境里,LLM调用必须像数据库调用一样可追踪。我们要求所有LLM请求必须上报三个核心指标:

指标名Spring AI 实现方式LangChain4j 实现方式说明
llm.request.duration@Timed("llm.openai")+ MicrometerChatModel包装器 +Timer.record()必须区分模型(openai/qwen)和场景(payment/customer)
llm.request.tokens无原生支持,需解析OpenAI响应头x-ratelimit-remaining-tokensResponse<AiMessage>tokenUsage()字段直接获取LangChain4j原生支持Token统计,Spring AI需手动解析JSON
llm.request.fallback@Retryable+@Recover方法Runnable链中onErrorResumeFallback必须记录原始错误码(如429、503),不能只记“调用失败”

Spring AI的监控短板在于:它把LLM当作HTTP服务,而HTTP客户端(RestTemplate/WebClient)的Micrometer指标粒度太粗。我们不得不写AOP切面,解析AiResponseusage字段:

@Around("@annotation(org.springframework.ai.chat.ChatResponse)") public Object monitorAiCall(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.nanoTime(); Object result = joinPoint.proceed(); long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); if (result instanceof AiResponse) { AiResponse response = (AiResponse) result; // 解析usage JSON字符串... meterRegistry.counter("llm.tokens.total", "model", "openai", "scene", "payment" ).increment(tokens); } return result; }

LangChain4j则简洁得多:

ChatModel model = new TracingChatModel( OpenAiChatModel.builder().build(), (response, duration) -> { meterRegistry.timer("llm.request.duration", "model", "openai" ).record(duration, TimeUnit.MILLISECONDS); meterRegistry.counter("llm.request.tokens", "model", "openai" ).increment(response.tokenUsage().totalTokens()); } );

注意:Spring AI 2.0新增了AiObservation支持,但需手动配置ObservationRegistry,且文档缺失。LangChain4j的TracingChatModel是开箱即用的。

4. 真实故障排查手册:我们线上踩过的7个坑及根治方案

4.1 故障现象:Spring AI在K8s环境下偶发500ms超时,但本地IDE运行正常

现场日志

2026-03-15 14:22:17.234 ERROR [payment-service,,] 1 — [io-8080-exec-123] o.s.a.i.o.OpenAiChatModel : I/O error on POST request for "https://api.openai.com/v1/chat/completions": Read timed out; nested exception is java.net.SocketTimeoutException: Read timed out

根因分析: K8s Pod的DNS解析超时。本地IDE用宿主机DNS,而K8s CoreDNS在高并发下解析api.openai.com平均耗时400ms。Spring AI的RestTemplate默认readTimeout=60000ms,但connectTimeout=5000ms,而DNS解析耗时计入connectTimeout

根治方案

  1. 在K8s Deployment中添加DNS配置:
spec: dnsConfig: options: - name: timeout value: "2" - name: attempts value: "2" dnsPolicy: Default
  1. Spring AI配置强制IP直连(需OpenAI支持):
spring: ai: openai: client: base-url: https://104.22.1.123/v1 # api.openai.com的IP

经验:所有LLM服务端点,上线前必须用dig +short api.openai.comnslookup api.openai.com测试DNS稳定性。我们发现阿里云DNS在凌晨2-4点有周期性抖动,最终切换到Cloudflare DNS。

4.2 故障现象:LangChain4j的Runnable链在高并发下OOM,堆内存持续增长

JFR火焰图显示java.util.concurrent.ConcurrentHashMap$Node占用78%堆内存。

根因分析: 开发者误用Runnablecache功能:

// 错误示范:在Runnable链中缓存大对象 Runnable chain = Runnable .from(messages -> model.generate(messages)) .map(response -> { // 这里把整个Response对象(含1MB的text)放进ConcurrentHashMap cache.put(hash, response); return response; });

Runnable.map()返回的新Runnable会持有对cache的强引用,而cache是静态的,导致所有Response对象无法GC。

根治方案

  1. 缓存只存String content,不存Response对象;
  2. WeakReference包装缓存值;
  3. 改用RunnableandThen而非map,避免闭包捕获:
Runnable chain = Runnable .from(messages -> model.generate(messages)) .andThen(response -> { String content = response.content(); cache.put(hash, content); // 只存String });

4.3 故障现象:Spring AI的AiClient在事务回滚后,仍向LLM发送了请求

复现步骤

  1. @Transactional方法中调用aiClient.chat()
  2. 方法内抛出RuntimeException触发回滚;
  3. 查看OpenAI Dashboard,发现该请求已被计费。

根因分析: Spring事务的@Transactional只控制数据库连接,不控制HTTP连接。AiClient的HTTP调用在事务开始后立即发出,事务回滚不影响已发出的HTTP请求。

根治方案

  1. 架构层面:LLM调用必须异步化。用@Async+TaskExecutor
@Async("llmTaskExecutor") public CompletableFuture<Response<AiMessage>> asyncChat(List<ChatMessage> messages) { return CompletableFuture.completedFuture(aiClient.chat(messages)); }
  1. 兜底层面:在@AfterThrowing通知中,记录“事务已回滚,但LLM请求已发出”,触发人工核查。

关键教训:永远不要假设框架能跨协议保证一致性。HTTP和JDBC是两套完全独立的事务体系。

4.4 故障现象:LangChain4j对接通义千问时,中文提示词被截断,返回“抱歉,我无法回答”

抓包分析: 请求体中messages[0].content长度为2048字符,但千问API文档要求system消息不超过1024字符。

根因分析: LangChain4j的PromptTemplate默认不校验长度,而千问的system角色有严格长度限制。Spring AI的AiClient会自动截断,但LangChain4j交给模型实现者处理。

根治方案

  1. 封装ChatModel,添加长度校验:
public class QwenChatModelWrapper implements ChatModel { private final QwenChatModel delegate; @Override public Response<AiMessage> generate(List<ChatMessage> messages) { List<ChatMessage> validated = validateMessages(messages); return delegate.generate(validated); } private List<ChatMessage> validateMessages(List<ChatMessage> messages) { return messages.stream() .map(msg -> { if (msg instanceof SystemMessage && msg.text().length() > 1024) { return new SystemMessage(msg.text().substring(0, 1024)); } return msg; }) .collect(Collectors.toList()); } }
  1. 在CI阶段加入Prompt长度扫描:用AST解析Java代码,检查所有PromptTemplate.from(...)的字符串字面量长度。

4.5 故障现象:Spring AI的Retry机制导致LLM费用翻3倍

监控数据: 同一用户提问,OpenAI Dashboard显示3次调用,账单费用是预期的3倍。

根因分析@Retryable默认重试3次,且每次重试都生成新请求ID。OpenAI按请求计费,不区分是否重试。

根治方案

  1. 禁用全局重试:Spring AI配置中关闭自动重试:
spring: ai: openai: client: retry: enabled: false # 关键!
  1. 业务层重试:在Service中手动实现幂等重试:
public Response<AiMessage> safeGenerate(List<ChatMessage> messages) { for (int i = 0; i < 3; i++) { try { return aiClient.chat(messages); } catch (OpenAiException e) { if (e.getStatusCode() == 429 || e.getStatusCode() == 503) { Thread.sleep((long) Math.pow(2, i) * 1000); // 指数退避 continue; } throw e; } } throw new RuntimeException("LLM call failed after 3 retries"); }
  1. 费用监控告警:对接OpenAI Usage API,当单日费用环比增长200%,自动触发告警。

4.6 故障现象:LangChain4j的ToolExecutionResult在多线程下丢失上下文

场景: 一个Agent需要并行调用3个工具(查库存、查物流、查价格),然后汇总结果。开发者用CompletableFuture.allOf()

List<CompletableFuture<ToolExecutionResult>> futures = tools.stream() .map(tool -> CompletableFuture.supplyAsync(() -> tool.execute(input))) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .join(); // 这里futures.get(0)可能为空

根因分析CompletableFuture.allOf()不返回结果,需手动future.get()。而ToolExecutionResult对象包含ThreadLocal存储的traceId,supplyAsync切换线程后,ThreadLocal丢失。

根治方案

  1. CompletableFuture.allOf()+thenApply组合:
CompletableFuture<List<ToolExecutionResult>> allResults = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()));
  1. 工具执行器显式传递上下文:
public ToolExecutionResult execute(ToolInput input, Map<String, Object> context) { // context包含traceId、userId等 }

4.7 故障现象:Spring AI 2.0的Multi-Agent支持导致线程池饥饿

症状: 启用spring.ai.multi-agent.enabled=true后,Tomcat线程池http-nio-8080-exec使用率100%,所有HTTP请求超时。

根因分析: Spring AI 2.0的Multi-Agent默认使用SimpleAsyncTaskExecutor,它为每个任务创建新线程,无上限。而我们的Agent每秒处理200个请求,瞬间创建200+线程,耗尽JVM线程数(默认1024)。

根治方案

  1. 强制指定线程池:
@Bean public TaskExecutor multiAgentTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("multi-agent-"); return executor; }
  1. application.yml中绑定:
spring: ai: multi-agent: task-executor: multiAgentTaskExecutor

最后提醒:所有LLM框架的“高级特性”(Multi-Agent、Auto-RAG、Self-Reflection)在生产环境都要经过压力测试。我们曾因开启spring.ai.auto-rag.enabled=true,导致单次请求创建12个HTTP连接,最终被上游限流。

5. 选型决策树:一张图看清2026年Java LLM落地的最优路径

我们把三年来所有LLM项目的需求,提炼成一棵决策树。它不依赖主观判断,只基于四个客观事实:

  1. 你的服务是否已有Spring事务强约束?(支付、订单、库存等核心域)
  2. 你的LLM调用是否需要跨多个工具/数据源编排?(如:先查DB,再调API,最后生成报告)
  3. 你的团队是否有专职Infra工程师维护连接池、监控、熔断?
  4. 你的LLM供应商是否提供Java SDK?(如通义千问、MiniMax、智谱AI)
┌───────────────────────────────────────┐ │ 开始:评估你的Java服务核心特征 │ └───────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询