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(); } }这段代码看似简单,但它背后藏着三层契约:
生命周期契约:
OpenAiChatModel和AiClient都是单例Bean,由Spring容器全权管理其创建、初始化、销毁。这意味着你不能在方法内new一个AiClient来规避线程安全问题——Spring会把它当作非法操作拦截(通过@Scope("prototype")强行绕过会导致连接池泄漏)。线程模型契约:
AiClient内部使用的RestTemplate或WebClient,其底层HTTP连接池(如Apache HttpClient的PoolingHttpClientConnectionManager)也是Spring托管的Bean。它的最大连接数、保活时间、路由并发数,全部受application.yml中spring.ai.openai.client.*配置控制。我们曾因未配置max-connections-per-route=10,导致在200QPS下连接池耗尽,所有LLM请求排队等待,而数据库连接池却空闲——因为两者用的是完全独立的连接池。事务穿透契约:这是最容易被忽略的致命点。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);这种写法带来的工程价值,体现在三个硬性保障上:
线程安全零假设:
ChatModel实现类(如OpenAiChatModel)内部所有状态(API Key、Base URL、Options)都是final的,构造时确定,运行时不可变。这意味着你可以放心地把它作为static final字段放在工具类里,或者在每个请求线程内new一个实例——性能损耗微乎其微,但彻底规避了共享状态引发的竞争条件。调用链可控性: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的集成。
- 错误边界清晰: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里。我们采用三级配置策略:
- 基础设施层:Kubernetes Secret挂载
/config/llm/目录,包含openai.key,qwen.endpoint等文件; - 框架层:Spring AI通过
@Value("file:/config/llm/openai.key")注入,LangChain4j通过System.getProperty("llm.openai.key")读取; - 业务层:用
@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: 60000LangChain4j调优方案(代码):
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")+ Micrometer | ChatModel包装器 +Timer.record() | 必须区分模型(openai/qwen)和场景(payment/customer) |
llm.request.tokens | 无原生支持,需解析OpenAI响应头x-ratelimit-remaining-tokens | Response<AiMessage>中tokenUsage()字段直接获取 | LangChain4j原生支持Token统计,Spring AI需手动解析JSON |
llm.request.fallback | @Retryable+@Recover方法 | Runnable链中onErrorResume | Fallback必须记录原始错误码(如429、503),不能只记“调用失败” |
Spring AI的监控短板在于:它把LLM当作HTTP服务,而HTTP客户端(RestTemplate/WebClient)的Micrometer指标粒度太粗。我们不得不写AOP切面,解析AiResponse的usage字段:
@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。
根治方案:
- 在K8s Deployment中添加DNS配置:
spec: dnsConfig: options: - name: timeout value: "2" - name: attempts value: "2" dnsPolicy: Default- 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.com和nslookup api.openai.com测试DNS稳定性。我们发现阿里云DNS在凌晨2-4点有周期性抖动,最终切换到Cloudflare DNS。
4.2 故障现象:LangChain4j的Runnable链在高并发下OOM,堆内存持续增长
JFR火焰图显示:java.util.concurrent.ConcurrentHashMap$Node占用78%堆内存。
根因分析: 开发者误用Runnable的cache功能:
// 错误示范:在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。
根治方案:
- 缓存只存
String content,不存Response对象; - 用
WeakReference包装缓存值; - 改用
Runnable的andThen而非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发送了请求
复现步骤:
- 在
@Transactional方法中调用aiClient.chat(); - 方法内抛出
RuntimeException触发回滚; - 查看OpenAI Dashboard,发现该请求已被计费。
根因分析: Spring事务的@Transactional只控制数据库连接,不控制HTTP连接。AiClient的HTTP调用在事务开始后立即发出,事务回滚不影响已发出的HTTP请求。
根治方案:
- 架构层面:LLM调用必须异步化。用
@Async+TaskExecutor:
@Async("llmTaskExecutor") public CompletableFuture<Response<AiMessage>> asyncChat(List<ChatMessage> messages) { return CompletableFuture.completedFuture(aiClient.chat(messages)); }- 兜底层面:在
@AfterThrowing通知中,记录“事务已回滚,但LLM请求已发出”,触发人工核查。
关键教训:永远不要假设框架能跨协议保证一致性。HTTP和JDBC是两套完全独立的事务体系。
4.4 故障现象:LangChain4j对接通义千问时,中文提示词被截断,返回“抱歉,我无法回答”
抓包分析: 请求体中messages[0].content长度为2048字符,但千问API文档要求system消息不超过1024字符。
根因分析: LangChain4j的PromptTemplate默认不校验长度,而千问的system角色有严格长度限制。Spring AI的AiClient会自动截断,但LangChain4j交给模型实现者处理。
根治方案:
- 封装
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()); } }- 在CI阶段加入Prompt长度扫描:用AST解析Java代码,检查所有
PromptTemplate.from(...)的字符串字面量长度。
4.5 故障现象:Spring AI的Retry机制导致LLM费用翻3倍
监控数据: 同一用户提问,OpenAI Dashboard显示3次调用,账单费用是预期的3倍。
根因分析:@Retryable默认重试3次,且每次重试都生成新请求ID。OpenAI按请求计费,不区分是否重试。
根治方案:
- 禁用全局重试:Spring AI配置中关闭自动重试:
spring: ai: openai: client: retry: enabled: false # 关键!- 业务层重试:在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"); }- 费用监控告警:对接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丢失。
根治方案:
- 用
CompletableFuture.allOf()+thenApply组合:
CompletableFuture<List<ToolExecutionResult>> allResults = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()));- 工具执行器显式传递上下文:
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)。
根治方案:
- 强制指定线程池:
@Bean public TaskExecutor multiAgentTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(100); executor.setThreadNamePrefix("multi-agent-"); return executor; }- 在
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项目的需求,提炼成一棵决策树。它不依赖主观判断,只基于四个客观事实:
- 你的服务是否已有Spring事务强约束?(支付、订单、库存等核心域)
- 你的LLM调用是否需要跨多个工具/数据源编排?(如:先查DB,再调API,最后生成报告)
- 你的团队是否有专职Infra工程师维护连接池、监控、熔断?
- 你的LLM供应商是否提供Java SDK?(如通义千问、MiniMax、智谱AI)
┌───────────────────────────────────────┐ │ 开始:评估你的Java服务核心特征 │ └───────────────────────────────────────┘ ↓ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────