一、项目介绍

1、概述

  • 本项目基于 ChromaDB 构建向量数据库,实现一个完整的检索增强生成(RAG, Retrieval-Augmented Generation)系统,用于提升大语言模型在私有知识库问答场景中的准确性与可控性
  • 项目核心流程包括:文档加载与清洗、文本分块(Chunking)、向量化嵌入(Embedding)、向量存储与索引构建,以及基于语义相似度的检索模块。在用户提问时,系统首先从 ChromaDB 中检索最相关的知识片段,再将其与问题一起输入大语言模型,从而生成基于上下文的高质量回答
  • 在模型层面,模型选择千问模型,引入 RAG 架构,有效缓解大模型“幻觉”问题,提高回答准确性与可追溯性
  • 项目最终实现了一个可用于知识库问答、企业文档检索与智能客服的轻量级 RAG 系统

2、技术栈

  • Python 基础语法
  • Pytorch 深度学习库
  • Transformer 架构
  • Langchain 框架
  • ChromaDB 向量数据库
  • FastAPI 服务器框架
  • Vue-cli 脚手架
  • MySQL 数据库

二、项目开发

1、知识库搭建

  • 知识库的搭建非常重要,知识库的质量直接影响 RAG 系统检索效果
  • 知识库的搭建属于离线行为,即在我们构建 RAG 系统的客户端和服务器之前,就应该把知识库构建好
  • 法律数据集.csv数据集为案例演示:
  • 数据集的解析和存入:
import os

import pandas as pd
from langchain_community.vectorstores import Chroma
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings

# 读取数据
df = pd.read_csv(r"./datasets/法律数据集.csv")
# 数据集提取
data = df['text'].tolist()
# 把分块后的文本转为Document对象
documents = [Document(page_content=item) for item in data]
# 向量模型
embedding_model = HuggingFaceEmbeddings(
    # 模型名称
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    # 缓存目录
    cache_folder=r"F:\workspace\AI\langchain_project\embedding_model"
)
# 持久化目录
persist_directory = "./low_db"
if os.path.exists(persist_directory):
    vectordb = Chroma(
        # 持久化目录
        persist_directory=persist_directory,
        # 集合名称
        collection_name="low",
        # 向量模型
        embedding_function=embedding_model
    )
    print("Chroma 向量数据库已加载")
else:
    vectordb = Chroma.from_documents(
        # 文档
        documents=documents,
        # 向量模型
        embedding=embedding_model,
        # 持久化目录
        persist_directory=persist_directory,
        # 集合名称
        collection_name="low",
        # 采用余弦相似度(cosine)作为向量距离度量方式来进行相似度搜索
        collection_metadata={"hnsw:space": "cosine"}
    )
    print("Chroma 向量数据库已生成并保存")
  • 测试:
import os
from langchain_chroma import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI

embedding_model = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
    cache_folder=r"F:\workspace\AI\langchain_project\embedding_model"
)

persist_directory = "./low_db"

vectordb = Chroma(
    persist_directory=persist_directory,
    collection_name="low",
    embedding_function=embedding_model
)
# 检索器,检索3个最相关的文档
retriever = vectordb.as_retriever(search_kwargs={"k": 3})

# 提示模板
template = """
你是一个知识问答助手。
根据以下检索到的上下文回答用户问题,
如果答案不在上下文中,请说明“资料中未提及”。
如果答案在上线文中,不需要输出答案来自于上下文内容,只需要输出最终的结果。

上下文:
{context}

问题:
{question}
"""
prompt = PromptTemplate(input_variables=["context", "question"], template=template)

llm = ChatOpenAI(
    model="qwen3.5-35b-a3b",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

rag_chain = (
        RunnableParallel({
            "context": retriever,
            "question": RunnablePassthrough()
        })
        | prompt
        | llm
        | StrOutputParser()
)

query = "公安机关接到家庭暴力报案后应当做什么事情?"
answer = rag_chain.invoke(query)
print("\n最终回答:\n", answer)

2、服务器开发

2.1 搭建服务器项目

  • 创建一个 FastAPI 项目

2.2 聊天接口配置

  • 创建一个聊天接口子路由,功能代码先不写,先实现模板构建
  • 在项目根目录下创建api/controller文件夹,然后创建文件ChatController.py
from fastapi import APIRouter, Request


# 创建路由对象
chat_router = APIRouter()


# 聊天接口
@chat_router.get("/chat")
def chat(request: Request,question: str):
    pass
  • main.py 中配置跨域、注册子路由、配置启动
import uvicorn
from fastapi import FastAPI
from api.controller.ChatController import chat_router
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 跨域
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://127.0.0.1:8000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册子路由
app.include_router(chat_router)

# 启动服务
if __name__ == '__main__':
    uvicorn.run(
        app="main:app",
        host="127.0.0.1",
        port=8080,
        reload=True,
    )

2.3 工具封装

  • 在项目根目录下创建api/utils文件夹,然后创建文件ChromaDBUtil.py,封装一个获取 ChromaDB 连接的工具
from langchain_chroma import Chroma
from langchain_huggingface import HuggingFaceEmbeddings

def get_chromadb_conn(persist_directory: str):
    # 向量模型
    embedding_model = HuggingFaceEmbeddings(
        model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
        cache_folder=r"F:\workspace\AI\langchain_project\embedding_model"
    )
    return Chroma(
        # 持久化目录
        persist_directory=persist_directory,
        # 集合名称
        collection_name="low",
        # 向量模型
        embedding_function=embedding_model
    )
  • 在项目根目录下创建api/utils文件夹,然后创建文件MyModel.py,封装一个加载模型的工具
import os

from langchain_openai import ChatOpenAI


def load_model():
    return ChatOpenAI(
        model="qwen3.5-35b-a3b",
        api_key=os.getenv("DASHSCOPE_API_KEY"),
        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    )

2.4 聊天接口实现

  • 主要采取 MVC 分层思想,在业务逻辑层完成对应的聊天功能代码处理

  • 在项目根目录下创建api/service文件夹,然后创建文件ChatService.py

from api.utils.ChromaDBUtil import get_chromadb_conn
from api.utils.MyModel import load_model

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough


def chat(question: str):
    # 获取向量数据库连接
    vectordb = get_chromadb_conn(r"F:\workspace\AI\chromadb_rag\feifan_rag_server\create_data\low_db")

    # 检索器
    retriever = vectordb.as_retriever(search_kwargs={"k": 10})

    # 提示词模板
    template = """
    你是一个智能问答助手。

    请先判断用户问题是否属于法律相关问题。

    【处理规则】

    1. 如果问题属于法律、法规、合同、劳动纠纷、侵权、婚姻、债务、刑事、行政等法律领域问题:
       - 请优先结合提供的上下文内容进行回答
       - 回答必须基于上下文信息
       - 如果上下文中未提及相关内容,请明确回复:
         “资料中未提及相关内容”

    2. 如果问题不属于法律领域问题:
       - 忽略上下文内容
       - 直接基于你的自身知识正常回答用户问题
       - 不要提及上下文或知识库

    【上下文】
    {context}

    【用户问题】
    {question}

    请直接输出最终答案。
    """

    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=template
    )

    # 加载模型
    llm = load_model()

    # RAG执行链
    rag_chain = (
            RunnableParallel({
                "context": retriever,
                "question": RunnablePassthrough()
            })
            | prompt
            | llm
            | StrOutputParser()
    )

    # 流式输出
    for chunk in rag_chain.stream(question):
        yield chunk
  • 修改api/controller/ChatController.py代码
from fastapi import APIRouter, Request
from starlette.responses import StreamingResponse

from api.service import ChatService

# 创建路由对象
chat_router = APIRouter()


# 聊天接口
@chat_router.get("/chat")
def chat(request: Request, question: str):
    return StreamingResponse(content=ChatService.chat(question), media_type="text/event-stream")

2.5 测试

image-20260515150522397

2.6 优化

  • 在 RAG 检索后,加入 rerank,提高检索结果的质量
  • 为了方便客户端解析,后端修改一下
  • 修改api/service/ChatService.py中的 chat 函数的返回值
return rag_chain.invoke(question)
  • 修改api/controller/ChatController.py代码
import json

from fastapi import APIRouter, Request
from starlette.responses import StreamingResponse

from api.service import ChatService

# 创建路由对象
chat_router = APIRouter()


# 聊天接口
@chat_router.get("/chat")
def chat(request: Request, question: str):
    result = ChatService.chat(question)

    def streaming_content():
        for i in result:
            yield f"data: {json.dumps({'content': i})}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(content=streaming_content(), media_type="text/event-stream")

3、客户端开发

3.1 搭建客户端项目

  • 创建以 vue-cli 项目
  • 下载以下库
"axios": "^1.3.3",
"dompurify": "^3.3.3",
"element-ui": "^2.15.14",
"marked": "^3.0.8",

3.2 对话页面设计

  • 套模板或者 AI 基于 element-ui 生成一个
<template>
  <div id="chat-container">

    <!-- 消息区 -->
    <div id="messages">

      <div
        v-for="(msg, index) in messages"
        :key="index"
        class="message"
        :class="msg.role"
      >

        <div class="avatar">
          {{ msg.role === 'assistant' ? '🤖' : '👤' }}
        </div>

        <div class="content">
          <div
            class="text-message"
            v-html="$renderMarkdown(msg.content)">
          </div>
        </div>

      </div>

    </div>

    <!-- 输入区 -->
    <div id="input-area">

      <div class="input-wrapper">

        <el-input
          type="textarea"
          v-model="input"
          :rows="2"
          placeholder="请输入你的问题..."
          @keydown.enter.native.prevent="sendMessage"
        >
        </el-input>

        <el-button
          type="primary"
          @click="sendMessage">
          发送
        </el-button>

      </div>

    </div>

  </div>
</template>

<script>
import Vue from "vue";

export default {
  name: "Index",

  data() {
    return {

      serverUrl: "http://127.0.0.1:8080/",

      input: "",

      messages: [
        {
          role: "assistant",
          content: "你好 👋,我是 AI 助手"
        }
      ]
    };
  },

  methods: {

    // 滚动到底部
    scrollToBottom() {

      this.$nextTick(() => {

        const box = document.getElementById("messages");

        box.scrollTop = box.scrollHeight;
      });
    },

    // 发送消息
    sendMessage() {

      if (this.input.trim() === "") return;

      const userMsg = this.input.trim();

      this.input = "";

      // 用户消息
      this.messages.push({
        role: "user",
        content: userMsg
      });

      // AI占位消息
      this.messages.push({
        role: "assistant",
        content: "AI 正在思考..."
      });

      this.scrollToBottom();

      const index = this.messages.length - 1;
      let aiContent = "";
      let finished = false;

      // SSE请求
      const params = new URLSearchParams({
        question: userMsg
      });

      const es = new EventSource(
        this.serverUrl + `chat?${params}`
      );
      let lastContent = "";
      es.onmessage = e => {
        if (e.data === "[DONE]") {
          finished = true
          es.close()
          return
        }

        let data = JSON.parse(e.data)

        if (data.content == "\n" && lastContent == "\n") {
          return
        }

        aiContent += data.content;
        Vue.set(this.messages, index, {
          "role": "assistant",
          "type": "text",
          "content": aiContent
        })

        this.$nextTick(() => {
          this.scrollToBottom()
        })
        lastContent = data.content;
      }

      es.onerror = () => {
        if (finished) return;
        es.close();
        Vue.set(this.messages, index, {
          role: "assistant",
          content: aiContent || "连接中断,请重试"
        });
      };
    }
  }
};
</script>

<style scoped>
/* ===== 页面 ===== */
body {
  margin: 0;
  background: #f5f7fa;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
}

#chat-container {
  width: 900px;
  max-width: 95%;
  height: 90vh;
  margin: 5vh auto;
  background: white;
  border-radius: 16px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}

/* ===== 消息区 ===== */

#messages {
  flex: 1;
  overflow-y: auto;
  padding: 24px;
  background: #f7f8fa;
}

/* ===== 消息 ===== */

.message {
  display: flex;
  gap: 10px;
  max-width: 75%;
  margin-bottom: 18px;
  padding: 12px 16px;
  border-radius: 14px;
  line-height: 1.6;
  font-size: 14px;
}

.message.user {
  margin-left: auto;
  background: #409EFF;
  color: white;
  flex-direction: row-reverse;
}

.message.assistant {
  background: white;
  border: 1px solid #ebeef5;
}

/* ===== 头像 ===== */

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: white;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* ===== 内容 ===== */

.content {
  flex: 1;
}

.text-message {
  white-space: pre-wrap;
  word-break: break-word;
}

/* ===== 输入区 ===== */

#input-area {
  padding: 16px;
  border-top: 1px solid #ebeef5;
  background: white;
}

.input-wrapper {
  display: flex;
  align-items: flex-end;
}

.el-input {
  flex: 1;
  margin-right: 10px;
}

.el-button {
  height: 52px;
}

</style>

3.3 测试

image-20260515162446293