《JavaAI应用开发实战》-LangChain4j实战全攻略

一个完整的 RAG 流程:
文档加载 → 文档拆分 → 文本向量化 → 写入向量库 → 基于向量做语义检索
今天我们就用 Java + LangChain4j + 通义千问的向量模型,从零跑通这一整条链路,而且搞两个版本:
- 内存版:用
InMemoryEmbeddingStore,写个测试就能跑通; - Chroma 版:用
ChromaEmbeddingStore,连上真正的向量数据库。
你学完之后,完全可以换成你们公司的 FAQ、退改签规则、产品手册,搭一个自己的“公司知识库问答机器人”。
一、先把任务说清楚:这节课到底要干嘛?
这节课,我们要做到三件事:
- 听得懂:
搞清楚 RAG 这条链路上都有哪些步骤,每一步是干啥的、为什么要这样设计。 - 写得出:
跟着我,一行行写完并跑通两个测试:
-
RagFlowTest:内存版向量库ChromaRagFlowTest:Chroma 版向量库
- 迁得动:
明白“内存版 → Chroma 版”怎么迁移,只改很少的代码,就能从 Demo 走向可落地的架构。
二、先把工具箱打开:本项目里有哪些主角?
RagFlowTest(非常重要)
用InMemoryEmbeddingStore<TextSegment>跑完完整链路:
-
- 从
airline_policy.txt读文档; - 拆成一段一段的
TextSegment; - 全部向量化后塞进内存向量库;
- 问一句“取消经济舱机票要扣多少钱?”;
- 看看最相关的片段是不是“经济舱退票”那段,并断言。
- 从
ChromaRagFlowTest
和RagFlowTest几乎一模一样,只是把向量库换成了ChromaEmbeddingStore<TextSegment>,连的是真实 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);
List<TextSegment> segments = splitter.split(document);
assertFalse("拆分结果不能为空", segments.isEmpty());
DocumentSplitters.recursive(...) 会优先按结构边界拆(句子、段落),不够再按长度强拆。
这比“纯按行”“纯按句”要鲁棒很多,适合作为默认策略。
拆完你会得到一个 List<TextSegment>,每个 TextSegment 就是一小段可检索的知识块。
4 向量化 + 内存入库:InMemoryEmbeddingStore
现在我们要给每个 TextSegment 发一张“语义身份证”,并存进一个“内存向量仓库”里:
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
embeddingStore.addAll(embeddings, segments);
这里有几个关键点:
embedAll(segments):
一次性帮你把所有片段都向量化,比循环embed更高效,也更优雅。InMemoryEmbeddingStore<TextSegment>:
是 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();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
EmbeddingMatch<TextSegment> 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():一个EmbeddingMatch<TextSegment>列表;- 每个
EmbeddingMatch里有:
-
score():相似度;embedded():原始TextSegment。
至此,一个完整的 RAG 流程(内存版)就打通了。
Chroma 版完整 RAG:把“Demo”迁到“向量数据库”
刚才我们炖的是“小锅菜”:一切都在内存里。
现在我们要上大菜:把向量存进 Chroma,变成一个可持久化、可共享的向量库。
对应的测试类:
src/test/java/com/xiaobian/ChromaRagFlowTest.java
1 构建 ChromaEmbeddingStore
在 setUp() 里,我们这样初始化向量库:
embeddingStore = ChromaEmbeddingStore.<TextSegment>builder()
.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);
List<TextSegment> segments = splitter.split(document);
List<Embedding> 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();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
List<EmbeddingMatch<TextSegment>> matches = result.matches();
System.out.println("[Chroma] 用户问题: " + query);
for (int i = 0; i < matches.size(); i++) {
EmbeddingMatch<TextSegment> 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("--------------------------------------------------");
}
EmbeddingMatch<TextSegment> 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做语义检索,而不是关键词匹配。
- 把文档变成
- 你跑通了两个完整测试:
-
- 内存版
RagFlowTest - Chroma 版
ChromaRagFlowTest
- 内存版
下一节课,我们就在这个基础上,把检索到的片段塞进对话模型 Prompt 里,
让大模型不再“胡说八道”,而是“有据可依”地回答问题。
这才是真正的:
“让大模型帮你干活,而不是陪你聊天。”
更多推荐




所有评论(0)