从零构建本地知识库问答系统:ollama-python与Django实战指南
问题引入:企业知识管理的困境与破局之道
想象这样的场景:你是一家中型企业的技术负责人,团队成员经常需要查阅内部文档、API手册和项目规范。但随着资料的不断积累,传统的文件搜索方式越来越低效——关键词匹配常常返回无关结果,新员工需要数周才能熟悉知识体系,远程团队成员更是面临访问限制。
更令人担忧的是数据安全问题:当你将敏感的技术文档上传到第三方AI服务时,商业机密泄露的风险如影随形。而使用传统搜索引擎,又无法理解上下文和专业术语,导致知识获取成本居高不下。
这就是为什么越来越多的企业开始转向本地知识库问答系统——一种能够在企业内部网络中运行,理解专业领域知识,并保护数据隐私的AI解决方案。而今天,我们将用ollama-python和Django构建这样一个系统,让企业知识管理变得高效而安全。
核心价值:为什么选择本地部署方案?
当你考虑构建知识库系统时,通常有两种选择:云端API服务或本地部署方案。让我们看看在实际工作中这两种方案的具体表现:
场景一:研发团队技术文档查询
使用云端API时,每次查询都需要将技术细节上传到外部服务器,不仅面临数据泄露风险,还可能因网络延迟导致每次查询等待3-5秒。而本地部署的系统响应时间通常在300毫秒以内,且所有数据处理都在企业内部网络完成。
场景二:财务报表分析
财务数据属于高度敏感信息,使用第三方服务时需要签署复杂的合规协议。本地部署方案则可以完全控制数据流向,满足金融行业严格的监管要求,同时避免按调用次数计费带来的成本累积。
场景三:离线办公环境
在没有网络连接的情况下(如封闭实验室、出差途中),云端API完全无法使用。而本地部署系统可以在断网环境下继续提供服务,确保工作不中断。
ollama-python正是实现这一目标的理想工具——它作为Ollama服务的Python客户端,让你能够轻松地在本地部署和管理大语言模型(LLM, Large Language Model),为企业知识库问答系统提供强大的AI支持。
场景化实践:构建本地知识库问答系统
⚙️ 环境准备与兼容性配置
1. 安装Ollama服务
Ollama支持多种操作系统,根据你的开发环境选择相应的安装方式:
Linux系统
# Ubuntu/Debian
curl -fsSL https://ollama.com/install.sh | sh
# 启动服务
ollama serve
macOS系统
# 使用Homebrew安装
brew install ollama
# 启动服务(会在后台运行)
ollama serve
Windows系统
- 访问Ollama官网下载Windows安装包
- 双击安装文件并按照向导完成安装
- 通过开始菜单启动Ollama服务
[!NOTE] Ollama服务默认运行在11434端口。如果该端口被占用,可以通过
OLLAMA_PORT环境变量修改端口号,例如:OLLAMA_PORT=11435 ollama serve
2. 拉取知识库专用模型
对于知识库问答场景,我们推荐使用专门优化的嵌入模型和对话模型:
# 拉取嵌入模型(用于文档向量化)
ollama pull nomic-embed-text
# 拉取对话模型(用于回答问题)
ollama pull gemma3:2b
3. 创建Django项目
# 创建虚拟环境
python -m venv venv
source venv/bin/activate # Linux/macOS
# 或在Windows上: venv\Scripts\activate
# 安装依赖
pip install django ollama python-dotenv
# 创建项目
django-admin startproject knowledge_base
cd knowledge_base
# 创建知识库应用
python manage.py startapp kb_app
🧠 核心原理:本地知识库的工作流程
在开始编码前,让我们先了解本地知识库问答系统的工作原理:
- 文档处理阶段:系统将文档转换为向量(Embedding)并存储在向量数据库中
- 查询阶段:用户提问被转换为向量,系统在向量数据库中查找相似内容
- 回答生成阶段:将找到的相关文档片段作为上下文,让LLM生成针对性回答
这种架构的优势在于:
- 减少模型输入长度,提高响应速度
- 让LLM专注于理解和生成,而非记忆大量知识
- 支持增量更新知识库,无需重新训练模型
💻 代码实现:从基础到优化
1. 项目配置
首先修改knowledge_base/settings.py:
import os
from pathlib import Path
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-default-key-for-dev')
DEBUG = os.getenv('DEBUG', 'True') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# 应用配置
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'kb_app', # 我们的知识库应用
]
# Ollama配置
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'http://localhost:11434')
EMBEDDING_MODEL = os.getenv('EMBEDDING_MODEL', 'nomic-embed-text')
CHAT_MODEL = os.getenv('CHAT_MODEL', 'gemma3:2b')
在项目根目录创建.env文件:
DJANGO_SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
OLLAMA_HOST=http://localhost:11434
EMBEDDING_MODEL=nomic-embed-text
CHAT_MODEL=gemma3:2b
2. 数据模型设计
编辑kb_app/models.py:
from django.db import models
import uuid
import logging
logger = logging.getLogger(__name__)
class Document(models.Model):
"""文档模型,存储知识库中的原始文档"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(max_length=255)
content = models.TextField()
file_path = models.CharField(max_length=512, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
class Meta:
ordering = ['-updated_at']
class Embedding(models.Model):
"""存储文档内容的向量表示"""
document = models.ForeignKey(Document, on_delete=models.CASCADE, related_name='embeddings')
content_chunk = models.TextField() # 文档的一部分内容
embedding_vector = models.JSONField() # 存储向量数据
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['document']),
]
class Query(models.Model):
"""存储用户查询记录"""
query_text = models.TextField()
response_text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
user_id = models.CharField(max_length=100, blank=True, null=True) # 可关联用户系统
def __str__(self):
return f"Query: {self.query_text[:50]}"
创建数据库表:
python manage.py makemigrations
python manage.py migrate
3. Ollama服务封装
创建kb_app/services/ollama_service.py:
from ollama import Client, AsyncClient, APIError
from django.conf import settings
import logging
from typing import List, Dict, Optional
logger = logging.getLogger(__name__)
class OllamaKnowledgeService:
"""Ollama知识库服务封装"""
def __init__(self):
self.host = settings.OLLAMA_HOST
self.embedding_model = settings.EMBEDDING_MODEL
self.chat_model = settings.CHAT_MODEL
self.client = Client(host=self.host)
def create_embedding(self, text: str) -> Optional[List[float]]:
"""
将文本转换为向量表示
参数:
text: 需要转换的文本
返回:
向量列表或None(出错时)
"""
try:
response = self.client.embeddings(
model=self.embedding_model,
prompt=text
)
return response.get('embedding')
except APIError as e:
logger.error(f"创建嵌入向量失败: {str(e)}")
return None
except Exception as e:
logger.error(f"嵌入处理未知错误: {str(e)}")
return None
def generate_answer(self, query: str, context: str) -> Optional[str]:
"""
根据查询和上下文生成回答
参数:
query: 用户查询
context: 相关文档上下文
返回:
生成的回答或None(出错时)
"""
try:
messages = [
{
"role": "system",
"content": "你是一个企业知识库助手。请根据提供的上下文信息回答用户问题。"
"只使用上下文中提供的信息,不要编造内容。如果无法从上下文找到答案,"
"请明确说明无法回答该问题。"
},
{
"role": "user",
"content": f"上下文: {context}\n\n问题: {query}"
}
]
response = self.client.chat(
model=self.chat_model,
messages=messages,
options={"temperature": 0.3} # 低温度使回答更确定
)
return response['message']['content']
except APIError as e:
logger.error(f"生成回答失败: {str(e)}")
return None
except Exception as e:
logger.error(f"回答生成未知错误: {str(e)}")
return None
class AsyncOllamaKnowledgeService(OllamaKnowledgeService):
"""异步版本的Ollama知识库服务"""
async def create_embedding_async(self, text: str) -> Optional[List[float]]:
"""异步创建嵌入向量"""
try:
async with AsyncClient(host=self.host) as client:
response = await client.embeddings(
model=self.embedding_model,
prompt=text
)
return response.get('embedding')
except APIError as e:
logger.error(f"异步创建嵌入向量失败: {str(e)}")
return None
except Exception as e:
logger.error(f"异步嵌入处理未知错误: {str(e)}")
return None
async def generate_answer_async(self, query: str, context: str) -> Optional[str]:
"""异步生成回答"""
try:
messages = [
{
"role": "system",
"content": "你是一个企业知识库助手。请根据提供的上下文信息回答用户问题。"
"只使用上下文中提供的信息,不要编造内容。如果无法从上下文找到答案,"
"请明确说明无法回答该问题。"
},
{
"role": "user",
"content": f"上下文: {context}\n\n问题: {query}"
}
]
async with AsyncClient(host=self.host) as client:
response = await client.chat(
model=self.chat_model,
messages=messages,
options={"temperature": 0.3}
)
return response['message']['content']
except APIError as e:
logger.error(f"异步生成回答失败: {str(e)}")
return None
except Exception as e:
logger.error(f"异步回答生成未知错误: {str(e)}")
return None
4. 文档处理工具
创建kb_app/utils/document_processor.py:
import os
import re
from pathlib import Path
from django.conf import settings
from kb_app.models import Document, Embedding
from kb_app.services.ollama_service import OllamaKnowledgeService
import logging
logger = logging.getLogger(__name__)
class DocumentProcessor:
"""文档处理工具,负责文档加载、分块和向量化"""
def __init__(self):
self.ollama_service = OllamaKnowledgeService()
self.chunk_size = 1000 # 文本块大小(字符)
self.chunk_overlap = 100 # 块之间的重叠字符数
def load_document(self, file_path: str) -> Optional[Document]:
"""加载文档并存储到数据库"""
try:
file_path = Path(file_path)
# 根据文件扩展名选择适当的读取方法
if file_path.suffix.lower() == '.txt':
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# 这里可以扩展支持其他格式(.pdf, .docx等)
# 创建文档记录
document = Document(
title=file_path.name,
content=content,
file_path=str(file_path)
)
document.save()
# 处理文档分块和向量化
self.process_document_chunks(document)
logger.info(f"成功加载文档: {file_path.name}")
return document
except Exception as e:
logger.error(f"加载文档失败: {str(e)}")
return None
def process_document_chunks(self, document: Document):
"""将文档分块并创建嵌入向量"""
content = document.content
chunks = self.split_into_chunks(content)
for chunk in chunks:
# 创建嵌入向量
embedding = self.ollama_service.create_embedding(chunk)
if embedding:
# 保存嵌入向量
Embedding.objects.create(
document=document,
content_chunk=chunk,
embedding_vector=embedding
)
else:
logger.warning(f"无法为文档块创建嵌入: {chunk[:50]}...")
def split_into_chunks(self, text: str) -> List[str]:
"""将文本分割成重叠的块"""
chunks = []
start = 0
text_length = len(text)
while start < text_length:
end = start + self.chunk_size
chunk = text[start:end]
# 如果不是最后一块,确保在句子边界处分割
if end < text_length:
# 寻找句子结束标记
end_punctuation = re.search(r'[。.!?]\s', chunk[-50:])
if end_punctuation:
# 调整结束位置到标点符号之后
end = start + len(chunk[:-50]) + end_punctuation.end()
chunks.append(chunk)
start = end - self.chunk_overlap # 重叠部分
return chunks
5. 视图实现(类视图)
编辑kb_app/views.py:
from django.views import View
from django.shortcuts import render, redirect
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.conf import settings
import json
import logging
import numpy as np
from kb_app.models import Document, Query, Embedding
from kb_app.services.ollama_service import AsyncOllamaKnowledgeService
from kb_app.utils.document_processor import DocumentProcessor
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name='dispatch')
class KnowledgeBaseAPIView(View):
"""知识库API接口"""
async def post(self, request):
"""处理知识库查询请求"""
try:
data = json.loads(request.body)
query_text = data.get('query')
if not query_text:
return JsonResponse(
{'error': '查询文本不能为空'},
status=400
)
# 1. 将查询转换为向量
ollama_service = AsyncOllamaKnowledgeService()
query_embedding = await ollama_service.create_embedding_async(query_text)
if not query_embedding:
return JsonResponse(
{'error': '无法处理查询,请稍后重试'},
status=500
)
# 2. 查找相似文档块(简化实现,实际应使用向量数据库)
embeddings = Embedding.objects.all()
similarities = []
for embedding in embeddings:
# 计算余弦相似度
vec1 = np.array(query_embedding)
vec2 = np.array(embedding.embedding_vector)
similarity = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
similarities.append((embedding, similarity))
# 取相似度最高的前3个文档块
similarities.sort(key=lambda x: x[1], reverse=True)
top_embeddings = [item[0] for item in similarities[:3]]
# 构建上下文
context = "\n\n".join([e.content_chunk for e in top_embeddings])
# 3. 生成回答
answer = await ollama_service.generate_answer_async(query_text, context)
if not answer:
return JsonResponse(
{'error': '无法生成回答,请稍后重试'},
status=500
)
# 保存查询记录
Query.objects.create(
query_text=query_text,
response_text=answer,
user_id=request.META.get('REMOTE_ADDR') # 使用IP作为临时用户标识
)
return JsonResponse({
'query': query_text,
'answer': answer,
'sources': [e.document.title for e in top_embeddings]
})
except json.JSONDecodeError:
return JsonResponse(
{'error': '无效的JSON格式'},
status=400
)
except Exception as e:
logger.error(f"API处理错误: {str(e)}")
return JsonResponse(
{'error': '服务器内部错误'},
status=500
)
class DocumentUploadView(View):
"""文档上传视图"""
def get(self, request):
"""显示上传表单"""
return render(request, 'kb_app/upload.html')
def post(self, request):
"""处理文档上传"""
if 'document' not in request.FILES:
return render(request, 'kb_app/upload.html', {
'error': '请选择要上传的文件'
})
file = request.FILES['document']
# 保存文件到临时位置
temp_path = f"/tmp/{file.name}"
with open(temp_path, 'wb+') as destination:
for chunk in file.chunks():
destination.write(chunk)
# 处理文档
processor = DocumentProcessor()
document = processor.load_document(temp_path)
# 清理临时文件
os.remove(temp_path)
if document:
return render(request, 'kb_app/upload.html', {
'success': f'文档 "{document.title}" 已成功添加到知识库'
})
else:
return render(request, 'kb_app/upload.html', {
'error': '文档处理失败,请重试'
})
class ChatInterfaceView(View):
"""聊天界面视图"""
def get(self, request):
"""显示聊天界面"""
return render(request, 'kb_app/chat.html')
6. URL配置
编辑knowledge_base/urls.py:
from django.contrib import admin
from django.urls import path
from kb_app.views import (
KnowledgeBaseAPIView,
DocumentUploadView,
ChatInterfaceView
)
urlpatterns = [
path('admin/', admin.site.urls),
path('', ChatInterfaceView.as_view(), name='chat_interface'),
path('api/query/', KnowledgeBaseAPIView.as_view(), name='knowledge_api'),
path('upload/', DocumentUploadView.as_view(), name='document_upload'),
]
7. 前端页面
创建kb_app/templates/kb_app/chat.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业知识库问答系统</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
h1 {
color: #2c3e50;
font-size: 24px;
}
.upload-link {
background-color: #3498db;
color: white;
padding: 8px 15px;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
transition: background-color 0.3s;
}
.upload-link:hover {
background-color: #2980b9;
}
.chat-container {
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
height: 600px;
}
.chat-history {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.message {
margin-bottom: 20px;
max-width: 80%;
}
.user-message {
margin-left: auto;
}
.message-content {
padding: 12px 18px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
}
.user-message .message-content {
background-color: #3498db;
color: white;
border-bottom-right-radius: 4px;
}
.ai-message .message-content {
background-color: #ecf0f1;
border-bottom-left-radius: 4px;
}
.message-meta {
font-size: 12px;
margin-top: 5px;
opacity: 0.7;
}
.ai-message .message-meta {
text-align: left;
padding-left: 18px;
}
.user-message .message-meta {
text-align: right;
padding-right: 18px;
}
.chat-input {
display: flex;
padding: 15px;
background-color: #f9f9f9;
border-top: 1px solid #e0e0e0;
}
#query-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
}
#query-input:focus {
border-color: #3498db;
}
#send-button {
margin-left: 10px;
padding: 12px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
transition: background-color 0.3s;
}
#send-button:hover {
background-color: #2980b9;
}
.loading-indicator {
display: none;
text-align: center;
padding: 10px;
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(52, 152, 219, 0.3);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 1s ease-in-out infinite;
display: inline-block;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.sources {
font-size: 12px;
margin-top: 8px;
padding-left: 18px;
color: #7f8c8d;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>企业知识库问答系统</h1>
<a href="/upload/" class="upload-link">上传文档</a>
</header>
<div class="chat-container">
<div class="chat-history" id="chat-history">
<div class="message ai-message">
<div class="message-content">
您好!我是企业知识库助手。请上传文档到知识库,然后向我提问,我会根据知识库内容为您解答。
</div>
</div>
</div>
<div class="loading-indicator" id="loading-indicator">
<div class="spinner"></div>
<span>正在思考...</span>
</div>
<div class="chat-input">
<input type="text" id="query-input" placeholder="请输入您的问题...">
<button id="send-button">发送</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const chatHistory = document.getElementById('chat-history');
const queryInput = document.getElementById('query-input');
const sendButton = document.getElementById('send-button');
const loadingIndicator = document.getElementById('loading-indicator');
// 发送消息
const sendMessage = async () => {
const query = queryInput.value.trim();
if (!query) return;
// 添加用户消息到历史记录
addMessageToHistory('user', query);
// 清空输入框并显示加载状态
queryInput.value = '';
loadingIndicator.style.display = 'block';
try {
// 调用API
const response = await fetch('/api/query/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: query })
});
const data = await response.json();
if (response.ok) {
// 添加AI回答到历史记录
addMessageToHistory('ai', data.answer, data.sources);
} else {
addMessageToHistory('ai', `错误: ${data.error || '无法获取回答'}`);
}
} catch (error) {
addMessageToHistory('ai', '抱歉,请求过程中出现错误,请稍后重试。');
console.error('API请求错误:', error);
} finally {
// 隐藏加载状态
loadingIndicator.style.display = 'none';
}
};
// 添加消息到历史记录
const addMessageToHistory = (role, content, sources = []) => {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}-message`;
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(contentDiv);
// 添加来源信息
if (role === 'ai' && sources && sources.length > 0) {
const sourcesDiv = document.createElement('div');
sourcesDiv.className = 'sources';
sourcesDiv.textContent = `来源: ${sources.join(', ')}`;
messageDiv.appendChild(sourcesDiv);
}
// 添加时间戳
const metaDiv = document.createElement('div');
metaDiv.className = 'message-meta';
const now = new Date();
metaDiv.textContent = now.toLocaleTimeString();
messageDiv.appendChild(metaDiv);
chatHistory.appendChild(messageDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
};
// 绑定事件
sendButton.addEventListener('click', sendMessage);
queryInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendMessage();
});
});
</script>
</body>
</html>
创建kb_app/templates/kb_app/upload.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文档 - 企业知识库</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e0e0e0;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #3498db;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.upload-form {
margin-top: 20px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input[type="file"] {
display: block;
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
button {
background-color: #3498db;
color: white;
border: none;
padding: 12px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← 返回知识库</a>
<h1>上传文档到知识库</h1>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form class="upload-form" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="document">选择文档文件(目前支持TXT格式)</label>
<input type="file" id="document" name="document" accept=".txt" required>
</div>
<button type="submit">上传并处理文档</button>
</form>
</div>
</body>
</html>
进阶拓展:从原型到生产环境
📊 性能监控与优化
1. 添加性能监控
创建kb_app/middleware/performance.py:
import time
import logging
from django.utils.deprecation import MiddlewareMixin
logger = logging.getLogger(__name__)
class PerformanceMonitorMiddleware(MiddlewareMixin):
"""性能监控中间件,记录请求处理时间"""
def process_request(self, request):
request.start_time = time.time()
def process_response(self, request, response):
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.info(
f"Request: {request.method} {request.path} "
f"Status: {response.status_code} "
f"Duration: {duration:.2f}s"
)
return response
在settings.py中添加中间件:
MIDDLEWARE = [
# ...其他中间件
'kb_app.middleware.performance.PerformanceMonitorMiddleware',
]
2. 向量存储优化
对于生产环境,建议使用专业的向量数据库如Chroma或FAISS替代Django ORM:
# kb_app/services/vector_store.py
import chromadb
from chromadb.config import Settings
class ChromaVectorStore:
"""Chroma向量数据库封装"""
def __init__(self, collection_name="knowledge_base"):
self.client = chromadb.Client(Settings(
persist_directory="./chroma_db",
anonymized_telemetry=False
))
self.collection = self.client.get_or_create_collection(name=collection_name)
def add_embeddings(self, document_id, embeddings, texts):
"""添加嵌入向量到数据库"""
self.collection.add(
documents=texts,
embeddings=embeddings,
ids=[f"{document_id}_{i}" for i in range(len(texts))]
)
self.client.persist()
def search_similar(self, query_embedding, n_results=3):
"""搜索相似向量"""
results = self.collection.query(
query_embeddings=[query_embedding],
n_results=n_results
)
return results
📈 模型评估与优化
1. 评估指标跟踪
创建kb_app/utils/evaluator.py:
import json
import os
from datetime import datetime
from django.conf import settings
class ModelEvaluator:
"""模型评估工具"""
def __init__(self, eval_dir="evaluation_results"):
self.eval_dir = os.path.join(settings.BASE_DIR, eval_dir)
os.makedirs(self.eval_dir, exist_ok=True)
def record_evaluation(self, query, reference_answer, model_answer, metrics):
"""
记录评估结果
参数:
query: 用户查询
reference_answer: 参考答案
model_answer: 模型生成的答案
metrics: 评估指标字典
"""
evaluation = {
"timestamp": datetime.now().isoformat(),
"query": query,
"reference_answer": reference_answer,
"model_answer": model_answer,
"metrics": metrics
}
# 保存到文件
filename = f"eval_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
filepath = os.path.join(self.eval_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(evaluation, f, ensure_ascii=False, indent=2)
return filepath
2. 模型参数调优
创建kb_app/utils/model_tuner.py:
from ollama import Client
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
class ModelTuner:
"""模型参数调优工具"""
def __init__(self):
self.client = Client(host=settings.OLLAMA_HOST)
self.base_model = settings.CHAT_MODEL
def create_model_variant(self, variant_name, system_prompt, parameters=None):
"""
创建模型变体
参数:
variant_name: 变体名称
system_prompt: 系统提示词
parameters: 模型参数字典
"""
try:
# 创建自定义模型
modelfile = f"""
FROM {self.base_model}
SYSTEM {system_prompt}
"""
if parameters:
for key, value in parameters.items():
modelfile += f"PARAMETER {key} {value}\n"
# 创建模型
response = self.client.create(
model=variant_name,
modelfile=modelfile.strip()
)
logger.info(f"成功创建模型变体: {variant_name}")
return response
except Exception as e:
logger.error(f"创建模型变体失败: {str(e)}")
return None
项目结构与部署指南
完整项目结构树
knowledge_base/ # 项目根目录
├── .env # 环境变量配置
├── manage.py # Django管理脚本
├── knowledge_base/ # 项目配置目录
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py # 项目设置
│ ├── urls.py # 主URL配置
│ └── wsgi.py
└── kb_app/ # 知识库应用
├── __init__.py
├── admin.py # 管理界面配置
├── apps.py
├── middleware/ # 中间件
│ ├── __init__.py
│ └── performance.py # 性能监控中间件
├── migrations/ # 数据库迁移文件
├── models.py # 数据模型
├── services/ # 服务层
│ ├── __init__.py
│ ├── ollama_service.py # Ollama服务封装
│ └── vector_store.py # 向量存储服务
├── templates/ # 模板文件
│ └── kb_app/
│ ├── chat.html # 聊天界面
│ └── upload.html # 文档上传界面
├── urls.py # 应用URL配置
├── utils/ # 工具函数
│ ├── __init__.py
│ ├── document_processor.py # 文档处理工具
│ ├── evaluator.py # 模型评估工具
│ └── model_tuner.py # 模型调优工具
└── views.py # 视图函数
部署注意事项
[!NOTE] 生产环境部署清单
- 设置
DEBUG=False并配置ALLOWED_HOSTS- 使用环境变量存储敏感信息,不要提交到代码仓库
- 配置适当的数据库(如PostgreSQL)替代SQLite
- 使用Gunicorn作为WSGI服务器,Nginx作为反向代理
- 配置日志轮转,避免日志文件过大
- 考虑使用Docker容器化部署,简化环境配置
运行项目
# 克隆仓库
git clone https://gitcode.com/GitHub_Trending/ol/ollama-python
# 进入项目目录
cd ollama-python
# 创建并激活虚拟环境
python -m venv venv
source venv/bin/activate # Linux/macOS
# 或在Windows上: venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
# 运行开发服务器
python manage.py runserver
总结与未来展望
通过本文的实践,你已经构建了一个功能完善的本地知识库问答系统。这个系统能够:
- 安全地存储和管理企业文档
- 提供快速、准确的知识查询服务
- 保护敏感数据不离开企业内部网络
未来,你可以进一步扩展这个系统:
- 多模态支持:集成图像理解能力,处理包含图片的文档
- 知识图谱:构建实体关系网络,提升回答的深度和准确性
- 多语言支持:添加跨语言问答能力,满足国际化需求
- 用户权限管理:实现基于角色的文档访问控制
本地大语言模型技术正在快速发展,通过ollama-python这样的工具,我们能够轻松构建安全、高效的AI应用,为企业知识管理带来革命性的变化。现在就开始探索,将你的企业知识库转变为智能助手吧!
GLM-5智谱 AI 正式发布 GLM-5,旨在应对复杂系统工程和长时域智能体任务。Jinja00
GLM-5-w4a8GLM-5-w4a8基于混合专家架构,专为复杂系统工程与长周期智能体任务设计。支持单/多节点部署,适配Atlas 800T A3,采用w4a8量化技术,结合vLLM推理优化,高效平衡性能与精度,助力智能应用开发Jinja00
jiuwenclawJiuwenClaw 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。Python0192- QQwen3.5-397B-A17BQwen3.5 实现了重大飞跃,整合了多模态学习、架构效率、强化学习规模以及全球可访问性等方面的突破性进展,旨在为开发者和企业赋予前所未有的能力与效率。Jinja00
AtomGit城市坐标计划AtomGit 城市坐标计划开启!让开源有坐标,让城市有星火。致力于与城市合伙人共同构建并长期运营一个健康、活跃的本地开发者生态。01
awesome-zig一个关于 Zig 优秀库及资源的协作列表。Makefile00