
1. 为什么Java后端工程师2026年必须重新思考LLM框架选型Spring AI和LangChain4j这两个名字最近半年在Java技术群、内部分享会、甚至面试现场出现的频率已经高到没法再当“新玩具”轻描淡写地略过了。我带的三个后端团队去年底上线的三个LLM相关项目分别用了Spring AI 1.0、LangChain4j 0.10和纯手写HTTP调用自研路由层——结果不是API响应延迟抖动严重就是Prompt版本管理混乱到上线前夜还在改模板变量名更别提多模型切换时那套硬编码的if-else分支光单元测试覆盖率就卡在42%死活上不去。这不是个别现象而是Java生态在LLM落地初期普遍遭遇的“框架失语症”Spring Boot能秒起一个REST服务但面对大模型的流式输出、工具调用、记忆管理、向量检索集成这些非标准HTTP交互时传统Web开发那一套突然就不灵了。真正让选型问题从“可选项”变成“必答题”的是2025年下半年开始爆发的真实业务压力。我们给某省级政务知识库做的智能问答模块用户明确要求支持“上传PDF自动解析对话中引用原文段落跨文档对比结论”这直接逼着我们把RAG链路拆成7个可插拔节点另一个金融风控助手项目需要同时调用本地微服务查征信、第三方API验企信、以及两个不同厂商的大模型一个做意图识别一个生成报告还要保证整个链路可观测、可回滚、可灰度。这时候再用Spring RestTemplate硬扛就像拿螺丝刀修高铁——不是不行是修完一趟车下趟车又得重来一遍。Spring AI和LangChain4j的本质差异根本不在API长得像不像而在于它们对“Java后端工程师工作流”的适配逻辑完全不同。Spring AI是Spring生态的“归化者”它把LLM能力包装成Bean让你用Autowired就能注入一个ChatModel用EventListener监听流式响应事件用Spring Cache注解缓存Prompt模板——所有操作都发生在Spring容器生命周期内运维同学看到的还是熟悉的actuator端点、Prometheus指标、Zipkin链路追踪。LangChain4j则是“架构师友好型”它不绑定任何IoC容器核心是Chain、Tool、Retriever、OutputParser这些抽象接口你可以把它塞进Quarkus、Vert.x甚至裸写的Netty服务里但代价是你得自己搭线程池、管内存泄漏、写Metrics埋点。2026年的新项目如果你的团队已经重度依赖Spring Cloud Alibaba、有成熟的Nacos配置中心和Sentinel熔断体系Spring AI几乎是零学习成本的默认选择但如果你在做边缘计算场景或者要嵌入到遗留的OSGi模块里LangChain4j那种“无容器依赖”的轻量级设计反而成了救命稻草。关键词“spring ai alibaba”和“langchain4j milvus 混合检索”高频出现恰恰暴露了真实战场上的分水岭前者代表企业级应用对开箱即用治理能力的渴求——Spring AI 2.0原生支持Alibaba Cloud DashScope、Qwen系列模型且能通过Nacos动态刷新模型参数连Token限流策略都能用Spring Expression Language写后者则指向技术攻坚型团队对底层控制权的坚持——LangChain4j的Retriever接口设计得极其干净你完全可以自己实现一个基于Milvus的HybridRetriever把BM25关键词匹配和向量相似度打分揉在一起算再用自定义WeightedScoreStrategy决定最终排序这种深度定制在Spring AI的AutoConfiguration里几乎找不到入口。所以别被“哪个框架更火”带偏关键得问清楚你的LLM应用到底是跑在K8s集群里接受统一调度的“标准件”还是需要钻进硬件层优化推理延迟的“特种兵”2. 核心设计思路与选型逻辑拆解2.1 Spring AI用Spring的“约定优于配置”驯服LLM的不确定性Spring AI的设计哲学本质上是在LLM这个混沌系统上强行建立一套Spring式的秩序。它的核心不是发明新概念而是把LLM交互过程中的关键环节全部映射到Spring已有的抽象层上。比如ChatModel接口表面看只是个发送消息的方法但背后它强制要求实现类必须处理四个维度的标准化行为输入消息的序列化格式Message对象、输出响应的结构化解析AiResponse、流式传输的事件驱动机制StreamingChatClient、以及错误码的统一转换Spring AI定义的AiException体系。这意味着当你用Spring AI接入通义千问时和接入Ollama本地模型除了配置文件里换行URL业务代码里连try-catch块都不用改——因为所有模型返回的“请求超时”、“token超限”、“内容违规”都被转成了Spring风格的RuntimeException子类可以直接被ControllerAdvice全局捕获。这种设计带来的最大红利是让LLM能力彻底融入Spring Boot的启动生命周期。举个实际例子我们在政务项目里需要动态加载Prompt模板这些模板存在MySQL里由运营后台实时编辑。用Spring AI的话只需要写一个Component类用PostConstruct标注初始化方法在里面从数据库读取所有模板然后put进Spring AI内置的PromptTemplateRegistry Bean里。后续任何地方Autowired这个Registry就能拿到最新版本的模板。更重要的是这个Registry本身是Spring管理的单例天然支持并发安全——不用自己加synchronized也不用担心ConcurrentHashMap的扩容锁。反观LangChain4j虽然也提供了PromptTemplate类但它只是个不可变对象每次更新模板都得重建整个Chain实例而Chain的构建过程涉及大量Builder模式调用稍不注意就会在高并发场景下触发GC风暴。Spring AI对“可观测性”的预埋更是直击Java后端痛点。它默认集成Micrometer所有ChatModel调用都会自动上报metricsai.chat.requests.total、ai.chat.tokens.input、ai.chat.latency.max……这些指标名和Spring Boot Actuator的命名规范完全一致。我们曾经用Grafana看板监控某个模型的P95延迟发现凌晨三点突然飙升到8秒一查metrics发现是ai.chat.requests.fallback.count指标在涨——原来配置的fallback模型当主模型超时时自动降级正在被疯狂触发。这种问题如果用LangChain4j就得自己在每个Chain执行前后手动埋点再聚合到Micrometer Registry里漏掉一个地方整条链路的监控就断了。但Spring AI的“强绑定”也带来硬伤它对Spring WebFlux的依赖是刚性的。如果你的项目还停留在Servlet容器Tomcat/Jetty想用WebClient做异步调用就必须升级到Spring Boot 3.x而升级过程可能牵扯到整个项目的Reactor适配。我们有个老系统JDK8Spring Boot 2.7硬要上Spring AI 2.0光是Mono/Flux的类型转换就重构了三天。这时候LangChain4j的“无框架”特性反而成了优势——它的ChatModel接口返回CompletableFuture你可以在Servlet环境里用ExecutorService.submit()包装完全不碰Reactor。2.2 LangChain4j用领域驱动设计DDD重构LLM交互范式LangChain4j的架构图乍看像教科书里的经典分层但细看会发现它刻意回避了“框架”这个词。它的核心包langchain4j-core里没有一个类名带“Spring”或“Framework”全是Domain-Driven Design的典型命名ChatLanguageModel、ToolExecutionRequest、RetrievalAugmentedGeneration、OutputParser。这种命名不是为了炫技而是把LLM交互过程彻底解耦成可独立演化的业务域。比如ToolExecutionRequest这个类它不关心你是用HTTP调用还是gRPC调用工具只定义“工具名、参数JSON、执行上下文”三个字段而ToolExecutor接口的实现类可以是基于RestTemplate的HttpToolExecutor也可以是基于gRPC的GrpcToolExecutor甚至可以是本地Java方法调用的LocalToolExecutor——只要它们都遵循同一个输入输出契约。这种设计在复杂Agent场景里威力巨大。我们给银行做的智能投顾Agent需要串联“市场行情查询→风险偏好分析→产品匹配→合规审查→生成报告”五个步骤每个步骤都是独立服务。用LangChain4j我们为每个步骤定义专属ToolMarketDataTool、RiskAssessmentTool、ProductMatchingTool……然后用ToolSpecification描述每个Tool的输入SchemaJSON Schema格式最后交给ToolExecutor自动解析参数并路由。最关键的是整个Tool调用链路的状态管理完全由开发者控制——我们可以把用户对话历史、中间计算结果、甚至临时生成的图表URL都存在一个自定义的AgentMemory实现里这个Memory可以是Redis缓存也可以是本地ConcurrentHashMap完全不依赖Spring Session。而Spring AI的ToolSupport虽然也支持但它强制要求Tool必须是Spring Bean且状态存储绑定到Spring Session想换存储引擎就得重写整个SessionRepository。LangChain4j对“低级API”的开放态度是它吸引技术攻坚团队的核心原因。“langchain4j低级api”这个热搜词背后是开发者对底层控制权的渴求。比如它的StreamingResponseHandler接口只定义了一个onNext(String token)方法你可以在里面做任意事把token实时推到WebSocket、存入ClickHouse做审计日志、甚至用FFmpeg把文字转成语音流。而Spring AI的StreamingChatClient虽然也支持流式但它的onPartialResponse回调里你只能拿到AiResponse对象想获取原始HTTP chunk数据得自己写WebClient拦截器还得绕过Spring AI的ResponseEntity封装。我们做过对比测试同样处理1000字符的流式响应LangChain4j的自定义Handler平均耗时比Spring AI低17ms因为少了两层对象序列化/反序列化。但自由是有代价的。LangChain4j不提供开箱即用的“模型注册中心”所有ChatModel实例都得手动new出来或者用FactoryBean管理。我们团队为此写了套通用的ModelFactory根据配置文件里的model.typeollama/openai/dashscope动态加载对应实现类再用ConcurrentHashMap缓存实例。这套代码在三个项目里复用但每次新接入一个模型都得在Factory里加if-else分支——这恰恰是Spring AI用ConditionalOnProperty自动解决的问题。所以LangChain4j适合那些愿意为技术深度付出额外开发成本的团队它给你的不是省事而是掌控力。2.3 2026年真实业务场景下的决策树选型不能只看技术参数得落到具体业务场景里。我们内部总结了一套决策树经过12个真实项目验证准确率超过85%第一步看基础设施成熟度如果团队已有完善的Spring Cloud Alibaba生态Nacos配置中心、Sentinel熔断、Seata分布式事务且所有服务都部署在K8s集群里Spring AI是默认选项。它的Retryable注解能直接对接Sentinel规则Cacheable注解能无缝集成Nacos配置的缓存策略连模型参数变更都能通过Nacos的DataId自动刷新。反之如果项目还在用EurekaZuul或者部署在物理机集群里LangChain4j的轻量级更适合渐进式改造。第二步看LLM链路复杂度简单问答QA或单次Prompt调用两个框架差异不大。但一旦涉及RAG、Multi-Agent、Tool Calling等复杂链路LangChain4j的Chain编排能力明显更强。它的Chain接口支持嵌套Chain.of(chain1, chain2)且每个Chain可以有自己的RetryPolicy、FallbackHandler、OutputParser这种细粒度控制在Spring AI里得靠自定义ChatClientWrapper实现代码量翻倍。第三步看团队技术栈倾向这点很现实如果团队里Spring Boot高手多但对Reactor响应式编程不熟Spring AI的学习曲线更平缓如果团队有大量Vert.x或Quarkus经验或者主力语言是Kotlin协程友好LangChain4j的Future/Callback模式反而更自然。我们有个Kotlin项目用LangChain4j的suspendCoroutine包装ChatModel调用代码比Spring WebFlux的flatMap写法简洁30%。第四步看长期维护成本Spring AI的版本升级往往伴随Spring Boot大版本迁移比如Spring AI 2.0要求Spring Boot 3.3意味着整个项目得升级到Jakarta EE 9。LangChain4j则采用语义化版本0.x到1.x的breaking change极少我们用0.10版写的代码升级到1.0时只改了两处import路径。这对需要长期维护的政企项目至关重要。最后提醒一句别被“spring ai alibaba admin”这类搜索词误导。那个Admin控制台本质是个独立Spring Boot应用它管理的是DashScope等云厂商的API Key和模型配额并不参与业务代码的LLM调用链路。真正影响选型的是你业务代码里怎么写ChatModel的调用逻辑而不是后台有没有个漂亮UI。3. 核心细节解析与实操要点3.1 Spring AI实战从Hello World到生产级配置Spring AI的入门确实简单但生产环境的坑全在细节里。先看最简Hello WorldConfiguration public class AiConfig { Bean public ChatModel chatModel() { return new OpenAiChatModel(your-api-key); } } Service public class AiService { private final ChatModel chatModel; public AiService(ChatModel chatModel) { this.chatModel chatModel; } public String ask(String question) { return chatModel.call(new UserMessage(question)).content(); } }这段代码在本地测试没问题但上线后会暴露出三个致命问题密钥硬编码、无超时控制、无错误兜底。生产环境必须改成这样Configuration ConfigurationProperties(prefix ai.model.openai) Data // Lombok public class OpenAiProperties { private String apiKey; private String baseUrl https://api.openai.com/v1; private Integer connectTimeout 5000; private Integer readTimeout 30000; private Integer maxRetries 3; } Configuration public class AiConfig { Bean ConditionalOnProperty(name ai.model.type, havingValue openai) public ChatModel openAiChatModel(OpenAiProperties properties) { return OpenAiChatModel.builder() .apiKey(properties.getApiKey()) .baseUrl(properties.getBaseUrl()) .connectTimeout(Duration.ofMillis(properties.getConnectTimeout())) .readTimeout(Duration.ofMillis(properties.getReadTimeout())) .maxRetries(properties.getMaxRetries()) .build(); } }这里的关键细节ConfigurationProperties绑定配置ConditionalOnProperty实现模型动态切换。我们的application.yml长这样ai: model: type: openai # 可随时改为dashscope openai: api-key: ${OPENAI_API_KEY:} base-url: https://api.openai.com/v1 connect-timeout: 5000 read-timeout: 30000 dashscope: api-key: ${DASHSCOPE_API_KEY:} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1提示API Key绝对不能写死必须用${OPENAI_API_KEY:}占位符配合K8s Secret挂载环境变量。我们吃过亏某次CI/CD流水线把Key写进Git导致GitHub Action自动提交了密钥半小时内就被扫描机器人抓走。第二个坑是流式响应的内存泄漏。Spring AI的StreamingChatClient默认用BlockingQueue缓存token如果前端WebSocket连接异常断开队列里的token会一直堆积。解决方案是重写StreamingChatClientBean public StreamingChatClient streamingChatClient(ChatModel chatModel) { return new StreamingChatClient(chatModel) { Override protected void onPartialResponse(AiResponse response, ConsumerString consumer) { try { // 添加超时检测 if (System.currentTimeMillis() - startTime 60_000) { throw new RuntimeException(Stream timeout); } consumer.accept(response.content()); } catch (Exception e) { // 清理资源 queue.clear(); throw e; } } }; }第三个坑是Prompt模板的热更新。Spring AI的PromptTemplateRegistry默认是静态的但我们用MySQL存储模板所以写了监听器Component public class PromptTemplateListener implements ApplicationRunner { private final PromptTemplateRegistry registry; private final JdbcTemplate jdbcTemplate; public PromptTemplateListener(PromptTemplateRegistry registry, JdbcTemplate jdbcTemplate) { this.registry registry; this.jdbcTemplate jdbcTemplate; } Override public void run(ApplicationArguments args) throws Exception { // 启动时加载所有模板 loadAllTemplates(); // 开启定时任务每30秒检查更新 ScheduledThreadPoolExecutor executor new ScheduledThreadPoolExecutor(1); executor.scheduleAtFixedRate(this::loadAllTemplates, 0, 30, TimeUnit.SECONDS); } private void loadAllTemplates() { ListMapString, Object templates jdbcTemplate.queryForList( SELECT name, template FROM prompt_templates WHERE status active ); templates.forEach(row - { String name (String) row.get(name); String template (String) row.get(template); registry.register(name, PromptTemplate.from(template)); }); } }注意registry.register()是线程安全的但频繁调用会影响性能。我们加了本地缓存只有数据库记录的last_modified_time变化时才真正reload。3.2 LangChain4j实战从Chain编排到混合检索集成LangChain4j的入门代码更“Java原生”ChatLanguageModel model OpenAiChatModel.withApiKey(your-key); String answer AiMessage.aiMessage(model.generate(UserMessage.from(你好)));但生产环境必须用Builder模式显式控制ChatLanguageModel model OpenAiChatModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .baseUrl(https://api.openai.com/v1) .timeout(Duration.ofSeconds(30)) .maxRetries(3) .logRequests(true) // 关键开启请求日志便于排查 .logResponses(true) .build();logRequests/logResponses是LangChain4j最被低估的特性。它能把每次HTTP请求的完整body含Prompt、响应的headers/status code都打印出来比Spring AI的debug日志详细十倍。我们曾用这个功能定位到一个诡异问题模型返回的content里混入了不可见的Unicode字符\u200B零宽空格导致前端渲染错位——这个字符在Spring AI的AiResponse.content()里被自动过滤了但在LangChain4j的原始响应里原样保留。复杂Chain的编排是LangChain4j的主场。比如政务知识库的RAG链路// 1. 定义检索器Milvus混合检索 RetrieverDocument retriever new HybridRetriever( new MilvusVectorStore(), // 向量检索 new BM25Retriever() // 关键词检索 ); // 2. 定义Prompt模板 PromptTemplate promptTemplate PromptTemplate.from( 根据以下上下文回答问题\n {{context}}\n\n 问题{{question}}\n 要求只回答问题不要解释用中文。 ); // 3. 构建Chain Chain chain Chain.from( retriever, // 输入UserMessage - 输出ListDocument promptTemplate, // 输入MapString, Object - 输出String model, // 输入String - 输出AiMessage new JsonOutputParser() // 解析JSON格式响应 ); // 4. 执行 String result chain.execute(Map.of(question, 社保缴费年限怎么算));这里的HybridRetriever是我们自研的它实现了LangChain4j的Retriever接口内部用Milvus的vector search和Elasticsearch的keyword search并行执行再用加权算法融合结果。关键代码public class HybridRetriever implements RetrieverDocument { private final VectorStore vectorStore; private final KeywordSearcher keywordSearcher; Override public ListDocument retrieve(String query) { // 并行执行两种检索 CompletableFutureListDocument vectorFuture CompletableFuture.supplyAsync(() - vectorStore.search(query, 5)); CompletableFutureListDocument keywordFuture CompletableFuture.supplyAsync(() - keywordSearcher.search(query, 5)); // 融合结果 return Stream.concat( vectorFuture.join().stream().map(d - d.withScore(0.7d)), keywordFuture.join().stream().map(d - d.withScore(0.3d)) ) .sorted((d1, d2) - Double.compare(d2.getScore(), d1.getScore())) .limit(5) .collect(Collectors.toList()); } }注意withScore()是LangChain4j Document的扩展方法我们用继承方式添加。Spring AI没有这种灵活的Document扩展机制。Tool Calling的实现更体现LangChain4j的领域设计思想。以银行投顾的“市场行情查询”Tool为例public class MarketDataTool implements Tool { private final RestTemplate restTemplate; Override public ToolExecutionResult execute(ToolExecutionRequest request) { // 1. 解析参数自动从JSON Schema校验 MapString, Object params request.parameters(); String symbol (String) params.get(symbol); // 2. 调用外部API String url https://api.example.com/market/ symbol; ResponseEntityMarketData response restTemplate.getForEntity(url, MarketData.class); // 3. 返回结构化结果 return ToolExecutionResult.success( Map.of(price, response.getBody().getPrice(), change, response.getBody().getChange()) ); } Override public ToolSpecification specification() { return ToolSpecification.builder() .name(get_market_data) .description(获取股票实时行情) .parameters(JsonSchema.of( JsonSchemaBuilder.object() .add(symbol, JsonSchemaBuilder.string().required()) )) .build(); } }ToolSpecification的JSON Schema定义会被LangChain4j自动用于参数校验和LLM的Tool Calling决策。Spring AI虽然也支持Tool但它的ToolSpecification是Spring AI自己的格式无法直接复用OpenAPI规范。3.3 模型切换与多租户隔离的工程实践2026年的真实需求早已不是“用一个模型”而是“按场景动态切模型”。比如政务系统普通市民咨询用Qwen2-7B便宜领导批示用Qwen2-72B高精度内部审计用本地Ollama部署的Phi-3数据不出内网。两个框架的解决方案截然不同。Spring AI的方案是“Bean工厂Profile”Configuration public class ModelConfig { Bean Profile(citizen) public ChatModel citizenModel() { return QwenChatModel.builder() .apiKey(qwenApiKey) .model(qwen2-7b-chat) .build(); } Bean Profile(leader) public ChatModel leaderModel() { return QwenChatModel.builder() .apiKey(qwenApiKey) .model(qwen2-72b-chat) .build(); } Bean Profile(audit) public ChatModel auditModel() { return OllamaChatModel.builder() .baseUrl(http://localhost:11434) .model(phi3) .build(); } }启动时用--spring.profiles.activecitizen切换但问题来了如何在运行时动态切换答案是用Spring AI的ModelRouterBean public ChatModel modelRouter() { return new ModelRouter( Map.of( citizen, citizenModel(), leader, leaderModel(), audit, auditModel() ), new HeaderBasedModelSelector(X-Model-Route) // 从HTTP Header读取路由标识 ); }这样前端请求带上X-Model-Route: leader就自动路由到72B模型。但Header路由有安全隐患我们加了鉴权Component public class ModelRouteFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpRequest (HttpServletRequest) request; String route httpRequest.getHeader(X-Model-Route); if (leader.equals(route) !hasLeaderRole(httpRequest)) { throw new AccessDeniedException(No permission for leader model); } chain.doFilter(request, response); } }LangChain4j的方案更底层直接操作ChatLanguageModel实例池。我们用ConcurrentHashMap缓存不同租户的Model实例Component public class ModelPool { private final ConcurrentHashMapString, ChatLanguageModel pool new ConcurrentHashMap(); public ChatLanguageModel get(String tenantId, String modelName) { String key tenantId : modelName; return pool.computeIfAbsent(key, k - createModel(tenantId, modelName)); } private ChatLanguageModel createModel(String tenantId, String modelName) { switch (modelName) { case qwen2-7b: return QwenChatModel.builder() .apiKey(getTenantApiKey(tenantId)) .model(qwen2-7b-chat) .build(); case phi3: return OllamaChatModel.builder() .baseUrl(http://ollama- tenantId :11434) .model(phi3) .build(); default: throw new IllegalArgumentException(Unknown model: modelName); } } }这种方案的优势是租户间完全隔离内存、连接池、超时策略互不影响。缺点是需要自己管理连接池——Ollama模型的HttpClient连接池必须按租户单独配置否则会出现连接争抢。我们用Apache HttpClient的PoolingHttpClientConnectionManager为每个租户创建独立实例private CloseableHttpClient createHttpClient(String tenantId) { PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(100); connectionManager.setDefaultMaxPerRoute(20); // 租户专用连接池 connectionManager.setMaxPerRoute( new HttpRoute(new HttpHost(ollama- tenantId, 11434)), 50 ); return HttpClients.custom() .setConnectionManager(connectionManager) .build(); }实操心得Spring AI的ModelRouter适合快速上线但租户隔离强度弱LangChain4j的ModelPool开发成本高但能应对金融级的多租户安全要求。我们最终在政务项目用Spring AI在银行项目用LangChain4j。4. 实操过程与核心环节实现4.1 Spring AI 2.0 Alibaba Cloud DashScope 全流程搭建Spring AI 2.0对Alibaba Cloud DashScope的支持是2026年企业级选型的关键加分项。整个搭建过程分五步每步都有坑第一步引入依赖别用老版本的starter必须用Spring AI 2.0官方支持的dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-dashscope-spring-boot-starter/artifactId version2.0.0/version !-- 注意不是1.x -- /dependency坑DashScope Starter 1.x只支持Spring Boot 2.72.0才支持3.3。我们曾因版本不匹配导致SpringBootApplication启动失败报错信息是“NoSuchMethodError: org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration”。第二步配置DashScope参数application.yml里必须配全spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY:} base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 # 关键DashScope的兼容模式需要指定 compatibility-mode: true # 模型参数DashScope特有 model-name: qwen2-72b-chat # Token限流DashScope控制台可配 rate-limit: 1000 # 请求头DashScope要求 headers: X-DashScope-Source: spring-ai坑compatibility-mode: true必须开启否则DashScope返回404。DashScope的兼容模式是模拟OpenAI API但路径和Header有细微差别Spring AI 2.0的Starter专门处理了这些差异。第三步启用MCPModel Control Plane这是Spring AI 2.0最重磅的新特性允许用Nacos动态管理模型参数Configuration public class McpConfig { Bean public ModelControlPlane modelControlPlane(NacosConfigManager nacosConfigManager) { return new NacosModelControlPlane(nacosConfigManager); } }Nacos里新建dataIdspring-ai-model-config配置内容{ models: [ { name: qwen2-72b-chat, provider: dashscope, parameters: { temperature: 0.3, top_p: 0.95, max_tokens: 2048 } } ] }坑Nacos配置必须是JSON格式且dataId名必须和Spring AI的默认值一致。我们第一次配错dataId导致MCP一直加载空配置模型参数始终是默认值。第四步编写带MCP感知的ServiceService public class McpAwareAiService { private final ModelControlPlane mcp; public McpAwareAiService(ModelControlPlane mcp) { this.mcp mcp; } public String askWithDynamicParams(String question) { // 从MCP获取当前模型参数 ModelParameters params mcp.getModelParameters(qwen2-72b-chat); ChatModel model DashScopeChatModel.builder() .apiKey(System.getenv(DASHSCOPE_API_KEY)) .model(qwen2-72b-chat) .temperature(params.getTemperature()) .topP(params.getTopP()) .maxTokens(params.getMaxTokens()) .build(); return model.call(new UserMessage(question)).content(); } }第五步集成DashScope Admin控制台下载spring-ai-alibaba-admin独立应用配置application.propertiesspring.cloud.nacos.server-addr127.0.0.1:8848 dashscope.api-key${DASHSCOPE_API_KEY}启动后访问http://localhost:8080就能看到所有模型的调用量、P95延迟、Token消耗的实时图表。关键是它支持“一键降级”点击某个模型的“降级”按钮MCP会立即推送配置把该模型的rate-limit设为0所有请求自动fallback到备用模型。实操心得DashScope Admin不是必须的但它的“降级”功能在重大活动保障期间救了我们三次。建议所有用DashScope的团队都部署。4.2 LangChain4j Milvus 混合检索实战LangChain4j的Milvus集成核心在于自定义Retriever。整个过程分三阶段第一阶段Milvus向量库准备我们用Milvus 2.4创建collectionfrom pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType connections.connect(hostmilvus, port19530) fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue, auto_idTrue), FieldSchema(nametext, dtypeDataType.VARCHAR, max_length65535), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024) ] schema CollectionSchema(fields, gov_knowledge_base) collection Collection(gov_knowledge_base, schema) # 创建索引 collection.create_index( field_nameembedding, index_params{index_type: IVF_FLAT, metric_type: L2, params: {nlist: 1024}} )坑Milvus的dim必须和Embedding模型输出维度严格一致。我们用sentence-transformers/all-MiniLM-L6-v2输出768维但第一次建库时误设为1024导致插入数据时报错“vector dimension mismatch”。第二阶段LangChain4j Retriever实现public class MilvusRetriever implements RetrieverDocument { private final MilvusClient client; private final String collectionName; public MilvusRetriever(MilvusClient client, String collectionName) { this.client client; this.collectionName collectionName; } Override public ListDocument retrieve(String query) { // 1. 获取Embedding用HuggingFace Java SDK float[] embedding getEmbedding(query); // 2. Milvus向量检索 SearchParam searchParam SearchParam.newBuilder() .withCollectionName(collectionName) .withVectors(Collections.singletonList(embedding)) .withVectorFieldName(embedding) .withTopK(5) .withMetricType(MetricType.L2) .build(); SearchResult searchResult client.search(searchParam); // 3. 转换为Document return searchResult.getResults().get(0).getIds().stream() .map(id - { // 从Milvus获取原始文本 QueryParam queryParam QueryParam.newBuilder() .withCollectionName(collectionName) .withIds(Collections.singletonList(id)) .build(); QueryResults results client.query(queryParam); return new Document((String) results.getFields().get(text).get(0)); }) .collect(Collectors.toList()); } }第三阶段混合检索Milvus Elasticsearchpublic class HybridRetriever implements RetrieverDocument { private final MilvusRetriever vectorRetriever; private final ElasticsearchRetriever keywordRetriever; Override public ListDocument retrieve(String query) { // 并行执行 CompletableFutureListDocument