1. 从Java开发者到AI应用构建者的角色转变很多写了五六年Java的朋友最近都在问同一个问题手上这套Spring Boot的功夫到底能不能直接迁移到AI应用开发上答案是能而且比想象中顺滑得多。Spring AI这个项目的出现本质上就是让Java开发者不用去学Python那一套LangChain、LlamaIndex的生态直接用自己熟悉的注解、依赖注入、自动配置那一套东西把大模型能力接进现有的Spring Boot工程里。我最初接触Spring AI是在一个餐饮SaaS项目的智能化改造需求上。客户要求把已有的订单系统加上自然语言查询功能让店长能用大白话问上周哪些菜品的退单率最高这种问题。当时团队里有人提议用Python单独搭一个服务通过HTTP接口跟Java主服务通信。这个方案我直接否了——多维护一套技术栈、多一套部署流程、多一套监控告警运维成本翻倍不说团队里没人愿意长期维护那个Python服务。后来用Spring AI的ChatClient加上Function Calling在原有Spring Boot工程里加了不到三百行代码就把事情办了。这个经历让我意识到Spring AI真正的价值不在于它比Python生态更强而在于它把AI能力变成了Java开发者已有的技能树上自然生长出来的一根新枝。你不需要重新理解什么是异步、什么是依赖注入、什么是AOP这些概念在Spring AI里全部沿用。你需要新学的只有三件事怎么跟大模型对话ChatClient、怎么让模型调用你的Java方法Tool/Function Calling、怎么把外部数据喂给模型RAG。这篇文章面向的是有Java基础、用过Spring Boot、但对AI应用开发还没有系统认知的开发者。我会按照一条完整的学习路线来展开从环境搭建到第一个对话接口再到工具调用、RAG检索增强、多轮记忆管理最后落到AI Agent的构建思路上。每一块都会给出可运行的代码和我在实际项目中踩过的坑。2. 环境搭建与第一个可运行的对话接口2.1 版本选择Spring AI 1.0与2.0的取舍截至我写这篇内容的时候Spring AI的稳定版本是1.0.x系列2.0还在里程碑阶段。很多人在选版本的时候会纠结要不要直接上2.0我的建议是生产项目用1.0.x学习练手可以用2.0尝鲜。原因很直接。1.0.x的API已经稳定文档齐全社区里能搜到的问题和答案也多。2.0虽然引入了一些更优雅的抽象比如对Agent的原生支持更好但API还在变动你今天写的代码下周可能就要改。我去年在一个内部工具项目上用了2.0的M6版本结果升级到M7的时候Tool Calling的接口签名变了改了半天。Maven依赖这块Spring AI的版本管理做得比较规范用BOM统一管理版本号dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后按需引入具体的starter。比如你要接OpenAI兼容的模型服务dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId /dependency这里有个容易踩的坑Spring AI的starter命名在1.0正式版之后有过调整早期教程里写的spring-ai-openai-spring-boot-starter和后来文档里的spring-ai-starter-model-openai是不同时期的命名。如果你照着老教程引入依赖发现找不到先检查一下版本号对应的文档。2.2 配置文件里的关键参数Spring AI的自动配置做得相当到位大部分情况下你只需要在application.yml里填几个关键参数spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.example.com chat: options: model: gpt-4o-mini temperature: 0.7 max-tokens: 2048temperature这个参数值得单独说一下。它的取值范围通常是0到2值越低输出越确定、越保守值越高输出越发散、越有创造性。做数据提取、分类、代码生成这类任务时我一般设0.1到0.3做文案创作、头脑风暴时设0.7到1.0。很多人不管什么场景都用一个默认值结果要么提取数据时模型乱发挥要么写文案时干巴巴的。max-tokens控制的是模型单次回复的最大长度。这个值设太小会导致回复被截断设太大又浪费额度。我的经验是对话场景设1024到2048够用长文生成场景设4096以上。注意这个参数在不同模型提供商那里的名称可能不一样有的叫max-tokens有的叫max-completion-tokens具体看对应starter的文档。2.3 第一个ChatClient调用Spring AI的核心接口是ChatClient它的设计借鉴了WebClient和RestClient的流式API风格用起来很顺手RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { this.chatClient builder .defaultSystem(你是一个专业的Java技术顾问回答要简洁准确。) .build(); } GetMapping(/chat) public String chat(RequestParam String message) { return chatClient.prompt() .user(message) .call() .content(); } }这段代码里有几个设计点值得展开。ChatClient.Builder是自动注入的Spring AI会根据你引入的starter自动配置好底层的模型客户端。defaultSystem设置的是系统提示词相当于给模型定了一个角色和回答风格这个设置对该ChatClient实例的所有调用都生效。.call()是同步调用返回ChatResponse对象.content()取出文本内容。如果你需要流式输出就是那种一个字一个字往外蹦的效果把.call()换成.stream()返回类型改成FluxStringGetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String message) { return chatClient.prompt() .user(message) .stream() .content(); }流式输出在Web场景下配合SSEServer-Sent Events使用前端用EventSource接收。这个在聊天界面里体验差别很大——同步调用要等模型全部生成完才返回用户盯着空白屏幕好几秒流式输出几乎立刻开始显示内容感知延迟低很多。提示流式接口记得设置produces MediaType.TEXT_EVENT_STREAM_VALUE否则浏览器可能不会按SSE协议处理响应。3. 让模型调用你的Java代码Tool Calling实战3.1 Tool Calling解决的是什么问题大模型有两个天生的短板一是知识有截止日期二是不能主动获取实时数据或执行操作。你问它今天北京的天气怎么样它只能告诉你它训练数据里某个时间点的信息没法真的去查。Tool Calling有些地方叫Function Calling就是解决这个问题的机制。你告诉模型我这里有这几个方法可以调用分别是干什么的、需要什么参数模型在对话过程中判断需要调用某个方法时会返回一个结构化的调用请求你的代码执行这个方法把结果再喂回给模型模型基于结果生成最终回复。整个流程是这样的用户提问帮我查一下订单号12345的状态模型判断需要调用queryOrderStatus方法参数是orderId12345你的Java代码执行这个方法拿到结果已发货预计明天送达把结果返回给模型模型生成自然语言回复订单12345目前状态是已发货预计明天送达。3.2 用Tool注解定义可调用的方法Spring AI提供了Tool注解把普通的Java方法暴露给模型Component public class OrderTools { private final OrderRepository orderRepository; public OrderTools(OrderRepository orderRepository) { this.orderRepository orderRepository; } Tool(description 根据订单号查询订单状态返回订单的当前状态和预计送达时间) public String queryOrderStatus( ToolParam(description 订单号纯数字) String orderId) { Order order orderRepository.findById(orderId) .orElseThrow(() - new IllegalArgumentException(订单不存在)); return String.format(订单状态%s预计送达%s, order.getStatus(), order.getEstimatedDelivery()); } }description这个属性非常关键模型就是靠它来判断什么时候该调用这个方法。写description有几个原则说清楚这个方法做什么、什么场景下用、参数是什么含义。我见过有人写description 查询订单太模糊了模型经常在该调用的时候不调用不该调用的时候乱调用。参数上的ToolParam同理描述要具体。订单号纯数字比订单号好因为模型知道不需要加前缀、不需要加引号。然后在ChatClient调用时注册这些工具GetMapping(/order/chat) public String orderChat(RequestParam String message) { return chatClient.prompt() .user(message) .tools(new OrderTools(orderRepository)) .call() .content(); }3.3 工具调用的边界与常见问题实际用下来Tool Calling有几个坑必须提前知道。第一个坑是工具数量不宜过多。我一开始图省事把一个Service里十几个方法全标了Tool结果模型经常选错方法。后来精简到五六个最核心的准确率明显提升。经验值是单次对话注册的工具控制在10个以内超过的话考虑做分组或者用路由层先判断意图。第二个坑是返回值要控制长度。如果你返回一个包含几百条记录的JSON这些内容会全部塞进模型的上下文既浪费token又可能超出上下文窗口。正确做法是在工具方法内部就做好聚合和摘要只返回模型需要的关键信息。第三个坑是异常处理。工具方法抛异常时Spring AI会把异常信息传给模型模型可能会据此编造一个回复。比如你抛OrderNotFoundException模型可能回复订单不存在可能是您输错了订单号听起来合理但实际上是它编的。更稳妥的做法是在工具方法内部捕获异常返回一个明确的错误描述字符串让模型基于确定的信息回复。第四个坑是多轮工具调用的循环控制。有些复杂问题需要模型连续调用多个工具Spring AI默认会处理这个循环但你要注意设置最大迭代次数防止模型陷入死循环。这个在ChatOptions里可以配置。4. RAG把你的私有数据变成模型的知识4.1 为什么需要RAG模型的知识来自训练数据你公司的产品文档、内部规范、客户资料它一概不知。你有两个选择一是把这些数据拿去微调模型二是用RAGRetrieval-Augmented Generation检索增强生成在提问时把相关文档片段喂给模型。微调的成本高、周期长而且每次数据更新都要重新训练。RAG的优势在于数据实时更新、成本低、可解释性强你能看到模型是基于哪些文档片段回答的。绝大多数企业场景下RAG是更务实的选择。RAG的核心流程分两步离线索引和在线检索。离线阶段把你的文档切块、向量化、存入向量数据库在线阶段把用户问题向量化在向量数据库里找最相似的文档块连同问题一起发给模型。4.2 文档切块策略切块Chunking是RAG里最容易被忽视但影响最大的环节。切得太大检索出来的内容包含太多无关信息干扰模型切得太小上下文不完整模型理解不了。Spring AI提供了TokenTextSplitter来做切块TokenTextSplitter splitter new TokenTextSplitter(500, 100, 5, 10000, true);这几个参数分别是目标块大小token数、块之间重叠的token数、最小块大小、最大块大小、是否按句子边界切分。500 token的块大小适合大多数场景重叠100 token是为了防止关键信息刚好被切在边界上导致丢失。我的经验是技术文档、产品手册这类结构化程度高的内容按段落或章节切效果更好聊天记录、客服对话这类口语化内容按固定token数切就行。不要指望一个切块策略打天下不同来源的文档要分别处理。4.3 向量化与存储Spring AI的VectorStore抽象屏蔽了不同向量数据库的差异。开发阶段我一般用SimpleVectorStore内存版生产环境根据数据量选PGVector、Milvus、Redis等。Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel).build(); }加载文档并入库Component public class DocumentLoader { public void loadDocuments(VectorStore vectorStore, Resource resource) { ListDocument documents new TokenTextSplitter() .apply(List.of(new Document(resource))); vectorStore.add(documents); } }EmbeddingModel也是自动配置的Spring AI会根据你引入的starter选择合适的嵌入模型。注意嵌入模型和对话模型可以是不同的提供商比如对话用一家、嵌入用另一家配置上分开设置就行。4.4 检索增强的对话实现把检索和对话串起来Spring AI提供了QuestionAnswerAdvisorBean public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultAdvisors(new QuestionAnswerAdvisor(vectorStore)) .build(); }这样每次调用时框架会自动把用户问题拿去向量库检索把最相关的文档块拼进提示词里。检索的相似度阈值、返回条数这些参数可以在QuestionAnswerAdvisor的构造函数里调整。实测下来RAG效果好不好八成取决于文档切块和检索策略两成取决于模型本身。我见过太多人花大量时间调提示词却不去优化切块逻辑结果怎么调效果都上不去。注意RAG不是万能的。如果用户的问题需要跨多个文档块做推理或者需要精确的数值计算单纯靠向量检索效果会很差。这类场景要考虑结合知识图谱或者结构化查询。5. 多轮对话与记忆管理5.1 为什么模型会失忆大模型本身是无状态的每次API调用都是独立的。你跟它说我叫张三下一轮问我叫什么它不知道因为上一轮的对话内容没有自动带过来。多轮对话的实现方式是把历史消息一起发给模型。Spring AI提供了ChatMemory抽象来管理这个过程Bean public ChatClient memoryChatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); }ChatMemory有几种实现InMemoryChatMemory存在内存里重启丢失适合开发测试JdbcChatMemory存数据库适合生产。存储的内容就是对话历史每次调用时自动带上。5.2 上下文窗口的取舍历史消息不能无限带下去因为模型的上下文窗口有上限。MessageWindowChatMemory可以设置保留最近多少条消息Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); }20条消息大概对应10轮对话。设太小模型记不住前面说的内容设太大token消耗高而且太早的历史信息可能反而干扰当前对话。我的做法是分层处理最近5轮完整保留5到10轮之前的做摘要压缩10轮之前的直接丢弃。摘要压缩就是用模型把一段对话浓缩成几句话保留关键信息。这个逻辑需要自己实现Spring AI没有内置但基于它的API很容易做。5.3 会话隔离多用户场景下每个用户的对话历史必须隔离。Spring AI通过conversationId来区分chatClient.prompt() .user(message) .advisors(a - a.param(ChatMemory.CONVERSATION_ID, userId)) .call() .content();这个userId通常从登录态里取。我踩过一个坑早期版本里如果没传conversationId所有用户共用一份记忆A用户能看到B用户的对话内容。这个在测试环境不容易发现上线后就是严重的数据泄露。所以会话隔离这块一定要在开发阶段就验证到位。6. AI Agent的构建思路与落地场景6.1 Agent和普通对话应用的区别普通对话应用是你问我答Agent是你给目标我自己想办法完成。区别在于Agent有自主规划能力它能把一个复杂任务拆成多个步骤决定每一步用什么工具根据中间结果调整后续计划。举个例子。普通对话应用收到帮我分析上个月的销售数据并生成报告它只能基于已有知识泛泛而谈。Agent会这样做先调用数据库查询工具拿到上个月的数据然后调用数据分析工具做统计再调用图表生成工具画图最后调用文档生成工具输出报告。整个过程是模型自主编排的。Spring AI 1.0对Agent的支持还比较基础主要靠Tool Calling加上多轮循环来实现。2.0在这方面有增强但核心思路是一样的给模型一组工具让它自己决定调用顺序。6.2 用Tool Calling搭建一个简易Agent一个可用的Agent循环大概长这样public String runAgent(String goal, ListObject tools) { ListMessage messages new ArrayList(); messages.add(new SystemMessage(你是一个任务执行助手可以调用工具完成用户目标。)); messages.add(new UserMessage(goal)); for (int i 0; i MAX_ITERATIONS; i) { ChatResponse response chatModel.call(new Prompt(messages, ChatOptions.builder().tools(tools).build())); if (response.hasToolCalls()) { messages.add(response.getResult().getOutput()); for (ToolCall call : response.getToolCalls()) { String result executeTool(call); messages.add(new ToolResponseMessage(result, call.id())); } } else { return response.getResult().getOutput().getContent(); } } return 任务执行超时请简化目标后重试。; }这个循环的逻辑是把目标和工具给模型模型要么直接回复任务完成或无法完成要么请求调用工具。如果请求调用工具执行后把结果加进消息列表继续下一轮。MAX_ITERATIONS是安全阀防止无限循环。6.3 Agent落地的现实约束理想很丰满实际做Agent项目时约束很多。成本约束。Agent每轮循环都是一次完整的模型调用一个复杂任务可能循环十几轮token消耗是普通对话的几十倍。我做过一个测试让Agent完成查询本周新增用户并分析来源分布这个任务用了8轮循环消耗的token够普通对话用一整天。所以Agent场景下模型选型要更谨慎能用小模型的地方不要用大模型。可靠性约束。模型自主规划意味着你无法完全预测它的行为。它可能选错工具、传错参数、陷入循环。生产环境的Agent必须有完善的日志记录和人工兜底机制。我的做法是每个工具调用都记详细日志同时设置一个人工介入的开关当Agent连续失败超过阈值时自动转人工。延迟约束。多轮循环意味着用户要等更久。普通对话可能2秒返回Agent可能要20秒。这个体验差异很大前端必须做好加载状态和进度提示否则用户以为系统卡死了。6.4 适合Java团队切入的Agent场景不是所有场景都适合做Agent。我观察下来Java团队比较容易落地的是这几类内部运维助手。把常用的运维操作封装成工具查日志、重启服务、查监控指标让运维人员用自然语言操作。这类场景容错率高出错了影响可控。数据处理流水线。把数据查询、清洗、统计、导出封装成工具让业务人员用自然语言描述需求。这类场景步骤相对固定Agent的规划空间有限可靠性有保障。客服工单处理。把工单查询、分类、流转、回复模板封装成工具让Agent辅助客服处理工单。这类场景有明确的人工审核环节Agent出错也有人兜底。反过来涉及资金操作、生产环境变更、对外发送内容这类高风险场景现阶段我不建议让Agent自主执行最多做到Agent建议、人工确认。7. 学习路线中的几个关键决策点7.1 要不要学Python生态我的答案是了解即可不必深入。你需要知道LangChain、LlamaIndex大概是什么、解决什么问题这样在看技术文章时不会一头雾水。但没必要花时间去学怎么用它们写代码因为你的主战场在Java这边。真正值得花时间的是理解AI应用开发的通用概念提示词工程、RAG、Tool Calling、Agent、向量数据库、嵌入模型。这些概念是跨语言通用的理解了之后用Spring AI还是LangChain只是API调用方式的区别。7.2 模型选型的考量国内可选的模型服务不少选型时主要看几个维度能力推理、代码、中文理解、价格按token计费不同模型差好几倍、稳定性响应速度、可用性、合规性数据出境、内容安全。我的建议是开发阶段用便宜的小模型快速迭代验证流程跑通后再切换到能力更强的大模型做效果调优。不要一上来就用最贵的模型成本扛不住而且很多问题不是模型能力不够是提示词或流程设计有问题。Spring AI的好处是切换模型提供商只需要改配置和依赖代码基本不用动。所以选型不用太纠结先跑起来再说。7.3 从Demo到生产的距离网上大部分Spring AI教程停留在Demo阶段一个Controller、一个ChatClient、跑通就完事。但Demo到生产之间有一大段路要走。可观测性。每次模型调用的输入输出、耗时、token消耗都要记录。出问题时你要能查到是哪次调用、什么参数、返回了什么。Spring AI提供了ChatClient的拦截器机制可以在这里埋点。降级策略。模型服务不可用时怎么办我的做法是准备一个规则引擎兜底常见问题用规则匹配直接回答匹配不到的返回服务繁忙请稍后重试。虽然体验差一些但至少服务不中断。成本控制。设置每日token消耗上限超过阈值告警或限流。我见过一个项目因为没做限制被恶意刷接口一天烧掉几千块。内容安全。用户输入和模型输出都要过一遍内容审核。输入侧防止提示词注入输出侧防止生成不当内容。这块Spring AI没有内置需要自己接审核服务。8. 15篇实战教程的推进节奏建议如果要把这条学习路线拆成15篇教程来推进我建议的节奏是这样的第一阶段第1-3篇基础打通。环境搭建、第一个对话接口、流式输出。目标是能跑起来一个最简单的问答服务。第二阶段第4-6篇核心能力。提示词工程、Tool Calling、多轮记忆。目标是能做一个有实际用途的对话应用。第三阶段第7-10篇RAG专题。文档加载、切块策略、向量存储、检索增强对话。目标是能让模型回答基于私有数据的问题。第四阶段第11-13篇Agent进阶。Agent循环、多工具编排、错误处理。目标是能做一个自主完成多步任务的Agent。第五阶段第14-15篇生产化。可观测性、降级策略、成本控制、内容安全。目标是把前面的Demo变成能上线的服务。这个节奏的好处是每一阶段都有可交付的成果不会出现学了很久还做不出东西的情况。而且每一阶段的知识都是下一阶段的基础不会出现跳跃。我在带团队的时候发现很多人卡在第二阶段就放弃了原因是Tool Calling的调试比较麻烦模型不按预期调用工具时很难排查。我的建议是这一阶段多花时间把日志打详细每次调用都把模型的原始返回打出来看。看多了你就知道模型在什么情况下会选错工具怎么调整description能引导它选对。另外一个实用技巧准备一组测试用例每次改完提示词或工具定义后跑一遍看通过率有没有变化。这个习惯能帮你避免改了一个地方另一个地方坏了的情况。测试用例不用多十来个覆盖主要场景就行。最后说一个心态上的事。AI应用开发这个领域变化很快今天的最佳实践下个月可能就过时了。但底层的东西——怎么设计提示词、怎么组织工具、怎么管理上下文——这些是相对稳定的。把精力放在这些底层能力上具体的API变化花半天就能跟上。
