一、大模型的“知识盲区”:为何会“胡说八道”?
要真正理解RAG的价值,必须先认清大模型“知识缺失”的本质。很多人误以为大模型像一个无所不知的“活字典”,实际上它的“知识”完全来源于训练数据,而这些数据本身存在三个难以克服的局限:- 知识的时间滞后性:无法获取实时信息
大模型的训练数据都有明确的截止时间点。尽管厂商会定期更新模型版本,但由于训练成本极高,不可能频繁进行全量训练。因此,模型对新发生的事件、政策变化或行业动态缺乏感知能力,导致其在处理时效性强的问题时表现不佳。 - 垂直领域数据匮乏:专业内容覆盖不足
训练数据多以通用语料为主,涵盖日常对话、百科知识、新闻资讯等广泛主题,但在医疗、法律、金融等高度专业化领域,相关高质量数据占比极低。例如,当询问“某种罕见病的前沿治疗方案”时,模型只能基于过往公开资料作答,难以引用近期发表于权威期刊的临床研究成果,因为这些内容可能未被纳入原始训练集。 - 私有数据不可见:企业内部信息无法触达
对企业和组织而言,最关键的业务知识往往存储在内部系统中,如产品代理政策、客户服务响应标准、研发流程文档等。这些数据不会出现在公开网络中,自然也不会进入大模型的训练体系,导致AI无法访问和利用这些关键信息。
二、RAG的核心原理:从依赖记忆到实时查证的跃迁
RAG的基本思想非常直观,类似于人类写作时的“查阅资料+撰写结论”过程:先查找权威来源的信息,再据此组织语言输出。在技术层面,这个过程被自动化为两个步骤—— 检索 → 生成。 [此处为图片1]1. 核心机制:用“精确检索结果”替代“模糊记忆推断”
传统模式下,大模型的回答依赖于训练阶段形成的“概率记忆”。比如被问及“某疾病的治疗方式”,它会根据训练数据中高频出现的内容推测最可能的答案。而引入RAG后,整个流程发生根本转变:- 用户提问:“2025年某行业最新的税收优惠政策是什么?”
- 系统执行检索:自动从预构建的“2025年行业政策知识库”中提取与“税收优惠”相关的原文段落。
- 模型生成回答:基于检索到的真实政策文本,整合成清晰易懂的答复,并可附带信息出处说明。
2. 技术流程详解:从数据准备到答案输出的完整链条
看似简单的“检索+生成”背后,实则包含三大核心技术环节。下面我们逐层拆解:环节一:知识库构建 —— 实现专业内容的“机器可读化”
RAG的前提是有可供检索的专业知识源。该阶段的目标是将分散的文档(如政策文件、操作手册、技术白皮书)转化为结构化、可检索的形式。主要分为以下三步: 第一步:文档切分(Chunking)—— 将长文本拆解为语义完整的片段直接将数百页的PDF丢给系统无法实现精准定位。因此需将文档切割成若干小块(Chunk),每个块承载一个独立的知识单元。切分讲究策略:过细会导致上下文断裂,过粗则影响检索精度。通常建议按段落或小节划分,每块控制在200–500字之间,确保语义完整性和检索效率。 第二步:向量化嵌入(Embedding)—— 赋予文本“语义坐标”
计算机无法直接理解文字含义,因此需使用Embedding模型(如BGE、Sentence-BERT)将每个文本块转换为一组数字向量。其核心逻辑是:语义相近的句子,其向量距离也更接近。例如,“减税措施”与“税收减免”会被映射到相似的空间位置,而与“天气预报”相距甚远。这种表示方式实现了基于语义而非关键词的匹配能力。 第三步:向量数据库存储 —— 搭建“智能知识书架”
所有向量化后的文本片段被存入专用的向量数据库(如Milvus、Pinecone、Weaviate),形成一个支持快速语义搜索的知识仓库。就像图书馆给书籍编号上架,这套系统能让AI在毫秒级时间内找到最相关的内容。 [此处为图片2]
环节一:向量存储——为何选择向量数据库?
经过转化的文本向量需存入专用的“向量数据库”中,例如 Milvus、Chroma 或 Pinecone,而非传统的 MySQL 等关系型数据库。这是因为向量数据库具备高效计算“相似度”的能力——当用户提问被转化为向量后,系统可从百万甚至千万级别的向量数据中,迅速定位最相近的若干个文本片段(Chunk)。这就像一个智能化的书架,当你提出“查找有关税收优惠的信息”时,它能立即找出所有相关的“便签条”内容。
[此处为图片1]环节二:信息检索——精准匹配相关结果
在用户发起提问后,系统首先使用与构建知识库相同的 Embedding 模型,将问题转换为向量形式,随后利用该向量在向量数据库中进行“相似性搜索”。这一过程并非精确匹配关键词,而是基于语义相似度进行排序,通常返回 Top5 至 Top10 最相关的 Chunk 作为候选结果。
为了进一步提升检索准确率,常采用多种优化策略:
- 多轮检索:若首轮检索效果不佳,系统可根据原始问题和初步结果生成更精准的查询表达式,进行二次或多次检索,逐步逼近最优答案。
- 权重调节机制:对最新发布的文档或权威来源赋予更高的检索权重,使其在排序中优先出现,从而增强结果的时效性与可信度。
环节三:生成控制——引导大模型基于事实作答
检索到的相关文本片段不能直接呈现给用户,还需通过大语言模型进行整合与润色,输出自然流畅的回答。关键在于“Prompt 工程”的设计:将用户问题与检索出的多个 Chunk 内容组织成结构化提示词,并明确指令:“请依据以下参考资料作答,禁止虚构信息;若资料中无相关内容,请如实说明‘未查询到相关信息’。”
典型的 Prompt 模板如下:
参考资料: [Chunk1内容] 来源:《2025年某行业税收优惠政策通知》 [Chunk2内容] 来源:某税务局官方解读文件 用户问题:2025年某行业最新税收优惠政策是什么? 请基于上述参考资料,用简洁的语言回答用户问题,并在回答末尾标注信息来源。如果参考资料中没有相关信息,请勿编造,直接说明“未查询到相关信息”。
通过这种机制,大模型的回答将严格依赖于检索所得的真实资料,同时附带引用出处,显著增强了输出内容的可验证性与专业可信度。
3. RAG 与 微调对比:为何 RAG 更适合专业领域应用?
关于“RAG 和微调如何选择”的疑问较为常见,以下是两者的详细对比,帮助明确各自的适用边界:
| 对比维度 | RAG(检索增强生成) | 微调(Fine-tuning) |
|---|---|---|
| 数据成本 | 较低,无需标注数据,可直接使用原始文档 | 较高,需大量高质量标注训练样本 |
| 算力消耗 | 较低,仅需完成向量化及检索计算 | 较高,依赖大规模模型训练资源 |
| 知识更新速度 | 快,只需更新知识库即可实时生效 | 慢,需重新准备数据并执行完整微调流程 |
| 可解释性 | 高,回答内容可追溯至具体参考资料 | 低,输出基于模型内部“记忆”,难以溯源 |
| 典型应用场景 | 专业知识问答、时效性强的问题响应、企业内部知识查询 | 模型风格定制、特定任务优化(如翻译、摘要生成等) |
三、基于 LangChain4j 的 RAG 实现方案
在理解了 RAG 的核心原理之后,接下来我们将借助 LangChain4j——一款专为 Java 生态打造的大模型开发框架,更适合集成进企业级业务系统——来实现完整的 RAG 流程。相较于 Python 生态中的 LangChain,LangChain4j 能更好地兼容 Spring Boot 等主流企业级开发框架,便于后续系统对接与部署。
3.1 整体架构设计
RAG 的实现分为两个主要阶段:
- 索引阶段(Indexing):对原始文档进行处理,并将其向量化后存入向量数据库。
- 检索阶段(Retrieval):在接收到用户查询时,从数据库中检索相关内容,并注入 Prompt 中供模型生成回答。
核心组件包括:
- EmbeddingStoreIngestor:负责索引阶段的数据摄入与分段处理。
- RetrievalAugmentor:主导检索阶段的协调工作。
- DefaultRetrievalAugmentor:默认实现类,用于整合各模块协同运行。
3.2 索引阶段的具体实现
核心类为 EmbeddingStoreIngestor,其主要处理流程如下:
public IngestionResult ingest(List<Document> documents) {
log.debug("Starting to ingest {} documents", documents.size());
if (documentTransformer != null) {
documents = documentTransformer.transformAll(documents);
log.debug("Documents were transformed into {} documents", documents.size());
}
List<TextSegment> segments;
if (documentSplitter != null) {
segments = documentSplitter.splitAll(documents);
log.debug("Documents were split into {} text segments", segments.size());
} else {
// 文档转换阶段(可选)
if (documentTransformer != null) {
documents = documentTransformer.transformAll(documents);
}
[此处为图片1]
对原始文档进行清洗、标准化处理,并补充或优化元数据信息,提升后续处理的准确性与一致性。
文档分割处理(关键步骤)
if (documentSplitter != null) {
segments = documentSplitter.splitAll(documents);
} else {
segments = documents.stream().map(Document::toTextSegment).collect(java.util.stream.Collectors.toList());
}
将较大的文档切分为更小的文本片段(TextSegment),便于后续处理。
主要原因包括:
- 适应大模型有限的上下文窗口;
- 提高检索时的精确度;
- 有效控制计算资源消耗和使用成本。
常用分割策略:基于段落或句子进行递归切分,支持设置重叠区域以保留上下文连贯性。
文本片段后处理(可选)
if (textSegmentTransformer != null) {
segments = textSegmentTransformer.transformAll(segments);
}
[此处为图片2]
此步骤可用于增强文本段的信息表达,例如在每个片段前添加所属文档的标题、章节名或生成简要摘要,从而提升向量检索的相关性和效果。
生成向量表示(核心环节)
log.debug("开始对 {} 个文本片段进行向量化", segments.size());
Response<List<Embedding>> embeddingsResponse = embeddingModel.embedAll(segments);
log.debug("完成对 {} 个文本片段的向量化", segments.size());
利用指定的 EmbeddingModel 将文本内容转化为高维向量。
该向量能够捕捉文本的语义特征,语义相近的内容在向量空间中的距离也更接近。
存入向量数据库
log.debug("开始将 {} 个文本片段及其向量存储至向量库", segments.size());
embeddingStore.addAll(embeddingsResponse.content(), segments);
log.debug("已完成向量数据存储");
将生成的向量与对应的原始文本段一并写入 EmbeddingStore 中,以便后续快速检索。
支持多种主流向量数据库实现,如 Pinecone、Milvus、Qdrant 等。
3.3 检索阶段(Retrieval)实现说明
核心组件:DefaultRetrievalAugmentor
检索执行流程如下:
@Override
public AugmentationResult augment(AugmentationRequest augmentationRequest) {
ChatMessage chatMessage = augmentationRequest.chatMessage();
String queryText;
if (chatMessage instanceof UserMessage userMessage) {
queryText = userMessage.singleText();
} else {
throw new IllegalArgumentException("不支持的消息类型: " + chatMessage.type());
}
}
[此处为图片3]
ChatMessage augmentedChatMessage = contentInjector.inject(contents, chatMessage);
List<Content> contents = contentAggregator.aggregate(queryToContents);
Map<Query, Collection<List<Content>>> queryToContents = process(queries);
Collection<Query> queries = queryTransformer.transform(originalQuery);
Query originalQuery = Query.from(queryText, augmentationRequest.metadata());
return AugmentationResult.builder()
.chatMessage(augmentedChatMessage)
.contents(contents)
.build();
执行流程解析
阶段一:查询变换(Query Transformation)
系统首先对原始查询进行转换处理:
Collection<Query> queries = queryTransformer.transform(originalQuery);
基础实现类 DefaultQueryTransformer 不做额外扩展,仅保留原查询:
public Collection<Query> transform(Query query) {
return singletonList(query); // 返回单一元素的列表,内容为原始查询
}
而更复杂的实现如 ExpandingQueryTransformer,则利用大语言模型(LLM)生成多个语义相近但表达不同的查询变体。这种方式有助于覆盖更多潜在匹配信息,从而提升内容召回的广度与准确性。
[此处为图片1]阶段二:查询分发与内容获取(Query Routing & Retrieval)
接下来进入核心处理逻辑 process(queries) 方法,该方法根据查询数量和路由策略决定如何检索内容:
private Map<Query, Collection<List<Content>>> process(Collection<Query> queries) {
if (queries.size() == 1) {
Query query = queries.iterator().next();
Collection<ContentRetriever> retrievers = queryRouter.route(query);
if (retrievers.size() == 1) {
ContentRetriever retriever = retrievers.iterator().next();
List<Content> contents = retriever.retrieve(query);
return singletonMap(query, singletonList(contents));
}
else if (retrievers.size() > 1) {
Collection<List<Content>> contents = retrieveFromAll(retrievers, query).join();
return singletonMap(query, contents);
}
else {
return emptyMap();
}
}
else if (queries.size() > 1) {
Map<Query, CompletableFuture<Collection<List<Content>>>> queryToFutureContents = new ConcurrentHashMap<>();
queries.forEach(query -> {
CompletableFuture<Collection<List<Content>>> futureContents =
supplyAsync(() -> queryRouter.route(query), executor)
.thenCompose(retrievers -> retrieveFromAll(retrievers, query));
queryToFutureContents.put(query, futureContents);
});
return join(queryToFutureContents);
}
else {
return emptyMap();
}
}
此过程根据传入的查询集合大小分为三种情况处理:
- 单个查询:通过 queryRouter 确定一个或多个内容检索器(ContentRetriever)。若存在多个检索器,则并行调用各检索器获取结果,并合并返回;若无匹配检索器,则返回空映射。
- 多个查询:对每个查询独立启动异步任务,分别完成路由与内容拉取,最终统一等待所有任务完成后再汇总结果。这种设计提升了整体吞吐效率,尤其适用于扩展后的多变体查询场景。
- 无查询输入:直接返回空结果集,避免无效操作。
整个流程充分考虑了灵活性与性能平衡,支持同步与异步混合模式,确保在复杂查询环境下仍能高效响应。
[此处为图片2]步骤 3:内容检索(Content Retrieval)
在内容检索阶段,核心实现类为 EmbeddingStoreContentRetriever,其主要职责是通过向量相似度匹配从存储中获取最相关的内容。以下是该类的关键方法:
public List<Content> retrieve(Query query) {
Embedding embeddedQuery = embeddingModel.embed(query.text()).content();
EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(embeddedQuery)
.maxResults(maxResultsProvider.apply(query))
.minScore(minScoreProvider.apply(query))
.filter(filterProvider.apply(query))
.build();
EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
return searchResult.matches().stream()
.map(embeddingMatch -> Content.from(
embeddingMatch.embedded(),
Map.of(
ContentMetadata.SCORE, embeddingMatch.score(),
ContentMetadata.EMBEDDING_ID, embeddingMatch.embeddingId()
)
))
.collect(Collectors.toList());
}
执行流程如下:
- 将输入的查询文本转换为嵌入向量:
embeddingModel.embed(query.text()) - 构建向量搜索请求,配置最大返回结果数、最低相似度阈值以及过滤条件
- 调用向量数据库进行相似性搜索:
embeddingStore.search(searchRequest) - 将匹配到的结果转换为
Content对象列表,并按相似度得分排序返回
支持的检索器类型
系统提供多种检索器实现,可根据不同场景选择使用:
- EmbeddingStoreContentRetriever:基于向量数据库的语义检索
- WebSearchContentRetriever:通过网络搜索引擎获取实时信息
- AzureAiSearchContentRetriever:支持混合模式的高级检索,结合关键词与向量搜索
当存在多个检索源时,系统可并行调用各检索器以提升响应效率和召回率。
智能路由机制
LanguageModelQueryRouter 利用大语言模型(LLM)分析用户查询意图,自动判断并选择最合适的数据源或检索策略,实现智能化的查询分发。
而默认的路由策略由 DefaultQueryRouter 实现,负责将查询请求定向至预设的目标检索器。
步骤 4:内容聚合(Content Aggregation)
为了整合来自多个检索通道的结果,系统采用 Reciprocal Rank Fusion (RRF) 算法进行融合排序。
public static List<Content> fuse(Collection<List<Content>> listsOfContents, int k) {
ensureBetween(k, 1, Integer.MAX_VALUE, "k");
Map<Content, Double> scores = new LinkedHashMap<>();
for (List<Content> singleListOfContent : listsOfContents) {
for (int i = 0; i < singleListOfContent.size(); i++) {
Content content = singleListOfContent.get(i);
double currentScore = scores.getOrDefault(content, 0.0);
int rank = i + 1;
double newScore = currentScore + 1.0 / (k + rank);
scores.put(content, newScore);
}
}
List<Content> fused = new ArrayList<>(scores.keySet());
fused.sort(Comparator.comparingDouble(scores::get).reversed());
return fused;
}
RRF 融合算法的核心逻辑包括:
- 评分公式为:
score = 1.0 / (k + rank),其中k是调节参数(通常取值为 60),rank表示内容在单个结果列表中的位置(从 1 开始计数) - 同一内容若出现在多个检索结果中,其得分将累加
- 最终结果按总得分降序排列,确保高相关性内容排在前列
示例说明:
假设有两个检索结果列表:
- 列表1: [cat, dog]
- 列表2: [cat, parrot]
经 RRF 融合后,"cat" 因在两个列表中均靠前出现,获得更高综合得分,最终排序优先级最高。
[此处为图片2]cat: 1/(60+1) + 1/(60+1) = 0.0328
dog: 1/(60+2) = 0.0161
parrot: 1/(60+2) = 0.0161
最终排序结果为:[cat, dog, parrot](由于 cat 出现两次,其排名更靠前)
DefaultContentAggregator 采用两阶段的 RRF 融合策略:
@Override
public List<Content> aggregate(Map<Query, Collection<List<Content>>> queryToContents) {
// 第一阶段:针对每个查询,融合来自多个检索源的内容
Map<Query, List<Content>> fused = fuse(queryToContents);
// 第二阶段:将所有查询对应的结果进行统一融合
return ReciprocalRankFuser.fuse(fused.values());
}
第一阶段任务:对每一个独立的查询,整合不同来源返回的内容列表。
第二阶段任务:将各个查询所获得的内容结果再次融合,生成全局排序后的最终内容序列。
步骤 5:内容注入(Content Injection)
DefaultContentInjector 负责将检索到的相关内容嵌入原始用户消息中,具体实现如下:
@Override
public ChatMessage inject(List<Content> contents, ChatMessage chatMessage) {
if (contents.isEmpty()) {
return chatMessage;
}
Prompt prompt = createPrompt(chatMessage, contents);
if (chatMessage instanceof UserMessage userMessage) {
return userMessage.toBuilder()
.contents(List.of(TextContent.from(prompt.text())))
.build();
} else {
return prompt.toUserMessage();
}
}
系统默认使用的提示模板定义如下:
public static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = PromptTemplate.from(
"""
{{userMessage}}
Answer using the following information:
{{contents}}""");
经过注入处理后生成的实际提示示例如下:
用户问题:什么是 RAG?
请使用以下信息回答:
[检索到的相关内容1]
[检索到的相关内容2]
[检索到的相关内容3]
[此处为图片1]
3.4 完整数据流图
四、总结
RAG 技术的核心机制在于:通过“从外部知识库中检索相关信息”(通常基于向量相似度匹配),并将这些相关片段补充进用户的原始输入提示中。这种方式使得大语言模型能够访问并利用原本不具备的新知识,有效弥补其在专业领域知识、时效性信息以及私有数据方面的不足。


雷达卡


京公网安备 11010802022788号







