大模型LLM(四)--大模型RAG原理及实现(SpringAI+RAG+Neo4j)

1、RAG的原理

RAG 检索增强生成(Retrieval Augmented Generation),通俗一点讲就是向LLM 提问一个问题(qustion),从向量数据库中检索相关的信息,并将检索到的信息和问题(answer)一起来构建LLM的提示词(Prompt),LLM 最后生成答案的过程。下面是一个简单的RAG应用的示意图。


RAG应用的示意图

完整的RAG应用流程主要包含两个阶段:
数据准备阶段:资料解析和切分——>向量化(embedding)——>向量数据库入库
应用阶段:用户提问——>数据检索(召回)——>将查询结果注入Prompt——>LLM生成答案

相似度计算的算法:
1、余弦相似度 - 衡量向量方向相似性
2、欧式距离 - 计算向量空间距离
3、点积 - 向量内积运算

下面会讲述怎么构建一个RAG系统,其中Large Language Model的构建可以返回查看之前的博客《大模型LLM(一)--本地大模型部署Qwen 流式响应 FastAPI》

2、项目地址

github开源的地址:
https://github.com/xujinhelaw/chat-bot-ananas.git
gittee开源的地址:
https://gitee.com/xuhelaw/chat-bot-ananas.git
项目结构如下

chat-bot-ananas/ (根项目)
├── chat-bot-frontend/ (前端模块)
│   ├── public/
│   ├── src/
│   │   ├── assets/
│   │   ├── components/
│   │   │  ├── ChatView.vue(前台聊天界面代码)
│   │   │  ├── RagManage.vue(RAG文档管理界面代码)
│   │   ├── views/
│   │   ├── App.vue
│   │   └── main.js
│   ├── package.json(前台依赖配置和前台应该启动代码)
│   └── vite.config.js
└── chat-bot-backend/ (后端模块)
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   └── org/
│   │   │   │       └── ananas/
│   │   │   │           └── chatbotbackend/
│   │   │   │           │    ├── controller/
│   │   │   │           │        └── ChatController.java(后端大模型调用和开放外部接口)
│   │   │   │           │    └── config/
│   │   │   │           │        └── AiConfig.java(类配置文件)
│   │   │   │           └── rag/(Rag后端代码实现)
│   │   │   │           │    ├── controller/
│   │   │   │           │        ├── ConfigController.java(智能聊天助手的配置接口代码)
│   │   │   │           │        ├── FileUploadController.java(Rag文件上传接口代码)
│   │   │   │           │        ├── SearchController.java(Rag检索接口代码)
│   │   │   │           │        └── ...
│   │   │   │           │    ├── model/
│   │   │   │           │        ├── Chunk.java(Rag资料分块对象代码)
│   │   │   │           │        ├── ChunkMatchResult.java(Rag检索结果对象代码)
│   │   │   │           │        ├── Document.java(Rag文档对象代码)
│   │   │   │           │    ├── repository/
│   │   │   │           │        └──  DocumentRepository.java(Rag的Dao实现存储内容和检索内容代码)
│   │   │   │           │    └── service/
│   │   │   │           │        ├── PdfProcessingService.java(Rag文档处理服务代码)
│   │   │   │           │        └── SearchService.java(Rag相似度检索服务代码)
│   │   │   │           └──  ChatBotBackendApplication.java(后端应用启动代码)
│   │   │   │
│   │   │   └── resources/
│   │   │       ├── static/ (Vue 构建后的文件将放在这里)
│   │   │       ├── templates/
│   │   │       └── application.yml(后端配置文件)
└── install/ (大模型服务端模块)
│   ├── ragdata/ (rag文档)
│   │   └── 证券公司监督管理条例.pdf(rag文档)
└── llm-server/ (大模型服务端模块)
│   ├── api.py(大模型、Embedding模型启动和开放接口代码)
│   ├── chatmachine.py(大模型访问客户端代码)
│   ├── download.py(大模型、Embedding模型下载代码)
│   ├── environment.yml(大模型部署和访问客户端需要的依赖包)
└── pom.xml(后端依赖管理pom文件)
└── pom.xml (根 POM,管理子模块)
└──settings.xml(maven仓配置文件)

3、RAG应用的执行效果

3.1 前后台启动

后台启动

前台启动

3.2 LLM Qwen和Embedding Model模型启动

image.png

3.3 上传文档和进行语义搜索

搜索信息:证券公司从事证券自营业务不得有哪些行为
原始资料中搜索无法搜到,因为《证券公司监督管理条例.pdf》(该文档在代码仓中包含)里面的搜索是完全匹配


原始资料信息

语义搜索可以检索到向量数据库的内容


文档管理搜索结果

提问内容:
证券公司从事证券自营业务不得有哪些行为?
未开启RAG的返回结果

开启RAG的返回结果跟《证券公司监督管理条例.pdf》文档中的四十三条结果内容完全一致


开启RAG的返回结果

4、下载并安装Embedding Model模型

选用的模型是bge-large-zh-v1.5,到modelscope查看要下载的模型的名称
https://www.modelscope.cn/home

embed模型信息

#通过modelscope的包进行模型下载
from modelscope import snapshot_download
# 第一个参数为模型名称,参数cache_dir为模型的下载路径
model_dir = snapshot_download('BAAI/bge-large-zh-v1.5', cache_dir='./')

5、启动embedding模型bge-large-zh-v1.5

在工程中的位置为api.py,跟embedding模型相关的代码如下:

#用于启动embedding大模型
from pydantic import BaseModel
from typing import List, Dict, Any
from sentence_transformers import SentenceTransformer

USE_Embedding = True      # 控制是否启用 Embedding模型
if torch.cuda.is_available():
    DEVICE = "cuda"
    DEVICE_ID = "0"  # 可根据需要修改为其他GPU设备ID
    CUDA_DEVICE = f"{DEVICE}:{DEVICE_ID}"
    TORCH_DTYPE = torch.float16
    print("使用 GPU + FP16!")
else:
    DEVICE = "cpu"
    DEVICE_ID = ""
    CUDA_DEVICE = "cpu"
    TORCH_DTYPE = torch.float32
    print("使用 CPU + FP32!")

# 创建FastAPI应用
app = FastAPI()
# 添加 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 或指定具体域名,如 ["http://localhost:6006"]
    allow_credentials=True,
    allow_methods=["*"],  # 允许所有方法(GET, POST, OPTIONS 等)
    allow_headers=["*"],  # 允许所有头
)

# ================== Embedding大模型 请求/响应模型定义 ==================

class EmbeddingRequest(BaseModel):
    input: List[str]  # Spring AI 发送的是 input 字段
    model: str = None  # 可选


class UsageInfo(BaseModel):
    prompt_tokens: int = 0
    total_tokens: int = 0


class DataItem(BaseModel):
    embedding: List[float]
    index: int
    object: str = "embedding"


class OpenAIEmbeddingResponse(BaseModel):
    data: List[DataItem]
    model: str = "bge-large-zh-v1.5"
    usage: UsageInfo
    object: str = "list"

# ================== Embedding大模型 API ==================
@app.post("/v1/embeddings", response_model=OpenAIEmbeddingResponse)
async def create_embeddings(request: EmbeddingRequest):
    texts = request.input

    if not texts or len(texts) == 0:
        raise HTTPException(status_code=400, detail="输入文本不能为空")

    try:
        # 生成嵌入向量
        embeddings = embedding_model.encode(
            sentences=texts,
            batch_size=32,
            convert_to_numpy=True,
            normalize_embeddings=True  # BGE 推荐归一化
        )

        # 构造响应数据
        data_items = [
            DataItem(embedding=emb.tolist(), index=i)
            for i, emb in enumerate(embeddings)
        ]

        # 简单统计 token 数(BGE 不分词,按空格粗略估算)
        prompt_tokens = sum(len(text.split()) for text in texts)
        usage = UsageInfo(prompt_tokens=prompt_tokens, total_tokens=prompt_tokens)

        return OpenAIEmbeddingResponse(data=data_items, usage=usage)

    except Exception as e:
        logger.error(f"嵌入向量生成失败: {e}")
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == '__main__':
    # 加载embedding大模型
    embedding_model_path = "./BAAI/bge-large-zh-v1.5"
    embedding_model_name = "BAAI/bge-large-zh-v1.5"
    if USE_Embedding and os.path.exists(embedding_model_path):
        print("正在加载embedding大模型... ...")
        start_time = datetime.datetime.now()
        print(f"embedding大模型路径为:{embedding_model_path}")
        embedding_model = SentenceTransformer(embedding_model_name, device=DEVICE)
        end_time = datetime.datetime.now()
        cos_time = (end_time - start_time).seconds
        print(f"embedding大模型加载完成。耗时:{cos_time} 秒。")
    print("模型加载完成。")
    # 启动FastAPI应用
    uvicorn.run(app, host='0.0.0.0', port=6006)  # 在指定端口和主机上启动应用

6、安装向量数据库

选择的向量数据库是Neo4j,详细的安装指导,可以参考这篇博客
https://www.jianshu.com/p/ed0c0708e0c7

7、构建RAG知识管理应用

基于Spring AI和Vue构建RAG知识管理应用

7.1 后台实现

application.yml配置

spring:
  # AI 配置
  ai:
    openai:
      # 嵌入模型配置
      embedding:
        options:
          model: bge-large-zh-v1.5 # 模型名在这里
          dimensions: 1024 # 向量维度

# Neo4j 配置
  neo4j:
    uri: neo4j://localhost:7687 #另外http:////localhost:7474 管理页面
    authentication:
      username: neo4j
      password: password # 修改为你的密码

maven依赖

    <!-- Spring Data Neo4j -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-neo4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-neo4j</artifactId>
        </dependency>
        <!-- Spring Data Neo4j End -->

这里只介绍3个核心代码的实现,其他代码实现请自行下载代码仓中的代码进行编译即可直接启动
FileUploadController 文档上传接口实现

@RestController
public class FileUploadController {

    @Autowired
    private PdfProcessingService pdfProcessingService;

    @PostMapping("/upload-pdf")
    public ResponseEntity<String> uploadPdf(@RequestParam("file") MultipartFile file) {
        try {
            // 验证文件
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("文件不能为空");
            }
            if (!file.getContentType().equals("application/pdf")) {
                return ResponseEntity.badRequest().body("仅支持 PDF 文件");
            }

            // 处理 PDF
            pdfProcessingService.processPdf(file);

            return ResponseEntity.ok("PDF 上传并处理成功!");
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(500).body("文件处理失败: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(500).body("内部服务器错误: " + e.getMessage());
        }
    }
}

PdfProcessingService 文档解析和分割实现
(代码请自行下载查看)
DocumentRepository 负责将向量数据库的存储和检索,包括余弦相似度的实现

@Repository
public interface DocumentRepository extends Neo4jRepository<Document, Long> {
    Document findByDocId(String docId);

    /**
     * 根据查询向量在 Chunk 节点中搜索最相似的文本块
     * 使用余弦相似度排序,返回前 n 个结果
     *
     * @param queryEmbedding 查询文本的嵌入向量
     * @param topK           返回前 K 个最相似的结果
     * @return 包含相似度分数和文本内容的 Map 列表
     */
    @Query("MATCH (c:Chunk)-[:PART_OF]->(d:Document) " +
            "WHERE c.embedding IS NOT NULL " +
            "WITH c, d, " +
            "  reduce(s = 0.0, i IN range(0, size(c.embedding) - 1) | s + c.embedding[i] * $queryEmbedding[i]) AS dotProduct, " +
            "  sqrt(reduce(s = 0.0, x IN c.embedding | s + x^2)) AS normA, " +
            "  sqrt(reduce(s = 0.0, x IN $queryEmbedding | s + x^2)) AS normB " +
            "WITH c, d, dotProduct / (normA * normB) AS similarity " +
            "WHERE similarity > 0.6 " +
            "ORDER BY similarity DESC " +
            "LIMIT $topK " +
            "RETURN " +
            "  c.text AS text, " +
            "  d.source AS source, " +
            "  c.chunkIndex AS chunkIndex, " +
            "  similarity")
    List<ChunkMatchResult> findSimilarChunks(@Param("queryEmbedding") List<Double> queryEmbedding, @Param("topK") int topK);

    /**
     * 保存一个 Chunk,并建立与 Document 的关系
     * 假设 Chunk 实体中有一个 @Relationship 的 Document 字段
     */
    @Query("CREATE (c:Chunk) " +
            "SET c = { " +
            "  chunkId: $chunkId, " +
            "  text: $text, " +
            "  chunkIndex: $chunkIndex, " +
            "  embedding: $embedding " +
            "} " +
            "WITH c " +
            "MATCH (d:Document {docId: $docId}) " +
            "CREATE (c)-[:PART_OF]->(d) " +
            "RETURN c")
    Chunk saveChunk(
            @Param("chunkId") String chunkId,
            @Param("text") String text,
            @Param("chunkIndex") Integer chunkIndex,
            @Param("embedding") List<Double> embedding,  // 注意:用 Double
            @Param("docId") String docId
    );
}

7.2 前台实现

在原来的聊天页面,添加了RAG的开关和RAG资料的管理页面


聊天页面

点击“文档管理”,进入向量文档管理页面,进行文档的管理,同时还实现了相似度查询功能


向量文档管理页面

代码仓中包含准备的数据《证券公司监督管理条例.pdf》,文档位置如下:
└── install/ (大模型服务端模块)
│   ├── ragdata/ (rag文档目录)
│   │   └── 证券公司监督管理条例.pdf(rag文档)

前台代码实现RagManage.vue

<template>
  <div id="ragmanage">
    <!-- 顶部导航栏 -->
    <div class="top-nav">
      <router-link to="/">
        <button class="back-btn">返回聊天</button>
      </router-link>
      <h1>PDF 文档上传与向量存储</h1>
    </div>

    <!-- 上传区域 -->
    <div class="upload-container">
      <input
        type="file"
        ref="fileInput"
        @change="handleFileSelect"
        accept=".pdf"
        style="display: none"
      />
      <button @click="triggerFileInput" :disabled="uploading">
        {{ uploading ? "上传中..." : "选择 PDF 文件" }}
      </button>
      <span v-if="selectedFile" class="file-name">{{ selectedFile.name }}</span>

      <button
        @click="uploadFile"
        :disabled="!selectedFile || uploading"
        class="upload-btn"
      >
        {{ uploading ? "处理中..." : "上传并处理" }}
      </button>
    </div>

    <div v-if="message" class="message" :class="messageType">
      {{ message }}
    </div>

    <!--  新增:相似度查询区域 -->
    <div class="query-container">
      <h2>语义搜索</h2>
      <input
        v-model="queryText"
        type="text"
        placeholder="输入您想搜索的语句..."
        class="query-input"
        :disabled="searching"
      />
      <button
        @click="performSearch"
        :disabled="!queryText || searching"
        class="search-btn"
      >
        {{ searching ? "搜索中..." : "相似度查询" }}
      </button>
    </div>

    <!--  新增:搜索结果显示 -->
    <div v-if="searchResults.length > 0" class="results-container">
      <h3>搜索结果</h3>
      <div
        v-for="(result, index) in searchResults"
        :key="index"
        class="result-item"
      >
        <div class="result-header">
          <strong>{{ result?.source || "未知文件" }}</strong>
          <span class="score">相似度: {{ result?.similarity.toFixed(4) }}</span>
        </div>
        <p class="result-content">{{ result.text }}</p>
      </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";

export default {
  name: "RagManage",
  data() {
    return {
      selectedFile: null,
      uploading: false,
      message: "",
      messageType: "",

      //  新增:查询相关数据
      queryText: "",
      searching: false,
      searchResults: [],
    };
  },
  methods: {
    triggerFileInput() {
      this.$refs.fileInput.click();
    },
    handleFileSelect(event) {
      const file = event.target.files[0];
      if (file && file.type === "application/pdf") {
        this.selectedFile = file;
        this.message = "";
        this.messageType = "";
      } else {
        this.selectedFile = null;
        this.message = "请选择一个有效的 PDF 文件。";
        this.messageType = "error";
      }
    },
    async uploadFile() {
      if (!this.selectedFile) return;

      const formData = new FormData();
      formData.append("file", this.selectedFile);

      this.uploading = true;
      this.message = "正在上传和处理文件...";
      this.messageType = "info";

      try {
        const response = await axios.post(
          "http://localhost:8080/upload-pdf",
          formData,
          {
            headers: {
              "Content-Type": "multipart/form-data",
            },
          }
        );

        this.message = response.data;
        this.messageType = "success";
        this.selectedFile = null;
        this.$refs.fileInput.value = "";
      } catch (error) {
        console.error("Upload error:", error);
        this.message = error.response?.data || "上传失败,请重试。";
        this.messageType = "error";
      } finally {
        this.uploading = false;
      }
    },

    //  新增:执行语义搜索
    async performSearch() {
      if (!this.queryText.trim()) return;

      this.searching = true;
      this.searchResults = [];
      this.message = "";
      this.messageType = "";

      try {
        const response = await axios.post("http://localhost:8080/search", {
          query: this.queryText.trim(),
        });

        this.searchResults = response.data;
      } catch (error) {
        console.error("Search error:", error);
        this.message = "搜索失败:" + (error.response?.data || "网络错误");
        this.messageType = "error";
      } finally {
        this.searching = false;
      }
    },
  },
};
</script>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容