简介这是一份面向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, stopNone) - str: response, _ self.model.chat( self.tokenizer, prompt, history[], temperatureself.temperature, top_pself.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 中文 embeddingWebUI 并发一个人用没问题。如果把 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 python3.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 能不能看到 CUDApython -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_DEVICES0 \ langchain-chatglm:latest--gpus all是把所有 GPU 暴露给容器-v /data/models:/model_cache把宿主机已经下载好的模型目录挂载进容器避免容器内重复下载几百 MB 到几 GB 的权重。CUDA_VISIBLE_DEVICES0指定只用第一张卡防止 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, pdfFalse, sentence_size250, overlap_size50): 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这段代码里pdfTrue时先把 PDF 抽取出的多余换行压缩成单个空格因为 PDF 的文本层经常把同一段话拆成多行不处理的话分句会碎掉。re.split(r([。]), text)保留分隔符让句子不会丢标点这是中文分句的关键。最后一段把相邻句子拼接起来超过sentence_size就切一块然后把尾部overlap_size字符拼到下一块开头。切分参数我给一个可复制的经验区间sentence_size250适合普通制度文档和 FAQ150 适合合同条款这类每句信息量大的文本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_K4时模型会取相似度最高的 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_p0.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 到 200overlap_size 保持 30 到 50重新建库。这个组合能解决大部分“看起来合理但是编的”问题。5.4 中文分句乱断pdf 预处理开关没打开现象上传 PDF 后问答引用的片段是半句话或者一段被截成好几段检索结果乱七八糟。原因PDF 抽取的文本层换行位置和语义无关程序按换行切分时把完整句子拦腰截断。chinese_text_splitter.py 里pdf参数没打开或者打开了但sentence_size设得过大过小。解决调用文本分割器时把pdfTrue传进去让它先压缩多余换行和空白。同时把 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_permnum_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同一份制度的小版本差异都能抓出来。去重之后重新建库检索结果的多样性会明显改善。这套三层验证加去重的习惯我是被一次事故逼出来的。当时调好一个合同知识库团队拿它跑测试答案流畅得让所有人满意结果一核对原文发现模型把两份不同合同里的违约金条款拼到一起了。从那以后我每次搭知识库问答都会强制走一遍先打印召回原文再用相同问题问两遍最后让模型引一句文档里的原话。第一次完整走完就抓到了一个看起来合理、实际上是混合编造的答案。希望帮到你也祝你的知识库少一些幻觉、多一些可追溯的依据。本文还有配套的精品资源点击获取