教程雨

OKX新手入门教程导航,收录OKX注册、充值、买币、提现等基础操作教程

LangChain+RAG构建企业级智能客服知识库问答系统

AI智能客服实战:LangChain+RAG构建企业级知识库问答系统

前言

在2026年的企业数字化转型浪潮中,AI智能客服已经成为各行业降本增效的核心工具。与传统关键词匹配客服不同,基于RAG(检索增强生成)技术的智能客服能够理解上下文、从企业知识库中精准检索信息,并生成自然流畅的回答。本文将带你从零构建一个企业级知识库问答系统,涵盖文档处理、向量检索、大模型调用、提示词优化等核心环节。完成这个项目后,你将掌握构建真实AI应用的全套技能。

RAG检索增强生成架构:文档向量化与语义检索流程

项目概述与技术架构

项目目标

我们将构建一个企业知识库智能问答系统,具备以下核心能力:支持上传企业文档(PDF、Word、TXT等格式)、自动向量化存储、基于语义检索匹配相关文档片段、大模型生成准确答案。这个系统可以部署到企业内网,为员工提供产品文档查询、内部流程咨询、技术问题解答等服务。

技术栈选型

本项目采用以下技术组合,每个组件都经过2026年企业级应用验证:

LangChain框架负责整体流程编排,它提供了文档加载、文本分割、向量存储、检索链等完整工具链,版本选择langchain-0.3.x系列。向量数据库选用Chroma,这是一个轻量级但功能完整的向量库,支持本地部署,0.5.x版本对检索性能做了显著优化,Windows、macOS、Linux三大平台均可直接安装。Embedding模型使用sentence-transformers的all-MiniLM-L6-v2,这个模型体积小(约90MB)、速度快、效果稳定,中文支持良好。大模型层可以根据实际需求选择OpenAI GPT-4o、国内的通义千问V5、DeepSeek-V3,或者通过Ollama本地部署的开源模型如Llama 3、Qwen2。

整体架构分为五个层级:文档层负责处理各类企业文档;处理层完成文本分割和向量化;存储层管理Chroma向量数据库;检索层实现语义相似度匹配;生成层调用大模型输出最终答案。

环境准备与依赖安装

系统要求

确保你的开发环境满足以下条件:Python版本3.10或更高,Windows系统需要Windows 10/11,macOS需要10.15及以上,Linux推荐Ubuntu 20.04或CentOS 8。磁盘空间建议预留5GB以上用于存储向量数据库。

安装步骤

创建项目目录并进入:

Windows系统打开PowerShell执行:

powershell

mkdir ai-knowledge-qa && cd ai-knowledge-qa
python -m venv venv
.\venv\Scripts\activate
pip install langchain langchain-community langchain-openai chromadb sentence-transformers pypdf python-docx python-dotenv tiktoken

macOS和Linux系统打开终端执行:

bash

mkdir -p ai-knowledge-qa && cd ai-knowledge-qa
python3 -m venv venv
source venv/bin/activate
pip install langchain langchain-community langchain-openai chromadb sentence-transformers pypdf python-docx python-dotenv tiktoken

如果使用本地模型(通过Ollama部署),需要额外安装:

bash

pip install langchain-ollama

并确保Ollama已在本地运行:

bash

# macOS/Linux安装Ollama
curl -fsSL https://ollama.com/install.sh | sh

# Windows从 https://ollama.com/download 下载安装

# 拉取模型
ollama pull qwen2.5:7b  # 中文支持好的开源模型

项目目录结构

创建完成后,项目结构如下:

plaintext

ai-knowledge-qa/
├── config/
│   └── settings.py          # 配置文件
├── data/
│   └── knowledge_base/       # 知识库文档目录
├── src/
│   ├── document_loader.py    # 文档加载模块
│   ├── text_splitter.py       # 文本分割模块
│   ├── vector_store.py       # 向量存储模块
│   ├── retriever.py          # 检索模块
│   ├── chain.py              # 问答链模块
│   └── main.py               # 主程序入口
├── .env                      # 环境变量(API密钥等)
├── requirements.txt          # 依赖清单
└── README.md                 # 项目说明

核心模块实现

配置管理

在config/settings.py中集中管理所有配置,便于后续调整:

python

# config/settings.py
import os
from dotenv import load_dotenv

load_dotenv()

class Settings:
    # 大模型配置
    LLM_PROVIDER = os.getenv("LLM_PROVIDER", "openai")  # openai, ollama, zhipu
    
    # OpenAI配置(如使用OpenAI)
    OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
    OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
    OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
    
    # Ollama配置(如使用本地模型)
    OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
    OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen2.5:7b")
    
    # 向量数据库配置
    CHROMA_PERSIST_DIRECTORY = os.getenv("CHROMA_PERSIST_DIRECTORY", "./data/chroma_db")
    EMBEDDING_MODEL = os.getenv("EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2")
    
    # 文本分割配置
    CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", 500))
    CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", 50))
    
    # 检索配置
    TOP_K = int(os.getenv("TOP_K", 4))  # 检索返回的相关文档数

settings = Settings()

.env文件示例:

env

# 选择大模型提供商:openai, ollama, zhipu
LLM_PROVIDER=ollama

# Ollama配置(使用本地模型)
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:7b

# 向量数据库路径
CHROMA_PERSIST_DIRECTORY=./data/chroma_db

# 文本分割参数
CHUNK_SIZE=500
CHUNK_OVERLAP=50

# 检索数量
TOP_K=4

文档加载模块

支持多种文档格式的加载是企业知识库的基础需求:

python

# src/document_loader.py
from langchain_community.document_loaders import (
    PyPDFLoader,           # PDF文档
    Docx2txtLoader,        # Word文档(.docx)
    TextLoader,             # 纯文本文件
    UnstructuredMarkdownLoader  # Markdown文件
)
from langchain.schema import Document
from pathlib import Path
import logging

logger = logging.getLogger(__name__)

class DocumentLoader:
    """企业文档加载器,支持多种格式"""
    
    def __init__(self):
        self.loader_map = {
            '.pdf': PyPDFLoader,
            '.docx': Docx2txtLoader,
            '.txt': TextLoader,
            '.md': UnstructuredMarkdownLoader
        }
    
    def load_file(self, file_path: str) -> list[Document]:
        """加载单个文件"""
        path = Path(file_path)
        suffix = path.suffix.lower()
        
        if suffix not in self.loader_map:
            logger.warning(f"不支持的文件格式: {suffix}")
            return []
        
        try:
            loader_class = self.loader_map[suffix]
            loader = loader_class(str(path))
            documents = loader.load()
            
            # 添加元数据:来源文件名
            for doc in documents:
                doc.metadata["source"] = path.name
            
            logger.info(f"成功加载文件: {path.name}, 包含 {len(documents)} 个文档块")
            return documents
        except Exception as e:
            logger.error(f"加载文件失败 {path.name}: {str(e)}")
            return []
    
    def load_directory(self, directory: str, extensions: list = None) -> list[Document]:
        """批量加载目录下的所有文档"""
        if extensions is None:
            extensions = ['.pdf', '.docx', '.txt', '.md']
        
        all_docs = []
        dir_path = Path(directory)
        
        for ext in extensions:
            for file_path in dir_path.rglob(f"*{ext}"):
                docs = self.load_file(str(file_path))
                all_docs.extend(docs)
        
        logger.info(f"目录 {directory} 共加载 {len(all_docs)} 个文档块")
        return all_docs

# 使用示例
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    loader = DocumentLoader()
    # 加载单个文件
    docs = loader.load_file("./data/knowledge_base/product_manual.pdf")
    print(f"加载了 {len(docs)} 个文档块")

文本分割模块

将长文档合理分割是检索效果的关键。分割太短会丢失上下文,分割太长则引入噪声:

python

# src/text_splitter.py
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from config.settings import settings

class TextSplitter:
    """智能文本分割器,保持语义完整性"""
    
    def __init__(
        self,
        chunk_size: int = None,
        chunk_overlap: int = None,
        length_function = len
    ):
        self.chunk_size = chunk_size or settings.CHUNK_SIZE
        self.chunk_overlap = chunk_overlap or settings.CHUNK_OVERLAP
        self.length_function = length_function
        
        # 使用递归字符分割器,按优先级尝试不同的分隔符
        self.splitter = RecursiveCharacterTextSplitter(
            separators=[
                "\n\n",    # 段落分隔(优先,保持语义完整)
                "\n",      # 换行符
                "。",      # 中文句号
                "!",      # 中文感叹号
                "?",      # 中文问号
                ",",      # 中文逗号
                " ",       # 英文空格
                ""         # 单字符(最后兜底)
            ],
            chunk_size=self.chunk_size,
            chunk_overlap=self.chunk_overlap,
            length_function=length_function,
            add_start_index=True  # 添加起始位置索引
        )
    
    def split_documents(self, documents: list[Document]) -> list[Document]:
        """分割文档列表"""
        if not documents:
            return []
        
        # 去重:检查是否有重复内容的文档块
        seen_content = set()
        unique_docs = []
        for doc in documents:
            content_hash = hash(doc.page_content)
            if content_hash not in seen_content:
                seen_content.add(content_hash)
                unique_docs.append(doc)
        
        # 执行分割
        splits = self.splitter.split_documents(unique_docs)
        
        print(f"原始文档块: {len(unique_docs)}, 分割后: {len(splits)}")
        return splits
    
    def split_text(self, text: str) -> list[str]:
        """分割单段文本"""
        return self.splitter.split_text(text)

# 使用示例
if __name__ == "__main__":
    from src.document_loader import DocumentLoader
    
    loader = DocumentLoader()
    docs = loader.load_file("./data/knowledge_base/sample.txt")
    
    splitter = TextSplitter(chunk_size=500, chunk_overlap=50)
    splits = splitter.split_documents(docs)
    
    print(f"分割后的文档块数: {len(splits)}")
    for i, split in enumerate(splits[:3]):
        print(f"\n--- 文档块 {i+1} ---")
        print(f"内容长度: {len(split.page_content)} 字符")
        print(f"内容预览: {split.page_content[:100]}...")

向量存储模块

使用Chroma存储文档向量,支持持久化和增量更新:

python

# src/vector_store.py
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.schema import Document
from config.settings import settings
from pathlib import Path
import logging

logger = logging.getLogger(__name__)

class VectorStore:
    """向量数据库管理器"""
    
    def __init__(self):
        self.embeddings = HuggingFaceEmbeddings(
            model_name=settings.EMBEDDING_MODEL,
            model_kwargs={'device': 'cpu'},  # 可改为 'cuda' 使用GPU加速
            encode_kwargs={'normalize_embeddings': True}  # 归一化向量
        )
        self.vectorstore = None
    
    def create_vectorstore(self, documents: list[Document], persist_directory: str = None) -> Chroma:
        """从文档创建向量存储"""
        if not documents:
            raise ValueError("文档列表不能为空")
        
        persist_dir = persist_directory or settings.CHROMA_PERSIST_DIRECTORY
        Path(persist_dir).parent.mkdir(parents=True, exist_ok=True)
        
        logger.info(f"开始创建向量数据库,文档数: {len(documents)}")
        
        self.vectorstore = Chroma.from_documents(
            documents=documents,
            embedding=self.embeddings,
            persist_directory=persist_dir
        )
        
        logger.info(f"向量数据库创建完成,存储路径: {persist_dir}")
        return self.vectorstore
    
    def load_vectorstore(self, persist_directory: str = None) -> Chroma:
        """加载已存在的向量数据库"""
        persist_dir = persist_directory or settings.CHROMA_PERSIST_DIRECTORY
        
        if not Path(persist_dir).exists():
            raise FileNotFoundError(f"向量数据库不存在: {persist_dir}")
        
        self.vectorstore = Chroma(
            persist_directory=persist_dir,
            embedding_function=self.embeddings
        )
        
        logger.info(f"向量数据库加载完成,文档数: {self.vectorstore._collection.count()}")
        return self.vectorstore
    
    def add_documents(self, documents: list[Document]):
        """向现有向量库添加文档"""
        if self.vectorstore is None:
            raise RuntimeError("向量数据库未初始化,请先创建或加载")
        
        self.vectorstore.add_documents(documents)
        logger.info(f"添加了 {len(documents)} 个文档块")
    
    def get_retriever(self, search_kwargs: dict = None):
        """获取检索器"""
        if self.vectorstore is None:
            raise RuntimeError("向量数据库未初始化")
        
        default_search_kwargs = {"k": settings.TOP_K}
        if search_kwargs:
            default_search_kwargs.update(search_kwargs)
        
        return self.vectorstore.as_retriever(
            search_type="similarity",  # 基于相似度的检索
            search_kwargs=default_search_kwargs
        )
    
    def similarity_search(self, query: str, k: int = None) -> list[Document]:
        """直接执行相似度搜索"""
        if self.vectorstore is None:
            raise RuntimeError("向量数据库未初始化")
        
        k = k or settings.TOP_K
        return self.vectorstore.similarity_search(query, k=k)
    
    def similarity_search_with_score(self, query: str, k: int = None) -> list[tuple]:
        """带相似度分数的搜索"""
        if self.vectorstore is None:
            raise RuntimeError("向量数据库未初始化")
        
        k = k or settings.TOP_K
        return self.vectorstore.similarity_search_with_score(query, k=k)
    
    def delete_collection(self):
        """删除向量库集合"""
        if self.vectorstore:
            self.vectorstore.delete_collection()
            self.vectorstore = None
            logger.info("向量库已清空")

# 使用示例
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    from src.document_loader import DocumentLoader
    from src.text_splitter import TextSplitter
    
    # 加载文档
    loader = DocumentLoader()
    docs = loader.load_directory("./data/knowledge_base")
    
    # 分割文档
    splitter = TextSplitter()
    splits = splitter.split_documents(docs)
    
    # 创建向量库
    vector_store = VectorStore()
    vector_store.create_vectorstore(splits)
    
    # 测试检索
    results = vector_store.similarity_search("产品的退换货政策是什么?")
    print(f"\n检索到 {len(results)} 个相关文档块")
    for i, doc in enumerate(results):
        print(f"\n--- 结果 {i+1} ---")
        print(doc.page_content[:200])

检索模块

检索质量直接影响最终回答的准确性,这里实现几种高级检索策略:

python

# src/retriever.py
from langchain.schema import Document
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from typing import Optional

class Retriever:
    """高级检索器,支持多种检索策略"""
    
    def __init__(self, vectorstore, documents: list[Document] = None):
        self.vectorstore = vectorstore
        self.documents = documents
    
    def get_basic_retriever(self, search_kwargs: dict = None):
        """获取基础向量检索器"""
        return self.vectorstore.as_retriever(
            search_type="similarity",
            search_kwargs=search_kwargs or {"k": 4}
        )
    
    def get_ensemble_retriever(self, weight_vector: float = 0.5, weight_bm25: float = 0.5):
        """获取融合检索器(向量检索 + BM25关键词检索)"""
        if not self.documents:
            raise ValueError("融合检索需要提供原始文档列表")
        
        # 向量检索器
        vector_retriever = self.get_basic_retriever()
        
        # BM25关键词检索器
        bm25_retriever = BM25Retriever.from_documents(self.documents)
        bm25_retriever.k = 4
        
        # 融合两种检索结果
        ensemble_retriever = EnsembleRetriever(
            retrievers=[vector_retriever, bm25_retriever],
            weights=[weight_vector, weight_bm25]  # 权重可调整
        )
        
        return ensemble_retriever
    
    def get_contextual_compression_retriever(self, base_retriever, compressor):
        """获取上下文压缩检索器,减少检索结果中的冗余内容"""
        from langchain.retrievers import ContextualCompressionRetriever
        
        return ContextualCompressionRetriever(
            base_compressor=compressor,
            base_retriever=base_retriever
        )

class SearchResultFormatter:
    """检索结果格式化工具"""
    
    @staticmethod
    def format_results(results: list[Document], max_content_length: int = 500) -> str:
        """格式化检索结果,便于查看和调试"""
        if not results:
            return "未找到相关结果"
        
        formatted = []
        for i, doc in enumerate(results, 1):
            content = doc.page_content
            # 截断过长内容
            if len(content) > max_content_length:
                content = content[:max_content_length] + "..."
            
            source = doc.metadata.get("source", "未知来源")
            formatted.append(
                f"【结果 {i}】来源: {source}\n"
                f"内容: {content}\n"
            )
        
        return "\n".join(formatted)
    
    @staticmethod
    def extract_sources(results: list[Document]) -> list[str]:
        """提取检索结果的所有来源"""
        sources = []
        for doc in results:
            source = doc.metadata.get("source", "未知")
            if source not in sources:
                sources.append(source)
        return sources

# 使用示例
if __name__ == "__main__":
    from src.vector_store import VectorStore
    
    vector_store = VectorStore()
    vector_store.load_vectorstore()
    
    retriever_manager = Retriever(vector_store.vectorstore)
    
    # 获取基础检索器
    retriever = retriever_manager.get_basic_retriever()
    
    # 执行检索
    query = "如何申请产品退换货?"
    docs = retriever.get_relevant_documents(query)
    
    # 格式化输出
    formatter = SearchResultFormatter()
    print(formatter.format_results(docs))
    print(f"\n涉及来源: {formatter.extract_sources(docs)}")

问答链模块

整合检索和生成,构建完整的RAG问答链:

python

# src/chain.py
from langchain_openai import ChatOpenAI
from langchain_ollama import ChatOllama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain.schema import BaseRetriever
from config.settings import settings
import logging

logger = logging.getLogger(__name__)

class QAChain:
    """问答链管理器"""
    
    def __init__(self, retriever: BaseRetriever):
        self.retriever = retriever
        self.llm = self._create_llm()
        self.qa_chain = self._create_qa_chain()
    
    def _create_llm(self):
        """创建大模型实例"""
        provider = settings.LLM_PROVIDER.lower()
        
        if provider == "openai":
            return ChatOpenAI(
                model=settings.OPENAI_MODEL,
                api_key=settings.OPENAI_API_KEY,
                base_url=settings.OPENAI_BASE_URL,
                temperature=0.3,  # 较低的随机性,保持回答一致性
                max_tokens=1000   # 限制回答长度
            )
        elif provider == "ollama":
            return ChatOllama(
                base_url=settings.OLLAMA_BASE_URL,
                model=settings.OLLAMA_MODEL,
                temperature=0.3,
                options={"num_ctx": 4096}  # 上下文窗口大小
            )
        elif provider == "zhipu":
            # 智谱AI配置
            return ChatOpenAI(
                model="glm-4",
                api_key=settings.OPENAI_API_KEY,
                base_url="https://open.bigmodel.cn/api/paas/v4",
                temperature=0.3
            )
        else:
            raise ValueError(f"不支持的大模型提供商: {provider}")
    
    def _create_qa_chain(self) -> RetrievalQA:
        """创建问答链"""
        
        # 自定义提示词模板,引导模型基于检索结果回答
        prompt_template = """你是一个企业知识库问答助手。请根据以下检索到的文档内容,回答用户的问题。

如果检索结果中没有相关信息,请直接说明"抱歉,知识库中暂无相关内容",不要编造答案。

如果检索结果中有相关信息,请基于内容给出准确回答,并在回答末尾注明参考来源。

---
检索到的文档内容:
{context}
---

用户问题: {question}

请给出回答:"""
        
        PROMPT = PromptTemplate(
            template=prompt_template,
            input_variables=["context", "question"]
        )
        
        # 创建检索问答链
        qa_chain = RetrievalQA.from_chain_type(
            llm=self.llm,
            chain_type="stuff",  # 将所有检索结果拼接后一次输入
            retriever=self.retriever,
            return_source_documents=True,  # 返回参考文档
            chain_type_kwargs={
                "prompt": PROMPT,
                "document_variable_name": "context"
            },
            verbose=True
        )
        
        return qa_chain
    
    def ask(self, question: str) -> dict:
        """向知识库提问"""
        logger.info(f"收到问题: {question}")
        
        result = self.qa_chain({"query": question})
        
        return {
            "answer": result["result"],
            "source_documents": result["source_documents"]
        }
    
    def ask_with_sources(self, question: str) -> str:
        """提问并格式化返回带来源的回答"""
        result = self.ask(question)
        
        answer = result["answer"]
        sources = result["source_documents"]
        
        # 添加参考来源
        if sources:
            source_names = list(set([doc.metadata.get("source", "未知") for doc in sources]))
            answer += f"\n\n📚 参考来源: {', '.join(source_names)}"
        
        return answer

# 使用示例
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    from src.vector_store import VectorStore
    from src.retriever import Retriever
    
    # 加载向量库
    vector_store = VectorStore()
    vector_store.load_vectorstore()
    
    # 创建检索器
    retriever_manager = Retriever(vector_store.vectorstore)
    retriever = retriever_manager.get_basic_retriever()
    
    # 创建问答链
    qa_chain = QAChain(retriever)
    
    # 提问测试
    question = "我们公司的产品质保期是多久?"
    answer = qa_chain.ask_with_sources(question)
    
    print(f"\n问题: {question}")
    print(f"\n回答:\n{answer}")

主程序入口

整合所有模块,提供命令行交互界面:

python

# src/main.py
import argparse
import logging
from pathlib import Path

from config.settings import settings
from src.document_loader import DocumentLoader
from src.text_splitter import TextSplitter
from src.vector_store import VectorStore
from src.retriever import Retriever, SearchResultFormatter
from src.chain import QAChain

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class KnowledgeBaseQA:
    """企业知识库问答系统主类"""
    
    def __init__(self, knowledge_base_path: str = None):
        self.knowledge_base_path = knowledge_base_path or "./data/knowledge_base"
        self.vector_store = None
        self.qa_chain = None
        self.loader = DocumentLoader()
        self.splitter = TextSplitter()
        self.retriever_manager = None
    
    def initialize(self, rebuild: bool = False):
        """初始化系统"""
        print("=" * 50)
        print("🏢 企业知识库智能问答系统")
        print("=" * 50)
        
        # 检查或创建向量库
        vector_db_path = Path(settings.CHROMA_PERSIST_DIRECTORY)
        
        if rebuild or not vector_db_path.exists():
            logger.info("正在构建向量数据库...")
            self._build_vectorstore()
        else:
            logger.info("正在加载已有向量数据库...")
            self.vector_store = VectorStore()
            self.vector_store.load_vectorstore()
        
        # 创建检索和问答链
        self.retriever_manager = Retriever(self.vector_store.vectorstore)
        retriever = self.retriever_manager.get_basic_retriever()
        self.qa_chain = QAChain(retriever)
        
        print("✅ 系统初始化完成!")
        print(f"📁 知识库路径: {self.knowledge_base_path}")
        print(f"💬 大模型: {settings.LLM_PROVIDER} - {getattr(settings, f'{settings.LLM_PROVIDER.upper()}_MODEL', 'N/A')}")
        print("-" * 50)
    
    def _build_vectorstore(self):
        """构建向量数据库"""
        # 加载文档
        print("📂 正在加载文档...")
        documents = self.loader.load_directory(self.knowledge_base_path)
        
        if not documents:
            print("⚠️  未找到任何文档,请先将文档放入知识库目录!")
            print(f"   路径: {self.knowledge_base_path}")
            return
        
        print(f"   已加载 {len(documents)} 个文档块")
        
        # 分割文档
        print("✂️  正在分割文档...")
        splits = self.splitter.split_documents(documents)
        print(f"   分割为 {len(splits)} 个文本块")
        
        # 创建向量库
        print("🔢 正在生成向量并存储...")
        self.vector_store = VectorStore()
        self.vector_store.create_vectorstore(splits)
    
    def chat(self, question: str) -> str:
        """回答问题"""
        if not self.qa_chain:
            return "系统未初始化,请先运行 initialize()"
        
        try:
            return self.qa_chain.ask_with_sources(question)
        except Exception as e:
            logger.error(f"问答失败: {str(e)}")
            return f"抱歉,处理您的问题时出现错误: {str(e)}"
    
    def interactive(self):
        """交互式问答"""
        print("\n💡 输入您的问题,按回车发送。输入 'quit' 或 'exit' 退出。")
        print("💡 输入 'reload' 重新加载知识库。\n")
        
        while True:
            try:
                question = input("👤 您: ").strip()
                
                if question.lower() in ['quit', 'exit', 'q']:
                    print("\n👋 再见!")
                    break
                
                if question.lower() == 'reload':
                    self.initialize(rebuild=True)
                    continue
                
                if not question:
                    continue
                
                print("\n🤖 AI: ", end="")
                answer = self.chat(question)
                print(answer)
                print()
                
            except KeyboardInterrupt:
                print("\n\n👋 再见!")
                break
            except Exception as e:
                print(f"\n❌ 错误: {str(e)}")


def main():
    parser = argparse.ArgumentParser(description="企业知识库智能问答系统")
    parser.add_argument("--path", "-p", type=str, default="./data/knowledge_base",
                        help="知识库文档目录路径")
    parser.add_argument("--rebuild", "-r", action="store_true",
                        help="强制重建向量数据库")
    
    args = parser.parse_args()
    
    # 创建系统实例
    qa_system = KnowledgeBaseQA(knowledge_base_path=args.path)
    qa_system.initialize(rebuild=args.rebuild)
    qa_system.interactive()


if __name__ == "__main__":
    main()

运行与测试

准备测试数据

data/knowledge_base/目录下放置一些测试文档,例如创建product_faq.txt

plaintext

产品退换货政策

1. 退换货时限
自收到商品之日起7天内可以申请退换货,15天内可以申请换货。

2. 退换货条件
- 商品未被人为损坏
- 包装配件完整
- 附带购买凭证

3. 退换货流程
登录官网 -> 进入"我的订单" -> 选择订单 -> 点击"申请退换货" -> 填写原因 -> 提交审核

4. 运费说明
- 因质量问题退换货:由我们承担运费
- 因个人原因退换货:需自行承担运费

启动系统

Windows系统:

powershell

cd ai-knowledge-qa
.\venv\Scripts\activate
python src/main.py --path ./data/knowledge_base

macOS/Linux系统:

bash

cd ai-knowledge-qa
source venv/bin/activate
python src/main.py --path ./data/knowledge_base

测试示例

系统启动后,输入以下问题进行测试:

plaintext

👤 您: 产品的退换货政策是什么?

🤖 AI: 根据知识库中的信息,我们公司的产品退换货政策如下:

1. **退换货时限**:自收到商品之日起7天内可申请退换货,15天内可申请换货。

2. **退换货条件**:
   - 商品未被人为损坏
   - 包装配件完整
   - 附带购买凭证

3. **退换货流程**:登录官网 → 进入"我的订单" → 选择订单 → 点击"申请退换货" → 填写原因 → 提交审核

4. **运费说明**:
   - 因质量问题退换货由我们承担运费
   - 因个人原因退换货需自行承担运费

📚 参考来源: product_faq.txt

性能优化建议

检索优化

向量检索的关键参数是top_k值和similarity_threshold。如果检索结果噪声过多,可以提高检索数量后通过重排序(rerank)筛选最相关的几条。中文场景建议使用专门的中文Embedding模型,如moka-ai/m3e-base,能显著提升中文语义理解效果。

回答质量优化

通过调整prompt模板可以改善回答质量。增加”请分点回答”等约束让回答更有条理,添加”如果不确定请明确说明”可以减少幻觉问题。对于专业术语较多的行业,可以在prompt中加入领域背景信息。

大模型选择

在线模型(如GPT-4o、通义千问)效果较好但有API成本,本地模型(如Llama 3、Qwen2)可以离线使用但需要较高硬件配置。建议开发测试阶段使用在线模型,生产部署根据数据敏感性选择本地模型或私有化部署。

扩展功能

添加流式输出

修改QAChain以支持流式输出,提升用户体验:

python

def ask_stream(self, question: str):
    """流式问答"""
    from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
    
    # 创建流式回调处理器
    streaming_handler = StreamingStdOutCallbackHandler()
    
    # 创建支持流式输出的链
    streaming_chain = RetrievalQA.from_chain_type(
        llm=self.llm,
        chain_type="stuff",
        retriever=self.retriever,
        return_source_documents=True,
        callbacks=[streaming_handler]
    )
    
    return streaming_chain({"query": question})

支持多轮对话

集成对话记忆功能,实现多轮上下文理解:

python

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain

def create_conversational_chain(retriever):
    """创建支持多轮对话的检索链"""
    
    memory = ConversationBufferMemory(
        memory_key="chat_history",
        return_messages=True,
        output_key="answer"
    )
    
    condense_question_prompt = """给定对话历史和用户问题,重新组织成一个独立、完整的问题。

对话历史:
{chat_history}

用户问题: {question}

独立完整的问题:"""
    
    CONDENSE_QUESTION_PROMPT = PromptTemplate(
        template=condense_question_prompt,
        input_variables=["chat_history", "question"]
    )
    
    qa_chain = ConversationalRetrievalChain.from_llm(
        llm=self.llm,
        retriever=retriever,
        memory=memory,
        condense_question_prompt=CONDENSE_QUESTION_PROMPT,
        return_source_documents=True
    )
    
    return qa_chain

总结

通过本文的实战项目,你已经掌握了构建企业级RAG知识库问答系统的完整技能。从文档处理、文本分割、向量化存储到检索问答,每个环节都有深入讲解。这个系统可以灵活扩展,支持多种文档格式、多种大模型,以及流式输出、多轮对话等高级功能。

关键要点回顾:使用LangChain作为核心框架简化开发流程;Chroma作为轻量级向量库便于快速原型开发;合理的文本分割策略直接影响检索效果;通过prompt工程可以优化回答质量和格式;根据实际场景选择在线API或本地部署的大模型。

希望这个项目能帮助你在实际工作中快速落地AI应用,如果你在实践过程中遇到问题,欢迎在评论区交流讨论。代码已同步到GitHub,可以直接clone下来运行测试。

你学会了吗?动手试试构建自己的知识库问答系统吧!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注