Java后端做RAG从4步入门到文档入库实战RAG系列①你是否遇到过大模型答非所问或者瞎编答案的尴尬做Dream-SaaS的时候我需要让AI回答用户上传的文档内容结果DeepSeek给我编了一段完全不存在的产品说明。那一刻我才明白模型根本没见过我的私有文档它只能猜。RAG检索增强生成就是来解决这个问题的。本系列定位面向Java后端开发者从零开始讲透RAG的原理、入门、实战和调优。没有Python没有LangChain只有Spring Boot pgvector DeepSeek的真实项目经验。读完这个系列你能在自己的Java项目里跑通RAG问答。一、RAG到底解决什么问题大模型训练用的数据是公开的、通用的知识。你问它Java基础、设计模式它门清。但你问它「我上传的这份产品文档第3页写了什么」它只能编——术语叫「幻觉」说白了就是它不知道只能猜。RAGRetrieval-Augmented Generation检索增强生成就是来解决这个问题的。原理很简单别让模型自己编把答案先查出来连同问题一起扔给模型。类比一下就像你考试时带的小抄——不是自己背不下来而是有些内容实时查更准。二、Java后端视角看RAG核心就4步说RAG高大上的很多但用你熟悉的概念理解它就是一次数据处理流水线。第一步文档分块原始文档可能是一整本200页的用户手册直接扔进去模型也处理不了。得先切成小块——每块几百到上千字。这操作你熟不熟像不像ETL里的数据拆分把一个大表拆成小表把长文本拆成段落。道理一模一样。第二步向量化每个文本块扔进Embedding模型输出一串数字向量——你可以理解为一个「语义指纹」。类比一下ElasticSearch的倒排索引是按关键词建索引。而向量索引是按「语义」建索引。「加班申请流程」和「怎么补假条」在ES里可能匹配不上但在向量空间里距离很近。第三步检索用户提问 → 把问题也转成向量 → 在向量数据库里找最相似的N个文本块。这一步说白了就是相似度查询。pgvector里写起来长这样SELECT content, 1 - (embedding [你的问题向量]) AS similarity FROM document_chunks ORDER BY embedding [你的问题向量] LIMIT 3;是pgvector的距离运算符1 - 距离就是相似度。你天天写SQL这玩意儿换个函数而已。第四步生成把检索到的文本块和用户问题一起塞进Prompt模板请根据以下参考资料回答用户问题。 参考资料 {chunk1} {chunk2} {chunk3} 用户问题{question} 回答这就是模板拼接Java里你写过无数遍了。Spring AI Alibaba帮你把Embedding、检索、Prompt组装好代码写出来大概长这样Bean public ChatModel dashScopeChatModel() { return DashScopeChatModel.builder() .apiKey(apiKey) .model(deepseek-chat) .build(); } Bean public VectorStore pgVectorStore(JdbcTemplate jdbcTemplate) { return new PgVectorStore(jdbcTemplate, dashScopeEmbeddingModel()); }剩下的就是配置分块策略、设置相似度阈值跟配置数据库连接池参数一个思路。三、我的踩坑经历写完第一版RAG去测试发现一个奇怪的现象同一个文档有的问题答得很好换个问法答案就离谱了。排查了一圈发现问题出在两个地方一是分块策略。我把文档按固定长度切结果一个问题的答案被切成两半检索时只能捞到半截。后面改成按段落、按语义切效果立刻不一样。二是检索质量。不是「搜到了就行」而是「搜对了才行」。Top-K取多少、相似度阈值设多高、要不要加重排序——这些都得调。说白了RAG的难点不在「能不能跑通」而在「跑通了能不能用」。分块和检索优化才是真正拉开差距的地方。四、技术栈选型说明一下为什么是Spring AI Alibaba pgvectorLangChain很火但它是Python生态。我试过用LangChain4j文档少、坑多、社区活跃度也不够。Spring AI Alibaba是Java自己的方案跟Spring Boot无缝集成出了问题能看源码能调试。向量数据库为什么选pgvector而不是Milvus一句话够用就行而且运维简单。2核8G的服务器跑个pgvector没压力数据跟你其他业务表放一起不用多维护一套系统。等量上来了再迁也来得及。Embedding模型用DashScope通义千问的API国内直接调不用科学上网延迟也低。Java搭RAG实战二四步跑通文档入库维度配错直接404上篇聊了Java做RAG为什么选PgVector而不是Milvus这篇直接上手——我用Spring AI把入库侧全跑通了。整个链路是个ETL流水线文档 → 读取 → 分块 → 向量化入库。对应Spring AI三个组件下面逐个拆。一、文档加载TikaDocumentReader基于Apache TikaPDF、Word、Markdown一行代码搞定var resource new InputStreamResource(file.getInputStream()); ListDocument documents new TikaDocumentReader(resource).get();解析后的Document对象包含getText()纯文本内容getMetadata()元数据Mapsource字段自动写入文件名注意扫描版PDF无法提取文字需要配合OCR复杂表格结构可能丢失建议用Apache POI按行列提取。二、文本分块为什么分、怎么分为什么必须分块Embedding模型有最大输入限制v1/v2是2048 tokensv4是32K tokens单个chunk包含多个主题时检索噪声增大经验值正文块300800 tokens重叠区50100 tokensTokenTextSplitter工作原理三步走Token化分块 → 找句子边界断开 → 过滤过短块// 默认配置 TokenTextSplitter splitter new TokenTextSplitter(); ListDocument chunks splitter.apply(documents); // 自定义参数技术文档 TokenTextSplitter splitter new TokenTextSplitter(600, 300, 10, 5000, true);关键参数参数默认值说明defaultChunkSize800每块目标Token数minChunkSizeChars350最少字符数低于此尝试合并minChunkLengthToEmbed5低于此直接丢弃maxNumChunks10000单文档最大块数keepSeparatortrue是否保留分隔符中文场景的坑TokenTextSplitter的默认分隔符是英文标点.?!\n中文断句效果差。Spring AI Alibaba提供的SentenceSplitter基于NLP模型识别中文句子边界更适合中文文档。分块策略对比策略原理优点缺点适用场景TokenTextSplitter按Token数切简单块均匀可能在句中断开通用快速上手SentenceSplitterNLP识别句子边界中文友好依赖预训练模型中文文档首选语义分块Embedding相似度决定语义最准计算成本高长文档/论文文档结构分块按标题章节切结构对齐要求文档有清晰结构技术规范/法规三、元数据检索过滤的关键分块后要给每个chunk补上元数据后面检索能按字段过滤for (Document chunk : chunks) { chunk.getMetadata().put(source_file, file.getOriginalFilename()); chunk.getMetadata().put(chunk_id, UUID.randomUUID().toString()); chunk.getMetadata().put(upload_time, System.currentTimeMillis()); }为什么要做这步入库了10份专利文档检索时只想从发明专利类型的文档里搜就需要metadata里有category字段。Spring AI的FilterExpression支持按metadata精确过滤SearchRequest request SearchRequest.builder() .query(合同违约) .topK(5) .similarityThreshold(0.7) .filterExpression(category 法律) .build();更复杂的多条件过滤FilterExpression filter FilterExpression.builder() .and( FilterExpression.builder().gte(year, 2020).build(), FilterExpression.builder().lte(year, 2023).build(), FilterExpression.builder().in(author, 张三, 李四).build(), FilterExpression.builder().eq(category, 法律).build() ) .build();如果过滤条件极其复杂跨文档JOIN、聚合可以用两阶段检索先SQL过滤出docId列表再用FilterExpression的in条件做向量检索。四、向量入库VectorStore PgVector配置要点spring: ai: vectorstore: pgvector: initialize-schema: true index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1024 # 必须和Embedding模型一致我踩的维度坑text-embedding-v4默认输出1024维如果PgVector配成1536维v1/v2的维度入库直接报404。排查方法int dimensions embeddingModel.dimensions(); System.out.println(当前模型维度 dimensions);入库代码// VectorStore.add()自动调用EmbeddingModel无需手动embed vectorStore.add(chunks);PgVector索引选择索引类型特点适用场景HNSW查询快构建慢内存占用高实时检索数据量中等IVFFlat构建快查询需调probes数据量大可接受近似HNSW推荐作为首选参数建议CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) WITH (m 16, ef_construction 64); -- 查询时调整召回率 SET hnsw.ef_search 100;五、完整ETL代码Service public class DocumentProcessor { private final VectorStore vectorStore; public DocumentProcessor(VectorStore vectorStore) { this.vectorStore vectorStore; } public ListDocument processAndStore(MultipartFile file) throws IOException { // 1. 读取文档 var resource new InputStreamResource(file.getInputStream()); ListDocument documents new TikaDocumentReader(resource).get(); // 2. 分块 TokenTextSplitter splitter new TokenTextSplitter(); ListDocument chunks splitter.apply(documents); // 3. 元数据 for (Document chunk : chunks) { chunk.getMetadata().put(source_file, file.getOriginalFilename()); chunk.getMetadata().put(chunk_id, UUID.randomUUID().toString()); } // 4. 入库 vectorStore.add(chunks); return chunks; } }六、text-embedding模型选型特性v1v2v3v4输出维度固定1536固定1536可选1024/768/512等可选2048/1536/1024/768等最大输入20482048819232000多语言主流语种日韩德俄50100含代码计费-0.0007元/千token-0.0005元/千token新项目直接用v4 1024维效果和成本的平衡点。踩坑总结坑现象解决方案维度不匹配入库报404确认Embedding模型维度配对pgvector的dimensions中文断句差分块在句中切断用SentenceSplitter或自定义分隔符chunk大小不当检索噪声大/上下文丢失根据文档类型调chunkSize经验值300~800 tokens在线体验dream-saas.com下篇预告RAG系列②将带来检索问答全链路三层架构 SSE流式输出混合检索实战向量 BM25关键词双路召回Rerank重排序qwen3-rerank完整接入敬请期待。作者宋哥转AI