Spring AI 大特性,你知道几个?

张开发
2026/4/18 5:51:19 15 分钟阅读

分享文章

Spring AI 大特性,你知道几个?
前面几篇聊了 Spring AI 的搭建、特色功能和一些偏聊天场景的案例。今天换个口味聊两个我最近在生产环境里折腾出来的真实案例——多模态数据处理和批量流水线。说实在的现在的AI教程十个有九个都在讲“怎么写一个聊天机器人”但企业里真正的需求往往是上千份合同批量提取关键字段、会议录音自动生成纪要、PDF里的表格和图片一起理解……这些东西才是Java工程师真正能发光发热的地方。一、多模态数据处理从“只认文字”到“能看懂PDF里的表格和图片”先说第一个案例也是让我最头疼的一个。真实的痛点PDF里的信息藏在表格里去年年底接了个需求客户是做供应链金融的每天收到几百份供应商传来的采购合同PDF。这些PDF格式五花八门有的里面只有文字有的是扫描件有的还嵌了表格和产品图片。他们的需求很简单但也很棘手自动识别合同里的关键字段合同编号、签订日期、金额、产品清单然后存到数据库里。以前怎么做人工录入。效率低不说还容易出错。Spring AI 1.0 之后的多模态支持让我看到了希望。Spring AI 通过统一的AudioTranscriptionModel和SpeechModel接口支持音频转录和语音合成覆盖了 OpenAI Whisper、Azure OpenAI 等主流提供商。更关键的是很多模型提供商比如 OpenAI 的 GPT-4V、Azure OpenAI、Google Gemini都开始支持多模态输入也就是能看懂图片、PDF里的内容。架构思路ETL 多模态识别我设计的方案分三步提取层用 Spring AI 的 ETL 框架读取 PDFSpring AI 提供了多种DocumentReader实现包括支持 PDF 的PagePdfDocumentReader、支持多格式的TikaDocumentReader等。但普通文本提取器只能拿到文字表格和图片信息会丢失。所以这里需要特殊处理——针对表格部分用模型的多模态能力直接“看图说话”。转换层用DocumentTransformer处理分块和内容增强Spring AI 内置了TokenTextSplitter进行智能分块以及KeywordMetadataEnricher和SummaryMetadataEnricher这种利用大模型生成关键词和摘要的增强器。加载层通过DocumentWriter写入向量数据库或业务数据库VectorStore是官方提供的向量数据库写入器实现。代码实现读取 PDF 多模态识别以下是核心的代码片段第一步读取 PDF 文件importorg.springframework.ai.document.Document;importorg.springframework.ai.reader.tika.TikaDocumentReader;importorg.springframework.ai.reader.pdf.PagePdfDocumentReader;importorg.springframework.core.io.InputStreamResource;importorg.springframework.core.io.Resource;ServicepublicclassContractReaderService{/** * 使用 TikaDocumentReader 读取 PDF适合纯文本 PDF * Tika 可以读取 PDF、DOCX、PPTX、HTML 等 50 种格式 */publicListDocumentreadWithTika(MultipartFilefile){ResourceresourcenewInputStreamResource(file.getInputStream());TikaDocumentReaderreadernewTikaDocumentReader(resource);returnreader.read();}/** * 按页读取 PDF适合需要保留页面结构的场景 * 每个页面作为一个独立的 Document 对象保留页码元数据 */publicListDocumentreadPageByPage(MultipartFilefile){ResourceresourcenewInputStreamResource(file.getInputStream());PagePdfDocumentReaderreadernewPagePdfDocumentReader(resource);ListDocumentdocumentsreader.read();// 每个 document 的 metadata 中包含 pageNumber 信息returndocuments;}}第二步把 PDF 页面转换成图片交给多模态模型识别这是关键步骤。对于含有复杂表格和图片的 PDF光靠文本提取不够需要用多模态模型直接“看”页面内容。ServicepublicclassMultimodalExtractorService{privatefinalChatClientchatClient;privatefinalPdfToImageConverterpdfConverter;// 自研组件基于 PDFBoxpublicMultimodalExtractorService(ChatClient.Builderbuilder){this.chatClientbuilder.build();this.pdfConverternewPdfToImageConverter();}/** * 从 PDF 页面中提取合同关键字段 * 直接交给多模态模型如 GPT-4V、Qwen-VL去“看”页面截图 */publicContractInfoextractFromPage(MultipartFilefile,intpageNum){// 1. 将 PDF 页面转为图片byte[]pageImagepdfConverter.convertPageToImage(file,pageNum);// 2. 通过 ChatClient 调用多模态模型// 注意需要配置支持多模态的模型如 OpenAI 的 gpt-4o、Azure OpenAI 等StringresultchatClient.prompt().user(userSpec-userSpec.text(请从这份合同的第pageNum页中提取以下字段合同编号、签订日期、甲方公司名称、合同总金额。以 JSON 格式返回。).media(MimeTypeUtils.IMAGE_PNG,pageImage)// 多模态输入).call().content();// 3. 将 JSON 结果映射成 Java 对象returnJsonMapper.fromJson(result,ContractInfo.class);}}看到没关键就这一行.media()——直接把图片丢给模型去“看”。Spring AI 的ChatClient对多模态输入的支持就是这样简洁你不需要自己去封装 HTTP 请求、处理 base64 编码什么的。踩过的坑这个案例我踩了不少坑说两个最要命的坑一PDF 转图片的质量直接影响识别率。如果页面分辨率太低模型连字都看不清。我最后用的是 PDFBox ImageIO 的组合输出 PNG 格式分辨率设到 150 DPI 以上识别率才稳定在 90% 以上。坑二表格识别别用文本提取器。我一开始用PagePdfDocumentReader提取纯文本结果多列表格的行列关系完全乱掉了。后来改用多模态方式——直接把页面截图喂给模型让模型自己去理解表格结构准确率从 60% 飙升到 95%。坑三大文档的分页处理需要控制并发。一份合同几十页每页都调用一次模型 API串行处理太慢了。后面会讲到用虚拟线程做并发批量调用这个场景就很适合。二、ETL Pipeline构建企业级数据处理流水线说完多模态接着聊 Spring AI 的 ETL 框架。这是 Spring AI 1.0 最重磅的功能之一专门为 RAG 场景的数据准备阶段设计的。ETL 是什么ETL 是 Extract提取、Transform转换、Load加载的缩写。在 Spring AI 里它做的事情就是从各种来源读取原始文档PDF、Word、Excel、网页、JSON 等把文档切分成适合向量化的小块最后存到向量数据库里。Spring AI 的 ETL 框架围绕三个核心接口构建接口职责内置实现举例DocumentReader提取原始文档TikaDocumentReader,PagePdfDocumentReader,JsonReader,MarkdownDocumentReaderDocumentTransformer转换/增强文档TokenTextSplitter,KeywordMetadataEnricher,SummaryMetadataEnricherDocumentWriter写入目标存储VectorStore,FileDocumentWriter这三个接口的巧妙之处在于它们的函数式设计——DocumentReader是SupplierListDocumentDocumentTransformer是FunctionListDocument, ListDocumentDocumentWriter是ConsumerListDocument可以像流水线一样串联起来。实战构建一个文档处理流水线假设你有一个文件夹里面有 PDF、Word、Markdown 三种格式的文档你想把它们全部读出来、切分成合适大小的小块、提取关键词和摘要、最后存到向量数据库里。importorg.springframework.ai.document.Document;importorg.springframework.ai.reader.tika.TikaDocumentReader;importorg.springframework.ai.transformer.splitter.TokenTextSplitter;importorg.springframework.ai.transformer.metadata.KeywordMetadataEnricher;importorg.springframework.ai.transformer.metadata.SummaryMetadataEnricher;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.FileSystemResource;ServicepublicclassDocumentETLPipeline{privatefinalVectorStorevectorStore;privatefinalChatModelchatModel;// 用于生成关键词和摘要publicDocumentETLPipeline(VectorStorevectorStore,ChatModelchatModel){this.vectorStorevectorStore;this.chatModelchatModel;}/** * 执行完整的 ETL 流水线 * 函数式风格writer.accept(transformer.apply(reader.read())) */publicvoidprocessFile(StringfilePath){// 1. Extract: 用 TikaDocumentReader 读取文件支持 PDF/Word/Markdown 等 50 格式DocumentReaderreadernewTikaDocumentReader(newFileSystemResource(filePath));ListDocumentrawDocumentsreader.read();// 2. Transform: 切分成合适大小的小块TokenTextSplitter 是 workhorseDocumentTransformersplitternewTokenTextSplitter(800,200);// chunkSize800, overlap200ListDocumentchunkedDocumentssplitter.apply(rawDocuments);// 3. Transform: 用 AI 提取关键词加到 metadata 里KeywordMetadataEnricherkeywordEnrichernewKeywordMetadataEnricher(chatModel,5);ListDocumentenrichedWithKeywordskeywordEnricher.apply(chunkedDocuments);// 4. Transform: 用 AI 生成摘要加到 metadata 里SummaryMetadataEnrichersummaryEnrichernewSummaryMetadataEnricher(chatModel);ListDocumentfinalDocumentssummaryEnricher.apply(enrichedWithKeywords);// 5. Load: 写入向量数据库vectorStore.write(finalDocuments);log.info(处理完成共生成 {} 个文档块,finalDocuments.size());}}关于TokenTextSplitter的参数选择多说两句chunkSize是每个块的最大 token 数overlap是块与块之间重叠的 token 数。overlap的作用是保留块边界的上下文避免重要信息被“切”在边缘。经过多次测试800 token 的块大小搭配 200 token 的重叠长度在检索准确性和 token 消耗之间取得了比较好的平衡。更优雅的流水线写法上面的代码是分步写的Spring AI 的函数式设计允许你直接把流水线串联成一条链// 一行代码完成整个 ETL 流程vectorStore.write(newSummaryMetadataEnricher(chatModel).apply(newKeywordMetadataEnricher(chatModel,5).apply(newTokenTextSplitter(800,200).apply(newTikaDocumentReader(resource).read()))));虽然可读性差点但那种“数据像流水一样流过一个个处理器”的感觉真的很爽。三、批量处理 虚拟线程让 AI 应用从“能跑”到“跑得快”聊完 ETL 框架再说一个我最近特别喜欢的特性——结合 Java 虚拟线程Virtual Threads做批量 AI 调用。痛点串行调用大模型太慢了做过 AI 应用的都知道调用一次大模型 API 的延迟通常在 1 到 3 秒之间。如果你需要处理 1000 条数据串行调用就是 1000 到 3000 秒——差不多一个小时到一个半小时。这在生产环境是不可接受的。虚拟线程Java 21完美解决了这个问题。虚拟线程是轻量级线程专为 IO 密集型任务设计。调用大模型 API 是典型的 IO 密集型任务大部分时间在等待网络响应虚拟线程可以在等待时让出 CPU实现极高的并发度。实战批量总结 1000 篇文档下面这个例子展示了如何用虚拟线程并发调用大模型批量生成文档摘要ServicepublicclassBulkSummarizationService{privatefinalChatClientchatClient;privatestaticfinalintBATCH_SIZE100;// 每批 100 个请求publicvoidsummarizeDocuments(ListDocumentdocuments){// 分批次处理避免一次性请求太多触发 API 限流for(inti0;idocuments.size();iBATCH_SIZE){ListDocumentbatchdocuments.subList(i,Math.min(iBATCH_SIZE,documents.size()));processBatch(batch);}}privatevoidprocessBatch(ListDocumentbatch){// 创建虚拟线程执行器Java 21 特性try(ExecutorServiceexecutorExecutors.newVirtualThreadPerTaskExecutor()){// 为每个文档创建一个并发任务ListCompletableFutureDocumentSummaryfuturesbatch.stream().map(doc-CompletableFuture.supplyAsync(()-{try{// 调用大模型生成摘要StringsummarychatClient.prompt().user(请为以下文档生成 200 字以内的摘要\ndoc.getContent()).call().content();returnnewDocumentSummary(doc.getId(),summary);}catch(Exceptione){log.error(处理文档 {} 失败,doc.getId(),e);returnnull;// 单条失败不影响整批}},executor)).toList();// 等待本批次所有任务完成ListDocumentSummaryresultsfutures.stream().map(CompletableFuture::join).filter(Objects::nonNull).toList();// 批量保存结果到数据库summaryRepository.saveAll(results);log.info(批次处理完成成功 {} / {},results.size(),batch.size());}}}这段代码的核心思路先把数据分批每批 100 个避免 API 限流或内存溢出每批内部用虚拟线程并发调用大模型 API用CompletableFuture.join()等待整批完成单条失败不影响整批通过filter(Objects::nonNull)过滤掉失败的我实测过处理 1000 篇文档串行大约需要 50 分钟平均每篇 3 秒。用虚拟线程并发每批 100 个并发处理时间直接压缩到 3 分钟以内。虚拟线程的轻量级特性决定了你开几百个并发也不会像传统线程池那样 OOM这是 Java 21 带给 AI 应用最大的性能红利。四、再聊两个用过的坑写完案例顺手补充两个在这两个场景里踩过的坑。坑一TikaDocumentReader 的内存问题。Tika 虽然格式支持全面但它会把整个文件加载到内存里。处理超大 PDF几百 MB时很容易 OOM。我的解决方案是超过 50MB 的文件改用PagePdfDocumentReader按页处理配合虚拟线程做流式处理每读一页就处理一页内存占用大大降低。坑二虚拟线程的并发数不是越高越好。虽然虚拟线程本身很轻量但目标 API 是有速率限制的。我一开始设了每批 500 个并发结果直接把 OpenAI 的 API 限流给触发了一堆 429 错误。后来根据 API 的 RPM每分钟请求数限制来调整并发数经验值是 RPM ÷ 60 × 虚拟线程处理单条的平均耗时。比如 RPM3000单条约 3 秒那并发控制在 150 左右比较安全。坑三ETL 流程中的元数据丢失。TokenTextSplitter切分文档后元数据会复制到每个切分出来的小块上这是好事。但如果你用了多个DocumentTransformer中间的某个步骤可能会意外修改或丢失元数据。我的建议是在流水线的最后一步做一次元数据完整性检查确保每个块都保留了关键的元数据字段比如原始文件名、页号、文档类型。五、写在最后聊了三个实战案例——多模态 PDF 信息提取、ETL 数据流水线、批量并发调用——回头看它们背后其实是一个共同的思路Spring AI 不只是帮你调模型更是帮你把 AI 能力“工程化”地嵌入到现有系统中。多模态 PDF 识别解决了“AI 如何看懂复杂格式文档”的问题。ETL 框架解决了“如何标准化地处理海量异构数据”的问题。虚拟线程批量调用解决了“如何让 AI 应用从单条调用走向大规模生产”的问题。我一直在强调一件事Java 开发者做大模型应用优势不在模型训练那是 Python 的地盘而在于工程化。Spring AI 给的恰恰就是工程化的武器——统一抽象、函数式流水线、与 Spring 生态无缝集成、对 Java 21 新特性的拥抱。下一篇文章可能聊聊MCP模型上下文协议和Session API这些是 Spring AI 最近更新里比较新也比较值得关注的方向。如果你在生产环境用 Spring AI 做了什么有意思的事也欢迎来分享。

更多文章