
简介面向希望基于本地知识库构建智能问答系统的开发者与算法工程师该资源提供了一套基于LangChain与ChatGLM-6B等LLM的完整项目实践覆盖文档加载、文本分割、向量化Embedding、模型接入与自动应答等关键环节。包内共75个文件以Python源码、配置文件和说明文档为主另含图片与部署脚本压缩包整体约17.77MB目录按应用层级组织便于定位与二次开发。目前已有936人学习下载浏览量说明该方案具有一定认可度适合正在入门RAG与大模型落地的人群参考。除可运行代码外资源还附有依赖清单、Dockerfile、部署文档与更新记录并支持PaddleEmbedding、ModelScope等多种接入方式可帮助读者快速搭建本地知识库问答环境深入理解从文档处理到LLM生成答案的全链路实现也可用作毕业设计或课题研究的基线方案。1. 单机跑起来的本地知识库问答不微调也能答对关键是先查再答我手上有一批内部制度文档总共 20 万字老板要一个“问它就能答答完能给依据”的入口预算只有一张 16G 显存的卡。最先想到的是微调 ChatGLM-6B可真把数据整理完、训练一轮时间足够我把整套制度重新抄一遍。后来换成了 LangChain ChatGLM-6B 的方案模型不微调只把文档切分、向量化每次提问先检索出最相关的几段再拼进 Prompt 让模型回答。这就是这份项目实践资源的真实定位——一个基于本地知识库的自动问答系统适合被私有文档困住、又不想把数据交给外部服务的企业工具也适合刚接触 LLM 应用开发的人拿来理解 RAG 完整链路。它真正解决两件事一是文档太多但没法直接提问二是内部资料不能走公网 API。你需要的是能跑起来的代码而不是又一张架构图。2. 拆开 LangChain-ChatGLM 项目文件、入口与最小部署链路2.1 目录里真正影响部署的关键文件解压后看到一长串文件名第一反应是“这到底要看哪个”。实际上核心链路只依赖少数几个文件其余是运行时依赖、文档和 Docker 配置。我先把真正影响你能不能跑起来的文件列出来。文件名在链路里的角色什么时候需要改chinese_text_splitter.py把文档切成检索用的片段回答太碎或漏信息时改 sentence_sizepaddle_embedding.py把文本转成向量切换 Embedding 模型时改模型名chatglm_llm.py把 ChatGLM 封装成 LangChain 可用的 LLM改 Prompt 模板、对话轮次时动它chatllm.py串联检索与生成的核心问答入口CLI 和 WebUI 都会调这里app.pyWebUI 入口只做体验时直接运行cli.py命令行问答入口批量测试、写脚本时用modelscope_hub.py从 ModelScope 拉取模型内网部署时换缓存目录requirements.txt锁定依赖版本依赖冲突时第一个查它除了这些包里还有 nltk_data、paddlepaddle 相关目录、Dockerfile 和 docs 下的 deploy.md、faq.md。nltk_data 不是业务代码但它管着英文分词语料放错位置会在启动时报punkt not foundpaddlepaddle 相关文件管着 Paddle Embedding 的运行环境。这些都属于“不直接写逻辑、但缺了必然翻车”的依赖。我一般拿到这种资源包不会先跑pip install而是先把 docs/deploy.md 和 README 翻一遍。因为这种项目迭代快requirements.txt里的版本快照很可能和你手头环境不兼容deploy.md 里通常会写明哪个版本需要 Python 3.8、哪个版本不兼容 CUDA 11。先看再装能省掉一半踩坑时间。2.2 WebUI 和 CLI 的调用链路app.py 和 cli.py 是两套入口但最终都会落到同一个检索问答链路。理解这条链路比记住某个函数名更重要因为每次升级代码都会改名唯独链路不会大变。我拿到的包里链路可以简化成下面这个伪代码结构# 调用链路cli.py / app.py - chatllm - chatglm_llm embedder from chatllm import ChatLLM def answer_with_rag(question, retriever, llm): # 问题不直接丢给模型先去知识库窗口里捞相关片段 snippets retriever.get_relevant_documents(question)[:3] context \n.join([s.page_content for s in snippets]) prompt f请根据以下资料回答问题\n{context}\n\n问题{question} return llm.generate(prompt)参数说明retriever是从向量库构造出来的检索器snippets是命中的文档片段[:3]控制最多取三段防止 Prompt 超过模型上下文llm是chatglm_llm.py里封装的 ChatGLM 实例llm.generate是 LangChain 统一接口。如果你发现拿到的包里ChatLLM类名不一样不用慌找cli.py里最终调用的那个方法链路逻辑是一样的。CLI 和 WebUI 的区别只在于“问题从哪来、结果展示到哪”。CLI 适合一次性验证WebUI 适合给人试用。所以我调试时永远先用 cli.py 跑一句再开 app.py。2.3 一份可以照抄的部署步骤部署这件事最怕的就是拿生产环境直接试错。我自己的习惯是先在虚拟机或者 Docker 里走通一遍再考虑上线。下面是我按照这个资源包整理出的最小步骤# 第一步创建虚拟环境避免污染系统 Python python -m venv venv source venv/bin/activate # 第二步安装依赖 pip install -r requirements.txtrequirements.txt里会同时出现 torch、paddlepaddle 和 langchain 的依赖。GPU 机器上要确认 torch 版本和 CUDA 匹配纯 CPU 机器可以把 paddlepaddle 换成 CPU 版本否则安装时间会很长。# 第三步准备模型到本地缓存目录 mkdir -p model_cache python modelscope_hub.py --model ZhipuAI/ChatGLM-6B --cache_dir model_cache参数说明--model指定模型仓库名--cache_dir指定下载目录。如果你在公网下载 HuggingFace 模型总是中断用 ModelScope 这个脚本会省力很多它支持断点续传。下载完以后把模型路径填到配置文件里之后加载模型都会走本地目录不再碰网络。# 第四步跑通 CLI python cli.py --query 离职需要提交哪些材料如果命令行能打印出有依据的回答链路就通了。然后再执行python app.py打开 WebUI完成你的第一个本地知识库问答。这套步骤看起来简单但每一步都可能出错后面第 5 章我专门写了几条高频踩坑记录。3. 模型加载与 Embedding 选型把显存花在刀刃上3.1 ChatGLM-6B 的加载路径HuggingFace 与 ModelScopeChatGLM-6B 是一个开源双语对话语言模型参数量约 6B。FP16 全量加载大概需要 14GB 显存16G 显卡勉强够用8G 显卡就必须量化。项目里同时保留了 HuggingFace 和 ModelScope 两条加载路径原因很实际HuggingFace 在海外国内网络环境下大文件下载容易中断ModelScope 下载更顺滑适合第一次部署。常见做法是先把模型用 ModelScope 拉到model_cache然后让 transformers 从本地路径加载。代码逻辑是这样的# 本地缓存模型路径 model_path model_cache/ZhipuAI/ChatGLM-6B tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModel.from_pretrained(model_path, trust_remote_codeTrue).half().cuda()参数说明trust_remote_codeTrue表示允许执行仓库里的自定义模型代码ChatGLM-6B 的权重文件里带了自定义实现不加这个参数会直接报错.half()是把模型转成 FP16显存占用从 FP32 的 24GB 降到 14GB 左右.cuda()负责搬到 GPU。如果你一定要用 HuggingFace 路径from_pretrained的第一个参数直接写模型 ID 即可但建议先通过snapshot_download下载到本地再改成本地路径加载。模型加载路径决定的是“显存够不够”而 Embedding 选型决定的是“检索准不准”两者要分开考虑。3.2 三种 Embedding 的取舍Paddle、通用句向量与默认本地知识库问答里Embedding 模型承担的任务是把问题片段和文档片段映射到同一个向量空间。项目里单独写了paddle_embedding.py说明 Paddle 是主力选项之一。为什么不用默认的英文 Embedding 模型处理中文文档因为英文模型在中文短文本上的相似度排序经常答非所问。我按实际场景整理过一组对比方案中文效果资源占用适合场景Paddle Embedding较好依赖 paddlepaddle内存占用偏高中文文档为主、已有 Paddle 环境通用句向量模型好需要单独下载模型中英文混合、希望效果均衡默认英文模型差最低临时测试不建议上线如果你不想在 Paddle 上折腾可以把它替换成 sentence-transformers 里的多语言模型。paddle_embedding.py的本质是实现一个embed_query和embed_documents方法替换时只需要保证两个方法返回的向量维度一致from langchain.embeddings import HuggingFaceEmbeddings # 用多语言句向量替换 Paddle Embedding embedding HuggingFaceEmbeddings(model_nameyour-multilingual-embedding-model) query_vec embedding.embed_query(离职需要哪些材料) doc_vec embedding.embed_documents([离职流程说明, 请假审批制度])参数说明model_name指向你下载好的 Embedding 模型路径embed_query处理单个问题embed_documents处理切分后的文档片段。检查相似度时可以直接算两个向量之间的余弦相似度如果问题和正确片段得分低于 0.6基本可以判断 Embedding 选型不对。3.3 LLM 推理参数怎么给才不回环同样一段 Prompttemperature 不同回答风格完全不同。做本地知识库问答时我要求模型“稳定输出、有依据”所以核心参数要往低温度方向调。项目里chatglm_llm.py通常会把参数集中在一个配置里你可以在 config 文件或入口脚本里改成类似下面这样LLM_PARAMS { temperature: 0.2, # 低温度减少编造 top_p: 0.8, # 核采样保留一点多样性 max_length: 2048, # 生成的最大长度 max_token: 2048, # 上下文与回答总长度上限 history: [] # 不开启多轮记忆 }参数说明temperature控制在 0.1 到 0.3 之间知识库问答需要“给定资料就给结论”温度太高会开始自由发挥top_p控制在 0.7 到 0.9不影响事实性但能避免重复history我默认清空因为本地问答通常是单轮查询历史对话会占用上下文反而让模型忘记当前问题。一个很常见的坑是为了“多轮对话”把 history 开满结果上下文窗口被旧内容塞满新问题里的关键信息被截断。我建议先保持单轮等检索质量稳定后再考虑多轮。3.4 最小化显存的两个加载分支显存不够是最普遍的翻车点。16G 显卡加载 FP16 勉强够但如果同时跑 Embedding 模型和向量库内存也可能吃紧。8G 显卡就必须走量化分支。我通常会写成两个分支if use_int4: model AutoModel.from_pretrained(model_path, trust_remote_codeTrue) model model.quantize(4).half().cuda() else: model AutoModel.from_pretrained(model_path, trust_remote_codeTrue).half().cuda()参数说明quantize(4)把权重量化到 4bit显存占用能压到 6GB 左右牺牲一部分回答质量use_int4这个开关通过启动参数控制不要让量化逻辑和正常逻辑混在一个文件里否则调参时很难定位是加载问题还是生成问题。我一般建议有 12G 以上显存用 FP168G 到 12G 用 int48G 以下直接放弃本地推理考虑调用内部 API 服务。这个边界不是理论推导是我在不同卡上跑同一份文档压出来的经验。4. 文档切分与检索链路决定回答质量的不是模型而是分割4.1 ChineseTextSplitter 与默认按字符切的差别很多人第一次跑通后回答总是“差一点”不是模型不行而是文档被切坏了。LangChain 默认的RecursiveCharacterTextSplitter按字符长度切英文按空格边界好办中文直接按字符切片经常把“离职流程需要经过直属主管确认”切成“离职流程需要经”和“过直属主管确认”检索时只能捞到一半信息。项目里单独放了一个chinese_text_splitter.py就是为了解决中文断句问题。它先按句号、问号、感叹号等标点把文本切成句子再把句子拼成指定大小的块块与块之间保留重叠让检索片段保持语义完整。常见用法如下from chinese_text_splitter import ChineseTextSplitter text_splitter ChineseTextSplitter(pdfFalse, sentence_size250) docs text_splitter.split_text(whole_text)参数说明pdfFalse表示源文本不是 PDF不需要走 PDF 专用解析分支如果你传入的是 PDF 抽取出来的纯文本记得把pdf设成 False否则它会尝试按 PDF 页边界处理sentence_size250控制单个语义块的目标字符数不是绝对上限而是以句子为单位凑到这个长度。我自己的经验是制度类文档用 250 到 300技术手册可以放大到 400但不要超过 500否则检索回来的片段太长Prompt 会被撑满。4.2 问答链路加载、切分、向量化、检索、生成本地知识库问答的完整链路是固定的加载文档 → 文本切分 → 向量化 → 存向量库 → 检索 top k → 拼 Prompt → 生成回答。你不需要把每一步都写得非常复杂关键是每一步的数据格式要对上。用 LangChain 的标准写法可以这样理解from langchain.vectorstores import FAISS from langchain.chains import RetrievalQA # docs 是切分后的片段集合embedding 是选好的 Embedding 模型 vectordb FAISS.from_documents(docs, embedding) qa RetrievalQA.from_chain_type( llmchatglm_llm, chain_typestuff, retrievervectordb.as_retriever(search_kwargs{k: 3}) ) answer qa.run(离职需要提交哪些材料)参数说明chain_typestuff表示把检索到的片段全部塞进 Prompt适合片段少、上下文短的情况k3表示每次检索返回 3 个片段。k不是越大越好3 个 300 字的片段就是 900 字加上问题和历史很容易超过模型上下文如果结果不理想先调k而不是先调模型。在这条链路里最容易出错的是“向量库重建”。很多项目会在启动时检查缓存如果文档更新了但向量库没重建你问的问题永远基于旧文档。我习惯把“更新文档后必须重建向量库”写进启动脚本里宁可慢一点也不要回答过期内容。4.3 不建向量的“裸传上下文”模式为什么容易翻车有些项目为了省掉向量库把所有文档拼成一个大字符串每次提问都塞给模型。这种做法在小规模测试时能跑通文档一旦超过模型上下文就会发生三件事前半段被截断、后半段被截断、模型只能复读中间一小段。而且生成时间随文档长度线性上升用户体验非常差。我拿一个小测试做过对比模式1 万字文档20 万字文档全量塞给模型勉强能答但无依据上下文溢出直接报错先检索再生成秒级返回有片段依据秒级返回有片段依据裸传模式还有一个隐藏问题它会把文档里的噪音一并送给模型模型不知道该信哪句就开始自行发挥。先检索再生成的意义不只是省 Token更是帮你做了一次“信息筛选”让模型只基于相关片段作答。4.4 chunk_size 与 overlap 参数组合我调参时最常用的一组参数是sentence_size250和重叠长度 50 字。重叠的作用是防止关键句正好落在两个块的边界时被丢到任意一块。你可以理解为相邻两个块互相多给对方让出一点空间保证跨句信息不丢失。实际文档类型不一样推荐参数也不一样文档类型sentence_size 建议重叠长度建议制度、流程类25050技术手册、长段落300-40080对话记录、FAQ15030如果回答经常漏信息先看“漏掉的内容是不是跨了切块边界”。如果是把重叠调大而不是把块无限调大。如果回答频繁出现“未找到”也可能是检索的k太小或者 Embedding 模型对问题里关键词不敏感。这些都是可量化的调一次记一次不要凭感觉。5. 避坑与排查五条让我凌晨改代码的真实记录5.1 显存不足半精度、int4 与 device_map现象执行 cli.py 时模型加载到一半直接报CUDA out of memory进程被杀掉。原因ChatGLM-6B 在 FP16 下约需 14GB 显存但程序里如果同时加载了 Embedding 模型、构建了向量库显存峰值会更高。你看到的报错不一定来自 ChatGLM很可能是 Embedding 模型抢占了显存。解决先确认哪些组件在占用显存。用torch.cuda.mem_get_info()看一下剩余显存然后给模型加载分支加 int4 量化python cli.py --use_int4如果你没做这个参数就在chatglm_llm.py的加载逻辑里临时改成.quantize(4).half().cuda()。如果只是测试流程可以先把本机的 Embedding 模型换成 CPU 版本GPU 全部留给 ChatGLM之后再看是否需要量化。5.2 Paddle 与 torch 的依赖冲突现象按 requirements.txt 装完后导入项目时提示paddle找不到或者torch版本不是编译时预期的版本。原因requirements.txt 里同时有 paddlepaddle、torch、langchain 及其传递依赖protobuf、numpy这类底层包经常会互相覆盖版本导致 import 顺序变了就报错。解决不要在全局环境安装严格使用虚拟环境。如果环境已经乱了直接重建deactivate python -m venv venv_clean source venv_clean/bin/activate pip install -r requirements.txt安装完成后先跑一个最小测试python -c import torch, paddle; print(torch.__version__, paddle.__version__)。确认两个库都能导入再继续跑项目。如果 Paddle 只需要用于 Embedding可以考虑把它拆成单独服务用 FAISS 之外的轻量方案替代。5.3 模型下载卡住用 ModelScope 离线包现象模型下载到 60% 就停住重跑又从 0 开始HuggingFace 路径下永远下不完。原因大文件走公网不稳定下载连接超时后没有断点续传或者缓存校验失败。解决换成项目里的 modelscope_hub.py把模型拉到一个固定的 model_cache 目录之后再从本地路径加载。ModelScope 支持断点续传适合大文件。操作上就是python modelscope_hub.py --model ZhipuAI/ChatGLM-6B --cache_dir model_cache下载完成后把模型加载代码里的from_pretrained参数改成model_cache下的绝对路径并保证该路径不要随意删除。这样后续 Docker 部署时也可以直接把整个 model_cache 拷进内网镜像不依赖外部网络。5.4 中文被按空格切碎NLTK 词典缺失现象检索回来的片段里中文句子被按空格切成了单词片段比如“离职 需要 提交 申请 表”原本连贯的流程断成好几截回答也跟着变碎。原因项目某个环节用了基于英文的 Tokenizer中文分词没有对应词典退化成按空格和标点切分。解决优先改用 chinese_text_splitter.py 做切分并在环境变量里指定 nltk_data 路径export NLTK_DATA/path/to/your/nltk_data/path/to/your/nltk_data指向项目包里自带的 nltk_data 目录。设置后重启 cli.py再跑一次检索观察片段是否恢复了中文断句。如果还没恢复检查chinese_text_splitter.py内部有没有直接调用 NLTK 的punkt有的话确认punkt目录存在。5.5 Docker 离线部署时 nltk_data 缺失现象镜像在开发机启动正常拷到内网机器后启动时报LookupError: Resource punkt not found应用直接退出。原因开发机联网时自动下载了 nltk 的 punkt 语料但 Docker 镜像里没有内网机器又无法在线下载于是缺失。解决把项目包里的 nltk_data 直接拷进镜像并在 Dockerfile 里声明环境变量ENV NLTK_DATA/opt/nltk_data COPY nltk_data /opt/nltk_data参数说明ENV NLTK_DATA告诉 NLTK 去哪找语料COPY nltk_data /opt/nltk_data把资源包内的语料复制到目标路径。这是离线部署最容易忽略的一件事因为开发环境在线时一切都正常只有到了内网才暴露。6. 用 CLI 做一次可复现的质量验证分割参数怎么调调 Embedding 模型成本高但调分割参数完全可以靠批量跑题量化。我每次拿到新的知识库第一件事就是固定一组问题用 cli.py 跑一遍把结果存成日志。下面是我常用的脚本#!/bin/bash # 固定五连问每次改动分割/检索参数后跑一遍 questions(离职要提交哪些材料 报销额度上限是多少 年假怎么计算 加班费标准 请假找谁审批) for q in ${questions[]}; do echo Q: $q python cli.py --query $q echo done说明这些问题要选“能从文档里找到明确答案”的不要选开放问题。运行后把输出重定向到result_$(date %s).log这样每次调参的结果都能对比参数差异。我一般对比三种配置sentence_size200、sentence_size300、sentence_size500判断标准只有三条检索片段是否覆盖问题关键词、回答是否引用了片段中的内容、有没有出现“未找到”。配置常见现象调整方向150-200回答细碎经常遗漏流程后半段调大 sentence_size 或调大重叠300范围适中命中率最高保持500回答像在复读长段落找不准重点调小 sentence_size调大 k还有一次我把 sentence_size 直接改到 800结果所有回答都像在复读段落标题完全没法用。从那以后我每次改完分割器都强制走一遍这五连问把日志归档再决定要不要继续调。希望帮到你。整个资源包里的 cli.py、chinese_text_splitter.py、deploy.md 和 faq.md 都在拿到之后先跑通这套验证流程再换成你自己的文档。本文还有配套的精品资源点击获取