夜雨聆风学习资料网

ARTICLE · 1030881

我把100份公司文档喂给AI,它比老员工还懂业务——用LangChain搭本地知识库全过程

我把100份公司文档喂给AI,它比老员工还懂业务——用LangChain搭本地知识库全过程

【嗨翻Python】零基础入门系列第22篇让AI基于你自己的数据回答问题。不是通用AI,是"只懂你们公司"的AI。

· · ·

问一个困扰每个新员工的问题:

"产品A的技术参数在哪份文档里?"

老员工说:"找一下以前的文档……大概在某个文件夹里……"

如果有一个AI,读完了公司所有文档,你问什么它都知道出处——你敢不敢用?

· · ·

01

PART

什么是RAG?一句话说清楚

普通AI聊天:你问它问题,它靠训练数据回答。(可能瞎编)

RAG:你问它问题,它先去你的文档里查,再基于查到的内容回答。(有据可查)

RAG = 检索增强生成。翻译成人话:让AI先翻书,再答题。

真实场景:

公司知识库:新人问制度流程,AI秒回+附文档出处

客服系统:用户问产品细节,AI从产品手册里找答案

个人学习:把你的笔记全扔进去,变成一个会回答问题的"第二大脑"

踩坑提醒: RAG不是万能的。文档质量差,AI回答就差。垃圾进,垃圾出,这条铁律永远有效。

· · ·

02

PART

安装依赖

bash

pip install langchain langchain-openai langchain-community

pip install chromadb     # 向量数据库(本地运行,不需要安装服务)

pip install pypdf        # 读取PDF

pip install sentence-transformers  # 本地Embedding模型(省钱方案)

踩坑记录: LangChain的API变动极快,2026年最新版本的import路径和一年前完全不同。如果代码报错ModuleNotFoundError,先pip install --upgrade langchain,然后去官方文档看最新import路径。

· · ·

03

PART

核心流程:4步搭起来

第1步:加载你的文档

python

from langchain_community.document_loaders import (

    PyPDFLoader,    # PDF

    TextLoader,     # TXT

    DirectoryLoader # 批量加载整个文件夹

)

# 加载单个PDF

loader = PyPDFLoader("公司手册.pdf")

docs = loader.load()

print(f"加载了 {len(docs)} 页")

# 输出示例:加载了 42 页

# 批量加载一整个文件夹的txt

loader = DirectoryLoader(

    "./公司文档",

    glob="**/*.txt",           # 匹配所有txt

    loader_cls=TextLoader,

    loader_kwargs={"encoding": "utf-8"}

)

docs = loader.load()

print(f"共加载 {len(docs)} 个文档")

踩坑记录:

PDF里的表格、图片不会自动提取。如果需要表格数据,用pdfplumber

中文编码问题:Windows下TextLoader默认用utf-8,如果你的文件是GBK编码,会报错。用encoding="gbk"或转成utf-8。

第2步:切块——把大文档切成小段

python

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(

    chunk_size=500,       # 每段大约500个字符

    chunk_overlap=50,     # 前后重叠50个字符(防止信息被切断)

    separators=["\n\n", "\n", "。", ".", " "]  # 按段落>句子>空格切

)

chunks = text_splitter.split_documents(docs)

print(f"分割成 {len(chunks)} 个片段")

# 输出示例:分割成 387 个片段

为什么要有`chunk_overlap`?

假设一段关键内容刚好在切分边界,被切成两半。有了重叠,两边各保留一部分,检索时至少有一半能被找到。

踩坑记录:

chunk_size不是越大越好。太大会引入噪声(不相关内容),太小会丢失上下文。500-1000是经验值。

如果文档是FAQ格式(一问一答),用RecursiveCharacterTextSplitter效果差,应该按Q&A对切分。

第3步:向量化——把文字变成数字

python

# 方案A:用OpenAI的Embedding(效果好,要花钱)

from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

# 方案B:用本地模型(免费,不需要联网,效果够用)

from langchain_community.embeddings import HuggingFaceEmbeddings

embeddings = HuggingFaceEmbeddings(

    model_name="shibing624/text2vec-base-chinese"  # 中文效果不错

)

# 创建向量数据库

from langchain_community.vectorstores import Chroma

vectorstore = Chroma.from_documents(

    documents=chunks,

    embedding=embeddings,

    persist_directory="./my_knowledge_base"  # 本地存储路径

)

vectorstore.persist()  # 保存到磁盘

print("向量数据库构建完成!")

Embedding的本质: 把每段文字变成一个数字向量(比如1536个数字组成的列表)。意思相近的文字,向量距离近。这样AI就能"搜索"到语义相关的内容,而不只是关键词匹配。

踩坑记录:

第一次用本地模型会下载模型文件(约400MB),需要等一会儿。

chromadb的数据库文件不要手动删除或移动,会损坏。

第4步:构建问答链——让AI基于检索结果回答

python

from langchain_openai import ChatOpenAI

from langchain.chains import RetrievalQA

from langchain.prompts import PromptTemplate

import os

# 初始化大模型

llm = ChatOpenAI(

    model="deepseek-chat",  # 用便宜的模型就够

    api_key=os.getenv("OPENAI_API_KEY"),

    base_url="https://api.deepseek.com/v1",

    temperature=0  # 问答要准确,温度设0

)

# 自定义Prompt——这一步决定回答质量

prompt_template = """基于以下已知信息回答用户问题。

如果已知信息中没有答案,请回答"根据现有资料,我无法回答这个问题"。

不要编造已知信息中没有的内容。

已知信息:

{context}

用户问题:{question}

回答:"""

PROMPT = PromptTemplate(

    template=prompt_template,

    input_variables=["context", "question"]

)

# 构建检索问答链

qa_chain = RetrievalQA.from_chain_type(

    llm=llm,

    chain_type="stuff",           # 把所有检索结果一起塞给AI

    retriever=vectorstore.as_retriever(

        search_kwargs={"k": 4}    # 检索最相关的4个片段

    ),

    return_source_documents=True,  # 返回引用的原始文档

    chain_type_kwargs={"prompt": PROMPT}

)

# 提问!

result = qa_chain.invoke({"query": "员工请假流程是什么?"})

print(f"回答:{result['result']}")

print(f"\n参考来源:")

for i, doc in enumerate(result['source_documents'], 1):

    source = doc.metadata.get('source', '未知')

    print(f"  {i}. {source}")

关键设置:`k=4`。这是检索4个最相关的片段。设太少信息不够,设太多会引入噪声。4-6是经验值。

· · ·

04

PART

完整工具:一个可复用的知识库类

python

import os

from pathlib import Path

from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader

from langchain.text_splitter import RecursiveCharacterTextSplitter

from langchain_community.vectorstores import Chroma

from langchain_openai import ChatOpenAI

from langchain.chains import RetrievalQA

from langchain.prompts import PromptTemplate

class LocalKnowledgeBase:

    """本地知识库——可复用工具

    用法:

        kb = LocalKnowledgeBase(docs_dir="./公司文档", db_dir="./kb_data")

        kb.build()           # 第一次:构建索引

        kb.load()            # 之后:直接加载已有索引

        answer = kb.ask("你的问题")

    """

    def __init__(self, docs_dir="./docs", db_dir="./kb_data"):

        self.docs_dir = docs_dir

        self.db_dir = db_dir

        self.llm = ChatOpenAI(

            model="deepseek-chat",

            api_key=os.getenv("OPENAI_API_KEY"),

            base_url="https://api.deepseek.com/v1",

            temperature=0

        )

        self.embeddings = None

        self.vectorstore = None

    def build(self):

        """从文档构建知识库索引"""

        # 1. 加载文档

        all_docs = []

        # 加载txt文件

        txt_loader = DirectoryLoader(

            self.docs_dir, glob="**/*.txt",

            loader_cls=TextLoader,

            loader_kwargs={"encoding": "utf-8"}

        )

        all_docs.extend(txt_loader.load())

        # 加载PDF文件

        for pdf_file in Path(self.docs_dir).rglob("*.pdf"):

            loader = PyPDFLoader(str(pdf_file))

            all_docs.extend(loader.load())

        if not all_docs:

            raise FileNotFoundError(f"在 {self.docs_dir} 中没有找到文档!")

        print(f"✓ 加载了 {len(all_docs)} 个文档/页面")

        # 2. 切块

        splitter = RecursiveCharacterTextSplitter(

            chunk_size=500,

            chunk_overlap=50,

            separators=["\n\n", "\n", "。", ".", " "]

        )

        chunks = splitter.split_documents(all_docs)

        print(f"✓ 分割成 {len(chunks)} 个片段")

        # 3. 向量化并存入数据库

        # 使用本地Embedding(省钱)

        from langchain_community.embeddings import HuggingFaceEmbeddings

        self.embeddings = HuggingFaceEmbeddings(

            model_name="shibing624/text2vec-base-chinese"

        )

        self.vectorstore = Chroma.from_documents(

            documents=chunks,

            embedding=self.embeddings,

            persist_directory=self.db_dir

        )

        self.vectorstore.persist()

        print(f"✓ 知识库构建完成!数据保存在 {self.db_dir}")

    def load(self):

        """加载已有的知识库索引"""

        from langchain_community.embeddings import HuggingFaceEmbeddings

        from langchain_community.vectorstores import Chroma

        self.embeddings = HuggingFaceEmbeddings(

            model_name="shibing624/text2vec-base-chinese"

        )

        self.vectorstore = Chroma(

            persist_directory=self.db_dir,

            embedding_function=self.embeddings

        )

        print("✓ 知识库加载完成")

    def ask(self, question, k=4):

        """向知识库提问,返回答案和来源"""

        if not self.vectorstore:

            raise RuntimeError("请先调用 build() 或 load() 初始化知识库")

        prompt_template = (

            "基于以下已知信息回答用户问题。\n"

            "如果已知信息中没有答案,请回答\"根据现有资料,我无法回答这个问题\"。\n"

            "不要编造已知信息中没有的内容。\n\n"

            "已知信息:\n{context}\n\n"

            "用户问题:{question}\n\n回答:"

        )

        PROMPT = PromptTemplate(

            template=prompt_template,

            input_variables=["context", "question"]

        )

        qa = RetrievalQA.from_chain_type(

            llm=self.llm,

            chain_type="stuff",

            retriever=self.vectorstore.as_retriever(

                search_kwargs={"k": k}

            ),

            return_source_documents=True,

            chain_type_kwargs={"prompt": PROMPT}

        )

        result = qa.invoke({"query": question})

        sources = []

        for doc in result.get('source_documents', []):

            sources.append(doc.metadata.get('source', '未知来源'))

        return {

            "answer": result['result'],

            "sources": list(set(sources))  # 去重

        }

# ===== 使用示例 =====

if __name__ == "__main__":

    kb = LocalKnowledgeBase(docs_dir="./公司文档", db_dir="./kb_data")

    # 首次使用:构建索引(只需要一次)

    kb.build()

    # 之后使用:加载索引(秒级)

    # kb.load()

    # 提问

    result = kb.ask("员工请假流程是什么?")

    print(f"回答:{result['answer']}")

    print(f"来源:{result['sources']}")

· · ·

05

PART

进阶:给知识库加上对话记忆

普通的RAG每次提问都是独立的。加上对话记忆,AI能记住上下文,实现"追问"。

python

from langchain.memory import ConversationBufferMemory

from langchain.chains import ConversationalRetrievalChain

def create_conversational_kb(vectorstore, llm):

    """带对话记忆的知识库"""

    memory = ConversationBufferMemory(

        memory_key="chat_history",

        return_messages=True,

        output_key="answer"

    )

    qa = ConversationalRetrievalChain.from_llm(

        llm=llm,

        retriever=vectorstore.as_retriever(search_kwargs={"k": 4}),

        memory=memory,

        return_source_documents=True,

        output_key="answer"

    )

    return qa

# 使用

# qa = create_conversational_kb(kb.vectorstore, kb.llm)

# result = qa.invoke({"question": "员工请假流程是什么?"})

# print(result["answer"])

# # 可以追问!AI会记住上一轮的上下文

# result = qa.invoke({"question": "那病假呢?"})

# print(result["answer"])  # AI知道你在问"病假的请假流程"

踩坑记录:

对话记忆存在内存里,程序重启就没了。生产环境要把chat_history存到数据库。

ConversationBufferMemory会无限增长,对话轮数多了会超出token限制。商业项目要用ConversationSummaryMemory(自动总结历史对话)。

· · ·

06

PART

真实场景案例

场景1:新人入职问答机器人

背景: 公司有50页的员工手册、20页的IT操作指南、各种制度文档。新人入职第一周全在翻文档。

搭建过程:

1

把所有文档放到./公司文档文件夹

2

运行kb.build(),等待约30秒

3

部署成一个简单的Web界面(用Gradio,5行代码)

python

import gradio as gr

kb = LocalKnowledgeBase(docs_dir="./公司文档")

kb.load()

def answer(question):

    result = kb.ask(question)

    answer = result["answer"]

    sources = "\n".join(f"- {s}" for s in result["sources"])

    return f"{answer}\n\n---\n参考来源:\n{sources}"

demo = gr.Interface(fn=answer, inputs="text", outputs="markdown")

demo.launch(share=True)  # share=True生成公网链接

效果: 新人问"报销流程是什么",AI 3秒回答+列出制度出处。第一周HR咨询量下降60%。

场景2:技术文档问答

背景: 开发团队维护一套API文档,新人经常问老员工基础问题。

踩坑记录:

技术文档里代码块很多,RecursiveCharacterTextSplitter会把代码块切碎。解决方案:用MarkdownTextSplitter按标题层级切分。

代码片段的Embedding效果差(语义不像自然语言),建议在代码块前面加一句自然语言描述。

· · ·

07

PART

成本和性能参考

项目数值
100页文档构建索引约30秒(本地Embedding)
每次提问的API费用约¥0.001(DeepSeek)
向量数据库大小100页文档约10MB
支持的最大文档量Chroma单机可支撑数十万片段

省钱秘诀: Embedding用本地模型(一次性下载400MB),LLM用DeepSeek(¥1/百万token)。100页文档问答一整天,API费用不超过1块钱。

· · ·

08

PART

常见错误排查

错误原因解决
ModuleNotFoundErrorLangChain版本不对pip install --upgrade langchain
Connection refusedChroma数据库文件损坏删除kb_data文件夹重建
回答质量差chunk_size不合适调整到500-1000试试
中文乱码文件编码不是utf-8encoding="gbk"或转码
回答瞎编Prompt没限制加上"不要编造"的指令

· · ·

09

PART

行动清单

[ ] 准备3-5个文档(txt或PDF),放到一个文件夹

[ ] 安装依赖:pip install langchain langchain-community chromadb sentence-transformers

[ ] 运行本文的LocalKnowledgeBase代码,构建你的第一个知识库

[ ] 试着问5个你的文档里有的问题,看看回答质量

[ ] 调整chunk_size(300/500/1000),对比回答质量差异

· · ·

10

PART

踩坑实录:我搭知识库踩过的6个坑

以下是真实项目中踩过的坑,每一个都浪费了至少半天时间。你看完能省一周。

坑1:Embedding模型选错了,花钱还效果差

第一次搭知识库,我用了OpenAI的text-embedding-ada-002。效果确实好,但——100页文档构建一次索引,Embedding费用大约$0.1。看着不多?如果你要每周更新文档,一个月就是$0.4。而且每次请求都要联网,公司文档里可能有机密信息,传到OpenAI的服务器上?安全合规那关过不去。

后来换了本地的shibing624/text2vec-base-chinese。效果差了大概5%(主观感受),但完全免费、完全离线。对于公司内部文档,本地模型是更安全的选择。

坑2:PDF里的表格提取出来全是乱的

有一个客户的FAQ文档,里面有50多个表格。用PyPDFLoader提取出来的内容,表格数据全乱了——本来是一行一行的数据,被拼接成了一坨文字。

解决方案:表格密集的文档,用pdfplumber替代pypdf

python

import pdfplumber

def extract_tables_from_pdf(pdf_path):

    """从PDF中提取表格,返回结构化数据"""

    tables_data = []

    try:

        with pdfplumber.open(pdf_path) as pdf:

            for page_num, page in enumerate(pdf.pages, 1):

                tables = page.extract_tables()

                for table in tables:

                    if table:

                        # 把表格转成文本格式,方便AI理解

                        for row in table:

                            clean_row = [str(cell).strip() if cell else "" for cell in row]

                            tables_data.append(" | ".join(clean_row))

    except Exception as e:

        print(f"提取表格失败(第{page_num}页):{e}")

    return tables_data

坑3:Chroma数据库文件被锁,删不掉也建不了

有一次调试代码,反复删除重建数据库。结果persist()的时候报错,说文件被占用。原因是上一次程序异常退出,Chroma的锁没释放。

解决方案: 每次用完后显式关闭连接,或者在重建前先清理:

python

import shutil

import os

def reset_knowledge_base(db_dir):

    """安全地重置知识库"""

    if os.path.exists(db_dir):

        try:

            shutil.rmtree(db_dir)

            print(f"✓ 已删除旧数据库:{db_dir}")

        except PermissionError:

            print("✗ 数据库文件被占用,请先关闭所有使用它的程序")

            raise

坑4:对话记忆越聊越慢,最后token爆了

加了对话记忆后,一开始体验很好,能追问。但聊了20轮之后,每次提问都要等10秒以上,有时候直接报错"token超限"。

原因:ConversationBufferMemory把所有历史对话都塞进了Prompt。20轮对话就是40条消息,加上检索到的文档,轻松超过模型的上下文限制。

解决方案: 用ConversationSummaryMemory,它会自动总结历史对话,只保留摘要:

python

from langchain.memory import ConversationSummaryMemory

# 替换之前的ConversationBufferMemory

memory = ConversationSummaryMemory(

    llm=llm,  # 需要一个LLM来做总结

    memory_key="chat_history",

    return_messages=True,

    output_key="answer"

)

成本多了多少? 每次总结大约消耗200 token,按DeepSeek的价格,每次对话多花¥0.0002。几乎可以忽略。

坑5:Windows下中文编码,永远踩不完的坑

Windows中文版默认编码是GBK,但Python默认用utf-8。你的txt文档可能是GBK,可能是utf-8带BOM,可能是utf-8不带BOM。

一个函数解决所有编码问题:

python

def read_file_auto_encoding(filepath):

    """自动检测编码读取文件"""

    encodings = ["utf-8-sig", "utf-8", "gbk", "gb2312", "gb18030"]

    for enc in encodings:

        try:

            with open(filepath, "r", encoding=enc) as f:

                content = f.read()

            return content

        except (UnicodeDecodeError, UnicodeError):

            continue

    raise ValueError(f"无法识别文件编码:{filepath}")

坑6:API调用的偶发超时,生产环境必须处理

知识库上线后,偶尔有用户反馈"问了问题半天没反应"。排查发现是DeepSeek的API偶尔会超时(大概2%的概率)。

解决方案:加重试机制,别让用户看到错误:

python

import time

def ask_with_retry(qa_chain, question, max_retries=3):

    """带重试的问答"""

    for attempt in range(max_retries):

        try:

            result = qa_chain.invoke({"query": question})

            return result

        except Exception as e:

            if attempt < max_retries - 1:

                wait = 2 ** attempt  # 指数退避:1秒、2秒、4秒

                print(f"第{attempt+1}次失败,{wait}秒后重试...")

                time.sleep(wait)

            else:

                return {"result": "抱歉,服务暂时不可用,请稍后重试。", 

                        "source_documents": []}

· · ·

11

PART

效率对比:人工翻文档 vs AI知识库

场景手动方式AI知识库效率提升
新人查一个制度流程翻文档30分钟提问3秒出答案+出处600倍
10个新人各自查同样的问题10×30分钟=5小时10×3秒=30秒600倍
老员工被问基础问题每次打断15分钟让新人问AI,不打断老员工无法量化
HR每天回答重复咨询2小时/天0(AI接管)每天省2小时

别人还在翻文档找答案的时候,你的AI 3秒钟就把答案+出处都给你了。这不是效率差距,是维度的差距。

感谢阅读,欢迎点赞、分享与收藏。

相关学习资料