一个完整的RAG 流程文档加载 → 文档拆分 → 文本向量化 → 写入向量库 → 基于向量做语义检索今天我们就用 Java LangChain4j 通义千问的向量模型从零跑通这一整条链路而且搞两个版本内存版用InMemoryEmbeddingStore写个测试就能跑通Chroma 版用ChromaEmbeddingStore连上真正的向量数据库。你学完之后完全可以换成你们公司的 FAQ、退改签规则、产品手册搭一个自己的“公司知识库问答机器人”。一、先把任务说清楚这节课到底要干嘛这节课我们要做到三件事听得懂搞清楚 RAG 这条链路上都有哪些步骤每一步是干啥的、为什么要这样设计。写得出跟着我一行行写完并跑通两个测试RagFlowTest内存版向量库ChromaRagFlowTestChroma 版向量库迁得动明白“内存版 → Chroma 版”怎么迁移只改很少的代码就能从 Demo 走向可落地的架构。二、先把工具箱打开本项目里有哪些主角RagFlowTest非常重要用InMemoryEmbeddingStoreTextSegment跑完完整链路从airline_policy.txt读文档拆成一段一段的TextSegment全部向量化后塞进内存向量库问一句“取消经济舱机票要扣多少钱”看看最相关的片段是不是“经济舱退票”那段并断言。ChromaRagFlowTest和RagFlowTest几乎一模一样只是把向量库换成了ChromaEmbeddingStoreTextSegment连的是真实 Chroma 服务。理论打底RAG 整条链路长什么样先用一个简单的流程图把 RAG 画出来我们这节课重点关注左半边 中间文档怎么拆向量怎么算向量怎么存检索是怎么“按语义”而不是按关键词真正“问大模型”是下一步 RAG 的“G”Generation部分这里先不展开。内存版完整 RAG跟着RagFlowTest一步走1 创建向量模型QwenEmbeddingModel在setUp()里String apiKey System.getenv(QWEN_API_KEY); if (apiKey null || apiKey.trim().isEmpty()) { fail(环境变量 QWEN_API_KEY 未设置无法执行 RAG 测试); } // 这一行就创建好了向量化模型 embeddingModel QwenEmbeddingModel.builder() .apiKey(apiKey) .modelName(QwenModelName.TEXT_EMBEDDING_V3) .build();2 文档加载把 txt 读成 Document测试里第一步Document document ClassPathDocumentLoader.loadDocument( docs/airline_policy.txt, new TextDocumentParser() );可以理解为这一行把“文件”变成了“内存里的文档对象”后面所有处理都基于这个对象进行。3 文档拆分recursive splitter紧接着int maxSegmentSize 300; int overlap 50; DocumentSplitter splitter DocumentSplitters.recursive(maxSegmentSize, overlap); ListTextSegment segments splitter.split(document); assertFalse(拆分结果不能为空, segments.isEmpty());DocumentSplitters.recursive(...)会优先按结构边界拆句子、段落不够再按长度强拆。这比“纯按行”“纯按句”要鲁棒很多适合作为默认策略。拆完你会得到一个ListTextSegment每个TextSegment就是一小段可检索的知识块。4 向量化 内存入库InMemoryEmbeddingStore现在我们要给每个TextSegment发一张“语义身份证”并存进一个“内存向量仓库”里InMemoryEmbeddingStoreTextSegment embeddingStore new InMemoryEmbeddingStore(); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments);这里有几个关键点embedAll(segments)一次性帮你把所有片段都向量化比循环embed更高效也更优雅。InMemoryEmbeddingStoreTextSegment是 LangChain4j 自带的内存向量库实现不持久化进程挂了就没了。addAll(embeddings, segments)把“向量 原文片段”成批写进去。这一步结束后你已经有了一个可检索的“政策知识库”只不过它还在内存里。5 用户提问Query 也要向量化现在我们来假装一个真实用户问一句话String query 取消经济舱机票要扣多少钱; Embedding queryEmbedding embeddingModel.embed(query).content();这一步的本质把用户问题也变成同一个语义空间里的向量这样才能跟文档片段“在同一个坐标系里”比较距离。6 在内存向量库里做检索检索代码是这样的EmbeddingSearchRequest request EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(1) .minScore(0.5) .build(); EmbeddingSearchResultTextSegment result embeddingStore.search(request); EmbeddingMatchTextSegment topMatch result.matches().get(0); System.out.println(用户问题: query); System.out.println(最相关片段相似度: topMatch.score()); System.out.println(最相关片段内容: topMatch.embedded().text());解释一下参数queryEmbedding就是刚才问题的向量maxResults(1)只要最相关的一条minScore(0.5)如果相似度太低 0.5就直接不给结果了宁可说“查不到”。search返回的是一个EmbeddingSearchResult里面有matches()一个EmbeddingMatchTextSegment列表每个EmbeddingMatch里有score()相似度embedded()原始TextSegment。至此一个完整的 RAG 流程内存版就打通了。Chroma 版完整 RAG把“Demo”迁到“向量数据库”刚才我们炖的是“小锅菜”一切都在内存里。现在我们要上大菜把向量存进Chroma变成一个可持久化、可共享的向量库。对应的测试类src/test/java/com/xiaobian/ChromaRagFlowTest.java1 构建 ChromaEmbeddingStore在setUp()里我们这样初始化向量库embeddingStore ChromaEmbeddingStore.TextSegmentbuilder() .apiVersion(V2) .baseUrl(http://localhost:8000) .collectionName(flight_policies_test) .logRequests(false) .logResponses(false) .build(); // 为保证测试可重复清空该 collection embeddingStore.deleteAll();2 加载 拆分 向量化完全照抄内存版这三步在ChromaRagFlowTest里几乎没变Document document ClassPathDocumentLoader.loadDocument( docs/airline_policy.txt, new TextDocumentParser() ); DocumentSplitter splitter DocumentSplitters.recursive(300, 50); ListTextSegment segments splitter.split(document); ListEmbedding embeddings embeddingModel.embedAll(segments).content(); embeddingStore.addAll(embeddings, segments);你会发现除了向量库类型不一样代码写法是一样的。这就是我们一开始就选用EmbeddingStore统一抽象的好处。3 在 Chroma 里做检索检索逻辑也几乎一模一样只是多打印了所有结果方便你观察String query 取消经济舱机票要扣多少钱; Embedding queryEmbedding embeddingModel.embed(query).content(); EmbeddingSearchRequest request EmbeddingSearchRequest.builder() .queryEmbedding(queryEmbedding) .maxResults(segments.size()) .minScore(0.0) .build(); EmbeddingSearchResultTextSegment result embeddingStore.search(request); ListEmbeddingMatchTextSegment matches result.matches(); System.out.println([Chroma] 用户问题: query); for (int i 0; i matches.size(); i) { EmbeddingMatchTextSegment m matches.get(i); System.out.printf([Chroma] #%d score%.4f%n, i 1, m.score()); System.out.println(m.embedded().text()); System.out.println(--------------------------------------------------); } EmbeddingMatchTextSegment topMatch matches.get(0); String topText topMatch.embedded().text();断言同样是检查assertTrue([Chroma] 最相关片段应包含 经济舱, topText.contains(经济舱)); assertTrue([Chroma] 最相关片段应包含 退票, topText.contains(退票)); assertTrue([Chroma] 相似度应大于 0.5当前为 topMatch.score(), topMatch.score() 0.5);如果一切正常你会发现 Chroma 版和内存版在行为上是一致的同样的问题 → 命中同一段政策分数可能略有浮动但不会离谱。实战作业把自己的文档搬进来替换airline_policy.txt为你自己的 FAQ / 手册跑一遍RagFlowTest看看能不能命中你想要的条款。收个尾这节课你真正学到啥咱们最后 30 秒复盘一下你不再只是“调个大模型接口”而是能把文档变成Document把文档拆成一块一块的TextSegment用QwenEmbeddingModel把每一块变成向量用InMemoryEmbeddingStore/ChromaEmbeddingStore管理这些向量用search做语义检索而不是关键词匹配。你跑通了两个完整测试内存版RagFlowTestChroma 版ChromaRagFlowTest下一节课我们就在这个基础上把检索到的片段塞进对话模型 Prompt 里让大模型不再“胡说八道”而是“有据可依”地回答问题。这才是真正的“让大模型帮你干活而不是陪你聊天。”