
很多Java开发者看到“AI应用”这四个字第一反应是这玩意儿不是Python的活儿吗我写后端Java的怎么去凑这个热闹实话说我一开始也是这么想的。但Spring AI出现之后这个局面变了——你不需要去啃Python那套生态不需要重学PyTorch、Transformers也不用自己去拼HTTP请求去调大模型API直接用你熟悉的Java、Spring Boot那一套就能把大模型集成到业务系统里。这篇文章就是给你准备的Spring AI入门实战。我会从工程搭建讲到Prompt模板、结构化输出、工具调用再聊到RAG和Agent雏形最后把生产环境容易踩的坑也给你捋一遍。适合那些准备在Java项目里引入AI能力、但还不太确定从哪下手的后端开发同学也适合那些只听说过Spring AI、想看看它到底能干什么的初学者。1. 为什么Java开发者需要Spring AI1.1 AI集成在Java这边到底卡在哪了过去几年想要在Java项目里调用大模型基本就是自己写一个HttpClient往OpenAI、通义千问这些接口上怼JSON。听起来不难但真做起来就全是零零碎碎的活每个模型厂商的报文格式不一样OpenAI的、智谱的、阿里的参数名和结构都有微妙差别每次调用都要自己拼Prompt业务一变就得改代码模型返回的是一大段自然语言文本想把它变成Java对象还得自己写解析逻辑如果你想做多轮对话就得自己维护历史消息列表手动拼接上下文想做RAG或者Agent那套工作量大到基本等于自己造轮子。这些痛点LangChain在Python那边很早就解决了而Java这边的对应方案直到Spring AI面世才算真正形成气候。它的定位就是给Java生态一个大模型集成标准。1.2 大模型访问的“统一抽象层”Spring AI做的事情放在Spring家族里其实特别顺理成章。Spring Data统一了数据库访问Spring Cloud统一了微服务组件Spring AI则是统一了大模型的访问方式。你面向的是一套统一的ChatClient API底层不管是接OpenAI、DeepSeek、通义千问还是本地跑一个Ollama都通过配置切换模型厂商。就好比你写SQL不用关心底层是MySQL还是PostgreSQL写Spring AI代码也不用关心大模型服务商是谁。我用过一个很直白的比喻Spring AI就像大模型圈的JDBC。它定义了统一的规范把各家模型的差异挡在接口外面你的业务代码只跟抽象层打交道。要是哪天想换模型改个配置就能搞定不用把业务逻辑重写一遍。1.3 它能帮你省掉哪些脏活具体点说Spring AI帮你省掉的并不是“调API”这一步而是整个工程化链条Prompt管理支持类似Thymeleaf的模板语法把提示词做成模板动态传参输出控制输出解析成Java Bean让模型返回的内容可以直接被程序消费对话记忆内置ChatMemory和MessageWindow多轮对话上下文不用自己维护结构化输出BeanOutputConverter能直接让模型按你的JSON Schema返回然后转换成对象RAG全套文档读取、切分、向量化、检索、注入上下文一条链路都给你接好了工具调用用注解就能把Java方法暴露给模型实现Agent的基础能力可观测性和Micrometer Tracing集成调用链路上能看到模型调用的耗时和Token消耗。这些原本来回折腾的事现在一个框架全包了这就是我觉得Java开发者真的应该开始学Spring AI的原因。2. 起手式15分钟搭出你的第一个Spring AI应用2.1 准备工作与依赖引入先说版本。目前Spring AI 1.0.0 GA已经发布BOM坐标是spring-ai-bom可以直接用中央仓库的版本不需要额外配置什么特殊仓库。Spring Boot建议用3.3.x以上。工程本身是个普通的Spring Boot Web项目关键依赖就一个dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency /dependencies注意spring-ai-openai-spring-boot-starter这个starter兼容所有“OpenAI协议”的模型服务。国内很多模型厂家的API都兼容这个协议所以你并不一定非得用OpenAI官方服务换个配置就能接DeepSeek、通义千问这些。2.2 配置文件里的门道接着是application.yml我习惯把模型相关的配置单独放一块spring: ai: openai: base-url: https://api.deepseek.com api-key: ${AI_API_KEY} chat: options: model: deepseek-chat temperature: 0.7几个关键点说一下base-url一定要配。默认是OpenAI官方地址如果你用的是第三方兼容服务不换成自己的地址请求直接404api-key绝对不要硬编码。用环境变量或者配置中心管理我在后面“生产环境避坑”那节会仔细讲temperature是温度系数。值越低回答越保守、越稳定适合提取信息之类的任务值越高回答越发散、越有创造性适合文案生成。一般聊天场景0.7就行结构化提取建议调到0.2以下。如果你不想注册一堆云厂商账号还有个更省事的选择本地装一个Ollama然后引入spring-ai-ollama-spring-boot-starter配置里把base-url指向http://localhost:11434模型填qwen2.5之类的本地模型就行。好处是彻底免费、离线可用适合日常学习调试。2.3 呼吸之间第一个对话接口依赖和配置就绪后写代码就很快了。Spring AI的核心入口是ChatClient类似Web开发里的RestClient是你跟大模型交互的主通道。RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder.build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }启动项目后访问http://localhost:8080/chat?message你好就能看到模型返回的文字。到这里一个能跑起来的Java AI应用就诞生了。你可能会好奇这个ChatClient到底是什么时候被创建出来的。Spring AI的自动配置会在应用启动时根据spring.ai.openai下的配置自动创建对应的ChatModelBean再通过ChatClient.Builder帮我们组装好客户端。你只管在需要的地方注入ChatClient就行。3. 把Prompt这块玩明白3.1 System Prompt和User Prompt的区别很多初学者不知道给模型的输入其实可以拆成两层系统提示词System Prompt和用户提示词User Prompt。System Prompt是给模型立人设、定规则的它对本次对话全局生效User Prompt则是用户真正的输入。举个例子同样一个问题“什么是AQS”不同的System Prompt会带来完全不同的回答不设System Prompt模型可能给出通用解释也可能啰嗦一堆设置“你是资深Java面试官回答要精炼、突出重点”回答就会更结构化像面试答案设置“你是初学者导师尽量用生活化例子解释”回答就会更通俗。在实际业务里System Prompt往往是你的业务规则比如“你是客服助手只回答商品相关问题不知道就说不清楚”这是产品逻辑的一部分比用户输入本身更值得花时间去打磨。3.2 用PromptTemplate做动态提示词在Spring AI里写Prompt我强烈建议用模板而不是在代码里拼字符串。模板语法和Thymeleaf很像用{变量名}做占位符。String systemPrompt 你是一位{role}请用{language}回答用户问题。 回答要求简洁、准确、不超过{maxLength}字。 ; GetMapping(/chat/template) public String chatWithTemplate(RequestParam String question) { PromptTemplate template new PromptTemplate(systemPrompt, Map.of(role, Java面试导师, language, 中文, maxLength, 200)); Prompt prompt template.create(); return chatClient.prompt(prompt).user(question).call().content(); }用模板有几点好处提示词的调整不需要重新编译代码模板可以放到配置中心统一管理团队成员可以不懂代码但会改Prompt。这里顺便提一个细节chatClient.prompt(prompt).user(question)这种写法实际上是同时发送两条消息一条是最初构造好的System消息一条是当前的User消息。如果你需要多轮上下文还得靠ChatClient的Advisor机制稍后我会讲到。3.3 打字机效果流式输出业务里如果需要“打字机”效果比如AI逐字回复就不能用同步调用了得用流式接口。Spring AI直接返回Flux天然适配WebFlux。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString streamChat(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }前端用EventSource或者fetch流式读取就行。这里的底层原理并不神秘——大模型的API本身支持SSEServer-Sent Events模型输出是分批推送的。Spring AI做的事情是把这些分批的Token用Flux包装起来让你能以一种响应式的方式消费。3.4 多轮对话别把上下文丢给前端处理如果每次请求都是独立的模型是没有记忆的上一轮的对话内容它完全不知道。要做聊天机器人必须维护上下文。Spring AI给了一个非常简洁的方案使用ChatMemory和MessageWindowAdvisor。我一般这样配置Bean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultAdvisors(new MessageWindowChatMemoryAdvisor( new InMemoryChatMemory(), 10, WindowType.LAST_N)) .build(); }这里的10表示保留最近10轮对话。模型API的上下文窗口是有限的不能无限叠加历史消息所以要按窗口大小滑动淘汰旧消息。从成本角度看每轮对话都会把历史消息重新发给模型一遍Token消耗是成倍增长的。消息窗口越大成本越高响应越慢。所以一般聊天场景10轮左右比较合理需要更长期记忆的场景得上RAG或者外部存储的那种方案而不是靠窗口无限拉长。4. 让AI回答可以被程序消费结构化输出4.1 为什么需要结构化输出聊天场景下模型返回一大段自然语言没问题反正人看得懂。但如果你要把模型接到业务流程里比如从用户留言中提取工单信息、从合同里抓关键字段那模型返回的就不能是“一段流畅的中文”必须是规整的JSON。一种土办法是告诉模型“请返回JSON”然后自己用Jackson解析。听起来可行但实际用起来满是坑模型输出经常带多余的说明文字、Markdown代码块标记偶尔还会因为和上下文拼接导致JSON格式断裂解析直接失败。Spring AI解决这个问题的方案是BeanOutputConverter。4.2 用BeanOutputConverter直接得到Java对象这个类的思路是先根据你给的Java类的字段定义动态生成一个JSON Schema然后把这段Schema作为约束放在Prompt里让模型输出拿到输出后再自动用ObjectMapper把JSON反序列化成你的Java对象。看代码更直观。假设我要从一段用户反馈里提取工单信息public record OrderExtraction( String orderId, String customerName, String complaintType, String description, int priority ) { } GetMapping(/extract) public OrderExtraction extract(RequestParam String feedback) { var converter new BeanOutputConverter(OrderExtraction.class); String formatInstruction converter.getFormat(); String message 从用户反馈中提取工单信息只返回JSON不要额外输出。 反馈内容%s 输出格式要求%s .formatted(feedback, formatInstruction); return chatClient.prompt() .user(message) .call() .entity(converter); }注意call().entity(converter)这一句它会直接返回OrderExtraction对象解析和反序列化Spring AI全部帮我们做了。输出什么字段、字段类型对不对都从record定义推出来的。4.3 结构化输出的两个翻车点这里分享两个我踩过的坑。第一个是模型不老实偏要加说明文字。虽然Prompt里明确说了“只返回JSON”但它还是可能给你来一句“好的以下是提取结果json...”。解决办法是在System Prompt里强化约束同时把temperature调到0让输出确定性更高。第二个是字段类型不匹配。比如我给priority定义为int但模型输出了high反序列化直接报错。保险的做法是枚举类型都用String定义然后在Java侧自己做个转换别指望模型真的理解你的枚举值约束。模型是概率输出不是类型安全系统的产物这个认知要建立起来。4.4 工具调用让Java方法变成AI的“手”如果说结构化输出是“让AI按规矩说话”那工具调用Function Calling就是“让AI能干活”。核心逻辑是这样的你定义好一个Java方法通过注解告诉模型这个方法能做什么、参数是什么模型在回答时如果判断需要调用这个方法就会在响应里带一个特殊的“工具调用请求”Spring AI拦截到这个请求执行你的Java方法把方法结果再喂回给模型模型拿到结果后组织最终回答。举个例子。做一个查询订单状态的工具public class OrderTools { Tool(description 根据订单号查询订单当前状态) public String getOrderStatus(String orderId) { // 这里实际去查数据库 return 订单 orderId 已发货预计明天送达; } }然后把工具注册到ChatClientBean ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultTools(new OrderTools()) .build(); }这时你问它“帮我查一下订单20250121到哪了”模型会自己决定调用getOrderStatus(20250121)拿结果再回复你。这个能力就是Agent的雏形。你会发现AI不再只是个嘴替它能跟你的业务系统交互了。后面的进阶玩法——控制设备、查数据库、发邮件——都是这个思路的延伸。工具调用这块最需要注意的是模型只会根据你的方法描述决定调不调用。如果你的Tool注解描述写得含糊模型就会在不需要的时候瞎调或者需要的时候不调。描述的注释建议写清楚“什么场景下调用”“参数含义是什么”这部分和写API文档一样重要。5. RAG实战让AI拥有你的业务知识5.1 模型不知道你公司的内部情况大模型的知识来自训练数据截止日期之前的世界它知道但你们公司内部的制度、你们产品的使用手册、上个季度的经营数据这些它一概不知。有人想那我直接把文档塞进Prompt里让AI读不就行了短文档可以但一份几十页的PDF塞进去Token直接爆炸还超出上下文窗口限制而且每次请求都传一遍既慢又贵。RAG检索增强生成解决的正是这个问题。思路就两步先把文档切块、向量化存进向量数据库用户提问时先去向量库里检索最相关的几个片段只把这几段拼进Prompt再让模型基于检索到的内容回答。简单说就是把“查文档”这件事从“全都塞给AI”变成“先检索再回答”既省钱又精准。5.2 基于SimpleVectorStore的完整可跑示例Spring AI把RAG链路串得很顺。我直接给一个最小可运行例子模型接的还是DeepSeek那一套向量存储用Spring AI自带的SimpleVectorStore它是个内存级向量库数据不持久化重启就丢但用来跑通逻辑足够了。先准备一份文档放在src/main/resources/docs/company.txt随便写几条公司的内部制度。然后Bean VectorStore vectorStore(EmbeddingModel embeddingModel) { return new SimpleVectorStore(embeddingModel); }EmbeddingModel这个Bean也是Spring AI自动配置好的默认用模型服务商的embedding接口。注意DeepSeek目前没开放embedding接口如果你接的是DeepSeek得再配一个兼容的embedding模型比如Ollama本地的nomic-embed-text或者换通义千问的embedding模型。具体配置按模型商文档来。在应用启动时把文档加载进去Component public class DataInitializer implements ApplicationRunner { private final VectorStore vectorStore; public DataInitializer(VectorStore vectorStore) { this.vectorStore vectorStore; } Override public void run(ApplicationArguments args) { var reader new PagePdfDocumentReader(classpath:/docs/company.pdf); // 如果只是txt可以用 TextDocumentReader ListDocument documents reader.get(); TextSplitter splitter new TokenTextSplitter(200, 50); ListDocument chunks splitter.apply(documents); vectorStore.add(chunks); System.out.println(已写入 chunks.size() 个文档片段); } }这里TokenTextSplitter(200, 50)表示每个片段约200个Token相邻片段重叠50个Token。这个参数很关键切太小语义不完整切太大检索精度下降重叠的目的是不让一句话被拦腰截断。查询接口就很有味道了用QuestionAnswerAdvisorGetMapping(/qa) public String qa(RequestParam String question) { return chatClient.prompt() .user(question) .advisors(QuestionAnswerAdvisor.builder() .vectorStore(vectorStore) .build()) .call() .content(); }QuestionAnswerAdvisor干的事是先把用户问题向量化去向量库里检索相似片段然后把检索结果包装成Prompt上下文一并交给模型。整个过程对业务代码几乎无感这是Spring AI在设计上非常舒服的一点。5.3 向量检索的原理可以这样理解向量检索说起来玄乎本质上就是“找相似”。每个文档片段被embedding模型映射成一个高维向量比如768维语义相近的文本在向量空间里距离更近。检索时把用户问题也映射成向量然后算它跟所有文档片段的夹角余弦取最接近的前K个。所以这里有个很实际的经验向量检索检索的是“语义相似”而不是“关键词匹配”。你搜“公司年假怎么请”也能匹配到写“休假制度”的片段这是传统SQL LIKE做不到的。但向量检索不是万能的它对数字和专有名词很迟钝。比如用户问“2024年Q3营收多少”向量相似度可能把含有很多财务术语的文档排前面但具体数值可能是错位的。这种场景得靠元数据过滤甚至SQL召回辅助别把RAG当全知全能。6. 生产环境必须盯住的四个坑6.1 密钥管理与成本失控把API Key直接写在application.yml里是很多入门项目踩过的坑我自己也踩过。一旦代码传到公开仓库别人就拿着你的Key疯狂调用一天烧掉几千块钱是分分钟的事。我建议至少做到这几条Key从环境变量读取配置文件里只写占位符有条件就放配置中心或密钥管理服务云厂商的控制台里设置月度消费限额超了就报警日志、异常信息里不要打印完整的Key。6.2 超时、限流与重试大模型接口的响应速度很不稳定快的时候几百毫秒慢的时候几十秒也是有的。Spring AI里可以配置重试机制spring: ai: openai: chat: options: timeout: 30s但重试一定要设上限而且要对“模型返回空”“模型返回解析不了的内容”这些情况做降级处理。比如AI服务挂了是直接报错还是返回一个预设的兜底文案这个降级策略建议在接口设计阶段就想好不要等到线上出问题再补救。6.3 模型的“幻觉”跟真实数据混在一起RAG链路最危险的事是模型把检索到的真实内容和“脑补”的内容混在一起用户根本分辨不出来。我实测在回答公司政策类问题时模型偶尔会在正确片段后面加一句“根据公司规定员工每月可报销500元通讯费”而这条规定根本不存在。对策有两个方向一是Prompt里明确要求“只能基于提供的资料回答资料中没有的内容明确说明‘未在资料中找到’”二是后置校验把回答拿去和检索片段做相似度比对太低就说明模型没守住规矩。第二个方案实现起来稍微复杂但效果拔群。6.4 测试一定要花钱最后说一个很多Java开发者会忽视的点AI应用的测试是烧钱的因为每次runTestCase都会真实调用模型API。我见过的工程事故里测试环境一个月把季度预算烧光的情况真实发生过。建议测试策略这样定单元测试只测Prompt模板生成和结果解析不调用真实模型用Mock集成测试只跑几条关键链路并且对Token消耗做预估如果模型支持尽量用便宜的小模型跑测试比如gpt-4o-mini这种级别的别拿最强模型刷测试加一个全局开关测试环境彻底禁用外呼接口只走录制回放。这些事看起来琐碎但真到了线上每一件都可能决定你项目能不能平稳落地。7. 个人体会这个框架值不值得学如果只让我说一句那就是Spring AI值得学但别把它想得太神。它解决了Java开发者接入大模型的工程化问题但不解决业务问题。模型选型、Prompt设计、RAG的文档切分策略、Agent的工具编排这些才是AI应用的核心难点框架只是把外围的通信和组装工作帮你省掉了。我个人在实操中的建议是先跑通最简单的ChatClient体验一次完整链路再逐步加上模板、流式、结构化输出然后按照业务需求引入RAG和工具调用。不要一上来就搞一个特别完整的Agent架构那样调试起来会让你怀疑人生。另外一个小经验在团队里推广Spring AI时别直接甩官方文档那份文档更新太快很多示例API版本之间都对不上。你先写好两三个能跑的最小Demo让同事直接跑起来看到效果比任何文档都更有说服力。我当初就是靠一个流式聊天接口的演示让前端和后端同事都开始主动学这个框架。技术学得再多最终都要落到能跑、能维护、能控制成本的产品上。希望这篇文章能帮你平稳迈出第一步后面的事跑起来再遇到再说。