简介:这是一份面向AI开发者的本地知识库问答系统实践项目包,基于LangChain框架并结合ChatGLM-6B等系列大语言模型,实现针对私有文档的自动问答。资源共75个文件,压缩包约17.77MB,主要包含Python脚本、模型缓存与嵌入配置、Dockerfile、Markdown说明文档及演示图片等,覆盖文本分割、模型调用、向量检索、WebUI部署等核心环节,目录结构清晰,便于逐模块学习。目前已有935人学习下载。借助本项目,读者可掌握LangChain与ChatGLM的集成方式,理解本地知识库问答的完整流程,并获取可直接运行的工程骨架、离线部署说明与常见问题解答,适合用于快速搭建企业或个人私有知识库问答应用。
1. 资源与定位:LangChain + ChatGLM-6B 自动问答项目到底能做什么
LangChain 在本地知识库自动问答里被问得最多的一句话是:我自己的文档到底能不能直接对话?答案是可以,但中间要过的关卡不少。这份资源正好把整套链路完整落了一遍——用 LangChain 做编排,用 ChatGLM-6B 做生成模型,针对本地知识库实现自动问答,WebUI 和 CLI 两个入口都给了。我拆完之后的理解是:它的价值不在代码量,而在把 RAG 里最容易踩坑的文本切分、向量化、检索召回和 Prompt 拼接放在了一起,你改参数就能看到效果变化。适合两类人:一类是想快速搭起本地知识库问答 Demo 的工程师,另一类是已经用其他 LLM、想对照 LangChain 的抽象层做模型替换的熟手。先说结论:这套方案是检索增强生成,不是微调,知识库更新不用重新训练模型。
2. 选型分析:为什么是 LangChain + ChatGLM-6B,而不是微调或自训练
2.1 RAG 链路:Embedding 向量化、向量检索与大模型生成的闭环
本地知识库问答最容易被带偏的思路,是把所有文档拿去微调大模型,让它“记住”内容。实际工程里这条路基本走不通:文档每周更新,微调一次要几天;微调还会把模型的通用能力带偏,出现越调越笨的情况。所以主流方案是检索增强生成,也就是 RAG。这个资源包实现的就是这条链路,四个环节对应四个文件:
文档加载与清洗,把 txt、md、pdf、docx 读进来,去掉无效换行、网页标签和多余空白;文本切分,用 chinese_text_splitter.py 按中文标点把长文档切成 150 到 250 字左右的块;向量化与建库,由 embedding 模型把每个 chunk 转成向量写入向量库;检索与生成,提问时把问题向量化,检索 top_k 个相似 chunk,拼到 Prompt 里交给 ChatGLM-6B 生成答案。
RAG 和微调、纯关键词检索的差别,用一张表就能说清楚:
| 方案 | 知识更新成本 | 回答可解释性 | 需要的硬件 | 适用场景 |
|---|---|---|---|---|
| RAG(本资源) | 替换文档后重建向量库即可 | 高,能定位到原文片段 | 比微调低一个量级 | 企业知识库、合同问答、FAQ |
| 全参微调 | 每次重新训练,按天计算 | 低,模型内部权重不可见 | 高,6B 全参微调要多卡 A100 | 领域风格固化、任务单一 |
| 关键词检索 | 低 | 高 | 几乎无 | 精确匹配,无法处理语义改写 |
从这张表能看出,RAG 赢在“知识更新”和“硬件门槛”两个维度上。对大多数做内部知识库的团队来说,文档永远在变,今天加一份新制度,明天删一份旧版本,用 RAG 只需要重新向量化一次,花几分钟。而微调哪怕用 LoRA,跑一轮也要几个小时起,还要守着 loss 曲线看有没有过拟合。这也是我拿到这份资源包后先肯定它技术路线的理由:方向选对了,后面的工程细节才有讨论价值。
2.2 LangChain 的编排价值:chatglm_llm.py 为什么值得单独占一个文件
LangChain 这层抽象经常被人说成“黑匣子”,但它真正值钱的地方不在某个模型,而在于把 Document Loader、Text Splitter、Embeddings、Vector Store、Retriever、LLM 六类组件统一了接口。这个包里的 chatglm_llm.py 就是一个典型的模型适配层,它把 ChatGLM-6B 的生成接口包成 LangChain 的 LLM 类。这样上层写好的 RetrievalQA 链条不用关心底层是 ChatGLM 还是其他模型。
我按原文件逻辑简化一段,你看结构就明白了:
from langchain.llms.base import LLM from transformers import AutoModel, AutoTokenizer class ChatGLM(LLM): model_path: str = "/data/models/chatglm-6b" temperature: float = 0.7 top_p: float = 0.9 def _call(self, prompt: str, stop=None) -> str: response, _ = self.model.chat( self.tokenizer, prompt, history=[], temperature=self.temperature, top_p=self.top_p ) return response @property def _llm_type(self) -> str: return "chatglm-6b"这里的关键点是_call方法,LangChain 在链条执行到生成环节时会调用它,传入的是已经拼接好的完整 Prompt。history=[]表示单轮问答不保留历史,资源包里的 WebUI 版本会把历史传进来支持多轮对话。temperature和top_p控制生成随机性,事实类问答我一般调 0.1 到 0.3,防止模型自由发挥。
因为有了这层封装,后续换模型非常省事。想从 ChatGLM-6B 换成更大的 ChatGLM2-6B 或 Qwen 系列,只需要在这个文件里改模型加载和 chat 调用方式,上层检索链完全不用动。这也是 LangChain 在资源里存在的意义:编排比模型本身更值钱,模型是轮子,编排是底盘。
2.3 ChatGLM-6B 的硬件边界:INT4、FP16 怎么选
ChatGLM-6B 是 62 亿参数的中英双语模型,FP16 精度加载大约要 13GB 显存,量化到 INT4 之后可以压到 6GB 到 8GB。这个资源包能流行起来,很大程度是因为它给“显卡不够”的人留了条活路。我给的配置档位参考如下:
| 配置档位 | 显存需求 | 回答质量 | 典型用途 |
|---|---|---|---|
| FP16 全精度 | 约 13GB | 最佳 | 32GB 显存工作站、生产服务 |
| INT8 量化 | 约 8GB | 损失很小 | 24GB 显卡跑并发 |
| INT4 量化 | 约 6GB | 可接受 | 个人学习、16GB 显卡试跑 |
资源包里没有直接给现成的量化模型文件,但 README 和 faq.md 里说明了怎么用 transformers 的 load_in_8bit / load_in_4bit 参数加载。我在实际跑的时候,16GB 显存的卡用 INT4 加 text2vec 中文 embedding,WebUI 并发一个人用没问题。如果把 embedding 模型也放 GPU,显存会再往上走,这时候可以把 embedding 加载位置改成 CPU,向量化慢一点但不影响推理。
还有一个容易被忽略的点:即使显存够,也要看内存。ChatGLM-6B 的模型加载需要先把权重读进 CPU 内存再搬运到显存,深度学习工作站的 CPU 内存最好有 32GB 以上,否则加载过程会触发 OOM Killer。这个资源包里的 Dockerfile 和 OfflineDeploy.md 都提到了内存参数,我后面细说。
3. 环境落地:requirements、poetry、Docker 与模型仓的配置细节
3.1 依赖管理:requirements.txt 与 poetry.lock 怎么选
解压之后你会看到 requirements.txt、pyproject.toml、poetry.lock 同时存在。这不是冗余,而是给两种使用习惯的人分别准备的路径。pip 用户直接按 requirements.txt 装,poetry 用户可以用 pyproject.toml 锁定整套依赖环境。我一般调试阶段走 pip,因为快,迭代方便;做交付和复现别人环境时走 poetry,因为 lock 文件能把传递依赖也锁死,避免“在我机器上是好的”这种玄学问题。
推荐先建独立 conda 环境再装依赖,命令如下:
conda create -n langchain-chatglm python=3.9 conda activate langchain-chatglm cd LangChain-ChatGLM-Webui-master pip install -r requirements.txt如果网络到默认 PyPI 源比较慢,可以用清华镜像加速安装,这个不涉及任何额外网络工具,只是换了个源:
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simplePython 版本我建议锁 3.9。3.10 以上某些版本的 tokenizers 和 torch 组合会出兼容问题,3.8 又偏老,部分新依赖已经放弃。3.9 是这个资源包年代下最稳的档位。装完之后验证一下 torch 能不能看到 CUDA:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.device_count())"输出True 1才能继续往下走。如果这里输出 False,后面所有模型都会默认落到 CPU,慢到你怀疑人生。
3.2 模型文件与缓存路径:ModelScope 下载和本地模型路径
这个资源包里 modelscope_hub.py 的用途,是从 ModelScope 拉取 ChatGLM-6B 权重。model_cache 目录就是模型缓存根目录。我建议不要等第一次启动时让程序现场下载,而是先把模型下好,再指向本地路径。原因很简单:问答系统首次加载模型时要做几件耗时的事——下载权重、加载 tokenizer、初始化量化参数,这些事堆在一起,特别容易超时或显存抖动。
提前下载模型的脚本:
from modelscope import snapshot_download model_dir = snapshot_download( 'ZhipuAI/ChatGLM-6B', cache_dir='./model_cache' ) print(model_dir)代码里的第一参数是模型 ID,第二参数是缓存目录。执行之后会生成一个以模型 ID 命名的子目录,里面是权重、config.json 和 tokenizer 文件。启动 WebUI 时把路径指过去:
python app.py --model-path ./model_cache/ZhipuAI/ChatGLM-6B这里有个容易搞错的地方:ModelScope 下载的目录结构和 HuggingFace Hub 不完全一样,model_path要指到包含 config.json 的那一层,而不是外层缓存目录。如果你直接指到model_cache根目录,加载时会提示找不到模型配置文件,报错信息还不直观。我一般下完之后先看一眼目录层级再启动,省得白等几十分钟。
除了 ChatGLM-6B 本体,还需要中文 embedding 模型。资源默认是 text2vec-base-chinese,同样可以用 ModelScope 拉取。embedding 模型小,只有几百 MB,下载快,但别放到和 LLM 同一个 GPU 上跑,显存会打架。
3.3 Docker 部署:两个 Dockerfile 的分工
资源里有 Dockerfile 和 Dockerfile.Base 两个文件。Base 是基础镜像,用来预先装好 Python 环境和 CUDA 相关库;Dockerfile 是基于 Base 再打包项目代码和依赖。这么拆的好处是:基础镜像只要构建一次,项目代码改动时不用重新走一遍漫长的基础依赖安装。
构建命令如下:
docker build -f Dockerfile.Base -t langchain-chatglm-base:latest . docker build -f Dockerfile -t langchain-chatglm:latest .运行时,显存透传和模型目录挂载是两个最容易出错的地方。推荐参数:
docker run -d --gpus all \ -p 7860:7860 \ -v /data/models:/model_cache \ -e CUDA_VISIBLE_DEVICES=0 \ langchain-chatglm:latest--gpus all是把所有 GPU 暴露给容器,-v /data/models:/model_cache把宿主机已经下载好的模型目录挂载进容器,避免容器内重复下载几百 MB 到几 GB 的权重。CUDA_VISIBLE_DEVICES=0指定只用第一张卡,防止 WebUI 启动时把显存均摊到多卡上导致哪张卡都不够跑。
我用 Docker 跑这个包时最大的体会是:调试期别用 Docker,日志隔了一层,看报错费劲;确认稳定之后再用 Docker 交付给运维,环境一致性才体现出价值。资源里同时放两个 Dockerfile,说明作者自己也经历了这个从调试到交付的过程。
Docker 部署之外,OfflineDeploy.md 是给内网环境准备的。内网机器通常不能访问外部模型源,这时候的常规做法是:在一台能访问外网的机器上下好模型,打包后拷入内网,然后把model_path指向内网本地目录,同时保证代码里不会触发在线检查更新。这个文档把这部分步骤写得很清楚,我照着走一遍没有发现跳步。
4. 跑通 WebUI:核心链路拆解与 top_k、chunk_size 调参实录
4.1 启动入口与配置文件读取:app.py 做了什么
app.py 是 WebUI 的启动入口,常见实现方式是用 Gradio 封装,启动后默认监听 7860 端口。执行:
python app.py \ --model-path /data/models/chatglm-6b \ --embedding-model /data/models/text2vec-base-chinese \ --vector-store ./vector_store三个参数各管一段:model-path指向 ChatGLM-6B 权重目录,embedding-model指向中文向量模型,vector-store是向量库存放位置。启动日志里如果出现“loading model”和“creating embedding”两行,说明前两个环节已经过掉;接着看到“init vector store”,就等浏览器打开http://localhost:7860上传文档。
config.py 是整个项目的参数中枢,我把关键字段整理成下面这个模板,这也是我每次新起一个知识库项目时必改的地方:
MODEL_PATH = "/data/models/chatglm-6b" EMBEDDING_MODEL = "text2vec-base-chinese" CHUNK_SIZE = 250 OVERLAP_SIZE = 50 TOP_K = 4 SCORE_THRESHOLD = 0.5这些参数直接决定问答质量。CHUNK_SIZE是文本切块的目标长度,OVERLAP_SIZE是相邻 chunk 之间的重叠长度,TOP_K是召回的相似片段数,SCORE_THRESHOLD是相似度阈值,低于这个值的片段会被过滤掉。我在下面三个小节里逐个说它们怎么调。
4.2 中文文本分割:chinese_text_splitter.py 的 chunk 设定
文本切分是整个 RAG 链路里最容易被低估的一环。LangChain 自带的 CharacterTextSplitter 是按字符硬切的,对中文来说效果很差,经常一句话被劈成两半。这个资源包里的 chinese_text_splitter.py 专门解决了这个问题,它的核心逻辑是按中文句末标点做候选断点,再按sentence_size聚合。简化后大致是这样:
import re class ChineseTextSplitter: def __init__(self, pdf=False, sentence_size=250, overlap_size=50): self.pdf = pdf self.sentence_size = sentence_size self.overlap_size = overlap_size def split_text(self, text: str): if self.pdf: text = re.sub(r"\n{3,}", "\n", text) text = re.sub(r"[^\S\n]+", " ", text) parts = re.split(r"([。!?])", text) chunks, current = [], "" for i in range(0, len(parts) - 1, 2): sentence = parts[i] + parts[i + 1] if len(current) + len(sentence) > self.sentence_size and current: chunks.append(current) # 保留上一段末尾内容,避免语义在切点处被截断 current = current[-self.overlap_size:] + sentence else: current += sentence if current: chunks.append(current) return chunks这段代码里pdf=True时先把 PDF 抽取出的多余换行压缩成单个空格,因为 PDF 的文本层经常把同一段话拆成多行,不处理的话分句会碎掉。re.split(r"([。!?])", text)保留分隔符,让句子不会丢标点,这是中文分句的关键。最后一段把相邻句子拼接起来,超过sentence_size就切一块,然后把尾部overlap_size字符拼到下一块开头。
切分参数我给一个可复制的经验区间:sentence_size=250适合普通制度文档和 FAQ,150 适合合同条款这类每句信息量大的文本,400 以上适合技术手册里大段描述性内容。overlap_size一般取 sentence_size 的 20%,太小起不到上下文衔接作用,太大容易让两个 chunk 高度重复,浪费向量库容量。我在跑一份 20 页的运维手册时,250/50 的组合让答案里的引用片段完整了不少,明显好过之前用 500/50 的配置。
4.3 向量化与检索:embedding 方案选择与 top_k/score_threshold 经验值
资源包里 embedding 有三条路:默认的 text2vec、paddle_embedding.py 对应的 PaddleNLP 方案、jina_serving.py 对应的 Jina 服务化方案。三种我都试过,差异不在“能不能用”,而在使用场景:
| Embedding 方案 | 特点 | 适合场景 |
|---|---|---|
| text2vec-base-chinese | 本地离线运行,显存占用小 | 个人电脑、默认首选 |
| PaddleNLP embedding | 语义匹配强,依赖 paddlepaddle 库 | 已有 Paddle 环境的团队 |
| Jina Embeddings 服务化 | 需要通过 jina_serving.py 起服务,长文本支持好 | 需要处理超长文档的场景 |
向量化之后,检索效果由TOP_K和SCORE_THRESHOLD两个参数决定。TOP_K=4时,模型会取相似度最高的 4 个 chunk 拼进 Prompt。我做过的测试结论是:事实型问题,比如“合同里违约金怎么写的”,top_k 用 3 到 4 就够,多了会把不相关的东西带进来;综述型问题,比如“这个系统有哪些安全措施”,可以调到 6 到 8,让模型看到更多材料再总结。
SCORE_THRESHOLD是安全阀。默认 0.5 的意思是说,如果所有 chunk 的相似度都低于 0.5,就认为知识库里没有相关内容,不要硬答。实际使用中,0.5 对 text2vec 的余弦相似度来说偏宽松,我一般提到 0.6,减少那种“明明没有却硬答”的情况。但也别超过 0.7,否则提问换个说法就召回不到,用户体验很差。
4.4 Prompt 组装与生成参数:chatglm_llm 如何把检索结果变成回答
召回完成之后,还有一个关键环节是 Prompt 模板。资源包里默认的模板结构是:
from langchain.prompts import PromptTemplate template = """基于以下已知信息,回答用户问题。 已知信息: {context} 用户问题:{question} 如果已知信息不包含答案,请说知识库中暂无相关内容,不要编造。 """{context}替换成召回的 chunk 原文,{question}替换成用户问题。context的顺序由相似度从高到低排列,所以最相关的段落会出现在模型最先读到的地方。写模板时注意强调“不要编造”,这句话比什么花哨技巧都管用。ChatGLM-6B 本身有较强的对话惯性,不明确约束的话,它会像平时聊天一样顺着话头往下说,很容易一本正经编答案。
生成参数上,temperature建议 0.1 到 0.2 之间。RAG 任务和创意写作不一样,它要求答案贴着资料走,随机性越低越好。top_p=0.9保持默认即可。如果你发现回答总在绕弯子、不直接给结论,可以把模板里加一句“先直接回答,再补充依据”,比继续调生成参数更有效。
上传文档的操作路径是:WebUI 页面上传 txt/md/pdf/docx 文件,程序自动调用加载器读取,走 chinese_text_splitter 切分,再向量化入库。上传后可以去 vector_store 目录确认是否有索引文件生成,这个我在第 5 章会展开讲怎么排错。
5. 避坑篇:本地知识库问答的典型报错与排查流程
下面这些坑,是我在这个资源包上实际踩过、也看别人反复踩过的,按现象、原因、解决三段写清楚。
5.1 CUDA out of memory:显存配置不当
现象:WebUI 能启动,第一次提问后终端直接报CUDA out of memory,进程退出或界面无响应。
原因:ChatGLM-6B 的 FP16 权重占约 13GB,加上 embedding 模型和对话上下文,16GB 显存卡跑满配很容易爆。另外如果没指定CUDA_VISIBLE_DEVICES,程序可能把显存平铺到多卡上,每张卡分到的都不够。
解决:先按第 2 章的档位表决定量化等级,用load_in_4bit或load_in_8bit加载;把 embedding 模型放到 CPU 上;启动前用nvidia-smi确认没有其他进程占显卡。我自己的做法:16GB 卡统一走 INT4 + CPU embedding,能保住整个会话不断。
5.2 检索结果永远为空:向量库没有真正落盘
现象:上传文档后提问,回答总是“知识库中暂无相关内容”,日志里看不到检索到的 chunk。
原因:向量库没有持久化成功。常见原因是 vector_store 目录不存在或没有写权限,程序把向量写到了内存里,进程重启后数据丢失。
解决:检查启动命令里的--vector-store指向的目录是否可写,上传文档后确认目录里生成了索引文件。我一般先做一次最小验证:上传一个只有十几行的纯文本,提问用原文里的原句,能答上来说明链路通,再去处理复杂文档。
5.3 答案流畅但内容错误:召回与切分失配
现象:回答写得通顺自然,但和文档原文对不上,甚至引用了不存在的细节。
原因:这是最典型的“模型在编”场景。要么 chunk_size 太大,一个块里混入多个主题,向量检索定位不到具体段落;要么 top_k 太小,真正相关的块没被召回,模型只能用不完整的资料硬答。
解决:不要急着调模型参数,先看召回了什么。把TOP_K临时调大打印出召回片段,确认问题和段落的相关性。然后把 chunk_size 调小到 150 到 200,overlap_size 保持 30 到 50,重新建库。这个组合能解决大部分“看起来合理但是编的”问题。
5.4 中文分句乱断:pdf 预处理开关没打开
现象:上传 PDF 后,问答引用的片段是半句话,或者一段被截成好几段,检索结果乱七八糟。
原因:PDF 抽取的文本层换行位置和语义无关,程序按换行切分时把完整句子拦腰截断。chinese_text_splitter.py 里pdf参数没打开,或者打开了但sentence_size设得过大过小。
解决:调用文本分割器时把pdf=True传进去,让它先压缩多余换行和空白。同时把 sentence_size 降到 200 左右,让分句更细。更进一步,PDFG 里的表格需要提前抽取成纯文本再入库,否则表格结构在切分后完全丢失。
5.5 启动时反复尝试下载模型:模型仓路径配置错误
现象:程序启动后卡在加载模型阶段,日志显示在尝试联网下载权重,或者一直报“file not found”。
原因:model_path指向的目录不对,程序找不到本地模型就触发默认下载逻辑。ModelScope 下载的目录结构和 HuggingFace 的不完全一样,经常多一层子目录。
解决:先确认model_path指到包含config.json的那一层,再看权重目录里是否有pytorch_model.bin或model.safetensors。如果已经下载过但还是触发下载,检查代码里是否硬编码了在线模型 ID。最后的手段就是彻底离线:参考 OfflineDeploy.md,把模型拷到内网固定路径,并屏蔽外网访问,让程序只能找本地。
6. 进阶验证:先用三层检查法再谈优化
6.1 回答质量的三层检查法
资源跑通只是开始,回答质量才是真正要长期盯的事。我给自己定了一套三层检查法,每次改完参数都要走一遍。第一层是召回检查,把TOP_K临时调到 10,打印出检索到的原文片段,看召回的是不是和问题真正相关。如果召回的段落看起来能直接回答问题,再进入下一步;否则问题出在切分或 embedding 上,调模型生成参数没有意义。
第二层是受控测试,拿一份你完全熟悉的文档,比如部门制度,问三个事实型问题、两个综述型问题,把答案和原文逐句比对。重点看有没有原文没有的细节,这种幻觉最容易在 long text 场景出现。
第三层是多轮追问,在第一轮答案基础上追问“依据在文档哪一段”,看模型能不能给出可溯源的片段。能引到原文片段才算合格,引不出来说明 Prompt 没把约束传达到位。
6.2 给知识库做一次去重体检
另一个值得做的进阶操作是文本去重。如果你把多个部门的文档混入一个知识库,同一份制度可能以不同版本存在,重复内容会让检索结果被同一类片段占满,降低答案多样性。我常用 minhash 做近似去重,逻辑是计算每篇文档的 minhash 签名,再比较 Jaccard 相似度:
from datasketch import MinHash def build_minhash(text: str, num_perm: int = 128) -> MinHash: m = MinHash(num_perm=num_perm) for token in set(text.split()): m.update(token.encode("utf-8")) return m # 两篇文档相似度超过 0.85 就认为是重复版本 sim = minhash_a.jaccard(minhash_b)num_perm是签名长度,128 对文档级去重够用;jaccard返回 0 到 1 之间的相似度。阈值我一般取 0.85,同一份制度的小版本差异都能抓出来。去重之后重新建库,检索结果的多样性会明显改善。
这套三层验证加去重的习惯,我是被一次事故逼出来的。当时调好一个合同知识库,团队拿它跑测试,答案流畅得让所有人满意,结果一核对原文,发现模型把两份不同合同里的违约金条款拼到一起了。从那以后,我每次搭知识库问答都会强制走一遍:先打印召回原文,再用相同问题问两遍,最后让模型引一句文档里的原话。第一次完整走完就抓到了一个看起来合理、实际上是混合编造的答案。希望帮到你,也祝你的知识库少一些幻觉、多一些可追溯的依据。
本文还有配套的精品资源,点击获取