用 LangChain 搭建本地知识库问答

用 LangChain 搭建本地知识库问答

企业有 1000+ 文档,员工找个信息要花半天?用 LangChain 搭建知识库问答系统,3 分钟搞定!


为什么需要知识库问答?

上个月给一个公司做培训,他们 HR 说:

"员工天天问同样的问题:请假流程、报销标准、五险一金..."

我花了 2 天,用 LangChain 搭了个知识库问答机器人。

现在员工有问题直接问 AI,3 秒钟出答案,还能告诉你出自哪个文档。

HR 小姐姐说效率提升了 10 倍不止。


🛠️ 环境准备

1. 安装依赖

bash 复制代码
pip install langchain langchain-openai langchain-community chromadb pypdf python-docx

2. 准备文档

创建 docs/ 文件夹,放入你的文档:

复制代码
docs/
├── 员工手册.pdf
├── 报销流程.docx
├── 请假制度.txt
└── ...

3. 配置 API Key

.env 文件:

bash 复制代码
OPENAI_API_KEY=sk-xxxxxxxx
# 或用 DeepSeek(推荐,便宜 10 倍)
DEEPSEEK_API_KEY=sk-xxxxxxxx

💻 核心代码

场景 1:加载多种格式文档

python 复制代码
from langchain_community.document_loaders import (
    PyPDFLoader,
    Docx2txtLoader,
    TextLoader,
    DirectoryLoader
)

def load_documents(folder_path):
    """加载文件夹里的所有文档"""
    print(f"📚 正在加载 {folder_path} 里的文档...")
    
    # 加载 PDF
    pdf_loader = DirectoryLoader(
        folder_path,
        glob="**/*.pdf",
        loader_cls=PyPDFLoader
    )
    pdf_docs = pdf_loader.load()
    print(f"✅ 加载了 {len(pdf_docs)} 个 PDF 页面")
    
    # 加载 Word
    docx_loader = DirectoryLoader(
        folder_path,
        glob="**/*.docx",
        loader_cls=Docx2txtLoader
    )
    docx_docs = docx_loader.load()
    print(f"✅ 加载了 {len(docx_docs)} 个 Word 文档")
    
    # 加载 TXT
    txt_loader = DirectoryLoader(
        folder_path,
        glob="**/*.txt",
        loader_cls=TextLoader
    )
    txt_docs = txt_loader.load()
    print(f"✅ 加载了 {len(txt_docs)} 个 TXT 文件")
    
    # 合并所有文档
    all_docs = pdf_docs + docx_docs + txt_docs
    print(f"📊 总共加载 {len(all_docs)} 个文档片段")
    
    return all_docs

if __name__ == '__main__':
    docs = load_documents('docs')

踩坑提醒:

  • PDF 加载慢?用 PyPDFLoaderPDFMinerLoader 快 10 倍
  • Word 乱码?确保是 .docx 格式,老 .doc 不支持
  • 中文乱码?TXT 文件加 encoding='utf-8'

场景 2:智能分割文档

python 复制代码
from langchain.text_splitter import (
    CharacterTextSplitter,
    RecursiveCharacterTextSplitter,
    ChineseTextSplitter
)

def split_documents(docs):
    """智能分割文档"""
    print("✂️ 正在分割文档...")
    
    # 方案 1:按字符分割(适合英文)
    # splitter = CharacterTextSplitter(
    #     chunk_size=1000,
    #     chunk_overlap=200
    # )
    
    # 方案 2:递归分割(推荐,适合混合内容)
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
        length_function=len,
        separators=["\n\n", "\n", "。", "!", "?", ";", " "]
    )
    
    # 方案 3:中文专用(最推荐)
    # splitter = ChineseTextSplitter(
    #     chunk_size=1000,
    #     chunk_overlap=200
    # )
    
    split_docs = splitter.split_documents(docs)
    print(f"✅ 分割完成,共 {len(split_docs)} 个片段")
    
    # 显示第一个片段示例
    print(f"\n📝 片段示例:\n{split_docs[0].page_content[:200]}...")
    
    return split_docs

if __name__ == '__main__':
    from langchain_community.document_loaders import TextLoader
    loader = TextLoader('docs/员工手册.txt', encoding='utf-8')
    docs = loader.load()
    split_docs = split_documents(docs)

分割参数调优:

  • chunk_size: 每个片段的大小(500-2000)
  • chunk_overlap: 片段重叠部分(100-300)
  • 重叠越多,上下文越完整,但向量越多

场景 3:创建向量索引

python 复制代码
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
import os
from dotenv import load_dotenv

load_dotenv()

def create_vector_index(docs):
    """创建向量索引"""
    print("🔧 正在创建向量索引...")
    
    # 初始化 Embedding 模型
    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-small",  # 便宜又快
        # model="text-embedding-ada-002"  # 旧版
    )
    
    # 创建向量存储(本地)
    vectorstore = Chroma.from_documents(
        documents=docs,
        embedding=embeddings,
        persist_directory="./chroma_db"  # 保存到本地
    )
    
    print(f"✅ 向量索引创建完成")
    print(f"📊 共 {vectorstore._collection.count()} 个向量")
    
    return vectorstore

if __name__ == '__main__':
    # 假设已有 split_docs
    vectorstore = create_vector_index(split_docs)

踩坑记录:

  • 第一次慢?正常,1000 页文档大概 5-10 分钟
  • 第二次快?有缓存,秒建
  • 想删除重建?rm -rf chroma_db

场景 4:智能问答(带引用)

python 复制代码
from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

def create_qa_bot(vectorstore):
    """创建问答机器人"""
    print("🤖 正在初始化问答机器人...")
    
    # 初始化 LLM
    llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
    
    # 自定义提示词模板
    template = """
你是一个专业的知识库助手,根据提供的文档片段回答用户问题。

【要求】
1. 只根据提供的文档片段回答,不要瞎编
2. 如果文档里没有答案,就说"文档中没有相关信息"
3. 回答要简洁明了
4. 最后要注明引用来源

【文档片段】
{context}

【用户问题】
{question}

请用中文回答:
"""

    prompt = PromptTemplate(
        template=template,
        input_variables=["context", "question"]
    )
    
    # 创建问答链
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        retriever=vectorstore.as_retriever(
            search_type="similarity",  # 相似度搜索
            search_kwargs={"k": 3}     # 返回最相关的 3 个片段
        ),
        chain_type="stuff",
        return_source_documents=True   # 返回引用来源
    )
    
    print("✅ 问答机器人已启动")
    return qa_chain

def chat_with_bot(qa_chain):
    """与机器人对话"""
    print("\n💬 开始对话(输入 quit 退出)\n")
    
    while True:
        question = input("👤 你:")
        if question.lower() == 'quit':
            print("👋 再见!")
            break
        
        # 获取答案
        result = qa_chain({"query": question})
        
        # 显示答案
        print(f"🤖 AI: {result['result']}\n")
        
        # 显示引用来源
        if result['source_documents']:
            print("📚 引用来源:")
            for i, doc in enumerate(result['source_documents'], 1):
                source = doc.metadata.get('source', '未知')
                page = doc.metadata.get('page', '')
                print(f"  {i}. {source}{' 第' + str(page) + '页' if page else ''}")
                print(f"     内容:{doc.page_content[:100]}...")
            print()

if __name__ == '__main__':
    # 加载向量存储
    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma(
        persist_directory="./chroma_db",
        embedding_function=embeddings
    )
    
    # 创建机器人
    qa_chain = create_qa_bot(vectorstore)
    
    # 开始对话
    chat_with_bot(qa_chain)

运行效果:

复制代码
👤 你:请假流程是什么?

🤖 AI: 根据员工手册,请假流程如下:
1. 提前 3 天在 OA 系统提交申请
2. 直属领导审批
3. HR 备案
4. 紧急情况可电话请假,事后补手续

📚 引用来源:
  1. docs/员工手册.pdf 第 15 页
     内容:第三章 考勤管理 第二节 请假流程...
  2. docs/请假制度.txt
     内容:员工请假需提前申请,经领导批准后方可休假...

🎯 完整项目:企业知识库系统

python 复制代码
"""
企业知识库问答系统
功能:
1. 支持 PDF/Word/TXT 多种格式
2. 智能分割和索引
3. 问答带引用来源
4. 支持多轮对话
5. 可增量更新文档
"""

from langchain_community.document_loaders import (
    PyPDFLoader, Docx2txtLoader, TextLoader, DirectoryLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain.memory import ConversationBufferMemory
import os
from dotenv import load_dotenv

load_dotenv()

class KnowledgeBaseQA:
    def __init__(self, docs_folder="docs", persist_dir="./chroma_db"):
        self.docs_folder = docs_folder
        self.persist_dir = persist_dir
        self.vectorstore = None
        self.qa_chain = None
        self.memory = ConversationBufferMemory(
            return_messages=True
        )
    
    def load_and_index(self):
        """加载文档并创建索引"""
        print("📚 步骤 1: 加载文档...")
        
        # 加载所有文档
        loaders = [
            DirectoryLoader(self.docs_folder, glob="**/*.pdf", loader_cls=PyPDFLoader),
            DirectoryLoader(self.docs_folder, glob="**/*.docx", loader_cls=Docx2txtLoader),
            DirectoryLoader(self.docs_folder, glob="**/*.txt", loader_cls=TextLoader)
        ]
        
        all_docs = []
        for loader in loaders:
            docs = loader.load()
            all_docs.extend(docs)
            print(f"  ✅ 加载 {len(docs)} 个文档")
        
        print(f"\n✂️ 步骤 2: 分割文档...")
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=200,
            separators=["\n\n", "\n", "。", "!", "?", ";", " "]
        )
        split_docs = splitter.split_documents(all_docs)
        print(f"  ✅ 分割为 {len(split_docs)} 个片段")
        
        print(f"\n🔧 步骤 3: 创建向量索引...")
        embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
        
        self.vectorstore = Chroma.from_documents(
            documents=split_docs,
            embedding=embeddings,
            persist_directory=self.persist_dir
        )
        
        print(f"  ✅ 索引创建完成,共 {self.vectorstore._collection.count()} 个向量")
        
        # 创建问答链
        self._create_qa_chain()
    
    def _create_qa_chain(self):
        """创建问答链"""
        llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
        
        template = """
你是一个专业的企业知识库助手。

【要求】
1. 只根据提供的文档回答,不要编造
2. 如果文档中没有答案,明确告知用户
3. 回答要简洁、准确、专业
4. 最后注明引用来源

【对话历史】
{history}

【文档内容】
{context}

【用户问题】
{question}

请用中文回答:
"""

        prompt = PromptTemplate(
            template=template,
            input_variables=["history", "context", "question"]
        )
        
        self.qa_chain = RetrievalQA.from_chain_type(
            llm=llm,
            retriever=self.vectorstore.as_retriever(search_kwargs={"k": 3}),
            chain_type="stuff",
            return_source_documents=True
        )
    
    def chat(self, question):
        """对话"""
        if not self.qa_chain:
            return "⚠️ 请先调用 load_and_index() 加载文档"
        
        result = self.qa_chain({"query": question})
        
        response = {
            'answer': result['result'],
            'sources': []
        }
        
        # 提取引用来源
        for doc in result['source_documents']:
            source = {
                'file': doc.metadata.get('source', '未知'),
                'page': doc.metadata.get('page', ''),
                'content': doc.page_content[:200]
            }
            response['sources'].append(source)
        
        return response
    
    def update_documents(self):
        """增量更新文档"""
        print("🔄 增量更新文档...")
        # 简单实现:重新加载所有文档
        # 进阶:只加载新增/修改的文档
        self.load_and_index()
        print("✅ 更新完成")

def main():
    """主程序"""
    print("=" * 60)
    print("🤖 企业知识库问答系统")
    print("=" * 60)
    
    # 初始化系统
    kb = KnowledgeBaseQA(docs_folder="docs", persist_dir="./chroma_db")
    
    # 加载文档(第一次运行)
    kb.load_and_index()
    
    print("\n💬 开始对话(输入命令:help/quit/clear/update)\n")
    
    while True:
        user_input = input("👤 你:")
        
        if user_input.lower() == 'quit':
            print("👋 感谢使用,再见!")
            break
        elif user_input.lower() == 'help':
            print("""
📋 帮助:
- 直接输入问题,AI 会基于文档回答
- 输入 'clear' 清空对话历史
- 输入 'update' 重新加载文档
- 输入 'quit' 退出程序
            """)
            continue
        elif user_input.lower() == 'clear':
            kb.memory.clear()
            print("✅ 对话历史已清空")
            continue
        elif user_input.lower() == 'update':
            kb.update_documents()
            continue
        
        # 获取回答
        response = kb.chat(user_input)
        
        # 显示回答
        print(f"\n🤖 AI: {response['answer']}\n")
        
        # 显示引用
        if response['sources']:
            print("📚 引用来源:")
            for i, src in enumerate(response['sources'], 1):
                page_info = f" 第{src['page']}页" if src['page'] else ""
                print(f"  {i}. {src['file']}{page_info}")
            print()

if __name__ == '__main__':
    main()

⚠️ 血泪教训

  1. 文档格式要统一,编码用 UTF-8,不然中文乱码
  2. 分割参数要调优,太大丢失上下文,太小碎片化
  3. 向量数据库要持久化,不然每次重启都要重建
  4. 引用来源很重要,用户需要知道答案从哪来

💡 进阶玩法

1. 混合搜索(相似度 + 关键词)

python 复制代码
retriever = vectorstore.as_retriever(
    search_type="mmr",  # 最大边界相关
    search_kwargs={
        "k": 3,
        "fetch_k": 20,
        "lambda_mult": 0.5
    }
)

2. 多向量检索(Parent Document)

python 复制代码
from langchain.retrievers import ParentDocumentRetriever

parent_splitter = CharacterTextSplitter(chunk_size=2000, chunk_overlap=0)
child_splitter = CharacterTextSplitter(chunk_size=400, chunk_overlap=200)

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=InMemoryDocstore(),
    child_splitter=child_splitter,
    parent_splitter=parent_splitter
)

3. 增量更新

python 复制代码
# 只添加新文档
new_docs = loader.load()
new_splits = splitter.split_documents(new_docs)
vectorstore.add_documents(new_splits)

🌟 下篇预告

下一篇:《LangChain Memory 详解:让 AI 记住对话历史》

  • 5 种记忆类型详解
  • 长短期记忆结合
  • 对话摘要压缩
  • 企业级对话管理系统

觉得有用?点赞 + 关注不迷路! 🎉


标签: #LangChain #RAG #知识库 #AI #Python #向量数据库