首页
/ 从零构建本地知识库问答系统:ollama-python与Django实战指南

从零构建本地知识库问答系统:ollama-python与Django实战指南

2026-03-16 02:52:23作者:史锋燃Gardner

问题引入:企业知识管理的困境与破局之道

想象这样的场景:你是一家中型企业的技术负责人,团队成员经常需要查阅内部文档、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系统

  1. 访问Ollama官网下载Windows安装包
  2. 双击安装文件并按照向导完成安装
  3. 通过开始菜单启动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

🧠 核心原理:本地知识库的工作流程

在开始编码前,让我们先了解本地知识库问答系统的工作原理:

  1. 文档处理阶段:系统将文档转换为向量(Embedding)并存储在向量数据库中
  2. 查询阶段:用户提问被转换为向量,系统在向量数据库中查找相似内容
  3. 回答生成阶段:将找到的相关文档片段作为上下文,让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] 生产环境部署清单

  1. 设置DEBUG=False并配置ALLOWED_HOSTS
  2. 使用环境变量存储敏感信息,不要提交到代码仓库
  3. 配置适当的数据库(如PostgreSQL)替代SQLite
  4. 使用Gunicorn作为WSGI服务器,Nginx作为反向代理
  5. 配置日志轮转,避免日志文件过大
  6. 考虑使用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

总结与未来展望

通过本文的实践,你已经构建了一个功能完善的本地知识库问答系统。这个系统能够:

  1. 安全地存储和管理企业文档
  2. 提供快速、准确的知识查询服务
  3. 保护敏感数据不离开企业内部网络

未来,你可以进一步扩展这个系统:

  • 多模态支持:集成图像理解能力,处理包含图片的文档
  • 知识图谱:构建实体关系网络,提升回答的深度和准确性
  • 多语言支持:添加跨语言问答能力,满足国际化需求
  • 用户权限管理:实现基于角色的文档访问控制

本地大语言模型技术正在快速发展,通过ollama-python这样的工具,我们能够轻松构建安全、高效的AI应用,为企业知识管理带来革命性的变化。现在就开始探索,将你的企业知识库转变为智能助手吧!

登录后查看全文
热门项目推荐
相关项目推荐