在上一篇中,我们聊到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模板是这样的:

参考资料:

  1. [Chunk1内容] 来源:《2025年某行业税收优惠政策通知》
  2. [Chunk2内容] 来源:某税务局官方解读文件
    用户问题:2025年某行业最新税收优惠政策是什么?
    请基于上述参考资料,用简洁的语言回答用户问题,并在回答末尾标注信息来源。如果参考资料中没有相关信息,请勿编造,直接说明“未查询到相关信息”。

通过这种方式,大模型就会严格基于检索到的信息输出答案,还能标注来源,大大提升了可信度。

3. RAG vs 微调:为什么RAG更适合专业场景?

可能还有人纠结“RAG和微调该选哪个”,这里我们做个直接对比,帮大家理清适用场景:
对比维度

对比维度 RAG(检索增强生成) 微调(Fine-tuning)
数据成本 低,无需标注,直接用原始文档 高,需要大量标注好的训练数据
算力成本 低,仅需Embedding和检索算力 高,需要大模型级别的训练算力
知识更新 快,直接更新知识库即可,实时生效 慢,需要重新准备数据、重新微调
可解释性 高,可追溯回答对应的参考资料 低,回答基于模型“记忆”,无法追溯来源
适用场景 专业知识问答、时效性强的问题、企业内部知识查询 模型风格定制、特定任务优化(如翻译、摘要)

三、langchain4j的RAG技术实现

前面我们理清了RAG的核心原理,接下来通过LangChain4j(专为Java生态设计的大模型开发框架,对企业级业务系统更友好)进行实战,从代码层面拆解“知识库构建→检索→生成”全流程的具体实现。相比Python生态的LangChain,LangChain4j能更好地适配Spring Boot等主流企业开发框架,后续集成业务系统更顺畅。

3.1 整体架构

RAG 分为两个阶段:

  1. 索引阶段(Indexing):文档处理并存储到向量数据库
  2. 检索阶段(Retrieval):查询时检索相关内容并注入到提示中

核心组件:

  • EmbeddingStoreIngestor:负责索引阶段
  • RetrievalAugmentor:负责检索阶段
  • DefaultRetrievalAugmentor:默认实现,协调各组件

3.2 索引阶段(Indexing)实现

转换
分割
跳过
转换
跳过
原始文档
Document
文档转换器
DocumentTransformer
可选
转换后的文档
Document
文档分割器
DocumentSplitter
可选
文本段列表
List
单个文本段
TextSegment
文本段转换器
TextSegmentTransformer
可选
转换后的文本段
List
嵌入模型
EmbeddingModel
向量列表
List
向量存储
EmbeddingStore
存储完成
IngestionResult

核心类: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());
}

流程:

  1. 将查询文本转为向量:embeddingModel.embed(query.text())
  2. 构建搜索请求:设置 maxResults、minScore、filter
  3. 向量相似度搜索:embeddingStore.search(searchRequest)
  4. 返回最相关的 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 完整数据流图

检索阶段(Retrieval)
索引阶段(Indexing)
向量库
LLM回答
内容注入
内容聚合
向量检索
查询路由
查询转换
用户查询
存储到向量库
向量化
文本段转换
文档分割
文档转换
文档

四、总结

RAG技术的核心原理:通过“检索外部专业知识库”(向量检索)的方式,补充外部知识库中相关的信息到用户提示词中,从而大模型可以看见一些“新知识”,弥补大模型在专业、时效、私有知识上的短板。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐