如果你最近在 B 站刷过 Spring AI 相关的视频,应该会发现一个奇特现象:标题里全是“保姆级”“最新版”“最细”,弹幕里的问题却高度同质化——DeepSeek API 怎么配?结构化输出到底怎么定义实体类?向量存进 ES 为什么每次都重复?Langchain4j 和 Spring AI 到底该学哪个?
这说明一个事实:到了 2026 年,Java 后端接入大模型已经从“能不能跑通”进入“怎么用好”的阶段。Spring AI 2.0 也早已不是那个只封装几个 API 的玩具框架,而是一整套面向 LLM 应用的工程抽象。真正卡住开发者的,往往不是大模型本身,而是对这套抽象的理解。
这篇文章不打算重复视频里的每一步点击操作,而是把 Spring AI 2.0、Langchain4j 和 Spring AI Alibaba 三者的关系讲透,并用 DeepSeek 作为主力模型,完整走一遍 ChatClient 对话、结构化输出、工具调用、向量存储和 Agent 编排。读完你至少能解决三件事:第一,知道自己该选哪套框架;第二,能照着代码把 DeepSeek 接进 Spring Boot;第三,遇到 400 报错、连接中断、向量重复写入这些典型问题时有明确的排查方向。
1. 为什么 Spring AI 2.0 值得现在学
先说一个判断:Java 后端接入大模型,Spring AI 2.0 已经不再是“可选项”,而是事实上的主路径。
原因很简单。早期的 Java AI 应用,大多是拿 HttpClient 硬拼 OpenAI 或者 DeepSeek 的 REST 接口,把请求体拼成 JSON,再手动解析返回结果。这种写法做一次两次没问题,一旦涉及多轮记忆、工具调用、RAG、Agent,代码就开始失控。你会发现自己不停地在写“组装 Prompt、调用 API、解析 JSON、拼历史消息”的四件套,而且每接一个新模型都要重新来一遍。
Spring AI 2.0 做的事,本质上是把 Spring 生态里成熟的“约定优于配置”“依赖注入”“统一抽象”这套思想搬到了 LLM 应用开发上。你已经熟悉的 Bean、ConfigurationProperties、AOP、事务、缓存,都可以直接作用在大模型调用链路上。比如:
- ChatClient 统一了不同大模型的对话接口,换模型只改配置,不改业务代码。
- 结构化输出让模型直接返回你定义的 Java 实体类,省掉 JSON 字符串解析的脏活。
- @Tool 注解让模型能调用你自己的 Spring Bean 方法,工具注册和依赖注入无缝衔接。
- VectorStore 统一了 Milvus、ES、PGVector 等向量库的读写,RAG 从“引入 SDK + 自己管理集合”变成“配置 + 注入”。
对于国内 Java 开发者,还有一个现实优势:Spring AI Alibaba 把阿里云百炼、DashScope、通义千问、Qwen Embedding 这些国产链路做得非常顺手,和社区常用的 DeepSeek 搭配起来,可以组成一套完全国产、可落地的 AI 后端方案。
2. Spring AI 与 Langchain4j、Spring AI Alibaba 怎么选
这是每篇教程评论区都会吵起来的问题。先说结论:三套东西定位不同,不需要互斥,但主力选型时可以按项目背景决定。
Spring AI 是 VMware 官方(现在是 Broadcom 旗下 Spring 团队)推出的 AI 抽象层,定位是“Spring 生态里的 LLM 开发标准”。它的优点是和 Spring Boot 结合得最紧密,官方维护,社区资料多,未来升级路径清晰。缺点是生态成熟度还在爬坡,部分高级功能比如复杂的 Agent 编排、图流程,需要配合 Spring AI Alibaba 这类扩展才更好用。
Langchain4j 是社区项目,目标是复刻 Python 端 LangChain 的体验。它的优点是 API 设计灵活,功能覆盖全面,文档和示例也不少。缺点是它不属于 Spring 官方,有些抽象和 Spring Boot 的整合需要自己折腾,团队如果对 Spring 官方技术栈有强依赖,引入它会有“两套体系”的撕裂感。
Spring AI Alibaba 是阿里巴巴基于 Spring AI 做的增强包。它不完全是一套独立框架,更像是在 Spring AI 之上补齐了国产模型适配、Graph 编排、AI Studio 可视化调试这些能力。如果你在阿里云百炼上使用通义千问、Qwen Embedding,或者需要把多个 Agent 串成带分支的图流程,这个扩展价值很大。
| 维度 | Spring AI | Langchain4j | Spring AI Alibaba |
|---|---|---|---|
| 维护方 | Spring 官方 | 社区 | 阿里巴巴 + Spring AI 社区 |
| 定位 | LLM 应用统一抽象 | 更贴近 LangChain 的独立框架 | Spring AI 的国产化扩展 |
| Spring Boot 集成度 | 最高 | 中等,需自行适配 | 高 |
| 国产模型支持 | 需配置兼容端点 | 需配置兼容端点 | 开箱即用(百炼/DashScope) |
| Agent/Graph 编排 | 基础支持 | 较丰富 | 提供 Graph 编排和 Studio |
| 适合场景 | 标准 Spring Boot 项目 | 追求 LangChain 风格 API | 国内云厂商模型 + 复杂编排 |
我的建议是:如果是 2026 年新启动的 Java 后端 AI 项目,优先以 Spring AI 为主路径,需要国产模型和复杂编排时引入 Spring AI Alibaba。如果你个人偏好 LangChain 那套 API 风格,Langchain4j 也值得学,但不要在同一个项目里混用两套抽象,维护成本会很高。
3. 环境准备与前置条件
本文的示例代码基于 Spring Boot 3.x + Spring AI 2.0。版本具体以你创建项目时的实际依赖为准,核心思路通用。
需要准备的东西如下:
- JDK 17 或更高版本。Spring Boot 3.x 要求 JDK 17+,这是硬性前提。
- Maven 3.9+ 或 Gradle 8.x,建议优先用 Maven,和 Spring 官方示例一致。
- IDEA 或 VS Code,IDEA 对 Spring 项目体验更好。
- DeepSeek 开放平台账号,注册后创建一个 API Key。
- 可选的 Milvus 或 Elasticsearch,用于第 7 章的向量存储演示。
- 可选的阿里云百炼账号,用于获取 DashScope 的 API Key 和 Qwen Embedding 能力。
由于 Spring AI 的依赖版本更新很快,强烈建议使用 Spring Initializr 或 Aliyun Java Initializr 创建项目,并在依赖管理里引入 Spring AI BOM,避免手动管理一堆版本号。
在 pom.xml 里类似这样:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>其中${spring-ai.version}以你实际使用的版本为准。接下来开始写第一个能跑的程序。
4. 跑通第一个 DeepSeek 对话程序
其实 DeepSeek 接入 Spring AI 有两种常见姿势。第一种是使用 Spring AI 官方对 DeepSeek 的适配(如果当前版本支持);第二种,也是社区最常用的方式,是把 DeepSeek 当成 OpenAI 兼容端点来配置。本文采用第二种,兼容性最稳。
先添加依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> </dependency>然后在 application.yml 里配置 DeepSeek 的 Base URL 和 Key:
spring: application: name: spring-ai-deepseek-demo ai: openai: base-url: https://api.deepseek.com api-key: ${DEEPSEEK_API_KEY} chat: options: model: deepseek-chat temperature: 0.7这里说明一下为什么这么配置。DeepSeek 开放平台对外提供了兼容 OpenAI 协议的接口,所以 Spring AI 的 OpenAI 客户端可以直接复用。base-url指向https://api.deepseek.com,部分接口路径也可以写成https://api.deepseek.com/v1,两者在实际使用中都能通。deepseek-chat是 DeepSeek 的通用对话模型,如果要走推理模型,可以换成deepseek-reasoner。
现在写一个最简单的 Service,用 ChatClient 调用 DeepSeek:
package com.example.demo.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String chat(String message) { return chatClient.prompt(message) .call() .content(); } }注意,这里的ChatClient.Builder由 Spring AI 自动注入,不需要自己创建。如果你的版本中没有自动装配 Builder,可以手动构建,但 2.0 时代基本都默认支持。
接着写一个 Controller 暴露 HTTP 接口:
package com.example.demo.controller; import com.example.demo.service.ChatService; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/chat") public class ChatController { private final ChatService chatService; public ChatController(ChatService chatService) { this.chatService = chatService; } @GetMapping("/{message}") public String chat(@PathVariable String message) { return chatService.chat(message); } }启动项目后,浏览器访问:
http://localhost:8080/api/chat/你好如果配置正确,会收到 DeepSeek 返回的文本。这是整个 Spring AI 2.0 的最小闭环:配置模型端点,注入 ChatClient,调用模型。
这里真正容易踩坑的地方是 API Key 没生效。Spring 会从环境变量${DEEPSEEK_API_KEY}读取,如果你没有设置环境变量,就会启动报错或者调用 401。建议在 IDEA 的 Run Configuration 里配置环境变量,或者直接在 application.yml 里临时写一个值用于本地测试,但生产环境一定要用配置中心或环境变量,千万不要把 Key 提交到 Git。
5. 结构化输出:让模型按你的实体类返回 JSON
按上面的方式调用,模型返回的是字符串。真实项目里,我们需要的是可以直接填充到实体类的对象。
比如做一个订单信息抽取 Agent,用户输入“我要买 3 件白色 T 恤,收货地址是北京市朝阳区”,你希望模型返回的不是一段闲聊文字,而是下面这种结构化结果:
{ "items": [ { "name": "白色T恤", "quantity": 3 } ], "address": "北京市朝阳区" }Spring AI 2.0 中推荐用BeanOutputConverter做这件事。先定义实体类:
package com.example.demo.dto; import java.util.List; public class OrderInfo { private List<Item> items; private String address; public List<Item> getItems() { return items; } public void setItems(List<Item> items) { this.items = items; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public static class Item { private String name; private Integer quantity; public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } } }然后写一个 Service 方法:
package com.example.demo.service; import com.example.demo.dto.OrderInfo; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.converter.BeanOutputConverter; import org.springframework.stereotype.Service; @Service public class OrderExtractService { private final ChatClient chatClient; public OrderExtractService(ChatClient.Builder builder) { this.chatClient = builder.build(); } public OrderInfo extractOrder(String userInput) { BeanOutputConverter<OrderInfo> converter = new BeanOutputConverter<>(OrderInfo.class); String response = chatClient.prompt() .user(userInput) .user("请从用户输入中提取订单信息,严格按照要求的 JSON 格式返回。") .call() .content(); return converter.convert(response); } }这里比较关键的一行是converter.convert(response)。它会把模型返回的 JSON 字符串反序列化成OrderInfo对象,不需要你手动写 ObjectMapper 解析。同时,BeanOutputConverter内部还会生成一段字段说明的 Prompt 提示词,告诉模型应该输出什么样的 JSON 结构,这比你自己手写 Prompt 规范得多。
还有一个容易被忽略的点:结构化输出对模型的选择有要求。如果当前模型返回 JSON 不稳定,可以考虑在 Prompt 里补充“只输出 JSON,不要输出任何解释”,或者在配置里把temperature调低到 0.2 左右,减少随机性,解析成功率会有明显提升。
6. 函数调用与工具调用:给模型加“可执行能力”
结构化输出解决的是“模型怎么回答”,工具调用解决的是“模型怎么帮你做事”。真实场景中,模型本身不能查数据库、不能下单、不能调用第三方接口,它只能“请求”你去执行某个函数,并把执行结果交还给它继续推理。
Spring AI 2.0 中,一个非常实用的做法是使用@Tool注解标记 Spring Bean 的公开方法,让模型在对话中自动决定是否调用它。
比如定义一个订单查询工具:
package com.example.demo.tool; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; @Component public class OrderTools { @Tool(name = "queryOrderStatus", description = "根据订单号查询订单当前状态") public String queryOrderStatus( @ToolParam(description = "订单号") String orderId) { // 真实项目中这里会查数据库或调用订单服务 if ("A1001".equals(orderId)) { return "订单状态:已发货,预计明天送达"; } return "订单状态:不存在"; } }然后在 ChatService 里开启工具调用:
package com.example.demo.service; import com.example.demo.tool.OrderTools; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; @Service public class ToolChatService { private final ChatClient chatClient; public ToolChatService(ChatClient.Builder builder, OrderTools orderTools) { this.chatClient = builder .defaultTools(orderTools) .build(); } public String chat(String userMessage) { return chatClient.prompt(userMessage) .call() .content(); } }这样当用户问“我的订单 A1001 现在什么状态”时,模型会在对话过程中调用queryOrderStatus方法,然后把返回值组织成自然语言回复。
工具调用真正容易出问题的地方是 Bean 注入。defaultTools(orderTools)里传入的对象必须是被 Spring 容器管理的 Bean,否则@Tool注解不会被识别,工具列表为空,模型永远不会触发调用。另一个是描述要写清楚:模型靠description决定什么时候调用这个工具,描述写得太模糊,模型就不知道该不该调。
7. RAG 实战:Qwen Embedding + Milvus/ES 向量存储
RAG(检索增强生成)是当下 Java 后端接入大模型最常见的落地场景。与其让模型凭记忆回答,不如从自己的文档库里检索相关内容,再把检索结果拼进 Prompt,让模型基于这些资料回答。这样回答准确率更高,还能避免模型乱编。
一条典型的 RAG 链路是:
- 文档拆分成小块。
- 用 Embedding 模型把每一块转成向量。
- 向量写入 Milvus 或 Elasticsearch。
- 用户提问时,先把问题转成向量,去向量库检索 TopK 相关片段。
- 把片段和用户问题一起交给 DeepSeek 生成回答。
这里我们选择 Qwen Embedding 来做向量化,理由是它和国产链路配合顺畅,通过阿里云百炼的 DashScope 兼容接口即可调用。
在 application.yml 中追加配置:
spring: ai: embedding: model: openai openai: base-url: https://api.dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} embedding: options: model: text-embedding-v3注意,这里的spring.ai.openai配置和前面 DeepSeek 的配置会发生冲突,因为两者都叫 openai。生产项目中更推荐拆成两个 profile 或者用多个配置前缀隔离,本文示例为了简明,先单独建一个 RAG 相关 profile 演示。
如果你想用 Milvus,先添加依赖:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-milvus</artifactId> </dependency>然后配置 Milvus 连接信息:
spring: ai: vectorstore: milvus: client: host: 127.0.0.1 port: 19530 databaseName: default collectionName: spring_ai_docs metricType: COSINE如果使用 Elasticsearch,则添加:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId> </dependency>配置:
spring: ai: vectorstore: elasticsearch: uri: http://127.0.0.1:9200 indexName: spring-ai-docs代码里直接注入VectorStore就可以操作:
package com.example.demo.service; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.document.Document; import org.springframework.stereotype.Service; import java.util.List; @Service public class RAGService { private final VectorStore vectorStore; public RAGService(VectorStore vectorStore) { this.vectorStore = vectorStore; } public void saveDocument(String content, String docId) { Document doc = Document.builder() .id(docId) .text(content) .build(); vectorStore.add(List.of(doc)); } public List<Document> search(String query, int topK) { return vectorStore.similaritySearch(query, topK); } }这段代码里Document.builder().id(docId).text(content)的 API 形态在不同版本可能有细微差异,但核心思路不变:构造文档、写入向量库、相似性检索。
需要特别提醒的是,ES 作为向量存储时,有一个非常常见的坑:每次执行vectorStore.add(),ES 默认都会追加一条新文档,而不是覆盖旧文档。如果你反复写入同一批业务数据,向量库里会出现大量重复片段。解决办法有几种:
- 写入前先通过
vectorStore.delete(List.of(docId))删除旧的文档 id。 - 自定义 Document id 的生成规则,保证同一业务文档使用稳定的 id。
- 在 ES 索引设计上,把业务主键和向量字段绑定,写入时用 upsert 语义。
很多人在搜索时发现结果重复度高、答案质量差,其实不是模型问题,而是向量库里塞了大量重复内容。这个细节值得在项目初期就想清楚。
8. Spring AI Alibaba:Graph 编排与 Agent 落地
当业务从“单次问答”升级为“多步 Agent 流程”,只靠 ChatClient 明显不够。比如一个客服机器人,需要先判断用户意图,再决定是查订单、走退款流程还是转人工,步骤之间存在分支和条件判断。这种场景适合用 Graph 编排。
Spring AI Alibaba 提供了 Graph 模块,可以让你把 Agent 流程定义成一张图:节点是各种处理能力,边是流转条件。它的设计思路和 LangChain4j 的流程编排类似,但和 Spring AI 的模型抽象、Bean 体系结合更紧密,调试体验也更友好。
具体代码示例在 Spring AI Alibaba Graph 项目中,这里不展开写完整实现,因为不同版本 API 变化较大。需要掌握的核心思想是:
- 每个节点是一个 Java 方法或一个
Runnable。 - 节点之间通过条件边连接,类似状态机。
- 状态信息在节点之间传递,可以是自定义上下文对象。
- 最终输出仍然是统一的模型调用结果或业务结果。
另外 Spring AI Alibaba Studio 提供了图形化调试界面,可以把 Graph 流程可视化,观察每个节点的输入输出。对团队里不太熟悉代码的同事,这种方式比看日志直观得多。
对于普通项目,不需要一上来就上 Graph。先用 ChatClient + 工具调用 + 多轮记忆就能解决大量问题。只有当流程明显复杂、分支很多、需要多人维护时,Graph 编排的收益才会显现。
如果你关注的是“Spring AI 接入本地千问”“本地部署 DeepSeek”,那思路也一样:使用 Ollama 或 vLLM 这类本地推理服务,配置成 OpenAI 兼容端点,Spring AI 的接入方式不需要改变。本地模型和云模型的差异主要在推理速度和成本,不在编程模型。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
调用 DeepSeek 返回 400,提示reasoning_contentin thinking mode 必须回传 | 使用 deepseek-reasoner 推理模型,多轮对话未保存并回传reasoning_content字段 | 查看请求日志中的上下文字段 | 多轮对话时保存上一轮的reasoning_content,在下一次请求中原样传回;或换用deepseek-chat |
| 请求报 401 Unauthorized | API Key 配置错误或未配置 | 检查环境变量和 application.yml | 确认 DeepSeek 开放平台的 Key 是否正确,重新设置环境变量 |
| 模型返回内容经常被截断 | 上下文窗口不足或 maxTokens 太小 | 查看返回 metadata 中的 finish reason | 调整上下文长度限制,增大 maxTokens,或减少历史消息数量 |
| 结构化输出解析失败 | 模型返回了 JSON 以外的说明文字 | 打印模型原始返回内容 | 调整 Prompt 严格模式,降低 temperature,使用 BeanOutputConverter 的格式说明 |
| 工具调用一直不触发 | @Tool 方法所在类未被 Spring 管理,或描述不清晰 | 打印工具列表,确认模型实际拿到的工具定义 | 确保注入 Bean,完善 description 参数 |
| ES 中向量文档重复写入 | VectorStore.add 默认追加 | 检查 ES 索引中的文档 id | 写入前 delete 同 id,或使用稳定的 id 生成策略 |
| 连接中断或 Connection reset | 请求超时、网络不稳定或模型响应时间过长 | 查看服务端日志和网络监控 | 配置 HTTP 客户端超时时间,增加重试机制,对长耗时请求使用异步化 |
| 接入本地千问/DeepSeek 失败 | 本地推理服务未开启 OpenAI 兼容端点 | 用 curl 直接调本地接口验证 | 确认本地服务的兼容端点路径和模型名 |
reasoning_content的问题值得单独强调。DeepSeek 的推理模型deepseek-reasoner在返回结果时,除了正常的content,还会带一个reasoning_content字段,用于记录模型的推理过程。如果你做的系统拿到第一轮结果后,把响应里除了content之外的内容全丢了,第二轮把历史消息发回去时,API 就会因为缺少推理上下文而报 400。这是从热搜词里也能看到的高频问题,排查时先看请求体里的历史消息,再看返回字段是否完整。
10. 最佳实践与工程建议
把框架跑通只是第一步,真正决定项目能不能上生产的是工程细节。
第一,API Key 绝对不能写在代码里。Spring AI 支持环境变量、配置中心、Secrets 管理工具,至少也要用${ENV_VAR}的方式引用。团队协作时,本地开发统一用环境变量模板,生产环境走配置中心。
第二,模型调用要做超时和重试。大模型接口的响应时间波动很大,高峰期可能几十秒。默认 HTTP 客户端如果不设置超时,很容易出现连接池被占满、服务雪崩。建议在调用链路中设置合理的连接超时和读取超时,并对瞬时错误做带退避的重试。
第三,区分多轮记忆和上下文管理。Spring AI 提供了多种 Advisor 来管理聊天记忆,但要清楚记忆是存在内存里还是 Redis 里。生产环境推荐把多轮记忆放在 Redis,而不是应用内存,否则实例重启、水平扩容时用户上下文会丢失。
第四,向量化要控制成本。Embedding 调用是按文本量计费的,文档切块过大浪费 Token,切块过小又丢失语义。建议先按固定大小切块,再结合业务段落边界优化。同时写入向量库前做去重,避免重复数据撑爆存储、拉低检索精度。
第五,日志和链路追踪要设计好。每次模型调用的输入、输出、Token 消耗、耗时都应该记录。排查 Agent 问题时,没有完整日志几乎是寸步难行。建议用 MDC 把 traceId 贯穿整个请求链路,方便串联一次用户请求里的多次模型调用。
第六,安全边界要提前划清。工具调用直接暴露了执行能力,必须做鉴权、限流、操作审计。比如订单查询工具,要校验当前用户是否有权限查看该订单;退款工具,要有二次确认和风控。模型只是决策建议者,真正执行敏感操作前,业务系统必须有自己的校验逻辑。
11. 总结与下一步实践
Spring AI 2.0 真正改变的不是“调 API 的方式”,而是 Java 后端组织 AI 应用的思维方式。ChatClient 解决对话接入,结构化输出解决数据解析,工具调用解决能力扩展,VectorStore 解决知识库检索,Graph 编排解决复杂流程。把这五层理解透,无论未来大模型怎么换,你的工程底座都不会推翻。
建议你按这个顺序动手实践:先跑通第 4 章的 DeepSeek 对话,然后给同一个项目加上结构化输出和工具调用,最后再引入向量存储做 RAG。每一步都验证通过后再进入下一步,不要一上来就想搭建一个完整的 Agent 平台,那样出问题很难定位。
如果你在实践过程中遇到本文没有覆盖的报错,优先做两件事:看模型原始返回日志,确认问题出在请求前还是响应后;用官方 SDK 或 curl 直接调一次同一个接口,判断是链路问题还是框架问题。把这两个问题分清,大部分疑难杂症都能解决。