从零到一:基于大模型的知识库智能问答系统完整实现
·
本文详细介绍如何使用 Vue3 + Flask + 智谱AI 构建一个生产级的知识库问答系统。涵盖前后端分离架构、Prompt 工程、JWT 鉴权等核心技术,附完整源码。
目录
- 项目背景与架构设计
- 核心技术栈分析
- 系统架构与数据库设计
- 前端实现:Vue3 + Pinia 状态管理
- 后端实现:Flask RESTful API
- AI 问答模块:Prompt 工程实践
- 部署与性能优化
- 常见问题与解决方案
一、项目背景与架构设计
1.1 问题陈述
大语言模型(LLM)虽然知识丰富,但存在两个核心问题:
- 幻觉问题:模型在不知道答案时会自信地编造信息
- 隐私问题:企业私有文档不能上传到公共 API
本项目通过 Prompt 工程 和 知识库注入 解决这两个问题,实现基于私有文档的精准问答。
1.2 系统架构
采用经典的前后端分离 B/S 架构:
┌─────────────────────────────────────────┐
│ 浏览器(Vue3 SPA) │
│ - 用户界面 │
│ - 状态管理(Pinia) │
│ - 路由管理(Vue Router) │
└──────────────┬──────────────────────────┘
│ HTTP/JSON
┌──────────────▼──────────────────────────┐
│ Flask RESTful API 服务器 │
│ - 用户认证(JWT) │
│ - 知识库管理 │
│ - 问答业务逻辑 │
└──────┬───────────────────┬──────────────┘
│ │
┌──────▼──────┐ ┌───────▼────────┐
│ SQLite DB │ │ 智谱 AI API │
│ - 用户表 │ │ - GLM-4-Flash │
│ - 知识库表 │ │ - Prompt 工程 │
│ - 历史表 │ │ - 流式响应 │
└─────────────┘ └────────────────┘
1.3 系统截图


二、核心技术栈分析
2.1 前端技术栈
| 技术 | 版本 | 选择理由 |
|---|---|---|
| Vue 3 | 3.4 | Composition API 更灵活,性能更好 |
| Vite | 5.3 | 开发时热更新快,构建速度快 |
| Pinia | 2.1 | Vue3 官方推荐,API 简洁 |
| Vue Router | 4.3 | 原生路由守卫,支持动态路由 |
| Axios | 1.7 | 请求拦截器,统一错误处理 |
关键特性:
- Composition API 组织代码更清晰
- 响应式系统自动追踪依赖
- 虚拟 DOM 高效更新
2.2 后端技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Flask | 3.0 | 轻量级 Web 框架 |
| SQLAlchemy | 2.0 | ORM,数据库操作 |
| Flask-JWT-Extended | 4.6 | JWT 无状态鉴权 |
| ZhipuAI SDK | 2.1 | 大模型 API 调用 |
为什么选 Flask 而不是 Django:
- Django 过重,包含不需要的功能(模板、Admin)
- Flask 轻量,按需扩展
- 前后端分离场景下,只需要 API 层
2.3 AI 模型选择
使用智谱 AI 的 GLM-4-Flash 模型:
优势:
- 免费额度充足(注册即送)
- 响应速度快(适合实时问答)
- 支持 Prompt 工程
- 中文理解能力强
成本对比:
- OpenAI GPT-4:$0.03/1K tokens
- 智谱 GLM-4-Flash:免费(有额度限制)
- 本地 Llama 2:需要 GPU,部署复杂
三、系统架构与数据库设计
3.1 数据库 E-R 图
┌─────────────┐ ┌──────────────────┐
│ users │ │ knowledge_bases │
├─────────────┤ ├──────────────────┤
│ id (PK) │────┐ │ id (PK) │
│ username │ │ │ user_id (FK) │
│ password │ │ │ name │
│ created_at │ │ │ file_path │
└─────────────┘ │ │ char_count │
│ │ created_at │
│ └──────────────────┘
│ │
│ │ (1:N)
│ │
│ ┌──────▼──────────┐
│ │ chat_histories │
│ ├─────────────────┤
│ │ id (PK) │
│ │ user_id (FK) │
│ │ kb_id (FK) │
│ │ question │
│ │ answer │
│ │ tokens_used │
│ │ created_at │
│ └─────────────────┘
│
└─────────────────────┘
3.2 表结构详解
users 表:存储用户信息
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(256) NOT NULL, -- Werkzeug 加盐哈希
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
knowledge_bases 表:存储知识库元信息
CREATE TABLE knowledge_bases (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL FOREIGN KEY,
name VARCHAR(128) NOT NULL, -- 用户友好名称
filename VARCHAR(256) NOT NULL, -- UUID 化文件名
file_path VARCHAR(512) NOT NULL, -- 完整路径
file_size INTEGER, -- 字节数
char_count INTEGER, -- 字符数
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
chat_histories 表:存储问答记录
CREATE TABLE chat_histories (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL FOREIGN KEY,
kb_id INTEGER NOT NULL FOREIGN KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
tokens_used INTEGER, -- 消耗的 token 数
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
3.3 索引优化
# SQLAlchemy 中定义索引
class ChatHistory(db.Model):
__tablename__ = 'chat_histories'
# 复合索引:加速按用户和知识库的查询
__table_args__ = (
Index('idx_user_kb', 'user_id', 'kb_id'),
Index('idx_created_at', 'created_at'),
)
四、前端实现:Vue3 + Pinia 状态管理
4.1 Composition API 最佳实践
// src/views/ChatView.vue
import { ref, computed, watch, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { sendChat } from '@/api/chat'
export default {
setup() {
const route = useRoute()
const auth = useAuthStore()
// 响应式状态
const messages = ref([])
const inputText = ref('')
const thinking = ref(false)
const selectedKbId = ref('')
// 计算属性:判断是否可以发送
const canSend = computed(() =>
selectedKbId.value &&
inputText.value.trim().length > 0 &&
inputText.value.length <= 1000 &&
!thinking.value
)
// 方法:发送消息
async function handleSend() {
if (!canSend.value) return
const question = inputText.value.trim()
inputText.value = ''
// 添加用户消息到 UI
messages.value.push({
id: Date.now(),
role: 'user',
content: question,
time: new Date(),
})
scrollToBottom()
thinking.value = true
try {
// 调用后端 API
const res = await sendChat(selectedKbId.value, question)
// 添加 AI 回答
messages.value.push({
id: Date.now() + 1,
role: 'ai',
content: res.data.answer,
tokens: res.data.tokens_used,
time: new Date(),
})
} catch (e) {
// 错误处理
messages.value.push({
id: Date.now() + 1,
role: 'ai',
content: `错误:${e.message}`,
time: new Date(),
})
} finally {
thinking.value = false
await nextTick()
scrollToBottom()
}
}
// 监听知识库切换,清空对话
watch(selectedKbId, () => {
messages.value = []
})
return {
messages,
inputText,
thinking,
selectedKbId,
canSend,
handleSend,
}
}
}
4.2 Pinia 状态管理
// src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi } from '@/api/auth'
export const useAuthStore = defineStore('auth', () => {
// 状态
const token = ref(localStorage.getItem('token') || '')
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
// 计算属性
const isLoggedIn = computed(() => !!token.value)
// 方法
async function login(username, password) {
const res = await loginApi(username, password)
token.value = res.data.token
user.value = res.data.user
// 持久化到 localStorage
localStorage.setItem('token', res.data.token)
localStorage.setItem('user', JSON.stringify(res.data.user))
}
function logout() {
token.value = ''
user.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
return { token, user, isLoggedIn, login, logout }
})
4.3 Axios 拦截器
// src/api/request.js
import axios from 'axios'
const request = axios.create({
baseURL: '/api',
timeout: 60000,
})
// 请求拦截:自动注入 JWT Token
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截:统一错误处理
request.interceptors.response.use(
(response) => response.data,
(error) => {
// 401 未授权,跳转登录
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
const msg = error.response?.data?.msg || error.message
return Promise.reject(new Error(msg))
}
)
export default request
4.4 路由守卫
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: () => import('@/views/LoginView.vue'),
meta: { public: true }, // 公开路由
},
{
path: '/',
component: () => import('@/views/LayoutView.vue'),
children: [
{
path: 'kb',
component: () => import('@/views/KbView.vue'),
},
{
path: 'chat',
component: () => import('@/views/ChatView.vue'),
},
],
},
],
})
// 路由守卫:检查登录状态
router.beforeEach((to) => {
const auth = useAuthStore()
// 公开路由直接通过
if (to.meta.public) return true
// 需要登录的路由,检查 token
if (!auth.isLoggedIn) {
return { name: 'Login' }
}
})
export default router
五、后端实现:Flask RESTful API
5.1 JWT 鉴权机制
# app.py
from flask_jwt_extended import JWTManager, create_access_token, jwt_required
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY')
jwt = JWTManager(app)
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
# 查询用户
user = User.query.filter_by(username=username).first()
# 验证密码(Werkzeug 加盐哈希)
if not user or not check_password_hash(user.password_hash, password):
return {'code': 401, 'msg': '用户名或密码错误'}, 401
# 生成 JWT Token
token = create_access_token(identity=str(user.id))
return {
'code': 200,
'msg': '登录成功',
'data': {
'token': token,
'user': user.to_dict(),
}
}
# 保护的接口
@app.route('/api/kb', methods=['GET'])
@jwt_required()
def list_kb():
user_id = get_jwt_identity()
kbs = KnowledgeBase.query.filter_by(user_id=user_id).all()
return {
'code': 200,
'data': [kb.to_dict() for kb in kbs]
}
5.2 文件上传安全处理
from werkzeug.utils import secure_filename
import uuid
@app.route('/api/kb/upload', methods=['POST'])
@jwt_required()
def upload_kb():
user_id = get_jwt_identity()
if 'file' not in request.files:
return {'code': 400, 'msg': '未找到文件'}, 400
file = request.files['file']
# 1. 文件类型校验
if not file.filename.endswith('.txt'):
return {'code': 400, 'msg': '只支持 .txt 格式'}, 400
# 2. 文件名安全处理
safe_name = secure_filename(file.filename)
# 3. UUID 重命名,防止覆盖和路径注入
unique_filename = f"{uuid.uuid4().hex}_{safe_name}"
file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
# 4. 保存文件
file.save(file_path)
# 5. 统计文件信息
file_size = os.path.getsize(file_path)
with open(file_path, 'r', encoding='utf-8') as f:
char_count = len(f.read())
# 6. 保存到数据库
kb = KnowledgeBase(
user_id=user_id,
name=os.path.splitext(safe_name)[0],
filename=unique_filename,
file_path=file_path,
file_size=file_size,
char_count=char_count,
)
db.session.add(kb)
db.session.commit()
return {
'code': 201,
'msg': '上传成功',
'data': kb.to_dict()
}
5.3 统一响应格式
def success(data=None, msg='success', code=200):
"""统一成功响应"""
resp = {'code': code, 'msg': msg}
if data is not None:
resp['data'] = data
return jsonify(resp), code
def fail(msg='error', code=400):
"""统一失败响应"""
return jsonify({'code': code, 'msg': msg}), code
# 使用示例
@app.route('/api/kb/<int:kb_id>', methods=['DELETE'])
@jwt_required()
def delete_kb(kb_id):
user_id = get_jwt_identity()
kb = KnowledgeBase.query.filter_by(id=kb_id, user_id=user_id).first()
if not kb:
return fail('知识库不存在', 404)
# 删除文件
if os.path.exists(kb.file_path):
os.remove(kb.file_path)
# 删除数据库记录
db.session.delete(kb)
db.session.commit()
return success(msg='删除成功')
六、AI 问答模块:Prompt 工程实践
6.1 Prompt 工程核心
这是系统的灵魂所在。通过精心设计 System Prompt,约束模型的行为。
# ai_service.py
def build_system_prompt(kb_content: str) -> str:
"""构建系统 Prompt,将知识库内容注入"""
return f"""你是一个专业的知识库问答助手。
请严格根据以下知识库内容回答用户的问题。
规则:
1. 只根据知识库内容作答,不要编造知识库中没有的信息。
2. 如果知识库中没有相关内容,请明确告知用户"知识库中未找到相关信息"。
3. 回答要简洁、准确、有条理。
4. 使用中文回答。
========== 知识库内容 ==========
{kb_content}
================================
"""
6.2 处理流程
def ask_question(file_path: str, question: str) -> dict:
"""
完整的问答流程
"""
try:
# 1. 读取知识库文件
kb_content = load_knowledge_base(file_path)
# 2. 构建 Prompt
system_prompt = build_system_prompt(kb_content)
# 3. 调用大模型 API
response = _client.chat.completions.create(
model='glm-4-flash',
messages=[
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': question},
],
temperature=0.3, # 低温度,保证准确性
max_tokens=2048,
)
# 4. 提取回答和 token 消耗
answer = response.choices[0].message.content
tokens_used = response.usage.total_tokens
# 5. 返回结果
return {
'answer': answer,
'tokens_used': tokens_used,
'success': True,
}
except Exception as e:
return {
'answer': '',
'tokens_used': 0,
'success': False,
'error': str(e),
}
6.3 编码兼容性处理
def load_knowledge_base(file_path: str) -> str:
"""读取知识库,自动处理编码"""
try:
# 优先尝试 UTF-8
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
# 降级到 GBK(兼容中文 Windows 系统)
with open(file_path, 'r', encoding='gbk', errors='replace') as f:
content = f.read()
# 超长内容截断
MAX_CONTEXT_CHARS = 12000
if len(content) > MAX_CONTEXT_CHARS:
content = content[:MAX_CONTEXT_CHARS] + '\n\n[...内容过长,已截断...]'
return content
6.4 Prompt 工程最佳实践
设计原则:
1. 角色设定:明确告诉模型它是什么
"你是一个专业的知识库问答助手"
2. 内容注入:将知识库内容嵌入 Prompt
"根据以下知识库内容回答..."
3. 规则约束:明确要求和禁止
"只根据知识库内容作答"
"不要编造信息"
4. 兜底策略:处理知识库无相关内容的情况
"如果知识库中没有相关内容,请明确告知用户"
5. 温度参数:控制输出的随机性
temperature=0.3 低温度,保证准确性
temperature=0.7 高温度,增加创意性
七、部署与性能优化
7.1 生产环境部署
# 后端部署(使用 Gunicorn)
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5001 app:app
# 前端构建
cd kb-qa-frontend
npm run build
# 将 dist/ 目录部署到 Nginx 或 CDN
7.2 Nginx 反向代理配置
server {
listen 80;
server_name your-domain.com;
# 前端静态文件
location / {
root /path/to/kb-qa-frontend/dist;
try_files $uri $uri/ /index.html;
}
# 后端 API 代理
location /api/ {
proxy_pass http://127.0.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 长连接支持(AI 问答可能耗时)
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
7.3 性能优化建议
# 1. 数据库查询优化
# 使用 eager loading 避免 N+1 查询
kbs = KnowledgeBase.query.options(
joinedload(KnowledgeBase.owner)
).all()
# 2. 缓存知识库内容
from functools import lru_cache
@lru_cache(maxsize=128)
def load_knowledge_base(file_path: str) -> str:
"""缓存知识库内容,避免重复读取"""
# ...
# 3. 异步处理长耗时操作
from celery import Celery
@app.route('/api/chat', methods=['POST'])
def chat():
# 立即返回,后台处理
task = process_chat.delay(kb_id, question)
return {'task_id': task.id}
# 4. 分页查询历史记录
@app.route('/api/chat/history', methods=['GET'])
def get_history():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
pagination = ChatHistory.query.paginate(
page=page,
per_page=per_page,
error_out=False
)
return {
'items': [h.to_dict() for h in pagination.items],
'total': pagination.total,
'pages': pagination.pages,
}
八、常见问题与解决方案
8.1 大模型幻觉问题
问题:模型有时会编造知识库中没有的信息。
解决方案:
- 降低 temperature 参数(0.3 而不是 0.7)
- 在 Prompt 中明确禁止编造
- 引入向量数据库做语义检索(RAG 方案)
# 改进的 Prompt
system_prompt = """
你是一个严谨的知识库问答助手。
重要规则:
- 只能根据提供的知识库内容回答
- 如果知识库中没有相关信息,必须回答"知识库中未找到相关信息"
- 不允许推测、猜测或编造任何信息
- 即使你知道答案,如果知识库中没有,也不能回答
"""
8.2 超长文档处理
问题:知识库文档超过模型上下文窗口(通常 4K-8K tokens)。
解决方案:
# 方案 1:截断(当前实现)
MAX_CONTEXT_CHARS = 12000
if len(content) > MAX_CONTEXT_CHARS:
content = content[:MAX_CONTEXT_CHARS]
# 方案 2:分块 + 向量检索(推荐)
from langchain.text_splitter import CharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_text(content)
# 向量化存储
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(chunks, embeddings)
# 检索相关片段
relevant_chunks = vectorstore.similarity_search(question, k=3)
8.3 并发控制
问题:多个用户同时提问,API 调用频率限制。
解决方案:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=['200 per day', '50 per hour']
)
@app.route('/api/chat', methods=['POST'])
@limiter.limit('10 per minute') # 每分钟最多 10 个请求
@jwt_required()
def chat():
# ...
8.4 Token 成本控制
问题:大模型 API 按 token 计费,需要控制成本。
解决方案:
# 1. 监控 token 消耗
@app.route('/api/chat', methods=['POST'])
def chat():
result = ask_question(kb.file_path, question)
tokens_used = result['tokens_used']
# 保存到数据库,便于统计
history = ChatHistory(
tokens_used=tokens_used,
# ...
)
db.session.add(history)
db.session.commit()
# 2. 设置用户配额
class User(db.Model):
monthly_token_quota = db.Column(db.Integer, default=100000)
tokens_used_this_month = db.Column(db.Integer, default=0)
# 3. 检查配额
if user.tokens_used_this_month + tokens_used > user.monthly_token_quota:
return fail('本月 token 配额已用尽', 429)
九、总结与展望
9.1 项目亮点
- Prompt 工程:通过精心设计 System Prompt,有效解决大模型幻觉问题
- 前后端分离:清晰的架构,易于维护和扩展
- 安全设计:JWT 鉴权、密码哈希、文件名 UUID 化
- 工程规范:统一的 API 响应格式、完整的错误处理
9.2 改进方向
- 向量数据库:引入 Chroma/Faiss,实现 RAG 方案,支持超长文档
- 多模型支持:支持 PDF、Word、Markdown 等格式
- 多轮对话:保存对话历史,支持连续问答
- 容器化部署:Docker + Kubernetes,支持云端部署
- 监控告警:集成 Prometheus + Grafana,监控系统性能
9.3 学习资源
- Vue 3 官方文档:https://cn.vuejs.org
- Flask 官方文档:https://flask.palletsprojects.com
- SQLAlchemy 文档:https://www.sqlalchemy.org
- 智谱 AI 文档:https://open.bigmodel.cn/docs
- Prompt 工程最佳实践:https://platform.openai.com/docs/guides/prompt-engineering
附录:快速开始
环境要求
- Python 3.11+
- Node.js 18+
- 智谱 AI API Key(免费申请)
后端启动
cd kb-qa-backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # 填入 ZHIPUAI_API_KEY
python app.py
前端启动
cd kb-qa-frontend
npm install
npm run dev
默认账号
- 用户名:admin
- 密码:admin123
作者:沙蒿同学
发布时间:2026 年
更新时间:持续更新中
如有问题,欢迎在评论区讨论!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)