使用 LangChain 构建本地知识库问答系统

使用 LangChain 构建本地知识库问答系统

大语言模型虽然厉害,但在处理私人知识时总是差点意思。公司的内部文档、个人笔记、领域资料——这些内容模型根本记不住。RAG(检索增强生成)就是为了解决这个问题:先从你的知识库里找相关内容,再让AI根据这些资料来回答。

这篇文章会手把手教你用LangChain搭建一个本地知识库问答系统,不用花钱调用API,数据全都存在自己电脑上。

背景

先简单说说RAG的原理。传统LLM回答问题靠的是训练时学的知识,有截止日期,而且不可能涵盖你的私人数据。你问它公司内网的东西,它就开始编,说的有鼻子有眼但根本不对,这就是所谓的幻觉问题。

RAG的做法是:用户提问 → 系统去知识库检索相关文档 → 把检索到的内容连同问题一起发给LLM → LLM根据提供的资料生成回答。这样做的好处是回答有据可查,而且可以处理任何私人知识。

本地部署RAG的好处很实在:数据不离开你的电脑,不用付API调用费,一切都由自己控制,响应速度也快。

问题

实际做的时候会遇到几个麻烦:

一是文档格式杂。PDF、Word、Markdown、TXT,每种格式处理方式都不一样。二是向量模型选择多,到底用哪个最合适。三是检索效果不稳定,有时候明明有相关资料但就是检索不到。

LangChain提供了统一的抽象层,对接各种文档加载器、向量存储和语言模型,用起来比较省心。

实现步骤

1. 环境准备

创建Python虚拟环境后安装依赖:

pip install langchain langchain-community chromadb python-dotenv
pip install pypdf python-docx

需要安装的核心包就这些:langchain主框架、langchain-community社区集成、chromadb向量数据库、python-dotenv环境变量管理。根据你要处理的文档类型,再装对应的解析器。

2. 加载文档

文档加载是第一步。LangChain为不同格式提供了统一的Loader接口:

from langchain_community.document_loaders import TextLoader, PyPDFLoader, Docx2txtLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from pathlib import Path

def load_documents(directory: str):
    docs = []
    path = Path(directory)
    
    loaders = {
        .md: TextLoader,
        .txt: TextLoader,
        .pdf: PyPDFLoader,
        .docx: Docx2txtLoader,
    }
    
    for file_path in path.rglob("*"):
        if file_path.suffix.lower() in loaders:
            try:
                loader = loaders[file_path.suffix.lower()](str(file_path))
                docs.extend(loader.load())
                print(f"✓ 加载: {file_path.name}")
            except Exception as e:
                print(f"✗ 加载失败 {file_path.name}: {e}")
    
    return docs

这段代码会遍历指定目录,支持递归读取子文件夹,根据文件扩展名选择对应的加载器。

文档加载进来后通常太长,直接送入LLM会超限。需要把文档拆分成小块:

def split_documents(documents, chunk_size=500, chunk_overlap=50):
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        length_function=len,
        separators=["\n\n", "\n", "。", " ", ""]
    )
    
    splits = text_splitter.split_documents(documents)
    print(f"拆分为 {len(splits)} 个文本块")
    return splits

chunk_size和chunk_overlap两个参数决定了拆分颗粒度。chunk_size太小会破坏语义完整性,太大则可能引入过多无关信息。chunk_overlap表示相邻块之间的重叠字符数,设置重叠可以避免关键信息被切断。

3. 向量存储

将文本块转为向量并存入向量数据库。Chroma是开源的嵌入式向量库,不需要额外部署服务:

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma

def create_vector_store(splits, persist_directory="./vector_store"):
    embeddings = HuggingFaceEmbeddings(
        model_name="sentence-transformers/all-MiniLM-L6-v2",
        model_kwargs={device: cpu}
    )
    
    vectorstore = Chroma.from_documents(
        documents=splits,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    
    return vectorstore

这里用的是sentence-transformers/all-MiniLM-L6-v2嵌入模型,免费开源,在效果和速度之间比较平衡。有GPU的话把device改成cuda会快很多。

配置检索器:

def get_retriever(vectorstore, top_k=4):
    return vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": top_k}
    )

top_k参数控制返回最相关的前几个结果。

4. LLM集成

接入大语言模型。这里用Ollama提供的本地模型:

from langchain_community.chat_models import ChatOllama
from langchain import hub

def setup_qa_chain(vectorstore):
    llm = ChatOllama(model="llama3", temperature=0.7)
    
    # 从LangChain Hub加载现成的提示模板
    prompt = hub.pull("rlm/rag-prompt-llama3")
    
    # 用LCEL组合各组件
    qa_chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )
    
    return qa_chain

LCEL是LangChain的表达语言,用管道操作符把检索、格式化、prompt、LLM串起来。

5. 完整代码

把各部分组装起来:

from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.chat_models import ChatOllama
from langchain import hub
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 1. 加载文档
loader = TextLoader("knowledge_base/*.md")
documents = loader.load()

# 2. 拆分文档
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
splits = splitter.split_documents(documents)

# 3. 创建向量存储
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = Chroma.from_documents(splits, embeddings, persist_directory="chroma_db")

# 4. 配置问答链
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
llm = ChatOllama(model="llama3")
prompt = hub.pull("rlm/rag-prompt-llama3")

qa_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 5. 提问
question = "这份文档中关于xxx的内容是什么?"
answer = qa_chain.invoke(question)
print(answer)

运行效果

配置好之后运行程序,向知识库提问就能得到基于本地文档的回答。系统会先检索相关片段,再让LLM根据这些资料生成回答。

比如知识库里有一份公司制度文档,你问”年假怎么休”,系统会找到休假相关的内容,结合问题生成准确回答,还能标出来自哪个文件。

检索结果带有相关度分数,可以据此过滤不相关的内容。高级一点还可以做reranking(重排序),用更精准的模型优化排序结果。

小结

按本文的步骤,你应该能搭起一个完整的本地RAG问答系统。整个系统跑在本地,不依赖外部API,数据安全有保障。可以部署在本地服务器上,也可以打包成分发。

后续可以优化的方向包括:尝试不同嵌入模型提升检索效果、混合关键词和语义检索、加入对话历史支持多轮问答、或者用更强大的本地模型改善回答质量。RAG只是让AI理解私有知识的第一步,后面玩法还有很多。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇