RAG(Retrieval-Augmented Generation)是 LLM 应用最核心的模式之一。本章从原理到实践,覆盖 Easy/Naive/Advanced RAG 三种方案,深入解析索引管线、检索策略、重排序等核心机制。
5.1 为什么需要 RAG?
LLM 的两个根本局限:
- 知识截止日期:训练数据有截止日期,不知道训练后发生的事情
- 私有知识盲区:不知道企业内部文档、产品手册、私有代码库
RAG 的解决思路:在提问之前,先找到相关文档片段,连同问题一起发给 LLM。
swift
传统 LLM 调用:
User:"公司年假政策是什么?" → LLM → 编造/不知道
RAG 调用:
User:"公司年假政策是什么?"
→ 检索: 从企业文档库找到 "员工手册第3章:年假政策..."
→ Prompt: "根据以下信息回答:\n[员工手册第3章:年假政策...]\n\n用户问题:公司年假政策是什么?"
→ LLM → 准确回答
5.2 三种 RAG 方案对比
| 方案 | 适合阶段 | 配置复杂度 | 质量 |
|---|---|---|---|
| Easy RAG | 原型验证 | 极低(一行代码) | 一般 |
| Naive RAG | 入门 | 低 | 中等 |
| Advanced RAG | 生产环境 | 中-高 | 最优 |
5.3 Easy RAG:开箱即用
xml
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.18.1-beta28</version>
</dependency>
java
// 1. 加载文档(支持递归扫描)
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/data/docs");
// 2. 创建内存向量库并一键摄入
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
// 3. 创建 AI Service
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
// 4. 直接使用
String answer = assistant.chat("怎么配置 Easy RAG?");
Easy RAG 幕后是什么?
- 使用 Apache Tika 自动解析文档格式
- 使用 bge-small-en-v1.5(24MB,完全离线)作为嵌入模型
- 使用 300 token/30 token 重叠的递归分割器
⚠️ Easy RAG 适合原型和学习,生产环境建议升级到 Advanced RAG。
5.4 RAG 核心数据模型
Document → TextSegment → Embedding
scss
Document (整个文档)
│ .text() --- 文本内容
│ .metadata() --- 元数据(文件名、来源、日期等)
│ .toTextSegment() --- 转为 TextSegment
│
└── 分割 (DocumentSplitter)
│
▼
TextSegment (文档片段)
│ .text() --- 片段文本
│ .metadata() --- 继承自 Document + "index": "0", "1", ...
│
└── 嵌入 (EmbeddingModel)
│
▼
Embedding (向量)
.vector() --- float[]
.dimension() --- 向量维度
Metadata:Key-Value 元数据
java
Metadata meta = new Metadata()
.put("source", "employee_handbook_v2.pdf")
.put("chapter", "3")
.put("lastUpdated", "2025-03-15")
.put("author", "HR Department");
// 支持类型:String, Integer, Long, Float, Double, UUID
// 不支持:Date(用 String 代替)、Boolean
Embedding 的数学操作
java
Embedding queryEmbedding = embeddingModel.embed("What is RAG?").content();
Embedding docEmbedding = embeddingModel.embed("RAG stands for...").content();
// 余弦相似度
double similarity = CosineSimilarity.between(queryEmbedding, docEmbedding);
// 归一化(原地修改) ~将向量的长度(模长 / $L_2$ 范数)缩放到 1,同时保持向量的方向(语义信息)完全不变。归一化最核心的目的只有一个:大幅提升语义相似度计算的速度。
queryEmbedding.normalize();
5.5 索引管线(Ingestion Pipeline)
EmbeddingStoreIngestor 是文档到向量库的核心编排器:
java
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// === 阶段1: 文档转换(清洗/增强) ===
.documentTransformer(document -> {
document.metadata().put("ingestedAt", Instant.now().toString());
document.metadata().put("tenant", "acme-corp");
return document;
})
// === 阶段2: 文档分割 ===
.documentSplitter(DocumentSplitters.recursive(
1000, // 最大 chunk 大小(字符)
200, // 重叠量
new OpenAiTokenCountEstimator("gpt-4o-mini") // Token 估算器
))
// === 阶段3: TextSegment 转换 ===
.textSegmentTransformer(textSegment -> {
// 在片段前加上文件名,帮助 LLM 理解上下文
String fileName = textSegment.metadata().getString("file_name");
return TextSegment.from(
"Source: " + fileName + "\n" + textSegment.text(),
textSegment.metadata()
);
})
// === 阶段4: 嵌入模型 ===
.embeddingModel(embeddingModel)
// === 阶段5: 向量存储 ===
.embeddingStore(embeddingStore)
.build();
// 执行摄入
ingestor.ingest(document1);
ingestor.ingest(document2, document3);
IngestionResult result = ingestor.ingest(List.of(doc4, doc5));
// result.tokenUsage() 返回本次摄入的 Token 消耗
5.6 文档加载与解析
10+ 文档加载器
java
// 本地文件系统(支持 PathMatcher 过滤)
List<Document> docs = FileSystemDocumentLoader.loadDocuments("/data", new ApachePdfBoxDocumentParser());
List<Document> docs = FileSystemDocumentLoader.loadDocumentsRecursively("/data");
// ClassPath
List<Document> docs = ClassPathDocumentLoader.loadDocuments("manuals/");
// HTTP URL
Document doc = UrlDocumentLoader.load("https://example.com/page");
// 云存储
// AmazonS3DocumentLoader, AzureBlobStorageDocumentLoader,
// GoogleCloudStorageDocumentLoader, TencentCosDocumentLoader
// Web 抓取
// GitHubDocumentLoader, SeleniumDocumentLoader, PlaywrightDocumentLoader
7+ 文档解析器
| 解析器 | 适用格式 |
|---|---|
TextDocumentParser |
TXT, HTML, MD |
ApachePdfBoxDocumentParser |
|
ApachePoiDocumentParser |
DOC, DOCX, PPT, XLS |
ApacheTikaDocumentParser |
几乎所有格式(自动检测) |
DoclingDocumentParser |
现代文档解析器 |
MarkdownDocumentParser |
Markdown |
YamlDocumentParser |
YAML |
文档分割策略
java
// 按段落
DocumentByParagraphSplitter splitter = new DocumentByParagraphSplitter(500, 50);
// 按句子(需要 OpenNLP)
DocumentBySentenceSplitter splitter = new DocumentBySentenceSplitter(300, 30);
// 递归分割(推荐:依次尝试 \n\n → \n → 空格 → 字符)
DocumentSplitters.recursive(1000, 200, tokenCountEstimator);
// 按行 / 按单词 / 按字符 / 按正则
new DocumentByLineSplitter(1000, 200);
new DocumentByWordSplitter(500, 50);
new DocumentByCharacterSplitter(1000, 200);
new DocumentByRegexSplitter("(?<=\\.)\\s+", 500, 50);
5.7 向量存储(Embedding Store)
核心接口
java
public interface EmbeddingStore<Embedded> {
String add(Embedding embedding);
String add(Embedding embedding, Embedded embedded); // Embedded 通常是 TextSegment
void addAll(List<String> ids, List<Embedding> embeddings, List<Embedded> embedded);
List<EmbeddingMatch<Embedded>> search(EmbeddingSearchRequest request);
void remove(String id);
void removeAll(Filter filter);
void removeAll();
}
搜索请求
java
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding) // 查询的向量
.maxResults(5) // 返回最多 5 条
.minScore(0.75) // 最低相似度阈值
.filter(metadataKey("tenant") // 元数据过滤
.isEqualTo("acme-corp"))
.build();
EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
List<EmbeddingMatch<TextSegment>> matches = result.matches();
for (EmbeddingMatch<TextSegment> match : matches) {
double score = match.score(); // 0.0 ~ 1.0
String id = match.embeddingId();
TextSegment segment = match.embedded(); // 原始文本片段
}
元数据过滤(Filter DSL)
java
// 等值过滤
Filter filter = metadataKey("status").isEqualTo("published");
// 组合过滤
Filter filter = Filter.and(
metadataKey("tenant").isEqualTo("acme"),
Filter.or(
metadataKey("type").isEqualTo("manual"),
metadataKey("type").isEqualTo("policy")
)
);
// 范围过滤
Filter filter = metadataKey("year").isGreaterThanOrEqualTo(2023);
// 集合过滤
Filter filter = metadataKey("category").isIn("java", "kotlin");
// 子字符串过滤(仅 Milvus、PgVector、Qdrant 支持)
Filter filter = metadataKey("title").containsString("RAG");
// 不支持的 EmbeddingStore 会抛出 UnsupportedFeatureException
5.8 Naive RAG
java
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.75)
.filter(metadataKey("tenant").isEqualTo("acme-corp"))
// 动态过滤 ~调用时动态上下文信息,可以是租户信息、用户信息等
.dynamicFilter(query -> {
String userId = query.metadata().invocationParameters().get("userId");
return metadataKey("userId").isEqualTo(userId);
})
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.contentRetriever(retriever)
.build();
5.9 Advanced RAG:模块化管线

DefaultRetrievalAugmentor
java
RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
// === 查询转换 ===
.queryTransformer(new CompressingQueryTransformer(model))
// CompressingQueryTransformer: 将 "他住在哪?" + 历史 → "张三住在哪?"
// === 检索器(多个并行检索) ===
.contentRetriever(embeddingRetriever) // 向量检索
.contentRetriever(webSearchRetriever) // Web 搜索
// === 聚合器 ===
.contentAggregator(new ReRankingContentAggregator(scoringModel))
// 使用 Cohere 等 ScoringModel 对结果重排序
// === 注入器 ===
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("用户问题:{{userMessage}}\n\n参考资料:\n{{contents}}"))
.metadataKeysToInclude(List.of("source", "chapter")) // 附带元数据
.build())
// === 并发执行器 ===
.executor(Executors.newFixedThreadPool(4))
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.retrievalAugmentor(augmentor) // 使用 Advanced RAG
.build();
查询转换器选择
| 转换器 | 功能 | 适用场景 |
|---|---|---|
DefaultQueryTransformer |
原样传递 | 不需要改写 |
CompressingQueryTransformer |
LLM 压缩多轮对话 | "他在哪?" → "张三住在哪?" |
ExpandingQueryTransformer |
LLM 扩展为多个变体 | 提高检索召回率 |
RepeatingQueryTransformer |
重复检索请求 | 社区模块 |
各种 ContentRetriever
java
// 向量检索
EmbeddingStoreContentRetriever.builder()
.embeddingStore(store).embeddingModel(model)
.maxResults(5).minScore(0.75).build();
// Web 搜索
WebSearchContentRetriever.builder()
.webSearchEngine(googleEngine)
.maxResults(3).build();
// SQL 数据库(实验性)
SqlDatabaseContentRetriever.builder()
.dataSource(dataSource).chatModel(model).build();
// Azure AI Search(全文+向量+混合)
AzureAiSearchContentRetriever.builder()...
// Neo4j 知识图谱 → Cypher 查询
Neo4jContentRetriever.builder()...
// Elasticsearch(全文+向量+混合)
ElasticsearchContentRetriever.builder()...
WebSearchContentRetriever 示例
java
WebSearchEngine google = GoogleCustomWebSearchEngine.builder()
.apiKey(System.getenv("GOOGLE_API_KEY"))
.csi(System.getenv("GOOGLE_SEARCH_ENGINE_ID"))
.build();
ContentRetriever webRetriever = WebSearchContentRetriever.builder()
.webSearchEngine(google)
.maxResults(3)
.build();
5.10 RAG 作为 Tool(按需检索)
不是每次对话都需要 RAG。将检索包装为工具让 LLM 自己决定何时检索:
java
class SearchTool {
private final ContentRetriever retriever;
@Tool("Search for technical information about LangChain4j")
public String search(String query) {
return retriever.retrieve(new Query(query)).stream()
.map(content -> content.textSegment().text())
.collect(Collectors.joining("\n\n"));
}
}
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.tools(new SearchTool(retriever))
.build();
// "你好" → LLM 不调用 search(节省 Token)
// "LangChain4j 怎么配置 RAG?" → LLM 调用 search
5.11 获取检索来源
使用 Result 获取
java
interface Assistant {
Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("如何配置 RAG?");
String answer = result.content();
List<Content> sources = result.sources();
for (Content source : sources) {
String text = source.textSegment().text();
String fileName = source.textSegment().metadata().getString("file_name");
System.out.printf("[%s] %s%n", fileName, text);
}
流式获取
java
assistant.chat("How to do RAG with LangChain4j?")
.onRetrieved(sources -> {
System.out.println("检索到 " + sources.size() + " 个来源");
})
.onPartialResponse(System.out::print)
.onCompleteResponse(System.out::println)
.onError(Throwable::printStackTrace)
.start();
5.12 图文档转换(Graph Transformer)
将非结构化文档转换为知识图谱:
java
GraphTransformer transformer = new LLMGraphTransformer(
OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build()
);
Document doc = Document.from(
"Barack Obama was born in Hawaii and served as 44th President."
);
GraphDocument graph = transformer.transform(doc);
// 提取的节点和关系:
// GraphNode(name=Barack Obama, type=Person)
// GraphNode(name=Hawaii, type=Location)
// GraphEdge(from=Barack Obama, predicate=was born in, to=Hawaii)
// GraphEdge(from=Barack Obama, predicate=served as, to=President of the United States)
5.13 最佳实践速查
| 环节 | 建议 |
|---|---|
| 分割大小 | 300-1000 token,根据模型 context window 和内容密度调整 |
| 重叠 | 10-20% chunk 大小,保证跨块连续性 |
| 嵌入模型选择 | 中文特化模型(如 bge-large-zh)优于通用多语言模型 |
| Query Embedding | 与 Document Embedding 使用不同输入类型(Cohere/Voyage/Google 支持) |
| 结果数 | 3-5 条通常够用,过多会分散 LLM 注意力 |
| minScore | 0.7-0.8 过滤噪音,但也可能漏掉边缘相关信息 |
| 元数据注入 | 将文件名/章节/日期注入 prompt,帮助 LLM 判断信息可信度 |
| 重排序 | Cohere Rerank 等模型能显著提升结果排序质量 |
附录:
LangChain4j 中 RAG 相关类
