1、RAG的原理
RAG 检索增强生成(Retrieval Augmented Generation),通俗一点讲就是向LLM 提问一个问题(qustion),从向量数据库中检索相关的信息,并将检索到的信息和问题(answer)一起来构建LLM的提示词(Prompt),LLM 最后生成答案的过程。下面是一个简单的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模型启动

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

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

提问内容:
证券公司从事证券自营业务不得有哪些行为?

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

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

#通过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>