ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java研发AI落地实战:Spring AI与RAG知识库从零搭建

Java研发AI落地实战:Spring AI与RAG知识库从零搭建 1. 为什么 Java 研发现在必须重新理解 AI 落地过去一年半我身边不少 Java 同行经历了从“看热闹”到“真焦虑”的转变。焦虑的点很具体公司要求把大模型能力接进现有业务系统但团队里没人知道从哪下手网上教程一搜全是 PythonLangChain 的示例代码跑得挺欢可我们的订单系统、风控系统、工单系统全是 Spring Boot 写的总不能为了接个 AI 就把整个技术栈推倒重来吧。这种割裂感才是“AI 抢饭碗”这个说法真正让人不安的地方——不是 AI 本身有多可怕而是它带来的那套工具链和思维方式和 Java 研发的日常经验对不上。但实际情况是Java 生态在 AI 落地这件事上并没有掉队只是它的路径和 Python 那条线不一样。Python 那边偏向实验、偏向快速验证Java 这边偏向工程化、偏向稳定交付。一个 RAG 知识库在 Python 里可能两百行代码就能跑通 demo但要把它做成一个能扛住日均十万次调用、能灰度发布、能跟现有权限体系打通的线上服务Java 的工程能力反而是优势。Spring AI、Spring AI Alibaba 这些框架的出现本质上就是把大模型调用这件事“Spring 化”了——用你熟悉的 Bean 管理、依赖注入、配置中心那套东西去管理 AI 能力。这篇内容我想聊的不是“AI 会不会取代程序员”这种空对空的话题而是把 Java 研发真正落地 AI 时踩过的坑、做过的选型、写过的代码摊开来讲。核心会围绕几条线展开Spring AI 怎么接入、Flux 流式响应怎么处理、RAG 知识库怎么从零搭起来、以及在实际项目里怎么把这些东西和已有的 Java 后端架构缝合到一起。适合有 Java 基础、想在自己项目里落地 AI 能力的后端研发也适合正在做技术选型、需要判断“这条路走不走得通”的架构同学。看完你至少能清楚一件事Java 研发做 AI 落地不是从零学一门新语言而是把已有的工程能力迁移到一个新场景里。2. 整体设计思路Java 研发做 AI 落地的三条主线2.1 为什么选 Spring AI 而不是直接调 HTTP 接口很多人第一反应是调大模型不就是发个 HTTP 请求吗我用 RestTemplate 或者 WebClient 自己封装一下不就行了为什么要引入 Spring AI 这层框架这个问题我一开始也想过而且确实用 WebClient 手写过一版调用逻辑。手写的问题不在于“能不能跑通”而在于跑通之后的一堆琐事。第一是模型切换。今天用这个模型明天业务方说想换另一个试试效果手写的话你得改请求体格式、改鉴权方式、改返回解析每个模型的 API 结构都不一样。Spring AI 抽象了ChatClient和ChatModel接口换模型基本就是换一个 starter 依赖加改几行配置。第二是流式响应。大模型返回慢用户等不了必须用 SSE 或者 WebSocket 做流式输出手写这块要处理背压、要处理连接中断、要处理超时重试Spring AI 直接返回FluxString配合 WebFlux 天然就是流式的。第三是 RAG 相关的向量库操作、文档切分、Embedding 调用这些如果全手写工作量比业务代码本身还大。提示如果你的项目只是偶尔调一次大模型做个简单功能手写 HTTP 确实够用。但只要涉及多模型切换、流式输出、知识库检索中的任意一项Spring AI 带来的收益就远超学习成本。2.2 Flux 在 AI 场景里到底解决什么问题Flux 是 Reactor 里的一个概念简单说就是“一个能吐出零到多个元素的异步序列”。放到 AI 场景里它的价值特别直观大模型生成一段五百字的回答可能要花五到十秒如果等它全部生成完再返回给前端用户盯着转圈圈会以为系统卡死了。而流式输出是模型生成一个字就吐一个字前端像打字机一样显示出来体感上快很多。Flux 在这里扮演的角色就是承载这个“逐字吐出”的数据流。Spring AI 的流式接口返回FluxString你把它直接返回给 WebFlux 的 Controller框架会自动帮你转成 SSE 格式推给前端。整个过程不需要你手动管理连接、不需要手动分块Reactor 的背压机制会自动处理“生产太快消费太慢”的问题。我实测下来同样的模型、同样的 prompt流式输出的首字延迟能压到一秒以内而阻塞式输出用户要等八秒以上才能看到第一个字体验差距是数量级的。2.3 RAG 为什么是 Java 项目落地 AI 的最优切入点RAG 全称是检索增强生成说人话就是“先查资料再回答”。大模型本身的知识是训练时固定的你问它公司内部的报销制度、产品的接口文档、某个客户的合同条款它要么不知道要么一本正经地胡说。RAG 的思路是把这些内部资料提前处理好存进向量库用户提问时先去向量库里检索出最相关的几段内容再把这几段内容作为上下文一起发给大模型让它基于这些资料来回答。为什么说 RAG 是 Java 项目落地 AI 的最优切入点因为它对现有系统的侵入性最小。你不需要重新训练模型不需要标注大量数据不需要 GPU 集群只需要把已有的文档、数据库记录、工单内容做一次向量化处理接一个检索接口就能让 AI 回答“有据可依”。而且 RAG 的效果是可解释的——用户问一个问题你能告诉他这个答案是基于哪几篇文档生成的这在企业场景里非常重要。相比之下微调模型成本高、周期长、效果还不一定比 RAG 好对于绝大多数 Java 业务系统来说RAG 是性价比最高的选择。3. 核心细节解析Spring AI 接入与 RAG 知识库搭建3.1 Spring AI 项目依赖与配置的实操要点先讲依赖。Spring AI 的版本迭代比较快选版本的时候建议直接看官方文档的当前稳定版不要凭记忆写版本号。以接入某国产大模型为例pom 里通常需要两个依赖一个是 Spring AI 的核心 starter一个是具体模型的 starter。核心 starter 提供ChatClient、EmbeddingClient这些抽象接口模型 starter 提供具体实现。dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-core/artifactId version1.0.0-M4/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-zhipuai-spring-boot-starter/artifactId version1.0.0-M4/version /dependency配置文件里要填 API Key、模型名称、超时时间这些。这里有个坑不同模型的参数名不一样有的叫api-key有的叫apiKey有的放在spring.ai下面有的放在spring.ai.zhipuai下面。建议直接参考官方 starter 的spring-configuration-metadata.json文件里面列了所有可配置项比看博客靠谱。spring: ai: zhipuai: api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4 temperature: 0.7 embedding: options: model: embedding-2temperature这个参数值得单独说。它控制输出的随机性范围一般是 0 到 1。做知识库问答的时候建议调到 0.2 到 0.3让回答更稳定、更贴近检索到的资料做创意类功能比如文案生成可以调到 0.8 以上。我见过有人做客服问答把 temperature 设成 1.0结果同一个问题问两次答案完全不一样用户直接投诉。3.2 ChatClient 的构建与流式调用代码拆解Spring AI 的ChatClient用起来很像RestTemplate或者WebClient是链式调用的风格。构建一个 ChatClient 实例通常通过 Builder 模式把ChatModel注入进去再设置一些默认的系统提示词。Configuration public class AiConfig { Bean public ChatClient chatClient(ChatModel chatModel) { return ChatClient.builder(chatModel) .defaultSystem(你是一个专业的企业知识助手回答基于提供的参考资料不要编造。) .build(); } }流式调用是重点。同步调用用call()流式调用用stream()返回的是FluxString。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String question) { return chatClient.prompt() .user(question) .stream() .content(); }这里produces MediaType.TEXT_EVENT_STREAM_VALUE是关键它告诉 Spring 这个接口返回的是 SSE 流。前端用EventSource或者fetch的流式读取来接收。实测下来这个写法在 WebFlux 环境下工作得很好但如果你项目是传统的 Spring MVC基于 Servlet 的需要额外配置一个Flux到SseEmitter的适配否则可能不生效。这是很多人踩过的坑代码看着没问题但前端就是收不到流式数据最后发现是 MVC 和 WebFlux 的差异导致的。注意Spring MVC 和 WebFlux 对 Flux 返回值的处理方式不同。MVC 环境下建议用SseEmitter手动桥接或者直接引入 WebFlux 依赖走响应式栈。混用的时候要特别小心线程模型。3.3 RAG 知识库的文档处理与向量化流程RAG 的第一步是把文档变成向量。这个过程分三步读取文档、切分文本、调用 Embedding 模型生成向量。读取文档这块Spring AI 提供了DocumentReader接口常见格式比如 PDF、Markdown、纯文本都有对应实现。但实际项目里文档格式往往很杂有 Word、有 Excel、有数据库里的工单记录这时候需要自己写 Reader 实现把各种来源统一转成Document对象。切分文本是最容易被低估的一步。大模型有上下文长度限制你不能把一整本手册塞进去必须切成小块。Spring AI 提供了TokenTextSplitter按 token 数量切分。切分大小一般建议 500 到 1000 个 token块与块之间留 100 到 200 个 token 的重叠。为什么要重叠因为如果一句话正好被切在中间检索的时候可能两边都匹配不上重叠能保证语义的连续性。TokenTextSplitter splitter new TokenTextSplitter(800, 200, 5, 10000, true); ListDocument chunks splitter.apply(documents);向量化就是把每个文本块通过 Embedding 模型转成一串浮点数存进向量库。Spring AI 支持多种向量库比如 Redis、PgVector、Milvus、Chroma。选哪个如果项目里已经有 Redis直接用 Redis 的向量检索功能最省事如果对检索性能要求高、数据量大Milvus 更专业如果只是本地开发测试Chroma 最轻量。我个人的经验是中小规模的知识库十万条以内用 PgVector 或者 Redis 完全够用没必要上重型向量库。3.4 检索增强生成的完整链路与参数调优检索环节的核心是相似度计算。用户提问后先把问题也转成向量然后在向量库里找最相似的 Top-K 个文本块。K 值取多少一般 3 到 5 比较合适。取太少可能漏掉关键信息取太多会引入噪音而且上下文太长会拖慢模型响应速度、增加 token 成本。SearchRequest request SearchRequest.query(question).withTopK(4).withSimilarityThreshold(0.7); ListDocument docs vectorStore.similaritySearch(request);similarityThreshold是相似度阈值低于这个值的直接过滤掉。这个参数很关键设太低会检索出一堆不相关的内容设太高可能什么都检索不到。建议从 0.7 开始调根据实际效果微调。检索到文档后把它们拼成上下文和用户问题一起发给模型。Spring AI 提供了QuestionAnswerAdvisor这个 Advisor能自动完成“检索-拼接-调用”的流程代码很简洁。ChatResponse response chatClient.prompt() .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults())) .user(question) .call() .chatResponse();但实际项目里我建议不要完全依赖 Advisor 的自动拼接因为不同场景对上下文的组织方式要求不一样。比如客服场景可能希望把最相关的文档放在最前面法律场景可能希望标注每段资料的来源。手动控制检索和拼接虽然代码多一点但灵活性和可控性高很多。4. 实操过程从零搭建一个可用的 RAG 问答服务4.1 环境准备与项目骨架搭建先说环境。JDK 17 是底线Spring AI 的很多特性依赖较新的 Java 版本。Maven 3.8 以上Spring Boot 3.2 以上。如果你本地还在用 JDK 8建议先升级不然很多依赖会报版本冲突。项目骨架用 Spring Initializr 生成选 Spring Web、Spring AI、Redis如果打算用 Redis 做向量库这几个依赖。生成后先跑一个最简单的 ChatClient 调用确认 API Key 配置正确、网络能通。这一步看着简单但我见过不少人卡在这里——API Key 没配环境变量、模型名称写错、账户余额不足各种问题都有。先跑通最小闭环再往上加功能这是我一贯的做法。SpringBootTest class SmokeTest { Autowired private ChatClient chatClient; Test void testBasicChat() { String response chatClient.prompt() .user(你好请用一句话介绍你自己) .call() .content(); System.out.println(response); assertNotNull(response); } }这个冒烟测试跑通说明基础设施没问题了。接下来才是 RAG 的部分。4.2 文档入库从原始资料到向量库的完整流程假设我们要做一个产品文档问答助手原始资料是一堆 Markdown 文件。第一步是读取用TextReader或者自己写文件遍历逻辑。ListDocument documents new ArrayList(); Path docsPath Paths.get(src/main/resources/docs); Files.walk(docsPath) .filter(path - path.toString().endsWith(.md)) .forEach(path - { try { String content Files.readString(path); Document doc new Document(content, Map.of(source, path.getFileName().toString())); documents.add(doc); } catch (IOException e) { log.error(读取文档失败: {}, path, e); } });第二步是切分。这里要注意Markdown 文件有标题层级切分的时候最好保留标题信息这样检索出来的块能知道它属于哪个章节。Spring AI 的TokenTextSplitter默认不保留这种结构信息需要自己在 metadata 里补。TokenTextSplitter splitter new TokenTextSplitter(800, 200, 5, 10000, true); ListDocument chunks splitter.apply(documents);第三步是写入向量库。以 Redis 为例配置好RedisVectorStore后直接调用add()方法。vectorStore.add(chunks);这一步会调用 Embedding 模型把每个 chunk 转成向量然后存进 Redis。如果文档量大这个过程会比较慢建议做成异步任务或者分批处理。我实测过一千个 chunk 大概需要三到五分钟取决于 Embedding 模型的响应速度。提示文档入库是一次性工作但文档更新后需要重新入库。建议给每个文档加一个版本号或者更新时间戳更新时先删除旧向量再写入新向量避免新旧内容混在一起。4.3 检索与生成的联调让回答有据可依检索和生成的联调是整个 RAG 服务最核心的部分。先写一个检索接口输入问题返回最相关的几个文档块。public ListDocument retrieve(String question, int topK) { SearchRequest request SearchRequest.query(question) .withTopK(topK) .withSimilarityThreshold(0.7); return vectorStore.similaritySearch(request); }然后写生成逻辑把检索结果拼进 prompt。public String answer(String question) { ListDocument docs retrieve(question, 4); String context docs.stream() .map(Document::getContent) .collect(Collectors.joining(\n\n---\n\n)); String prompt 请基于以下参考资料回答用户问题。如果参考资料中没有相关信息请明确说明不知道不要编造。 参考资料 %s 用户问题%s .formatted(context, question); return chatClient.prompt().user(prompt).call().content(); }这个 prompt 模板里有一句很关键的话“如果参考资料中没有相关信息请明确说明不知道不要编造。”这句话能大幅降低模型胡说的概率。我试过不加这句话模型经常把检索到的无关内容和自己的训练知识混在一起给出一个看似合理但完全错误的答案。联调的时候建议准备一组测试问题覆盖三种情况知识库里有明确答案的、知识库里没有答案的、问题模糊需要模型追问的。每种情况跑几遍看回答质量是否稳定。如果发现检索不准先调topK和similarityThreshold如果检索准但回答不好调 prompt 模板和temperature。4.4 流式输出与前端对接的实操细节流式输出前面提过核心是返回FluxString并设置produces。但前端对接的时候有几个细节要注意。第一是 SSE 的格式。Spring 返回的FluxString会被包装成data: xxx\n\n的格式前端用EventSource接收时onmessage事件里的data字段就是内容。但如果内容里本身有换行可能会破坏 SSE 格式需要做转义处理。第二是连接中断的处理。用户可能中途关闭页面这时候后端还在生成会浪费 token。建议在前端加一个AbortController页面关闭时取消请求后端用Flux的doOnCancel钩子做清理。GetMapping(value /chat/stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(RequestParam String question) { return chatClient.prompt() .user(question) .stream() .content() .doOnCancel(() - log.info(客户端取消请求: {}, question)) .doOnError(e - log.error(流式输出异常, e)); }第三是超时设置。大模型生成长文本可能超过默认的 HTTP 超时时间需要在配置文件里把spring.mvc.async.request-timeout或者 WebFlux 对应的超时参数调大一般设成 60 秒以上比较稳妥。5. 常见问题与排查技巧实录5.1 依赖冲突与版本不兼容的排查思路Spring AI 依赖冲突是高频问题。典型症状是启动时报NoSuchMethodError或者ClassNotFoundException但代码明明没改过。原因通常是 Spring AI 的某个 starter 传递依赖了一个和项目里已有版本冲突的库比如 Reactor、Jackson、OkHttp。排查方法用mvn dependency:tree把依赖树打出来搜索冲突的库看哪个版本被实际引入。然后用exclusions排除掉传递依赖或者用dependencyManagement统一版本。mvn dependency:tree -Dincludesio.projectreactor:reactor-core我遇到过一次项目里 Spring Boot 版本是 3.1但 Spring AI 的某个 starter 要求 3.2结果 Reactor 版本对不上流式接口一直报错。升级 Spring Boot 版本后问题解决。所以选版本的时候Spring Boot 和 Spring AI 的兼容性要提前确认官方文档有兼容性矩阵。5.2 流式响应中断与超时的典型场景流式响应中断有好几种原因排查的时候要分情况。第一种是网络层中断。如果服务部署在网关后面网关可能有自己的超时设置比如 Nginx 默认 60 秒。大模型生成超过 60 秒的文本时网关会主动断开连接。解决办法是调大网关超时或者在应用层做心跳保活。第二种是模型侧超时。有些模型对单次请求的生成时长有限制超过就断开。这种情况只能控制 prompt 长度和max_tokens参数让生成在限制内完成。第三种是代码层面的背压问题。如果消费端处理太慢Reactor 会触发背压可能导致流被取消。检查Flux的订阅逻辑确保没有阻塞操作。注意流式接口的日志要打全包括请求开始、首字返回、流结束、异常断开这几个时间点。排查问题时这几个时间戳能快速定位是网络问题、模型问题还是代码问题。5.3 检索结果不准确时的调优清单检索不准是 RAG 最常见的痛点。我整理了一个调优清单按优先级排序。问题现象可能原因调优动作检索不到相关内容相似度阈值太高把similarityThreshold从 0.7 降到 0.5 试试检索到无关内容切分粒度太粗减小 chunk size增加重叠关键信息被切散切分点不合理按语义切分保留标题结构同义问题检索不到Embedding 模型不适合中文换一个中文优化过的 Embedding 模型答案不完整Top-K 太小把topK从 3 调到 5 或 6答案有编造prompt 约束不够强化“不知道就说不知道”的指令调优的时候一次只改一个参数改完跑测试集看效果不要一次改好几个否则不知道是哪个起了作用。5.4 成本控制与性能优化的实战经验大模型调用是花钱的尤其是 Embedding 和生成都按 token 计费。几个控制成本的实操经验。第一缓存。相同的问题如果被问过直接返回缓存结果不要重复调用模型。用 Redis 做一层缓存key 是问题的哈希value 是回答。缓存过期时间设个几小时既能省钱又不至于返回太旧的内容。第二控制上下文长度。检索到的文档块不要全塞进去按相似度排序取前几个就行。上下文越长token 消耗越大而且模型对长上下文的注意力会分散效果反而下降。第三Embedding 结果复用。文档入库时生成的向量要持久化存储不要每次检索都重新算。向量库本身就是干这个的用好它。第四异步处理。文档入库、批量问答这些耗时操作做成异步任务避免阻塞主线程也方便做限流和重试。性能方面首字延迟是用户体验的关键指标。优化首字延迟的手段包括用流式输出、选响应快的模型、减少检索耗时、预热连接池。我实测下来把检索从同步改成异步、把模型连接池预热首字延迟能从三秒降到一秒以内。6. 从单点落地到工程化Java 研发的 AI 能力迁移路径6.1 把 AI 能力封装成内部服务的最佳实践单点跑通 RAG 问答之后下一步是把它封装成团队内部可以复用的服务。这一步的核心思路是“AI 能力服务化”不要让每个业务模块都自己去调模型、管向量库。我的做法是建一个独立的 AI 服务模块对外暴露几个清晰的接口问答接口、文档入库接口、检索接口。业务方通过 HTTP 或者内部 RPC 调用不需要关心底层用的是哪个模型、向量库是什么。这样做的好处是模型切换、参数调优、成本控制都集中在一处不用改一堆业务代码。接口设计上问答接口建议同时提供同步和流式两个版本让调用方根据场景选择。文档入库接口要支持批量、支持异步、支持进度查询。检索接口可以单独暴露方便有些场景只需要检索不需要生成。public interface AiService { String ask(String question); FluxString askStream(String question); String ingest(ListDocument documents); ListDocument retrieve(String question, int topK); }这个接口定义看着简单但把 RAG 的核心能力都覆盖了。业务方拿到这个接口接进自己的系统就行不需要理解 RAG 的内部原理。6.2 与现有权限体系和业务数据的打通企业场景里AI 服务不能是孤立的必须和现有的权限体系打通。举个例子销售部门的员工问“上季度的销售数据”他只能看到自己负责的区域财务部门的员工问同样的问题看到的是全公司数据。如果 AI 服务不感知权限直接把所有数据都检索出来就是严重的数据泄露。打通权限的思路是在检索环节加过滤条件。向量库的 metadata 里存上文档的权限标签检索时根据当前用户的权限过滤。Spring AI 的SearchRequest支持传 filter 表达式可以按 metadata 过滤。SearchRequest request SearchRequest.query(question) .withTopK(4) .withFilterExpression(department sales region east);业务数据打通是另一个维度。很多企业的知识不在文档里而在数据库里。比如订单状态、库存数量、客户信息这些实时数据不适合做向量化但 AI 回答时又需要。解决办法是让 AI 服务能调用业务接口把实时数据查出来拼进上下文。这就是 Agent 的思路了——AI 不只是被动检索还能主动调用工具获取信息。6.3 从 RAG 到 Agent 的演进方向RAG 解决的是“知识问答”Agent 解决的是“任务执行”。两者的区别在于RAG 是“你问我答”Agent 是“你给目标我自己规划步骤去完成”。举个具体例子。用户说“帮我查一下上个月华东区销售额最高的三个产品然后给对应的销售负责人发一封总结邮件”。RAG 做不了这个因为它只能检索和回答。Agent 可以先调用销售数据接口查出数据再调用邮件接口发邮件中间可能还需要调用通讯录接口查负责人邮箱。整个过程是模型自己规划、自己调用工具、自己串联步骤。Java 生态里做 Agent 目前还在早期但思路是清晰的把每个业务能力封装成一个 Tool让模型根据用户意图选择调用哪个 Tool。Spring AI 的FunctionCallback机制就是干这个的把 Java 方法注册成模型可以调用的函数。Bean Description(查询指定区域的销售数据) public FunctionSalesQuery, SalesResult querySales() { return query - salesService.query(query.region(), query.period()); }这个方向值得 Java 研发重点关注因为 Agent 落地的瓶颈不在模型能力而在工程能力——怎么管理工具、怎么处理错误、怎么保证安全、怎么做审计。这些恰好是 Java 后端的强项。6.4 团队协作与知识沉淀的建议最后聊点软性的。AI 落地不是一个人的事需要团队协作。我的建议是建一个内部的 AI 实践文档把踩过的坑、调过的参数、验证过的方案都记下来。模型和框架迭代太快今天有效的配置下个月可能就变了有个文档能省很多重复劳动。另外prompt 模板建议做成可配置的不要硬编码在代码里。不同业务场景对 prompt 的要求不一样让业务方自己能调比每次改代码重新发布高效得多。可以做一个简单的 prompt 管理页面或者用配置中心管理。团队分工上建议有人专门负责模型和框架的跟进有人负责业务场景的对接有人负责基础设施和运维。AI 落地涉及的面很广一个人全包容易顾此失彼。我在实际项目里最大的体会是Java 研发做 AI 落地技术门槛没有想象中高真正的挑战在于思维方式的转变——从“确定性逻辑”转向“概率性输出”从“写死规则”转向“设计约束”。模型会犯错会不稳定会有幻觉工程上要做的是用缓存、用校验、用兜底策略把这些不确定性控制在可接受范围内。这件事 Java 研发其实很擅长我们做了这么多年的高可用、降级、熔断本质上都是在跟不确定性打交道。把这份经验迁移到 AI 场景里就是 Java 研发在 AI 时代真正的座位。
返回列表