当前位置: 首页 > news >正文

Java后端做RAG:从4步入门到文档入库实战

Java后端做RAG:从4步入门到文档入库实战

RAG系列①


你是否遇到过大模型"答非所问"或者"瞎编答案"的尴尬?做Dream-SaaS的时候,我需要让AI回答用户上传的文档内容,结果DeepSeek给我编了一段完全不存在的产品说明。那一刻我才明白:模型根本没见过我的私有文档,它只能猜

RAG(检索增强生成)就是来解决这个问题的。

本系列定位:面向Java后端开发者,从零开始讲透RAG的原理、入门、实战和调优。没有Python,没有LangChain,只有Spring Boot + pgvector + DeepSeek的真实项目经验。读完这个系列,你能在自己的Java项目里跑通RAG问答。


一、RAG到底解决什么问题

大模型训练用的数据是公开的、通用的知识。你问它Java基础、设计模式,它门清。但你问它「我上传的这份产品文档第3页写了什么」,它只能编——术语叫「幻觉」,说白了就是它不知道,只能猜。

RAG(Retrieval-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 + pgvector?

LangChain很火,但它是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 Tika,PDF、Word、Markdown一行代码搞定:

var resource = new InputStreamResource(file.getInputStream()); List<Document> documents = new TikaDocumentReader(resource).get();

解析后的Document对象包含:

  • getText():纯文本内容
  • getMetadata():元数据Map,source字段自动写入文件名

注意:扫描版PDF无法提取文字,需要配合OCR;复杂表格结构可能丢失,建议用Apache POI按行列提取。

二、文本分块:为什么分、怎么分

为什么必须分块?

  • Embedding模型有最大输入限制(v1/v2是2048 tokens,v4是32K tokens)
  • 单个chunk包含多个主题时,检索噪声增大
  • 经验值:正文块300800 tokens,重叠区50100 tokens

TokenTextSplitter工作原理

三步走:Token化分块 → 找句子边界断开 → 过滤过短块

// 默认配置 TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> 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 List<Document> processAndStore(MultipartFile file) throws IOException { // 1. 读取文档 var resource = new InputStreamResource(file.getInputStream()); List<Document> documents = new TikaDocumentReader(resource).get(); // 2. 分块 TokenTextSplitter splitter = new TokenTextSplitter(); List<Document> 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
多语言主流语种+日韩德俄50+100+含代码
计费-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


http://www.rkmt.cn/news/1466012.html

相关文章:

  • [智能体-255]:Retriever:RAG 核心底座、实现各类 RAG 的统一标准组件
  • Altium Designer新手必看:PCB设计里那些烦人的绿色报错,到底怎么一键搞定?
  • 深入glibc源码:图解_dl_fixup如何解析动态链接函数(附ret2dlresolve利用原理)
  • 零基础入门:在快马平台上手YOLOv8目标检测第一课
  • 告别卡顿与闪退:Quartus II 13.1在Win10/Win11下的稳定运行与性能优化配置全攻略
  • MATLAB行星齿轮动力学仿真脚本:含ode45求解器与完整系统建模函数
  • 别再只调参了!遗传算法解VRP时,这3个编码细节才是性能关键
  • MinerU2.5 Pro技术解析:1.2B参数SOTA PDF解析模型,完整部署教程(Transformers/vLLM/SGLang/Docker)
  • 给Jetson Nano B01换颗‘中国心’:手把手教你配置清华源并安装Python全家桶
  • C#上位机开发笔记:封装一个稳定可靠的欧姆龙NX PLC通信类库(附源码)
  • 用NodeMCU和Blinker自制万能红外遥控器,手把手教你让旧家电秒变智能(附完整代码)
  • Anaconda安装后必做的三件事:验证、配环境变量、创建你的第一个Python 3.8虚拟空间
  • 别再死磕D-H参数了!用Matlab Robotic Toolbox 10.4快速复现一个四轴机械臂(附完整代码)
  • LLM投毒:大模型数据层精准攻击与七道防御体系
  • 保姆级教程:用ROS和MAVROS搞定PX4 Offboard模式(附避坑指南)
  • 正统传承视角下的汕头高端私房菜核心技术标准拆解 - 奔跑123
  • 从漏洞原理到安全加固:手把手带你分析并修复ActiveMQ 5.x的Fileserver漏洞
  • 从自动驾驶到商品推荐:聊聊Smooth L1 Loss为何成了YOLO、Faster R-CNN的‘心头好’
  • MCP会成为AI时代的新中间件吗?
  • 挑选好用的固体饮料贴牌公司可以参考哪些参考依据?
  • 2026上海浦东可长短租的服务公寓!商务家庭租住全能适配 - 资讯速览
  • 真空脱泡搅拌机常见问题解答(2026最新专家版) - 资讯速览
  • 2026贵阳近郊烧烤山庄与团建聚餐一站式服务深度指南 - 企业名录优选推荐
  • HFSS新手避坑指南:手把手教你搞定威尔金森功分器仿真(附参数文件)
  • 从云端到边缘:基于土星云SE110S的智能视频分析轻量化部署方案(下)
  • 2026杭州室内游玩乐园新地标TOP5|室内浪漫避雨避晒,城市秘境成热门首选 - 资讯速览
  • HR系统选型第一步不是看产品,是做这件事|90%的人顺序搞反了
  • 2026年宜宾高县水上赛事完全选购指南 - 企业名录优选推荐
  • 提升学历,为什么一定要选正规函授站?本部、中介、函授站到底差在哪? - 奔跑123
  • 从0到1:打造99.99%高可用在线CRM系统的技术架构与实践