找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

221

积分

0

好友

31

主题
发表于 3 天前 | 查看: 7| 回复: 0

MCP 让 LLM 具备了调用外部程序的能力,能够完成自动化工作如获取上下文、操作文件系统等。而 RAG 同样能让 LLM 自动获取外部信息来增强上下文,不同的是 MCP 更偏向工具调用,用途广泛;RAG 则专注于知识检索,能从数据库中找出与问题相关的知识来增强 LLM 的上下文信息。

概述

背景

当前 LLM 生成的内容都基于训练时的已知信息,无法访问外部数据,因此难以回答训练数据以外的问题。例如询问内部文档内容时,LLM 可能产生错误回答,这种现象称为大模型幻觉。为解决幻觉问题,可以将文档内容与问题一起发送给 LLM,但当文档体量庞大时,相关上下文可能只是其中一小段,LLM 仍可能无法准确聚焦重点。这时需要具备检索能力的工具自动找出最相关的上下文片段交给 LLM,这正是 RAG 要解决的核心问题。

RAG 技术解析

RAG(Retrieval Augmented Generation)即检索增强生成,其核心思想是在 LLM 回答前,先通过检索系统从外部知识库找出相关问题内容,将这些内容与原始问题共同输入 LLM。

结合 RAG 的 LLM 回答流程如下:

  1. 用户提出问题
  2. 检索系统搜索知识库中的相关内容
  3. 知识库返回相关知识
  4. 检索系统将问题和知识发送给 LLM
  5. LLM 生成答案并返回给用户

RAG流程示意图

Embedding 机制

RAG 实施过程中需要解决两个关键问题:如何高效检索相关知识内容,以及知识库的存储方式。

针对第一个问题,RAG 引入 Embedding 模型,该模型将输入文本转换为固定长度的浮点型数组。例如 OpenAI 的 text-embedding-3-small 模型输出 1536 维数组,text-embedding-3-large 输出 3076 维数组。内容越相似,生成的数组也越相似,通过计算数组间距离即可判断文本相似度。

这类似于坐标系概念,数组可映射到高维坐标系中的点,通过点间距离衡量相似度。以下图三维坐标系为例,越接近的文字其映射点也越接近。

Embedding示意图

针对第二个问题,传统关系型数据库适合精确匹配检索,而 RAG 需要语义相似度检索,因此引入向量数据库作为知识库。向量数据库专门存储和检索高维向量,支持相似性搜索,能存储文本、音频、视频等非结构化数据经过 Embedding 后生成的向量,并能快速找到最相似的向量。常用向量数据库包括 Pinecone、Chroma、PostgreSQL + PGVector 等。

技术原理

工作流程

RAG 工作流程分为离线和在线两部分:

  • 离线部分:知识准备过程,提前上传文档并通过 Embedding 模型转换为向量存储到向量数据库
  • 在线部分:实时问答过程,用户问题经 Embedding 转换后,在知识库中检索最相似知识片段,与问题一起传入 LLM 生成答案

完整流程如下图所示:

RAG完整流程

文档分块(Chunking)

Chunking 指将文档分割成多个片段,分割质量直接影响后续检索准确性和 LLM 回答效果。在 RAG 中进行分块操作是因为相关问题可能只涉及文档的一小部分,直接提交全部文档可能导致 LLM 无法聚焦重点。

常见分块策略:

策略 描述 优点 缺点
固定大小拆分 按指定字数或 token 数切分 实现简单、速度快 可能割裂语句完整性
结构拆分 基于文档格式(HTML、Markdown)拆分 保留原始结构逻辑 依赖规范文档结构
语义拆分 根据语义边界(段落、句子)拆分 符合人类理解 实现复杂、计算开销大
递归拆分 先按大分隔符拆分,再逐级细化 平衡语义完整性和长度控制 设计复杂

这些策略可组合使用,目前没有适用于所有文档的统一策略,需要根据具体情况选择。

索引构建(Indexing)

在 RAG 中需要对向量构建索引以实现高效相似度计算。向量索引是用于高效索引和检索高维向量的数据结构,常用 ANN(近似最近邻搜索算法)在牺牲少量准确性同时显著提升搜索速度。

常见实现方式:

LSH(局部敏感哈希算法) 基于哈希结构的算法,设计哈希函数使相似向量映射到相同哈希桶的概率高。查询时只搜索相同或相似桶中的数据,避免全表扫描。

LSH示意图

Annoy算法 基于树结构的算法,构建"随机投影二叉树"划分向量空间。算法流程:随机选择两个向量生成超平面切割空间→投影所有向量→按中位数分割→递归划分直到向量数量小于阈值。搜索时递归查找最近叶节点作为候选向量。

Annoy示意图

HNSW(分层导航小世界算法) 基于图结构的算法,构建分层图网络,高层用于快速定位,低层用于精细搜索,类似跳表结构。

HNSW示意图

IVF(倒排文件索引) 基于聚类的算法,通过 k-means 聚类将向量分簇,每个簇建立倒排表。查询时找到最近几个簇,只在这些簇中搜索最近邻向量。

IVF示意图

这些算法都不保证找出真正最相近的向量,而是在部分候选区域查找以换取速度,需要权衡性能、准确性和计算资源。

相似度搜索

RAG 中通过计算向量距离判断相似度,常见计算方式:

欧几里得距离 计算两个向量间的直线距离,公式: 欧几里得公式 优点:简单直观,适合距离感任务;缺点:对长度敏感,不适合语义相似向量。

点积 计算两个向量乘积之和,公式: 点积公式 优点:简单高效,适合含权重的 Embedding 模型;组成:向量长度和语义方向相似性。

余弦相似度 计算两个向量夹角的余弦值,公式: 余弦相似度公式 优点:与语义方向一致,长度不同的相似文本仍有高相似度;取值范围 [-1,1],越接近1越相似。

重排序(Re-ranking)

向量索引算法虽快但牺牲了准确性,检索出的文档可能相似度高却不真正相关。因为"相似度"≠"相关性",相似度衡量语义相似,但不一定对问题有帮助。

例如查询"李清照文学风格",向量相似度高的可能包括杜甫相关文档,因为都是古代诗人,语义相似但不相关。

因此需要重排序操作:初步检索时获取较多候选文档(如 top-20),然后使用其他模型进行精细匹配,选出最相关的少量文档(如 top-5)。重排序模型在相关性排序上更准确但计算代价更高,通常只在 top-20/50 上运行。

重排序流程

提示模板(Prompt Template)

在 RAG 中,Prompt Template 将检索到的文档和用户问题组合成格式化 prompt 交给 LLM,能让 LLM 更聚焦上下文,减少幻觉问题,使回答更稳定专业。

可根据任务类型自定义模板,常见格式:

You are a helpful assistant. Based on the following context, answer the question.
Context:
{retrieved_documents}
Question:
{user_query}
Answer:

流程总结

回顾完整流程图,详细描述如下:

离线部分

  1. 对上传文档分块,分割成多个片段
  2. 使用 Embedding 模型将片段转换为向量
  3. 将向量存储到向量数据库并建立索引

在线部分

  1. 使用 Embedding 模型将用户问题转换为向量
  2. 在向量数据库中检索最相似的 top-k 知识片段
  3. 对检索片段重排序,保留最相关的 top-n 个
  4. 将知识片段与问题组合成格式化 prompt
  5. 将 prompt 提交给 LLM 生成最终答案

实践应用

本节介绍使用 Spring AI 框架 开发基于 RAG 的应用。

环境配置

环境 说明
JDK 17
SpringBoot 3.5.0
Spring AI 1.0.0
构建工具 Maven
LLM Qwen2.5-72B-Instruct
Embedding text-embedding-ada-002
向量数据库 PostgreSQL + PGVector

Embedding 实现

使用 OpenAI Embedding 模型进行向量化操作。

pom 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

配置文件:

spring:
  ai:
    openai:
      base-url: [这里填url]
      api-key: [这里填密钥]
      embedding:
        options:
          model: text-embedding-ada-002

代码示例:

@Autowired
private EmbeddingModel embeddingModel;

@GetMapping("/embedding")
public void embedding(String input) {
    System.out.println("input = " + input);
    float[] embeddings1 = embeddingModel.embed(input);
    System.out.println("length = " + embeddings1.length + ", array = " + Arrays.toString(embeddings1));
}

测试结果文本被转换为 1536 维向量。

向量数据库配置

使用 PostgreSQL 配合 PGVector 插件作为向量数据库

pom 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>

配置文件:

spring:
  ai:
    vectorstore:
      pgvector:
        initialize-schema: true
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
        max-document-batch-size: 10000
datasource:
    url: jdbc:postgresql://localhost/postgres
    username: [这里填用户名]
    password: [这里填密码]

initialize-schema 为 true 时,Spring AI 自动初始化向量数据库,相当于执行:

CREATE TABLE IF NOT EXISTS vector_store (
    id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
    content text,
    metadata json,
    embedding vector(1536)
);
CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);

代码示例:

@Autowired
private VectorStore vectorStore;

@GetMapping("/storeVector")
public void storeVector(@RequestParam List<String> input) {
    List<Document> documents = input.stream().map(Document::new).collect(Collectors.toList());
    vectorStore.add(documents);
}

@GetMapping("/similaritySearch")
public void similarSearch(String input) {
    SearchRequest query = SearchRequest.builder().query(input).topK(2).build();
    List<Document> similarDocuments = vectorStore.similaritySearch(query);
    String result = similarDocuments.stream()
            .map(Document::getText)
            .collect(Collectors.joining(System.lineSeparator()));
    System.out.println("result:\n" + result);
}

注意:调用 VectorStore 的 add 方法时无需手动调用 Embedding API,底层会自动转换,但需提前配置 Embedding 模型。

ETL 流程

ETL(提取、转换、加载)在 RAG 中对数据进行预处理,实现从原始数据源到结构化向量存储的流程。

Spring AI 提供 ETL 相关 API:

  • DocumentReader:实现 Extract 操作,常用实现类包括 TextReader、JsoupDocumentReader、MarkdownDocumentReader、PagePdfDocumentReader
  • DocumentTransformer:实现 Transform 操作,常用实现类包括 TokenTextSplitter、ContentFormatTransformer
  • DocumentWriter:实现 Load 操作,常用实现类包括 FileDocumentWriter、各种 VectorStore 类

三个组件协作完成 ETL 流程:

ETL流程

演示 TextReader、TokenTextSplitter、PgVectorStore 组合:

@Autowired
private VectorStore vectorStore;

@Value("classpath:/file.txt")
private Resource resource;

@GetMapping("/etl")
public void etl() {
    // extract
    TextReader textReader = new TextReader(this.resource);
    List<Document> extractedDoc = textReader.read();
    System.out.println("extract result: " + extractedDoc);

    // transform
    TokenTextSplitter splitter = new TokenTextSplitter(200, 200, 5, 10000, true);
    List<Document> transformedDoc = splitter.apply(extractedDoc);
    System.out.println("transform length = " + transformedDoc.size() + ", result: " + transformedDoc);

    // load
    vectorStore.add(transformedDoc);
}

TokenTextSplitter 参数说明:

  • chunkSize:每个文本块的目标 token 数,默认 800
  • minChunkSizeChars:每个文本块最少字符数,默认 350
  • minChunkLengthToEmbed:chunk 长度超过此值才包含进结果,默认 5
  • maxNumChunks:从文本中最多切出的 chunk 数,默认 1000
  • keepSeparator:是否保留分隔符,默认 true

RAG 在线部分

前面主要涉及数据处理(离线部分),现在介绍与 LLM 交互的在线部分。

pom 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-rag</artifactId>
</dependency>

为显示 LLM 交互日志,配置日志级别:

logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: DEBUG

LLM Bean 配置:

@Bean
public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
    return chatClientBuilder
            .defaultAdvisors(new SimpleLoggerAdvisor())
            .build();
}

RAG 流程代码:

@Autowired
private ChatClient chatClient;

@Autowired
private VectorStore vectorStore;

@GetMapping("/rag")
public void chatWithRag(String input) {
    System.out.println("input: " + input);
    Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
            .documentRetriever(VectorStoreDocumentRetriever.builder()
                    .similarityThreshold(0.5)
                    .vectorStore(vectorStore)
                    .build())
            .queryAugmenter(ContextualQueryAugmenter.builder()
                    .allowEmptyContext(true)
                    .build())
            .build();
    String result = chatClient.prompt()
            .advisors(retrievalAugmentationAdvisor)
            .user(input)
            .call()
            .content();
    System.out.println("result: " + result);
}

测试结果显示 Spring AI 在 RAG 中设置了 Prompt Template,不仅组合检索文档和问题,还指定回答规则:"如果答案不在上下文中,就说你不知道"、"避免使用'根据上下文'等说法"。

完整代码示例

pom 依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-rag</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置文件:

spring:
  ai:
    openai:
      base-url: [这里填url]
      api-key: [这里填密钥]
      chat:
        options:
          model: Qwen/Qwen2.5-72B-Instruct
      embedding:
        options:
          model: text-embedding-ada-002
    vectorstore:
      pgvector:
        initialize-schema: true
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536
        max-document-batch-size: 10000
datasource:
    url: jdbc:postgresql://localhost/postgres
    username: [这里填用户名]
    password: [这里填密码]
logging:
  level:
    org:
      springframework:
        ai:
          chat:
            client:
              advisor: DEBUG

Java 代码:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AIConfig {
    @Bean
    public ChatClient chatClient(ChatClient.Builder chatClientBuilder) {
        return chatClientBuilder
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
    }
}

@RestController
public class QwenController {
    @Autowired
    private ChatClient chatClient;

    @Autowired
    private EmbeddingModel embeddingModel;

    @Autowired
    private VectorStore vectorStore;

    @Value("classpath:/file.txt")
    private Resource resource;

    @GetMapping("/embedding")
    public void embedding(String input) {
        System.out.println("input = " + input);
        float[] embeddings1 = embeddingModel.embed(input);
        System.out.println("length = " + embeddings1.length + ", array = " + Arrays.toString(embeddings1));
    }

    @GetMapping("/storeVector")
    public void storeVector(@RequestParam List<String> input) {
        List<Document> documents = input.stream().map(Document::new).collect(Collectors.toList());
        vectorStore.add(documents);
    }

    @GetMapping("/similaritySearch")
    public void similarSearch(String input) {
        SearchRequest query = SearchRequest.builder().query(input).topK(2).build();
        List<Document> similarDocuments = vectorStore.similaritySearch(query);
        String result = similarDocuments.stream()
                .map(Document::getText)
                .collect(Collectors.joining(System.lineSeparator()));
        System.out.println("result:\n" + result);
    }

    @GetMapping("/etl")
    public void etl() {
        TextReader textReader = new TextReader(this.resource);
        List<Document> extractedDoc = textReader.read();
        System.out.println("extract result: " + extractedDoc);

        TokenTextSplitter splitter = new TokenTextSplitter(200, 200, 5, 10000, true);
        List<Document> transformedDoc = splitter.apply(extractedDoc);
        System.out.println("transform length = " + transformedDoc.size() + ", result: " + transformedDoc);

        vectorStore.add(transformedDoc);
    }

    @GetMapping("/rag")
    public void chatWithRag(String input) {
        System.out.println("input: " + input);
        Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
                .documentRetriever(VectorStoreDocumentRetriever.builder()
                        .similarityThreshold(0.5)
                        .vectorStore(vectorStore)
                        .build())
                .queryAugmenter(ContextualQueryAugmenter.builder()
                        .allowEmptyContext(true)
                        .build())
                .build();
        String result = chatClient.prompt()
                .advisors(retrievalAugmentationAdvisor)
                .user(input)
                .call()
                .content();
        System.out.println("result: " + result);
    }
}

未来展望

RAG 作为融合外部知识与 LLM 生成能力的技术路径,正在成为企业 AI 应用 的重要解决方案。它在增强 LLM 专业性和个性化能力方面展现出巨大潜力,随着技术演进和框架能力完善,RAG 将在更多真实场景中发挥关键作用。

您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-1 14:12 , Processed in 0.058180 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表