破解大模型“知识盲区”——RAG技术原理与实践
在上一篇中,我们聊到AI的本质是概率模型,核心是通过计算概率分布输出最优结果,还通过Dify开源平台了解了大模型交互的工程化逻辑。但实际使用中,让大模型回答“李白哪年出生”这种常识问题时顺风顺水,可一旦问“2025年某行业最新政策解读”“某企业内部产品研发规范”这类专业或时效性强的问题,它要么答非所问,要么“一本正经地胡说八道”。这并不是大模型“变笨了”,而是它从根源上就存在“知识短板”。今天我们
在上一篇中,我们聊到AI的本质是概率模型,核心是通过计算概率分布输出最优结果,还通过Dify开源平台了解了大模型交互的工程化逻辑。但实际使用中,让大模型回答“李白哪年出生”这种常识问题时顺风顺水,可一旦问“2025年某行业最新政策解读”“某企业内部产品研发规范”这类专业或时效性强的问题,它要么答非所问,要么“一本正经地胡说八道”。
这并不是大模型“变笨了”,而是它从根源上就存在“知识短板”。今天我们就来拆解这个问题的根源,以及解决它的核心技术——RAG(检索增强生成),看看这项技术如何给大模型装上“专业知识外挂”。
一、大模型的“致命短板”:为什么会“胡说八道”?
要理解RAG的价值,首先得搞清楚大模型“知识盲区”的本质。很多人误以为大模型像“活字典”一样存储了所有信息,实则不然,它的“知识”源于训练数据,而训练数据存在三个无法回避的局限:
1. 知识存在“时间差”:滞后性无法避免
大模型的训练数据有明确的“截止日期”。虽然模型厂商在不断更新迭代训练数据,但是其庞大的训练成本,不可能每时每刻都训练数据,不可避免的会出现一些知识的滞后,无法获取实时的数据。
2. 专业领域“知识贫化”:垂直数据不足
大模型的训练数据是“广谱性”的,覆盖生活、科技、文化等通用领域,但在医疗、法律、金融等垂直领域,专业数据的占比极低。比如让大模型判断“某罕见病的最新治疗方案”,它可能只能给出几年前的通用疗法,而无法提供权威医学期刊最新发表的临床研究成果——这些专业数据要么未公开,要么未被纳入训练集。
3. 企业内部“知识隔绝”:私有数据无法触及
对企业用户来说,更核心的痛点是大模型无法获取内部私有数据。比如员工想让AI解答“公司新产品的渠道代理政策”“客户服务的分级响应标准”,这些数据不会出现在公开训练集中,大模型自然无法给出准确答案。
看到这里可能有人会问:“不能通过‘微调’给大模型灌输这些知识吗?”确实可以,但微调的成本极高——需要准备大量标注数据、消耗巨额算力,而且每次更新知识都要重新微调,灵活性极差。这时候,RAG技术就成了更优解,它不用改变大模型本身,而是通过“外部检索”的方式给大模型补充精准知识。
二、RAG技术原理:从“死记硬背”到“实时检索”的革命
RAG的核心逻辑其实很简单,就像我们写文章时“查资料+写结论”的过程:先从权威资料里找到相关内容,再基于这些内容组织语言输出。对应到技术上,就是“检索→生成”两步走,只不过这两个步骤都实现了自动化。
1. 核心逻辑:用“检索到的精准信息”替代“模型的模糊记忆”
在没有RAG的情况下,大模型的回答完全依赖训练时学到的“模糊概率”——比如问“某病的治疗方案”,它会基于训练数据中出现过的相关内容,计算最可能的输出结果。而有了RAG之后,流程变成了:
1. 用户提问:“2025年某行业最新税收优惠政策是什么?”
2. 系统检索:自动从预先准备的“2025年行业政策知识库”中,找到与“税收优惠”相关的原文片段
3. 生成答案:大模型基于检索到的政策原文,整理成通俗易懂的回答,并标注信息来源
这个过程的关键在于:大模型的回答不再依赖“记忆”,而是依赖“检索到的权威信息”,从根源上解决了“胡说八道”的问题。
2. 全流程拆解:从数据到输出的每一步都在做什么?
看似简单的“检索+生成”,背后藏着三个关键技术环节,我们一步步拆解:
环节一:知识库构建——让专业数据“可检索”
要检索信息,首先得有“可检索的知识库”,这一步是RAG的基础,核心是把零散的专业文档(如政策文件、医学指南、企业手册)转化为“机器能理解并快速匹配”的形式。具体分三步:
- 第一步:文档拆分(Chunking)——把“厚书”拆成“便利贴” 直接把一本几百页的手册丢给系统,检索时根本无法定位到具体内容。所以要先做“拆分”,就像我们把厚书拆成一张张便利贴,每张便利贴只包含一个核心信息点。拆分有技巧:不能拆太碎(比如把一句话拆成几个词),也不能拆太粗(比如几页内容放一起),通常建议按“语义完整性”拆分,比如一个段落或一个小节拆成一个“Chunk”(片段),长度控制在200-500字左右。
- 第二步:向量嵌入(Embedding)——给“便利贴”贴“语义标签” 机器无法直接理解文本的“意思”,所以需要把每个Chunk转化为“向量”——一串数字。这个转化过程就是“Embedding”,由专门的Embedding模型(如BGE、Sentence-BERT)完成。核心原理是:语义相近的文本,转化后的向量也会“更像”(数字差异小)。比如“税收优惠”和“减税政策”的向量相似度会很高,而和“天气预报”的相似度就很低。这一步让机器实现了“语义理解”,而不是简单的“关键词匹配”。
- 第三步:向量数据库存储——建一个“智能书架” 转化后的向量需要存到专门的“向量数据库”里(如Milvus、Chroma、Pinecone),而不是传统的MySQL这类关系型数据库。因为向量数据库能快速计算“相似度”——当用户提问转化为向量后,它能在百万级、千万级的向量中,瞬间找到最相似的几个Chunk。就像一个智能书架,你说“我要找关于税收优惠的内容”,它立刻把相关的“便利贴”都抽出来。
环节二:检索过程——找到“最相关”的信息
当用户提出问题后,系统会先把问题也转化为向量(和构建知识库时用同一个Embedding模型),然后拿着这个向量去向量数据库里“找相似”。这个过程不是“精确匹配”,而是“相似度排序”,通常会返回Top5-10个最相关的Chunk。
为了提高检索精度,还有一些优化技巧:比如“多轮检索”——如果第一次检索结果不理想,基于用户问题和初步结果再生成一个“更精准的检索词”重新检索;再比如“权重调整”——给最新的文档或权威来源的文档设置更高权重,让它们更容易被检索到。
环节三:生成优化——让大模型“用检索信息说话”
检索到相关Chunk后,不能直接丢给用户,还要让大模型“翻译”成通顺的回答。这里的关键是“Prompt工程”——把用户问题和检索到的Chunk整合到一个Prompt里,明确告诉大模型:“基于以下参考资料,回答用户问题,不要编造信息,如果资料里没有答案就说不知道。”
一个典型的Prompt模板是这样的:
参考资料:
- [Chunk1内容] 来源:《2025年某行业税收优惠政策通知》
- [Chunk2内容] 来源:某税务局官方解读文件
用户问题:2025年某行业最新税收优惠政策是什么?
请基于上述参考资料,用简洁的语言回答用户问题,并在回答末尾标注信息来源。如果参考资料中没有相关信息,请勿编造,直接说明“未查询到相关信息”。
通过这种方式,大模型就会严格基于检索到的信息输出答案,还能标注来源,大大提升了可信度。
3. RAG vs 微调:为什么RAG更适合专业场景?
可能还有人纠结“RAG和微调该选哪个”,这里我们做个直接对比,帮大家理清适用场景:
对比维度
| 对比维度 | RAG(检索增强生成) | 微调(Fine-tuning) |
|---|---|---|
| 数据成本 | 低,无需标注,直接用原始文档 | 高,需要大量标注好的训练数据 |
| 算力成本 | 低,仅需Embedding和检索算力 | 高,需要大模型级别的训练算力 |
| 知识更新 | 快,直接更新知识库即可,实时生效 | 慢,需要重新准备数据、重新微调 |
| 可解释性 | 高,可追溯回答对应的参考资料 | 低,回答基于模型“记忆”,无法追溯来源 |
| 适用场景 | 专业知识问答、时效性强的问题、企业内部知识查询 | 模型风格定制、特定任务优化(如翻译、摘要) |
三、langchain4j的RAG技术实现
前面我们理清了RAG的核心原理,接下来通过LangChain4j(专为Java生态设计的大模型开发框架,对企业级业务系统更友好)进行实战,从代码层面拆解“知识库构建→检索→生成”全流程的具体实现。相比Python生态的LangChain,LangChain4j能更好地适配Spring Boot等主流企业开发框架,后续集成业务系统更顺畅。
3.1 整体架构
RAG 分为两个阶段:
- 索引阶段(Indexing):文档处理并存储到向量数据库
- 检索阶段(Retrieval):查询时检索相关内容并注入到提示中
核心组件:
- EmbeddingStoreIngestor:负责索引阶段
- RetrievalAugmentor:负责检索阶段
- DefaultRetrievalAugmentor:默认实现,协调各组件
3.2 索引阶段(Indexing)实现
核心类: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 {
segments = documents.stream().map(Document::toTextSegment).collect(toList());
}
if (textSegmentTransformer != null) {
segments = textSegmentTransformer.transformAll(segments);
log.debug("{} documents were transformed into {} text segments", documents.size(), segments.size());
}
log.debug("Starting to embed {} text segments", segments.size());
Response<List<Embedding>> embeddingsResponse = embeddingModel.embedAll(segments);
log.debug("Finished embedding {} text segments", segments.size());
log.debug("Starting to store {} text segments into the embedding store", segments.size());
embeddingStore.addAll(embeddingsResponse.content(), segments);
log.debug("Finished storing {} text segments into the embedding store", segments.size());
return new IngestionResult(embeddingsResponse.tokenUsage());
}
步骤详解
步骤 1:文档转换(可选)
if (documentTransformer != null) {
documents = documentTransformer.transformAll(documents);
}
- 清理、格式化、丰富元数据
步骤 2:文档分割(关键)
if (documentSplitter != null) {
segments = documentSplitter.splitAll(documents);
} else {
segments = documents.stream().map(Document::toTextSegment).collect(toList());
}
- 将大文档分割为较小的 TextSegment
- 原因:LLM 上下文窗口有限;提高检索精度;控制成本
- 常见策略:按段落/句子递归分割,支持重叠
步骤 3:文本段转换(可选)
if (textSegmentTransformer != null) {
segments = textSegmentTransformer.transformAll(segments);
}
- 可在每个段前添加标题或摘要,提升检索质量
步骤 4:向量化(核心)
Response<List<Embedding>> embeddingsResponse = embeddingModel.embedAll(segments);
- 使用 EmbeddingModel 将文本段转为向量
- 向量表示语义,相似文本的向量距离更近
步骤 5:存储到向量数据库
embeddingStore.addAll(embeddingsResponse.content(), segments);
- 将向量和原始文本段存储到 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("Unsupported message type: " + chatMessage.type());
}
Query originalQuery = Query.from(queryText, augmentationRequest.metadata());
Collection<Query> queries = queryTransformer.transform(originalQuery);
Map<Query, Collection<List<Content>>> queryToContents = process(queries);
List<Content> contents = contentAggregator.aggregate(queryToContents);
ChatMessage augmentedChatMessage = contentInjector.inject(contents, chatMessage);
return AugmentationResult.builder()
.chatMessage(augmentedChatMessage)
.contents(contents)
.build();
}
步骤详解
步骤 1:查询转换(Query Transformation)
Collection<Query> queries = queryTransformer.transform(originalQuery);
DefaultQueryTransformer 默认不做转换:
public Collection<Query> transform(Query query) {
return singletonList(query); // 直接返回原查询
}
高级实现如 ExpandingQueryTransformer 可扩展查询:
- 使用 LLM 生成多个查询变体
- 提高召回率
步骤 2:查询路由(Query Routing)
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 contentRetriever = retrievers.iterator().next();
List<Content> contents = contentRetriever.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();
}
}
- DefaultQueryRouter 将查询路由到指定的检索器
检索器有多种实现类:
- EmbeddingStoreContentRetriever(向量检索器)
- WebSearchContentRetriever(网络搜索检索器)
- AzureAiSearchContentRetriever(混合搜索检索器)
-
LanguageModelQueryRouter 可用 LLM 智能选择数据源
-
多个检索器时并行检索
步骤 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())
- 构建搜索请求:设置 maxResults、minScore、filter
- 向量相似度搜索:embeddingStore.search(searchRequest)
- 返回最相关的 Content 列表(按相似度排序)
步骤 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]
计算:
- 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) {
// First, for each query, fuse all contents retrieved from different sources using that query.
Map<Query, List<Content>> fused = fuse(queryToContents);
// Then, fuse all contents retrieved using all queries
return ReciprocalRankFuser.fuse(fused.values());
}
- 阶段1:对每个查询,合并来自不同检索器的结果
- 阶段2:合并所有查询的结果
步骤 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]
3.4 完整数据流图
四、总结
RAG技术的核心原理:通过“检索外部专业知识库”(向量检索)的方式,补充外部知识库中相关的信息到用户提示词中,从而大模型可以看见一些“新知识”,弥补大模型在专业、时效、私有知识上的短板。
更多推荐


所有评论(0)