Rocky Linux 从零搭建私有RAG知识库|FastAPI+豆包方舟+现代化Web界面(小白零门槛完整版)

整套方案开箱即用,复制代码、填写密钥、3步部署成功;解决Linux系统Chroma兼容报错、网页加载卡死、LangChain警告等全部坑点,自带网页对话+钉钉机器人双问答渠道,企业内网安全使用。

一、项目整体架构

本地文档(PDF/TXT/DOCX) → 文档切片+BGE向量化 → Chroma本地向量库持久化 → 用户提问相似度检索文档片段 → 带上文档上下文调用豆包方舟API → AI严格依据内部资料作答,禁止编造信息

  • 后端:FastAPI + Uvicorn

  • 向量库:Chroma

  • 向量模型:BAAI/bge-small-zh(本地缓存,一次下载永久离线)

  • 大模型:火山引擎豆包方舟(OpenAI标准格式调用)

  • 前端:原生HTML+Tailwind深色UI,无需前端编译环境

  • 系统:Rocky Linux 9.8(CentOS同源服务器系统)

二、前置准备工作

1、服务器预装依赖(root执行)

dnf install python3 python3-pip python3-virtualenv -y

2、新建项目文件夹&目录结构

mkdir -p /data/rag_doubao && cd /data/rag_doubao
# 自动创建所需文件夹
mkdir docs chroma_storage model_cache

最终文件结构(所有文件都放在rag_doubao根目录)

rag_doubao/
├── .env                # 密钥、参数配置文件(必须手动填写)
├── kb_loader.py        # 文档入库、向量化脚本
├── doubao_rag_core.py  # RAG核心检索+豆包调用逻辑
├── ding_bot.py         # 钉钉机器人消息推送
├── main_service.py     # FastAPI服务入口
├── index.html          # 网页对话前端页面
├── docs/               # 放入你的PDF、TXT、Word资料
├── chroma_storage/     # 向量数据库存储目录
├── model_cache/        # 向量模型本地缓存
└── venv/               # Python虚拟环境

3、创建虚拟环境并安装所有依赖

# 新建虚拟环境
virtualenv venv
# 激活虚拟环境
source venv/bin/activate

# 一键安装全部依赖,直接整条复制运行
pip install fastapi uvicorn langchain langchain-community openai python-dotenv chromadb pysqlite3-binary requests pdfplumber python-docx

4、提前申请豆包方舟密钥(必须)

  1. 登录火山引擎方舟平台,创建推理接入点

  2. 拿到2个关键值:ARK_API_KEY、接入点Endpoint ID(ARK_MODEL_ID)

  3. 钉钉机器人按需创建(不需要钉钉可随便填写占位值)

三、全部文件完整代码(按顺序逐个新建粘贴)

1、配置文件 .env(最重要,所有密钥写在这里)

新建文件.env,把下方内容复制进去,替换自己的密钥

# ==========豆包方舟配置==========
ARK_API_KEY=你的方舟API密钥
ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
ARK_MODEL_ID=你的接入点EndpointID
TEMPERATURE=0.1

# 向量库配置
CHROMA_DB_PATH=./chroma_storage
# 问答参数
TEMPERATURE=0.1
CHUNK_SIZE=600
CHUNK_OVERLAP=100
RETRIEVAL_TOP_K=4

2、文档入库脚本 kb_loader.py(上传文档后运行一次即可)

功能:读取docs文件夹内PDF/DOCX/TXT,自动切片、生成向量存入Chroma

__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')

import os
from dotenv import load_dotenv
# 全部更换为 community 路径
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

load_dotenv()
# 配置国内镜像下载模型
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

embedding_model = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5",
    cache_folder="./model_cache",
    model_kwargs={"trust_remote_code": True}
)

splitter = RecursiveCharacterTextSplitter(
    chunk_size=int(os.getenv("CHUNK_SIZE")),
    chunk_overlap=int(os.getenv("CHUNK_OVERLAP"))
)

def load_docs(dir_path="./docs"):
    doc_list = []
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)
    for file in os.listdir(dir_path):
        full_path = os.path.join(dir_path, file)
        try:
            if file.lower().endswith(".pdf"):
                loader = PyPDFLoader(full_path)
            elif file.lower().endswith(".docx"):
                loader = Docx2txtLoader(full_path)
            elif file.lower().endswith(".txt"):
                loader = TextLoader(full_path, encoding="utf-8")
            else:
                continue
            doc_list.extend(loader.load_and_split(text_splitter=splitter))
        except Exception as e:
            print(f"文件{file}读取失败:{str(e)}")
    return doc_list

def build_vector_db():
    docs = load_docs()
    if len(docs) == 0:
        print("docs文件夹无可用文档,跳过入库")
        return
    Chroma.from_documents(
        documents=docs,
        embedding=embedding_model,
        persist_directory=os.getenv("CHROMA_DB_PATH")
    )
    print(f"知识库构建完成,共入库切片:{len(docs)}")

if __name__ == "__main__":
    build_vector_db()

使用方法:把资料丢进docs文件夹,虚拟环境下执行

python3 kb_loader.py

3、核心RAG逻辑 doubao_rag_core.py(检索+调用大模型)

# 解决Rocky系统低版本sqlite导致Chroma启动失败
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')

import os
# HF国内镜像,防止拉取模型联网失败
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
from dotenv import load_dotenv
from openai import OpenAI
# 修正废弃的导入路径,消除黄色警告
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings

load_dotenv()

# 初始化豆包方舟客户端
client = OpenAI(
    api_key=os.getenv("ARK_API_KEY"),
    base_url=os.getenv("ARK_BASE_URL")
)

embedding = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5",
    cache_folder="./model_cache",
    model_kwargs={"trust_remote_code": True}
)

vectordb = Chroma(
    persist_directory=os.getenv("CHROMA_DB_PATH"),
    embedding_function=embedding
)
retriever = vectordb.as_retriever(search_kwargs={"k": int(os.getenv("RETRIEVAL_TOP_K"))})

def get_answer(user_question: str):
    # 检索私有资料
    related_docs = retriever.get_relevant_documents(user_question)
    context_text = "\n====文档片段====\n".join([d.page_content for d in related_docs])
    # 系统提示词,强制只能使用内部资料回答
    sys_prompt = f"""
你是企业内部知识库问答助手,严格依据下方【内部参考文档】回答员工问题。
禁止编造文档不存在的信息,无匹配资料直接回复:【暂无该问题对应的内部资料】
回答简洁准确,不要多余话术。
【内部参考文档】
{context_text}
"""
    resp = client.chat.completions.create(
        model=os.getenv("ARK_MODEL_ID"),
        messages=[
            {"role": "system", "content": sys_prompt},
            {"role": "user", "content": user_question}
        ],
        temperature=float(os.getenv("TEMPERATURE"))
    )
    return resp.choices[0].message.content

4、钉钉机器人文件 ding_bot.py(不需要钉钉也保留文件,不然服务报错)

__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')

import os
import time
import hmac
import hashlib
import base64
import requests
from dotenv import load_dotenv

load_dotenv()
RAG_API_URL = "http://127.0.0.1:8000/api/qa"
DING_WEBHOOK = os.getenv("DING_WEBHOOK")
DING_SECRET = os.getenv("DING_SECRET")

def ding_sign(timestamp: str):
    secret = DING_SECRET.encode("utf-8")
    sign_str = f"{timestamp}\n{DING_SECRET}".encode("utf-8")
    hmac_obj = hmac.new(secret, sign_str, digestmod=hashlib.sha256)
    sign_data = base64.b64encode(hmac_obj.digest()).decode("utf-8")
    return sign_data

def get_rag_answer(question: str):
    resp = requests.post(RAG_API_URL, json={"q": question}, timeout=25)
    return resp.json()["answer"]

def send_ding_msg(content: str):
    timestamp = str(round(time.time() * 1000))
    sign = ding_sign(timestamp)
    headers = {"Content-Type": "application/json"}
    body = {
        "msgtype": "markdown",
        "markdown": {
            "title": "知识库问答回复",
            "text": content
        },
        "timestamp": timestamp,
        "sign": sign
    }
    requests.post(DING_WEBHOOK, json=body, headers=headers)

5、服务入口 main_service.py(FastAPI后端接口)

# 顶部sqlite兼容代码
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')

from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, StreamingResponse
import uvicorn
import threading
import json
from doubao_rag_core import get_answer
from ding_bot import get_rag_answer, send_ding_msg

app = FastAPI(title="豆包私有知识库API")

# 跨域放行
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 1、原有普通问答接口(钉钉调用使用)
@app.post("/api/qa")
async def api_qa(q: str):
    return {"code": 200, "question": q, "answer": get_answer(q)}

# 2、流式问答接口(前端页面打字机输出效果)
@app.post("/api/stream/qa")
async def stream_qa(req: Request):
    data = await req.json()
    question = data.get("question", "").strip()
    full_ans = get_answer(question)
    return {"code":200,"data":full_ans}

    def stream_generator():
        for char in full_ans:
            yield json.dumps({"data": char}) + "\n"

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

# 钉钉回调接口保持不变
@app.post("/ding/callback")
async def ding_callback(req: Request):
    data = await req.json()
    msg_type = data.get("Msgtype")
    if msg_type != "text":
        return {"msg": "ignore"}
    question = data["text"]["content"].strip()
    if not question:
        return {"msg": "empty"}
    ans = get_rag_answer(question)
    md_text = f"""
**用户提问:**
{question}

**知识库回复:**
{ans}
"""
    send_ding_msg(md_text)
    return {"msg": "success"}

# 访问首页自动加载AI对话页面
@app.get("/")
async def index():
    return FileResponse("index.html")

# 后台运行uvicorn,不再启动streamlit
def start_api():
    uvicorn.run(app, host="0.0.0.0", port=8000)

if __name__ == "__main__":
    start_api()

6、前端页面 index.html(深色AI对话界面,样式完好、请求适配后端)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>企业私有知识库 | 字节豆包RAG</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#165DFF',
                        darkBg: '#17171a',
                        sideBg: '#232329',
                        chatBg: '#2c2c34',
                        textGray: '#a1a1aa'
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif']
                    }
                }
            }
        }
    </script>
    <style>
        .message-box::-webkit-scrollbar {width: 4px}
        .message-box::-webkit-scrollbar-thumb {background:#444;border-radius:4px}
        .input-area::-webkit-scrollbar {width:4px}
        .typing-dot {animation: blink 1.4s infinite both;}
        @keyframes blink {0%,80%,100%{opacity:0.3}40%{opacity:1}}
    </style>
</head>
<body class="bg-darkBg text-white h-screen flex overflow-hidden">
    <!-- 左侧侧边栏 -->
    <aside class="w-60 bg-sideBg flex flex-col shrink-0">
        <div class="px-4 py-5 border-b border-gray-700">
            <div class="flex items-center gap-2">
                <div class="w-8 h-8 rounded-md bg-primary flex items-center justify-center">
                    <i class="fa fa-book text-white"></i>
                </div>
                <span class="font-bold text-lg">私有知识库助手</span>
            </div>
            <p class="text-textGray text-xs mt-1">基于字节豆包 · 本地RAG</p>
        </div>

        <div class="flex-1 p-3 overflow-auto">
            <div class="text-textGray text-xs mb-2 px-2">对话记录</div>
            <div id="historyList" class="space-y-1"></div>
        </div>

        <div class="p-3 border-t border-gray-700">
            <div class="text-textGray text-xs mb-2 px-2">系统状态</div>
            <div class="text-sm px-2 py-2 rounded bg-chatBg text-textGray">
                <div class="flex items-center gap-2"><i class="fa fa-check text-green-400"></i>向量库正常</div>
                <div class="flex items-center gap-2 mt-1"><i class="fa fa-check text-green-400"></i>豆包API已连通</div>
            </div>
        </div>
    </aside>

    <!-- 主对话区域 -->
    <main class="flex-1 flex flex-col">
        <!-- 顶部导航 -->
        <header class="h-14 border-b border-gray-700 flex items-center px-6 justify-between">
            <h2 class="font-medium">智能问答对话</h2>
            <div class="flex gap-3">
                <button onclick="clearChat()" class="px-3 py-1 rounded bg-chatBg hover:bg-gray-600 text-sm">
                    <i class="fa fa-trash-o mr-1"></i>清空对话
                </button>
                <button onclick="refreshVector()" class="px-3 py-1 rounded bg-primary hover:bg-primary/80 text-sm">
                    <i class="fa fa-refresh mr-1"></i>刷新知识库
                </button>
            </div>
        </header>

        <!-- 聊天消息容器 -->
        <div id="chatBox" class="message-box flex-1 overflow-y-auto p-6 space-y-6 bg-[#1c1c22]">
            <div class="flex gap-3">
                <div class="w-9 h-9 rounded-md bg-primary shrink-0 flex items-center justify-center">
                    <i class="fa fa-robot"></i>
                </div>
                <div class="bg-chatBg rounded-lg px-4 py-3 max-w-[75%]">
                    <p>你好,我是本地知识库问答助手。<br>已加载本地PDF/DOCX/TXT文档资料,你的提问只会检索内部文档资料进行回答。</p>
                </div>
            </div>
        </div>

        <!-- 底部输入框 -->
        <div class="p-4 border-t border-gray-700">
            <div class="flex items-end gap-3 max-w-[90%] mx-auto">
                <textarea id="userInput" rows="3" placeholder="输入你的问题,回车发送..."
                    class="input-area flex-1 bg-chatBg rounded-lg px-4 py-3 outline-none resize-none border border-gray-600 focus:border-primary"></textarea>
                <button id="sendBtn" onclick="sendQuestion()" class="bg-primary hover:bg-primary/80 rounded-lg h-[74px] w-12 flex items-center justify-center">
                    <i class="fa fa-paper-plane"></i>
                </button>
            </div>
            <div class="text-textGray text-xs text-center mt-2">数据全部本地向量存储,不会上传你的文档</div>
        </div>
    </main>

    <script>
        const chatBox = document.getElementById('chatBox')
        const userInput = document.getElementById('userInput')
        const sendBtn = document.getElementById('sendBtn')
        let loading = false

        // 发送问题
        async function sendQuestion() {
            const question = userInput.value.trim()
            if (!question || loading) return
            userInput.value = ""

            // 渲染用户消息
            chatBox.innerHTML += `
                <div class="flex gap-3 justify-end">
                    <div class="bg-primary/80 rounded-lg px-4 py-3 max-w-[75%]">${question}</div>
                    <div class="w-9 h-9 rounded-md bg-gray-500 shrink-0 flex items-center justify-center">
                        <i class="fa fa-user"></i>
                    </div>
                </div>
            `
            // 机器人加载动画
            const respWrap = document.createElement('div')
            respWrap.className = "flex gap-3 resp-item"
            respWrap.innerHTML = `
                <div class="w-9 h-9 rounded-md bg-primary shrink-0 flex items-center justify-center">
                    <i class="fa fa-robot"></i>
                </div>
                <div class="bg-chatBg rounded-lg px-4 py-3 max-w-[75%]">
                    <span class="typing-dot">●</span>
                    <span class="typing-dot" style="animation-delay:0.2s">●</span>
                    <span class="typing-dot" style="animation-delay:0.4s">●</span>
                </div>
            `
            chatBox.appendChild(respWrap)
            chatBox.scrollTop = chatBox.scrollHeight
            loading = true
            sendBtn.disabled = true
            const resBox = respWrap.querySelector("div:nth-child(2)")

            try {
                // 普通POST请求,不再使用SSE流式读取
                const res = await fetch("/api/stream/qa", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ question: question })
                })
                const result = await res.json()
                resBox.innerText = result.data
            } catch (err) {
                resBox.innerText = "请求异常,请检查方舟密钥、网络与.env配置"
                console.error(err)
            }

            loading = false
            sendBtn.disabled = false
        }

        // 清空对话
        function clearChat() {
            chatBox.innerHTML = `
                <div class="flex gap-3">
                    <div class="w-9 h-9 rounded-md bg-primary shrink-0 flex items-center justify-center">
                        <i class="fa fa-robot"></i>
                    </div>
                    <div class="bg-chatBg rounded-lg px-4 py-3 max-w-[75%]">
                        <p>对话已清空,请重新提问</p>
                    </div>
                </div>
            `
        }

        // 重新执行入库脚本
        async function refreshVector() {
            alert("如需更新知识库,请在服务器执行:python3 kb_loader.py")
        }

        // 回车发送
        userInput.addEventListener("keydown", (e) => {
            if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault()
                sendQuestion()
            }
        })
    </script>
</body>
</html>

四、完整启动步骤(按顺序执行)

  1. 进入项目目录,激活虚拟环境

cd /data/rag_doubao
source venv/bin/activate
  1. 把你的资料文件放入docs文件夹,执行文档入库

python3 kb_loader.py
  1. 启动后端服务

python3 main_service.py
  1. 局域网其他电脑浏览器访问:http://服务器IP:8000 即可打开对话页面使用

五、高频报错&解决方案(小白必看)

  1. Chroma启动报错:sqlite版本过低 所有python文件头部都已经写入pysqlite兼容代码,无需额外操作,安装依赖时已经装了pysqlite3-binary

  2. 页面提问一直转圈加载 本次前后端接口已经配套修改为普通同步请求,不会再卡死;检查服务是否正常运行、方舟密钥是否填写正确。

  3. HuggingFace模型下载缓慢 代码内置hf国内镜像地址,模型自动缓存到model_cache,第二次运行无需联网。

  4. LangChainDeprecationWarning警告刷屏 只是旧写法提示,检索、问答功能完全正常,直接忽略即可。

  5. 局域网访问打不开页面 防火墙放行8000端口:firewall-cmd --add-port=8000/tcp --permanent && firewall-cmd --reload

六、系统优势&后续优化方向

优势

  1. 数据安全:原始文档只保存在本地服务器,仅推送检索片段至豆包接口,文件不会泄露

  2. 低成本:向量本地免费存储,豆包按量计费,个人/小团队成本极低

  3. 双使用入口:网页可视化对话 + 钉钉群快捷问答

  4. 低上手成本:全复制粘贴部署,无复杂编译、无容器门槛

  5. UI对标主流AI产品,深色护眼,自带加载动画、滚动美化

可自行拓展

  1. 增加网页文件上传功能,不用服务器上传文件更新知识库

  2. 恢复打字机流式输出效果

  3. 增加问答历史本地存储功能

  4. 增加账号密码登录鉴权,限制内网访问人员

  5. 回答展示引用的文档名称、页码溯源信息

面试有感:我决定构建一个真正懂 IT 的 AI 问答机器人 2026-07-21
Rocky Linux 私有 RAG 知识库进阶|LDAP 域控登录适配 + 企业级权限落地(无缝升级完整版) 2026-07-30