资讯动态

Java后端必看:Spring AI从入门到RAG与Tool Calling实战

发布时间:2026/9/26 14:53:39 来源:尧图企业网站定制
1. 为什么我劝Java后端尽早把Spring AI摸一遍先把结论撂在这儿如果你是一个写了三五年Spring Boot的Java后端最近又在被各种大模型应用RAG知识库Agent的需求追着跑那Spring AI这条线你绕不过去。我大概是从去年底开始系统性地把Spring AI从0.8一路跟到1.0 GA中间踩的坑、推翻的方案、重写的代码加起来够写一本小册子。这篇就把我这一路的东西摊开讲从它到底解决什么问题到Advisor、Tool Calling、RAG这几块怎么落地尽量讲透。Spring AI本质上是什么一句话它是Spring生态里那套依赖注入自动配置约定优于配置的哲学被原封不动搬到了大模型应用开发上。你以前写Service注入JdbcTemplate现在写Service注入ChatClient你以前用application.yml配数据源现在用同样的方式配模型参数。对Java后端来说学习曲线几乎是平的——这是它最大的价值也是它跟LangChain4j、跟Python那套LangChain最本质的区别。它能做什么聊天对话、流式输出、函数调用Tool Calling、向量检索增强RAG、多轮记忆、结构化输出、可观测性这些主流能力它都有。适合谁看我建议是三类人一是手上已经有Spring Boot项目、想低成本接入AI能力的后端二是被要求做企业级RAG知识库、但不想引入Python技术栈的团队三是想理解AI应用工程化到底长什么样的开发者。如果你只是想调个API玩一玩那其实用不用Spring AI都行但一旦涉及多模型切换、会话管理、工具编排它的优势就出来了。下面我按整体设计思路→核心机制拆解→实操落地→踩坑排查这个顺序展开中间会穿插大量我实际项目里的代码和参数能抄的直接抄。2. Spring AI的整体设计思路与选型考量2.1 它到底抽象了哪几层理解Spring AI关键是理解它的分层抽象。我把它拆成四层来看从下往上最底层是Model层对应ChatModel、EmbeddingModel、ImageModel这些接口。这一层屏蔽了OpenAI、智谱、通义、Ollama等不同厂商的差异你换模型基本只改配置不改代码。这里有个设计细节值得说Spring AI没有搞一个万能Model接口而是按能力拆分因为聊天模型和嵌入模型的输入输出结构完全不同硬合并只会让类型系统一团糟。往上一层是Client层核心是ChatClient。这是你日常打交道最多的东西它提供了流式stream()、同步call()、结构化输出entity()三种调用范式。ChatClient是线程安全的可以做成单例Bean全局注入这点跟RestTemplate是一个思路。再往上是Advisor层这是Spring AI最有意思的设计。Advisor本质是一个拦截器链请求进模型之前、响应出模型之后都能插一脚。记忆管理、RAG检索、日志、内容审核全都是Advisor。这个设计直接借鉴了Spring MVC的HandlerInterceptor和AOP的思想Java后端一看就懂。最顶层是应用编排层也就是Tool Calling、Agent、Workflow这些。Spring AI本身不提供完整的Agent框架这块Spring AI Alibaba补了一些但通过Tool Calling Advisor的组合你能拼出大部分Agent场景。2.2 为什么是Advisor而不是AOP有人会问既然都是拦截为什么不直接用Spring AOP我一开始也这么想后来发现不行。原因有三第一Advisor需要访问和修改对话上下文。AOP的切点通常只拿到方法参数而Advisor需要拿到完整的ChatClientRequest包含消息列表、模型选项、工具定义还要能改写它。用AOP你得把上下文塞进ThreadLocal非常别扭。第二Advisor有明确的顺序语义。比如记忆Advisor必须在RAG Advisor之前执行先加载历史再检索这个顺序通过getOrder()控制跟Spring的Ordered接口一脉相承。AOP的Order虽然也能排序但语义没这么清晰。第三Advisor是可组合的链。你可以动态往ChatClient上挂不同的Advisor组合不同业务用不同链。AOP的切面是静态织入的做不到这种运行时灵活组合。所以Advisor不是重复造轮子而是针对对话式AI这个特定场景重新设计的拦截机制。理解这一点你后面用起来就不会觉得别扭。2.3 版本选型1.0 GA还是继续观望我实测下来的建议是新项目直接上1.0.x GA。0.8到1.0之间API变动确实大ChatClient的构建方式、Advisor的接口签名都改过但1.0之后基本稳定了。如果你还在0.8迁移成本主要在Advisor和Tool Calling这两块其他还好。配套的Spring Boot版本我推荐3.3以上最好3.4。因为Spring AI 1.0依赖Spring Framework 6.2而6.2的一些特性比如对虚拟线程更好的支持在3.4里才完整。JDK的话17是底线21更好——虚拟线程在处理大量并发流式请求时收益明显我后面会讲。至于Spring AI Alibaba它是阿里在Spring AI基础上做的增强主要补了通义系列模型的深度适配、NL2SQL、以及一些Agent编排能力。如果你用阿里云百炼平台直接上它省事如果模型来源比较杂纯Spring AI 各家官方SDK也行。3. 核心机制拆解Advisor、Tool Calling、RAG到底怎么跑3.1 Advisor链的执行时序这块我必须讲细因为太多人在这里翻车。一个请求从ChatClient发出到拿到响应Advisor链的执行顺序是这样的请求阶段before方向按getOrder()从小到大执行响应阶段after方向按从大到小回卷。这跟Servlet Filter的链式调用一模一样。假设你挂了三个Advisor日志order0、记忆order100、RAGorder200那么日志Advisor记录原始请求记忆Advisor从存储加载历史消息插入到消息列表RAGAdvisor根据用户问题检索知识库把检索结果作为上下文拼进Prompt请求发给模型RAGAdvisor拿到响应可能做后处理记忆Advisor把本轮对话存回存储日志Advisor记录响应和耗时注意记忆Advisor的order一定要小于RAG Advisor否则会出现历史消息里没有本轮问题但RAG却检索了本轮问题的诡异现象。我踩过这个坑排查了半天。3.2 Tool Calling的完整生命周期Tool Calling也叫Function Calling是让模型调用你的Java方法的机制。它的流程比很多人想的复杂第一步你在构建ChatClient时通过.tools(new MyTools())注册工具。Spring AI会扫描这个对象上所有Tool注解的方法把方法名、描述、参数schema提取出来。第二步请求发给模型时这些工具定义会作为tools字段一起传过去。模型看到工具列表后如果判断需要调用它不会直接返回答案而是返回一个tool_calls结构里面包含要调用的工具名和参数。第三步Spring AI拦截到这个响应在本地反射调用你的Java方法拿到返回值。第四步把方法返回值作为一条tool角色的消息追加到对话里再次发给模型。模型这次基于工具结果生成最终答案。关键点在于这是一个多轮往返过程不是一次调用。所以你的工具方法要尽量快否则整个链路延迟会叠加。我一般要求工具方法执行时间控制在200ms以内超过的要么加缓存要么改成异步。3.3 RAG的两条技术路线RAG检索增强生成在Spring AI里有两套实现思路很多人搞混路线一Advisor式RAGQuestionAnswerAdvisor。这是Spring AI内置的你只需要配一个VectorStore挂上QuestionAnswerAdvisor它自动帮你做向量化问题→检索→拼Prompt。优点是开箱即用缺点是检索策略固定不好定制。路线二手动式RAG。你自己注入VectorStore手动调similaritySearch()拿到文档后自己拼Prompt。优点是灵活能做多路召回、重排序、混合检索缺点是要写的代码多。我的建议是原型阶段用路线一快速验证生产环境用路线二。因为生产环境的检索质量要求高内置的相似度检索经常不够用你需要加关键词过滤、元数据过滤、重排序这些。4. 从零搭建一个可运行的Spring AI工程4.1 依赖与配置先上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 dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-redis/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-advisors-vector-store/artifactId /dependency /dependencies如果你用智谱AI把spring-ai-starter-model-openai换成spring-ai-starter-model-zhipuai配置项前缀从spring.ai.openai改成spring.ai.zhipuai。智谱的模型名比如glm-4-plus嵌入模型用embedding-3这些在配置里写清楚就行。application.yml大概长这样spring: ai: openai: api-key: ${AI_API_KEY} base-url: https://api.openai.com chat: options: model: gpt-4o-mini temperature: 0.7 embedding: options: model: text-embedding-3-small data: redis: host: localhost port: 6379提示api-key千万别硬编码在yml里用环境变量或者配置中心。我见过有人把key提交到Git第二天就被刷爆了额度。4.2 第一个ChatClient配置类里把ChatClient做成BeanConfiguration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory, VectorStore vectorStore) { return builder .defaultSystem(你是一个专业的技术助手回答要简洁准确。) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build(), QuestionAnswerAdvisor.builder(vectorStore).build() ) .build(); } Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); } }这里MessageWindowChatMemory是内存版记忆只保留最近20条消息。生产环境要换成基于Redis或数据库的实现否则重启就丢。4.3 流式接口怎么写流式输出是AI应用的标配Spring AI用Flux返回GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestParam String message, RequestParam String conversationId) { return chatClient.prompt() .user(message) .advisors(a - a.param(ChatMemory.CONVERSATION_ID, conversationId)) .stream() .content(); }注意produces TEXT_EVENT_STREAM_VALUE这是SSE的标准MIME类型。前端用EventSource接收就行。conversationId通过Advisor的param传进去记忆Advisor靠它区分不同会话。实操心得流式接口一定要配超时和背压。我遇到过模型响应慢、客户端又不断重连导致连接数暴涨的情况。在WebFlux里加个.timeout(Duration.ofSeconds(60))能挡掉大部分问题。5. Tool Calling实战让模型调用你的业务方法5.1 定义一个工具类假设我们要让模型能查订单状态先写工具Component public class OrderTools { private final OrderService orderService; public OrderTools(OrderService orderService) { this.orderService orderService; } Tool(description 根据订单号查询订单状态返回订单的当前状态和预计送达时间) public OrderStatus queryOrder(ToolParam(description 订单号格式为ORD开头加12位数字) String orderNo) { return orderService.getStatus(orderNo); } }Tool的description非常关键模型就是靠它判断该不该调用这个工具。描述要写清楚这个工具做什么、参数是什么格式、返回什么。我见过有人写查询订单结果模型经常不调用改成上面那种详细描述后命中率大幅提升。5.2 注册与调用在构建ChatClient时注册ChatClient client builder .defaultTools(orderTools) .build(); String answer client.prompt() .user(帮我查一下订单ORD202401011234的状态) .call() .content();模型会自动识别出需要调用queryOrder提取订单号执行方法然后基于结果回答。5.3 工具设计的几个原则我总结了四条都是血泪教训第一工具粒度要适中。太细比如查订单金额查订单时间分开会导致模型频繁多次调用延迟高太粗一个工具干所有事会导致参数复杂、模型理解困难。一般一个工具对应一个明确的业务动作。第二参数类型要简单。尽量用String、Integer、Boolean这些基础类型避免嵌套对象。模型对复杂schema的解析准确率会下降。第三工具方法要幂等。因为模型可能因为超时重试而重复调用如果你的工具是扣款这种非幂等操作一定要加幂等键。第四返回值要精简。工具返回的内容会作为消息塞回给模型如果返回一大坨JSON既浪费token又干扰模型判断。只返回必要字段。6. RAG落地从文档入库到检索增强6.1 文档切分与向量化RAG第一步是把你的知识文档切块、向量化、存进向量库。Spring AI提供了TokenTextSplitterBean public VectorStore vectorStore(EmbeddingModel embeddingModel, RedisConnectionFactory factory) { return RedisVectorStore.builder(factory, embeddingModel) .indexName(tech-docs) .initializeSchema(true) .build(); } public void ingest(ListDocument documents) { TokenTextSplitter splitter new TokenTextSplitter(500, 100, 5, 10000, true); ListDocument chunks splitter.apply(documents); vectorStore.add(chunks); }TokenTextSplitter的参数含义chunkSize500每块500 token、minChunkSizeChars100、minChunkLengthToEmbed5、maxNumChunks10000、keepSeparatortrue。切块大小是个玄学500到1000 token之间比较通用。太小会丢上下文太大会稀释语义。注意切块时最好保留一定的重叠overlapSpring AI的splitter默认会做但重叠比例要自己调。我一般设10%到20%。6.2 检索策略的优化内置的QuestionAnswerAdvisor用的是纯向量相似度检索实际用下来召回率一般。我做了几个优化混合检索向量检索 关键词检索BM25两路结果合并去重。Spring AI本身不直接支持BM25但你可以用Elasticsearch或Redis的全文检索能力自己实现。元数据过滤给每个Document打上source、category、timestamp等元数据检索时用filterExpression过滤。比如只检索某个产品线的文档SearchRequest request SearchRequest.builder() .query(question) .topK(5) .filterExpression(category product-a) .build();重排序检索出topK比如20条后用一个重排序模型如Cohere Rerank或本地的小模型重新打分取top5。这一步对最终质量提升非常明显我实测能提升20%以上的相关性。6.3 手动RAG的完整实现生产环境我一般不用内置Advisor而是手动控制public String ragChat(String question, String conversationId) { // 1. 检索 ListDocument docs vectorStore.similaritySearch( SearchRequest.builder().query(question).topK(5).build() ); // 2. 拼上下文 String context docs.stream() .map(Document::getText) .collect(Collectors.joining(\n\n---\n\n)); // 3. 构造Prompt String prompt 基于以下参考资料回答问题。如果资料中没有相关信息请明确说明资料中未提及不要编造。 参考资料 %s 问题%s .formatted(context, question); // 4. 调用模型 return chatClient.prompt() .user(prompt) .advisors(a - a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); }这个Prompt模板里的不要编造很关键。不加这句模型经常在检索不到内容时硬编这在企业场景里是致命的。7. 常见问题与排查技巧实录7.1 问题速查表现象可能原因排查方向启动报No qualifying bean of type ChatModel依赖没引对或配置缺失检查starter依赖和spring.ai.*.api-key流式输出中文乱码编码未指定响应头加charsetUTF-8Tool Calling不触发工具描述不清或参数schema有问题打印请求日志看tools字段RAG检索结果不相关切块太大或嵌入模型不匹配调小chunkSize换嵌入模型记忆丢失用了内存版且服务重启换Redis或JDBC实现响应超时模型慢或工具方法阻塞加超时工具方法异步化并发下会话串了conversationId没传或传错检查Advisor param传递7.2 几个我踩过的深坑坑一Advisor顺序导致记忆污染。前面提过记忆Advisor的order必须小于RAG。我一开始没注意结果历史对话里混进了检索内容模型回答开始胡言乱语。坑二向量库维度不匹配。换嵌入模型时忘了重建索引导致检索报维度错误。嵌入模型的维度是固定的比如text-embedding-3-small是1536维换模型必须重新入库。坑三Tool Calling的循环调用。模型有时候会陷入调用工具→结果不满意→再调用的死循环。解决办法是设置最大迭代次数Spring AI的ToolCallingManager可以配置或者你在工具描述里明确此工具只调用一次。坑四流式接口的异常处理。Flux里的异常如果不处理客户端会收到一个断开的连接看不到任何错误信息。要用.onErrorResume()兜底返回一个友好的错误消息。7.3 性能优化的几个点虚拟线程JDK 21下开启spring.threads.virtual.enabledtrue能显著提升并发流式请求的吞吐。我压测下来同样硬件下QPS能提升2到3倍。连接池模型调用是HTTP请求底层HTTP客户端的连接池要调好。默认值往往偏小高并发下会成为瓶颈。缓存相同问题的回答可以缓存。用Caffeine做本地缓存key是问题会话上下文的hash。注意RAG场景下缓存要谨慎因为检索结果可能变化。异步工具耗时的工具方法用Async或CompletableFuture避免阻塞模型调用线程。8. 关于Agent和后续扩展的一些想法Spring AI本身不是完整的Agent框架但通过Tool Calling Advisor 多轮循环你能拼出一个基础的ReAct Agent。核心逻辑是让模型在每轮决定是调用工具还是给出最终答案如果是调用工具就执行后继续循环直到模型给出最终答案或达到最大轮数。Spring AI Alibaba在这块做了增强提供了更完整的Agent编排能力还有NL2SQL这种垂直场景的封装。如果你的场景是自然语言查数据库直接用它省很多事。至于Agentic RAG让Agent自主决定检索策略目前Spring AI还没有开箱支持需要自己实现。思路是把检索也做成一个Tool让模型自己决定什么时候检索、检索什么。这个方向很有意思但工程复杂度不低建议先把基础RAG跑稳再考虑。我个人的体会是Spring AI最大的价值不在于它功能多全而在于它让Java后端能用自己熟悉的方式进入AI应用开发。你不需要学Python不需要理解LangChain那套抽象用Spring Boot的思维就能把大部分场景做出来。当然它也有短板比如Agent编排能力弱、生态还在完善但对于企业级应用来说稳定、可维护、和现有技术栈无缝集成这些比花哨的功能重要得多。最后分享一个小技巧调试Spring AI时把日志级别调到DEBUGorg.springframework.ai包下的日志会打印完整的请求和响应包括发给模型的Prompt和工具调用详情。这个比任何调试工具都好用尤其是排查RAG和Tool Calling问题时。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑