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);

关键参数:

参数 默认值 说明
defaultChunkSize 800 每块目标Token数
minChunkSizeChars 350 最少字符数,低于此尝试合并
minChunkLengthToEmbed 5 低于此直接丢弃
maxNumChunks 10000 单文档最大块数
keepSeparator true 是否保留分隔符

中文场景的坑

TokenTextSplitter的默认分隔符是英文标点(.?!\n),中文断句效果差。Spring AI Alibaba提供的SentenceSplitter基于NLP模型识别中文句子边界,更适合中文文档。

分块策略对比

策略 原理 优点 缺点 适用场景
TokenTextSplitter 按Token数切 简单,块均匀 可能在句中断开 通用快速上手
SentenceSplitter NLP识别句子边界 中文友好 依赖预训练模型 中文文档首选
语义分块 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模型选型

特性 v1 v2 v3 v4
输出维度 固定1536 固定1536 可选1024/768/512等 可选2048/1536/1024/768等
最大输入 2048 2048 8192 32000
多语言 主流语种 +日韩德俄 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


更多推荐