ARTICLE · 1030881
我把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操作指南、各种制度文档。新人入职第一周全在翻文档。
搭建过程:
把所有文档放到./公司文档文件夹
运行kb.build(),等待约30秒
部署成一个简单的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
常见错误排查
| 错误 | 原因 | 解决 |
|---|---|---|
| ModuleNotFoundError | LangChain版本不对 | pip install --upgrade langchain |
| Connection refused | Chroma数据库文件损坏 | 删除kb_data文件夹重建 |
| 回答质量差 | chunk_size不合适 | 调整到500-1000试试 |
| 中文乱码 | 文件编码不是utf-8 | 用encoding="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秒钟就把答案+出处都给你了。这不是效率差距,是维度的差距。
感谢阅读,欢迎点赞、分享与收藏。