整套方案开箱即用,复制代码、填写密钥、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 -y2、新建项目文件夹&目录结构
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、提前申请豆包方舟密钥(必须)
登录火山引擎方舟平台,创建推理接入点
拿到2个关键值:
ARK_API_KEY、接入点Endpoint ID(ARK_MODEL_ID)钉钉机器人按需创建(不需要钉钉可随便填写占位值)
三、全部文件完整代码(按顺序逐个新建粘贴)
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>四、完整启动步骤(按顺序执行)
进入项目目录,激活虚拟环境
cd /data/rag_doubao
source venv/bin/activate
把你的资料文件放入
docs文件夹,执行文档入库
python3 kb_loader.py
启动后端服务
python3 main_service.py
局域网其他电脑浏览器访问:
http://服务器IP:8000即可打开对话页面使用
五、高频报错&解决方案(小白必看)
Chroma启动报错:sqlite版本过低 所有python文件头部都已经写入pysqlite兼容代码,无需额外操作,安装依赖时已经装了
pysqlite3-binary。页面提问一直转圈加载 本次前后端接口已经配套修改为普通同步请求,不会再卡死;检查服务是否正常运行、方舟密钥是否填写正确。
HuggingFace模型下载缓慢 代码内置hf国内镜像地址,模型自动缓存到model_cache,第二次运行无需联网。
LangChainDeprecationWarning警告刷屏 只是旧写法提示,检索、问答功能完全正常,直接忽略即可。
局域网访问打不开页面 防火墙放行8000端口:
firewall-cmd --add-port=8000/tcp --permanent && firewall-cmd --reload
六、系统优势&后续优化方向
优势
数据安全:原始文档只保存在本地服务器,仅推送检索片段至豆包接口,文件不会泄露
低成本:向量本地免费存储,豆包按量计费,个人/小团队成本极低
双使用入口:网页可视化对话 + 钉钉群快捷问答
低上手成本:全复制粘贴部署,无复杂编译、无容器门槛
UI对标主流AI产品,深色护眼,自带加载动画、滚动美化
可自行拓展
增加网页文件上传功能,不用服务器上传文件更新知识库
恢复打字机流式输出效果
增加问答历史本地存储功能
增加账号密码登录鉴权,限制内网访问人员
回答展示引用的文档名称、页码溯源信息