1. 为什么要在 Spring Boot 3.x 里做 AI 智能体先说一个这两年我观察到的现象不少 Java 后端团队看到 Python 生态里 LangChain、LlamaIndex 满天飞第一反应是“完了Java 是不是要被 AI 时代淘汰了”。然后转头一看公司的核心业务系统全是 Spring Boot用户数据、订单系统、权限体系都在 Java 这边。让 AI 能力跑在 Python 服务里再通过 HTTP 和 Java 服务来回调也不是不行但多了一层网络开销不说链路长了排查问题也麻烦。Spring Boot 3.x 集成 AI 智能体解决的就是这个问题。把大模型的能力当成一个普通 Bean 注入到你的 Spring 容器里和注入 JdbcTemplate、RedisTemplate 没什么本质区别。业务代码里该 Service 就 Service该 Transactional 就 TransactionalAI 调用只是其中一个环节。对 Java 团队来说学习成本低维护成本也可控不需要维护两套技术栈。这个主题适合谁我建议这几类人重点看后端 Java 工程师想在自己熟悉的技术栈里接入大模型又不想额外维护 Python 服务。技术负责人正在评估给现有业务系统加 AI 能力要投入多少成本。对 AI Agent智能体概念感兴趣但不想一上来就学一堆新框架的开发者。在正式动手之前我先把核心结论放前面这个方案能跑通而且生产可用的关键不在“调 API”而在“让模型能调你的方法”。我后面会花大篇幅讲这个。2. 方案选型别急着写代码先把框架看清楚2.1 三个主流的 Java AI 框架怎么选目前 Java 生态里做 AI 集成绕不开三个选项Spring AI、LangChain4j、以及直接手写 HTTP 调用各家大模型 API。直接手写 HTTP 调用我就不多说了简单场景能用但一旦涉及多轮对话、工具调用、流式输出、上下文记忆代码量会指数级膨胀而且每家模型的接口格式还不一样。除非只是做个小 Demo否则不建议。Spring AI 是 Spring 官方团队出的项目思路和 Spring Data 很像——把各种大模型提供方抽象成统一接口。你今天接的是通义千问明天要换 DeepSeek改一行配置就行。API 风格也是 Spring 程序员熟悉的 ChatClient、PromptTemplate 这套学习曲线很平缓。LangChain4j 则是社区项目理念更接近 Python 的 LangChainAgent、Memory、RAG 这些概念模型都给封装好了功能丰富但抽象层更厚出问题时调试链路更长。我自己的建议是如果你的团队已经深度使用 Spring Boot优先选 Spring AI。它的版本迭代确实快API 变化也频繁但胜在和后端框架的契合度高遇到问题社区也活跃。LangChain4j 更适合想用更“Agent 原生”的开发体验、且团队有充足时间踩坑的场景。2.2 Spring AI 的版本和模型兼容性Spring AI 的版本从 0.8.x 一路迭代到 1.0.0 GA 之后 API 才算基本稳定。如果项目刚起步请直接上 1.0.0 或更新版本别去碰老版本的教程——我看过太多从 0.9 迁移到 1.0 时被改名改到怀疑人生的案例。目前模型支持这块Spring AI 已经覆盖了主流的 OpenAI、通义千问、DeepSeek、智谱、Ollama 本地模型等等。通过 spring-ai-starter-model- 前缀的依赖区分。大部分国内模型服务都是 OpenAI 兼容协议所以哪怕官方没有单独出 Starter也可以用 OpenAI 的配置指向你的服务商地址只是要改一下 base-url。这里顺便说一个我从同事那里听来的坑Spring AI 对同一模型提供方的支持也有差异比如某些国产模型的 Function Calling 虽然兼容 OpenAI 格式但在工具描述过长时会被模型忽略导致 Agent 不调工具。这个我后面在“常见问题”章节会细讲。3. 项目搭建与基础集成3.1 创建工程和镜像源配置新建一个 Spring Boot 3.3.x 或 3.4.x 的项目Java 版本建议 17 以上我习惯用 21因为虚拟线程对高并发场景有帮助。构建工具可以用 Maven 或 Gradle我自己用 Maven 多一点配置直观。依赖这块官方文档推荐用 BOM 统一管理版本dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后加你需要的模型依赖比如用通义千问dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-dashscope/artifactId /dependency国内环境下Maven 中央仓库直接拉 Spring AI 的包可能比较慢实测经常卡在下载那几 MB 的 jar 上。建议在 Maven 的 settings.xml 里配置阿里云镜像仓库速度提升非常明显我身边几乎所有团队都是这么干的。注意Spring Boot 3.x 和 Spring AI 的版本兼容矩阵要核对清楚新版本 Spring Boot 不一定能直接用最新版 Spring AI。如果启动时报 NoSuchBeanDefinitionException 或者类似的问题先查版本。3.2 配置模型参数并跑通第一个对话在 application.yml 里配置模型的 API Key 和基础参数spring: ai: dashscope: api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-plus temperature: 0.7API Key 强烈建议用环境变量注入不要硬编码在配置文件里。项目代码一旦上传到 Git 仓库Key 泄漏就是安全事故。然后写一个最基础的服务类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 是 Spring AI 的核心门面它把 Prompt、模型调用、响应解析全包了。运行一个测试输入“用一句话介绍你自己”能看到模型正常返回集成就算通了。3.3 Prompt 模板和结构化输出硬编码字符串拼接 Prompt 是最原始的玩法稍微复杂一点就会出问题。比如你要做一个“旅游攻略生成器”Prompt 可能是你是一个旅游规划师请为去{城市}旅游的用户生成一份{天数}天的攻略风格要求{风格}这里的城市、天数、风格都是变量用 PromptTemplate 来处理PromptTemplate template new PromptTemplate( 你是一个旅游规划师请为去{city}旅游的用户生成一份{days}天的攻略 风格要求{style} ); Message message template.createMessage(Map.of( city, 成都, days, 3, style, 轻松休闲美食优先 ));还有一个很实用但容易被忽略的功能结构化输出。默认情况下模型返回的是自然语言文本你得自己写正则或者调用另一个模型去解析麻烦且不稳定。Spring AI 支持让模型直接输出 JSON 并映射到 Java 对象。比如你做一个“招聘简历初筛 Agent”希望模型返回候选人的技能列表和匹配度评分public record CandidateEvaluation( String name, ListString skills, int matchScore, String suggestion ) {} ChatClient chatClient ...; CandidateEvaluation result chatClient.prompt() .user(候选人的简历内容是...) .call() .entity(CandidateEvaluation.class);这里内部逻辑是Spring AI 在生成 Prompt 时会追加“请以 JSON 格式输出字段分别为 name, skills, matchScore, suggestion”的指令然后把模型的输出反序列化成对象。我踩过一个坑record 的字段名如果和模型输出的 key 不一致会解析失败。这时候要么统一命名要么用 JsonProperty 指定别名。4. 核心机制Function Calling 让模型有手有脚4.1 为什么单纯的“聊天”不算智能体把上面的对话、模板、结构化输出都做了充其量叫做“带 AI 功能的 Web 服务”和智能体还差一层窗户纸——模型只能生成文本不能真正触发业务动作。举个例子你做了一个“企业 IT 工单助手”用户说“帮我查一下工单 INC20240101 的处理进度”。如果模型不认识工单系统它只能回一句“很抱歉我暂时无法查询工单进度”。这就是为什么很多大模型聊得头头是道一落到实际业务就哑火。Function Calling工具调用解决了这个问题模型在回答之前会先生成一个结构化的“调用请求”告诉你的程序“我需要调用 getTicketStatus 这个函数参数是 INC20240101”。你的程序负责真正执行这个函数然后把结果回传给模型模型再基于结果组织最终回答。理解了这个机制你就掌握了智能体的技术核心模型是“大脑”负责推理和决策函数是你业务系统的“手脚”负责执行。4.2 用 Tool 注解暴露业务方法Spring AI 给 Function Calling 做了很友好的封装。在 Spring AI 1.0 里你只需要在 Bean 的方法上加上 Tool 注解然后注册给 ChatClient 即可。比如给工单助手写一个查询方法Service public class TicketTools { Tool(description 根据工单号查询工单当前状态输入参数为工单号) public String getTicketStatus(String ticketId) { // 这里调用真实的工单系统接口或者查数据库 return ticketService.queryStatus(ticketId); } }然后在构造 ChatClient 的时候把 Tool 关联进去Bean ChatClient chatClient(ChatClient.Builder builder, TicketTools ticketTools) { return builder .defaultTools(ticketTools) .build(); }当用户问“工单 INC20240101 什么进度”系统内部执行流程是模型收到问题发现需要工具输出一个函数调用指令类似 {“name”: “getTicketStatus”, “arguments”: {“ticketId”: “INC20240101”}}。Spring AI 拦截这个指令找到对应的 Tool 方法并调用。方法返回真实工单状态字符串“处理中预计今天 18:00 前解决”。这个结果被附加到对话上下文中模型继续生成最终回答“您好工单 INC20240101 当前状态为处理中预计今天 18:00 前解决。”整个过程用户是感知不到的体感上就是 AI 真的“查”了工单系统。4.3 多工具协作与参数类型选择真实业务里不可能只有一个工具。一个售前咨询机器人需要查库存、算运费、查优惠券、创建订单…… 这些方法都暴露出去。这里有几个实操注意事项第一工具方法的参数类型尽量用简单类型。Spring AI 通过解析方法签名生成模型的 JSON Schema如果参数是一个嵌套的复杂对象模型大概率生成不出正确的 JSON 参数。我踩过最狠的一次定义一个参数为 ListMapString, Object 的工具十次调用里有八次参数是空的。后来改成 String 类型先把 JSON 传进去再手动解析稳如老狗。第二Tool 的 description 要写得足够清楚。模型靠这段描述来决定“什么情况下调用这个方法”描述写得太笼统模型该调用的时候不调用写得太复杂模型又会过度调用。一个比较靠谱的摸板是“当用户需要查询X时使用输入参数为Y返回值是Z”。把触发条件、入参语义、返回内容都讲清楚。第三工具内部要做超时和容错。模型在业务高峰可能并发调用工具你的方法要有熔断机制失败时返回一个友好的错误信息而不是抛出异常让整个链路崩溃。5. 实战搭建一个多工具智能客服5.1 业务场景和预期效果这一节我拿一个模拟项目 X 来演示一个电商平台的售前客服机器人。这个机器人的目标是回答用户的商品咨询、库存查询、优惠计算和简单订单创建。先定义清楚这个智能体要会什么用户问“这款手机还有货吗”Agent 调库存接口。用户说“买两个能便宜多少”Agent 算优惠。用户说“帮我下一单”Agent 校验收货地址后创建订单。注意最后一件事——创建订单涉及写操作比单纯查询风险高得多。生产环境里对这类工具要有额外的确认机制比如先让模型生成一个“待确认订单”JSON 返回给前端由前端用户点击确认后再调用真正的下单方法。我在项目里就是这么干的用户体验上只多了一次点击但安全性提升了一个量级。5.2 编排多个工具方法的完整示例工具层把库存、优惠、订单三个 Service 包装成可被 Tool 识别的方法Service public class EcommerceTools { private final InventoryService inventoryService; private final PriceService priceService; private final OrderService orderService; public EcommerceTools(InventoryService inventoryService, PriceService priceService, OrderService orderService) { this.inventoryService inventoryService; this.priceService priceService; this.orderService orderService; } Tool(description 查询商品实时库存入参为商品ID返回可售库存数量) public String queryStock(String productId) { int stock inventoryService.getAvailableStock(productId); return 商品 productId 的库存为 stock 件; } Tool(description 计算购买商品总价入参为商品ID和购买数量返回原价和优惠后价格) public String calculatePrice(String productId, int quantity) { PriceInfo price priceService.calculate(productId, quantity); return 原价 price.originalAmount() 元优惠后 price.payAmount() 元; } Tool(description 根据商品ID、数量、收货人信息创建订单返回订单号) public String createOrder(String productId, int quantity, String receiverAddress) { String orderId orderService.createDraftOrder(productId, quantity, receiverAddress); return 订单 orderId 已创建待用户确认; } }5.3 让模型决定操作顺序智能体和传统规则引擎最大的区别是调用哪个工具、调用的顺序是什么不靠程序员 if-else而是让模型自己判断。面对“有货的话给我买两个送到某某地址”这种复合指令模型会自己规划先调用 queryStock确认库存充足。再调用 calculatePrice算价。最后调用 createOrder发起创建订单。这在代码层面不需要额外编排只要把工具都注册给 ChatClient模型的推理能力会自行决定。但要让这个流程可靠运行我建议在系统级 Prompt 里做一层约束比如你是电商售前客服帮助用户解答问题并完成购物。你必须遵守以下规则 1. 回答库存、价格相关问题前先调用对应工具获取真实数据。 2. 用户未确认下单前不要直接调用 createOrder 工具。 3. 如果工具返回的结果包含错误信息如实告知用户不要编造。这段规则看着不起眼但它大幅提升了工具调用的正确率。没有规则约束时模型经常在用户还没确认时就“自作主张”下单有了规则后它至少会先问一句“确认下单吗”。5.4 会话记忆从无状态到上下文感知到这里你会遇到第二个关键问题多轮对话怎么保持上下文用户先说“帮我查一下手机”模型调工具返回了库存信息。过一会儿用户追问“那两个呢”如果模型不记得之前说的“手机”是哪个型号这对话就废了。Spring AI 提供了 ChatMemory 抽象。最简单的做法是开启会话级记忆把同一会话的历史消息存起来每次请求时自动携带ChatClient chatClient ChatClient.builder(chatModel) .defaultSystem(你是电商客服) .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) .build();这里 ChatMemory 的实现可以是内存版InMemoryChatMemory单机部署够用如果是多实例部署建议用 Redis 自己实现一个因为内存版本在多实例下各自独立用户的请求打到不同实例上就记忆错乱了。我实际项目里用的是 Redis 存储消息列表序列化成 JSON 存 String读取时再反序列化。还有一个实际中的矛盾大模型的上下文窗口是有限且按 token 计费的。无限存历史消息很快窗口就爆了。处理策略是滑动窗口——只保留最近 N 轮对话。Spring AI 也提供窗口大小的配置比如只保留最近 20 条消息。做生产项目时这个配置必须调优历史上有个同事的项目因为没限制窗口大小在上下文里积累了上万条消息单次请求的 token 消耗高得离谱一个低价值会话烧掉几块钱。6. 进阶玩法多智能体协作与 RAG 知识库接入6.1 把一个复杂 Agent 拆成多个协作的垂直智能体电商客服如果只管售前咨询一个 Agent 就够了。但真实业务里还有售后、物流、投诉、知识问答等不同场景全塞进一个 Agent 会导致两个问题一是系统提示词过长模型容易“遗忘”后面的规则二是工具数量太多模型在“该调哪个”上犯错的概率急剧增加。切分为多智能体是更稳的做法。比如拆成售前 Agent负责商品咨询、库存、优惠。售后 Agent负责退换货、发票、售后进度。物流 Agent负责查物流轨迹、修改收货地址。实现方式可以靠一个“调度者”角色来做意图路由也可以用 Spring AI 的 ModelContext 机制做父子上下文传递。我这里分享一个最简洁的实现思路用一个路由 Agent 判断用户问题属于哪个领域然后调用对应领域的 Agent。路由 Agent 本身也用 ChatClient 实现只不过它的工具是“切换到售前”“切换到售后”“切换到物流”这几个方法Tool(description 当用户询问商品、库存、尺码、价格时调用) public String routeToPreSales() { return pre-sales; }拿到路由结果后程序再把用户消息转发给对应那个 Agent。这个方案的好处是每个垂直 Agent 都保持干净的上下文不会被无关场景干扰。6.2 用向量数据库解决“模型不知道的知识”还有一个高频需求企业的售后政策、退换货规则这些文档知识模型训练时没见过。你问它“你们七天无理由退换的条件是什么”它开始一本正经地编造政策这在生产里是不可接受的。解决思路是 RAG检索增强生成把企业文档向量化存入向量数据库用户提问时先检索最相关的文档片段和用户问题一起发给模型让模型基于给定文档回答。Spring AI 原生支持向量数据库抽象和 Redis、PostgreSQL(pgvector)、Milvus 等都能集成。我自己实战常用的组合是文档切分按固定大小 500 token 切分重叠 50 token避免语义断裂。向量化用 embedding 模型把切好的文本转成向量。存储检索用 pgvector因为直接用现有 PostgreSQL 不引入新组件运维最省心。配置上Bean VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return new PgVectorStore(jdbcTemplate, embeddingModel); }写入文档时对每段文本调 vectorStore.add()。查询时ListDocument docs vectorStore.similaritySearch( SearchRequest.builder() .query(七天无理由退换有什么条件) .topK(3) .build() );把检索到的文档内容拼到系统 Prompt 里。注意拼的时候要标清来源比如在每段文档前加“参考资料”前缀否则模型容易把检索到的和它自己内化的知识混在一起一旦检索到的资料和它的偏见冲突它大概率还是会按自己的来。我实际测试发现加上“请严格依据下面的参考资料回答不要使用参考资料之外的知识”这句话输出质量显著提升。7. 生产环境的常见问题与排查经验7.1 启动报依赖冲突Spring AI 1.x 引入了大量新依赖和已有的 Spring Boot 应用集成时最常见的报错是 NoClassDefFoundError尤其集中在 Jackson、Spring Core 这些组件上。DeepSeek 或者通义千问 Starter 内部依赖的版本和业务代码里固定版本的 Jackson 冲突。我的排查思路是先看完整堆栈定位到缺失的类再用 mvn dependency:tree 看一下依赖冲突路径确认是不是新旧版本混用。一般情况下把 Spring AI BOM 放在 dependencyManagement 的最前面能解决大部分版本冲突。7.2 模型不触发工具调用工具方法都写好了可模型就是不调用户问“库存多少”它非要说“我无法查询实时库存”。这类问题我遇过好多次原因通常出在工具描述不够明确。描述里没有包含“当用户问库存时”的触发条件模型不知道这个方法能解决用户的问题。模型本身不支持 Function Calling或者是系统 Prompt 里暗示了“不要使用工具”。工具方法抛了异常Spring AI 在 1.0 版本里对工具异常处理不友好异常消息被吞掉后模型表现得很困惑。排查手段把 Spring AI 的日志级别调到 DEBUG观察请求里是否携带了 tools 列表响应里是否有 function_call 字段。日志里看不到 function_call说明模型压根没打算调用工具直接检查描述有 function_call 但执行失败就检查返回的 JSON 参数格式。7.3 结构化输出解析失败用 .entity() 解析结构化输出时偶尔会报 JsonMappingException原因是模型返回的 JSON 里字段多了或少了。Spring AI 的解析器要求相对严格多一个未知字段在某些配置下又会报错。保险做法是在 record 或者实体类的 ObjectMapper 配置上启用 FAIL_ON_UNKNOWN_PROPERTIESfalse。如果不想改全局配置也可以用传统的 Map 接收然后手动转换牺牲一点优雅程度换稳定性。7.4 成本失控这是上线之后最容易被忽视的问题。对话历史的 token 随着轮数增长每轮请求都在重复发送之前的所有消息再加上工具返回的结果也被塞进上下文一个十轮以内的会话就可能烧掉上万个 token。我建议做一个简单的 CostFilter请求前估算消息的 token 数并记录日志超过阈值直接拒绝。同时给 ChatMemory 设置窗口大小只保留必要内容。很多团队做 AI 项目真正失败的原因不是技术跑不通而是账单先爆了。8. 一些个人的体会做 Spring Boot 3.x 集成 AI 智能体这个项目最大的感悟是AI 集成没有想象中那么“玄学”它和你以前做 HTTP 接口、消息队列、定时任务一样是一种普通的工程能力。差别在于以前你写的代码是确定性的而现在模型的输出是概率性的。承认这一点你就不会期待“每次调用都完美”而是会把精力花在怎么让“不完美的概率降到最低”上——写清楚工具描述、加好系统规则、做好异常兜底。踩过几次坑之后我现在的固定套路是先在本地用最便宜的国产模型跑通全链路再换商业模型做效果调优。因为开发阶段大把时间花在调试工具调用上用便宜模型能省下不少成本等到验证没问题了再切高价模型提升回答质量。这个流程看着不起眼但项目里帮我们团队省了至少一半的 API 账单。这套东西后续还能往两个方向扩展一个是把 RAG 文档源从手动导入改成定时同步线上知识库另一个是给多智能体的调度加一版可观测面板把每次路由决策、工具调用链都记录下来方便分析模型行为。总之框架已经把路铺好了剩下的就看你在自己业务里的想象力了。