资讯动态

LangChain与ChatGLM-6B本地知识库问答:RAG全链路实战解析

发布时间:2026/10/8 19:07:06 来源:尧图企业网站定制
简介随着大语言模型应用的深入检索增强生成RAG成为解决知识更新与幻觉问题的关键架构。RAG通过文档切分、向量化检索与生成模型结合让LLM能基于本地知识库输出可信答案。本文以基于LangChain和ChatGLM-6B的本地知识库问答系统为例完整解析从文档加载、中文自适应切分ChineseTextSplitter、PaddleEmbedding向量化、FAISS检索到ChatGLM-6B生成的RAG全链路并给出环境搭建、模型离线部署、INT4量化显存优化、检索参数调整及常见故障排查等实践要点。该方案无需外部API单机即可运行适用于企业内网知识管理、毕设及个人LLM应用开发。1. 为什么要拆这套基于LangChain和ChatGLM-6B的本地知识库问答先说场景有阵子我在帮一个团队整理内部知识库产品FAQ、历史故障记录、验收文档散在几十个Word和PDF里新人入职问一句报销流程是什么要翻半个小时的共享盘。这个项目的价值在于它不拿ChatGLM-6B联网乱答而是把本地文档切分、向量化、检索召回后交给LLM做最终组织——这正是现在大家常说的RAG落地路径。对要交毕设的学生、做课程设计的本科生、或者想入门LLM应用开发的从业者来说这份源码把加载文档→切分→embedding→检索→生成整条链路都串起来了代码量不大但每个环节都有对应的py文件。接下来我按架构→跑通→拆代码→排坑→调优的顺序把它讲透。2. 架构与模块拆解从文档到答案的完整链路2.1 本地知识库问答的完整链路一次提问背后发生了什么整个系统围绕一条典型的RAG链路展开。用户在Web界面或命令行输入一个问题系统先不急着调LLM而是走一遍检索流程把用户的自然语言问题做embedding拿这个向量去本地向量库做相似度检索召回最相关的几段文档片段再把问题召回片段拼成prompt交给ChatGLM-6B生成答案。拆开来看是六个阶段文档加载、文本切分、向量化入库、提问向量化、相似度检索、拼接生成。前三个阶段属于建库后三个阶段属于问答。这也是这个项目区别于直接拿LangChain跑个demo的地方——它有完整的chinese_text_splitter.py做中文适配有paddle_embedding.py做本地embedding有chatglm_llm.py把ChatGLM-6B封装成LangChain的LLM接口。那为什么不直接微调一个模型常见做法是微调成本和更新成本都太高知识库今天加一份文档微调就要重来一遍而RAG架构只需要把这新文档跑一遍切分入库问答侧代码一行不用动。这就是RAG在这两年成为本地知识问答主流方案的原因。理解这个链路后面所有参数调优和排坑都有地方落脚。2.2 选型对比为什么是LangChain、ChatGLM-6B和PaddleEmbedding选型是这个项目最值得抄作业的地方。三块核心组件每一块都有替代方案但作者选的组合对毕业设计/个人项目这个定位非常合适。LLM选ChatGLM-6B是因为它在6B这个体量上中文能力压得住且单张16G显卡用FP16能跑INT4量化后显存占用还能再砍一半相比之下同体量的其他开源模型要么中文效果打折要么对中文tokenizer的支持要额外调。LangChain负责编排它的RetrievalQA、VectorStore、DocumentLoader把前面的检索链路封装成了声明式API你用aiopencv库读文档、用FAISS建索引、用PromptTemplate拼文本全都是一行行所见即所得的调用。Embedding这块很关键项目用的是PaddleEmbedding也就是基于PaddleNLP的本地语义向量模型。如果换成OpenAI的embedding接口每次向量化都要联网调API对本地知识库场景来说是双重负担一是外部依赖二是中文文档的token开销不小。本地embedding模型虽然单条效果略逊但在语义相似度检索这个任务上够用而且是零成本、纯本地运行。三块放一起就是一套不依赖任何外部API、单机能跑通的中文本地问答方案。2.3 代码包里的文件清单与模块职责拿到压缩包后我建议先按表里的顺序把文件过一遍理解每个文件的职责再动手。这个项目不是那种十几个文件夹堆一起的黑匣子它的结构是入口在顶层核心实现集中在一个目录下文档和部署文件分开放。文件职责关键点cli.py命令行交互入口不依赖Web界面适合调试和验证app.pyWebUI问答入口带演示界面适合给答辩或展示用chatglm_llm.py把ChatGLM-6B封装成LangChain的LLM接口核心封装重写_call方法chinese_text_splitter.py中文文档的切分器解决了LangChain默认切分器切坏中文句子的问题paddle_embedding.py基于PaddleNLP的本地Embedding封装向量化本地模型modelscope_hub.py从ModelScope下载模型的辅助脚本国内环境下载模型最稳的路径config.py全局配置模型路径、向量库路径、切分参数都在这jina_serving.pyJina向量服务的备用实现想换语义向量方案时可以对照着改requirements.txt / pyproject.tomlpip与poetry两套依赖声明环境隔离用哪个都行Dockerfile / Dockerfile.Base容器化部署离线部署章节会重点讲OfflineDeploy.md / docs/faq.md离线部署与常见问题说明动手前先读这两篇把职责理清之后你在改代码时会很舒服改切分逻辑不动检索代码换Embedding方案不动LLM封装每一层都是解耦的。3. 环境搭建与首次启动模型下载、依赖安装、两种入口3.1 依赖安装pip与poetry两套方案怎么选项目同时给了requirements.txt和pyproject.toml说明作者自己用poetry管理但为了照顾用pip的人两套都放上了。我的习惯是先建一个干净的conda环境指定Python 3.8或3.9再装依赖。特别提醒一句requirements里的torch默认指向CPU版本还是CUDA版本取决于你装依赖的方式如果你有NVIDIA显卡最好先手动装好对应CUDA版本的torch再装其他依赖顺序反了容易出现torch是CPU版、跑起来慢到怀疑人生的情况。安装命令很简单但要注意先用conda把torch装好。conda create -n kbqa python3.9 conda activate kbqa # 先装CUDA版torch根据自己的CUDA版本选择 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 再用requirements安装剩余依赖 pip install -r requirements.txt # 如果prefer poetry则用 pip install poetry poetry install这套顺序背后的逻辑是requirements.txt里声明的torch依赖在pip解析时可能拉取CPU版而ChatGLM-6B在CPU上几乎不可用推理速度慢到无法接受。先手动装好GPU版torchpip在解析后续依赖时会发现torch已满足条件不再重复安装。poetry同理它会读pyproject.toml里锁定的版本如果与已有环境冲突会提示这时候poetry config virtualenvs.in-project true可以避免它另建虚拟环境。3.2 模型获取走ModelScope而不是HuggingFace模型下载是这个项目最容易被卡住的一步。ChatGLM-6B的权重文件十几个GBembedding模型也不小如果网络环境不稳定下载中断会让你怀疑人生。项目专门写了modelscope_hub.py就是因为在ModelScope上多文件下载和断点续传的体验比直接拉Git LFS更稳。我一般这样下载模型先建一个目录放权重mkdir -p model/chatglm-6b model/paddle-embedding # 用ModelScope SDK下载ChatGLM-6B python -c from modelscope import snapshot_download model_dir snapshot_download(ZhipuAI/chatglm-6b, revisionv1.1.0) print(model_dir) # 下载Paddle的本地embedding模型 python -c from modelscope import snapshot_download model_dir snapshot_download(PaddlePaddle/mengzi-bert-base, revisionmaster) print(model_dir) 这段代码调用了ModelScope的snapshot_download接口它返回的model_dir就是模型解压后的路径把这两个路径填进config.py即可。注意revision参数不写时默认拉取最新但某些模型的最新版本在transformers版本不兼容的情况下会报keyError建议按代码里的锁定版本走。下载完先验证目录结构ChatGLM-6B的目录下应该有pytorch_model.bin或分片bin文件、tokenizer.model、config.json缺一不可。3.3 两种入口什么时候用cli.py什么时候用app.py项目提供了两套问答入口这个设计很实用。cli.py走命令行交互不加载Web服务适合你改完代码快速验证——一条命令下去直接出结果日志打印也直观。app.py起一个有界面的Web服务适合给老师或同事演示。启动前的配置在config.py里核心是三个路径和一个参数。class Config: # LLM模型路径指向上面下载的chatglm-6b目录 llm_model_path model/chatglm-6b # embedding模型路径 embedding_model_path model/paddle-embedding # FAISS向量库持久化路径重新建库时清空该目录 vector_store_path vector_store/ # 知识库文档根目录放需要问答的Word/PDF/TXT kb_documents_path documents/路径配好之后启动就非常简单命令行问答用python cli.pyWeb界面用python app.py。首次启动会自动扫描documents目录下的文档做切分、向量化、写入FAISS。如果文档很多首次建库会比较慢日志会逐文件打印进度。建库完成后进入问答循环你输入公司报销流程是什么它会基于检索到的片段组织回答。注意首次启动时如果向量库目录已有内容项目不会自动重建你新增文档后需要手动清空vector_store目录再重启这一条在README里未必写了但经验上是高频翻车点。4. 核心代码逐段拆解切分、LLM适配与检索参数4.1 chinese_text_splitter.py为什么LangChain默认切分器会把中文切烂LangChain自带的RecursiveCharacterTextSplitter在英文场景表现不错因为它默认按\n\n、\n、空格、字符这个优先级切分英文单词天然以空格为边界。但中文没有空格边界默认切分器会把一整个句子硬生生从中间切断导致切出来的片段语义不完整检索时明明有相关文档却召不回来。这个项目的chinese_text_splitter.py解决了这个问题核心思路是把分隔符的优先级重新排列让中文标点成为优先切分点。from langchain.text_splitter import RecursiveCharacterTextSplitter class ChineseTextSplitter(RecursiveCharacterTextSplitter): def __init__(self, chunk_size200, chunk_overlap50, **kwargs): separators [ \n\n, \n, 。, , , , , 、, , ] super().__init__( separatorsseparators, chunk_sizechunk_size, chunk_overlapchunk_overlap, **kwargs ) def split_text(self, text: str): # 把连续多个换行合并避免空段落进入向量库 text re.sub(r\n{3,}, \n\n, text) return super().split_text(text)这段代码的逻辑是从句号、问号、感叹号开始切切出來的每个片段基本是一个完整的中文句子只有当一个句子超过chunk_size时才会退回到逗号、顿号甚至逐字切分。这样做的收益是片段内部语义完整向量化之后能代表一个完整的信息单元。参数chunk_size控制片段最大长度chunk_overlap控制相邻片段之间保留多少重叠字符——重叠的意义在于如果关键信息恰好在切分边界前后两个片段都能覆盖到它。常见设置是chunk_size 200到300、chunk_overlap 50如果文档偏长偏正式可以把chunk_size调到500但要同步加大chunk_overlap否则边界信息容易丢。4.2 chatglm_llm.py把ChatGLM-6B装进LangChain的接口里LangChain的RetrievalQA链路要求LLM实现两个方法_call和_llm_type。这个项目写了一个ChatGLMLLM类内部用HuggingFacePipeline加载模型然后把这层Adapter补上。理解这段代码你就看懂了这个系统是怎么让LangChain的检索逻辑和ChatGLM-6B的生成逻辑协同工作的。from langchain.llms.base import LLM from transformers import AutoTokenizer, AutoModel, TextStreamer from typing import Optional, List, Dict, Any class ChatGLMLLM(LLM): tokenizer: AutoTokenizer model: AutoModel history: List [] property def _llm_type(self) - str: return chatglm-6b def _call(self, prompt: str, stop: Optional[List[str]] None, **kwargs) - str: # 这里的prompt是LangChain拼好的问题上下文直接喂给GLM response, self.history self.model.chat( self.tokenizer, prompt, historyself.history ) return response这份代码的关键在_call输入是LangChain合好的包含检索结果的prompt输出是纯文本字符串。注意history是实例级变量它会让同一会话内的对话带上上下文——但这个特性在知识库问答里有副作用如果检索到的上下文经常变化history里的旧信息会干扰新问题的回答。我实际用下来知识库问答场景把history设为空列表往往效果更稳定尤其是面对换一个问题再问上一个问题这种场景时。另外如果你需要流式输出可以在加载model时传入TextStreamer把逐token生成过程打印出来能明显提升演示时的体验感。4.3 检索参数怎么调chunk_size、k值、相似度阈值检索效果不好80%的情况不是模型问题而是参数没调对。项目里影响答案质量的主要是三个参数文本切分的chunk_size、Top-K召回数量和相似度阈值。这三个参数在config.py和链路的构建代码里都能找到。参数建议范围作用调大/调小的后果chunk_size200~500每个向量片段的字符长度太大片段语义混杂太小信息不完整chunk_overlap50~100相邻片段重叠字符数太小边界信息丢失太大重复内容占向量库空间k召回数量4~8检索后返回给LLM的片段数太少漏召回太多无关片段干扰生成score阈值0.3~0.6相似度低于阈值的片段直接丢弃阈值过高答不知道过低答非所问我建议的调参顺序是先固定chunk_size和chunk_overlap把k调到6左右然后跑一组问题打印出检索到的片段和得分看召回的片段是否语义对题。如果召回的片段是切碎的半句话那就是chunk_size和overlap的问题如果召回片段不相关优先调低score阈值如果相关但答案输出不佳再调高k值。整个过程能把你的调参过程从玄学变成有依据的调试这也是这套源码最适合用来学习RAG调优的原因——每个参数都可以对照日志看到效果。5. 部署与避坑排查显存、离线模型、答非所问的五个坎5.1 显存不够怎么办量化加载与容器化部署ChatGLM-6B的FP16权重跑一次推理要占约14GB显存很多人的机器只有8G甚至6G显卡如果不做处理启动加载模型那一瞬间就OOM。项目里常见的做法是在加载模型时启用INT4量化把权重压缩到4bit显存占用降到6GB左右8G显卡可以跑效果会有轻微下降但知识库问答场景里生成质量的损失对比根本跑不起来是完全值得的。def load_model_with_quantization(model_path: str, quantize: str int4): from transformers import AutoTokenizer, AutoModel tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) if quantize int4: model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, load_in_4bitTrue, device_mapauto ) else: model AutoModel.from_pretrained(model_path, trust_remote_codeTrue) return tokenizer, model如果你要用Docker部署项目里的Dockerfile和Dockerfile.Base已经准备好了基础镜像配置。实际部署时最关键的命令是启动容器时把模型目录和知识库目录都挂载进去否则容器一重启模型就丢。常见做法是用docker run --gpus all -v /data/models:/app/model -v /data/docs:/app/documents这种挂载方式让模型的加载和知识库的扫描都在宿主机目录上完成方便后续更新模型和文档。5.2 离线部署的全流程OfflineDeploy.md里没写透的步骤项目的OfflineDeploy.md讲了离线部署的思路我在实际复现时踩过一个坑补充一下完整流程。离线部署的本质是把模型权重、依赖包、代码三样东西全部拷贝到目标机器整个环境不访问外网。但如果你直接在离线机器上pip install -r requirements.txt会因为没有镜像源而失败所以需要提前在联网机器上把依赖打包。# 在联网机器上执行 mkdir offline_packages pip download -r requirements.txt -d offline_packages/ # 拷贝到离线机器上执行 pip install --no-index --find-linksoffline_packages/ -r requirements.txt # 模型权重用离线方式拷贝保持目录结构 tar -zcf model.tar.gz model/注意两个细节一是pip download会连带下载所有依赖的依赖打包目录会比较庞大建议提前确认目标机器是Windows还是Linux两者的whl包不能混用二是模型权重的目录结构一定要原样拷贝config.py里的路径要和目标机器上完全一致多一级目录少一级目录都会导致加载报错。离线环境最容易出现的问题还有一个就是transformers和torch的版本冲突——你在联网机器上用的版本组合在那台机器上不可用解决方法是把pip freeze的结果一起带过去在目标机器上逐个安装锁定版本。5.3 常见问题排查现象、原因、解决这块我按实际踩坑记录来写每条都是现象→原因→解决的结构你遇到同类型问题时可以直接对照。1. 回答明显不来自知识库或者直接说我不知道但文档里明明有。现象问题涉及文档里明确写了的内容模型却给了个通用回答。原因检索阶段没有召回相关片段——可能是score阈值设太高也可能是切分把关键句子切碎了。解决先不做问答直接把问题拿去检索打印召回的片段和相似度得分观察是阈值问题还是切分问题。一般我会把score阈值从0.5降到0.3试一轮再把chunk_overlap从50调到75。2. 模型加载时报KeyError: encoder或AttributeError。现象用ModelScope下载的权重路径填进config.py之后启动时模型加载报错。原因transformers版本与模型要求的版本不匹配ChatGLM-6B的trust_remote_codeTrue机制依赖特定版本的transformers才能正确解析它的自定义代码。解决检查requirements.txt里锁定的transformers版本用pip show transformers确认当前版本如果不对就按锁定的版本重新装。3. 显存明明够用但跑两个问题后OOM。现象首个问题回答正常第二个问题直接报CUDA out of memory。原因history列表无限增长每轮输入的token都在累加。解决在_call里对history长度做截断只保留最近两轮对话或者干脆把history置空。4. 向量库建完问答系统说找不到任何文档。现象建库日志正常但问什么都是知识库中未找到相关内容。原因documents目录下没有能被加载器识别的文件格式或者文档是扫描版PDF纯图片没有文本层。解决检查文档格式支持列表扫描版PDF需要先用OCR工具转成文本常见做法是先把PDF用pdfplumber提取文字确认提取出的内容不是乱码或空白。5. 离线部署后依赖版本全部配对但页面起不来。现象python app.py在离线机器上报缺少某个动态库或libgomp相关错误。原因conda环境没有打包完全目标机器缺系统级的so文件。解决在联网机器上把整个conda环境用conda pack打成一个tar包拷贝过去解压后激活比逐个pip install要稳得多。6. 进阶调优把检索质量再抬一个台阶的落地技巧6.1 给答案加出处让回答可追溯毕设答辩时最怕评委问一句你怎么保证模型不是瞎编的。一个很实用的改进是在RetrievalQA链路上开启return_source_documentsTrue把检索到的原文片段和答案一起打印出来让AI的每一个结论都能指向知识库里的某段文字。这个改动不用动模型只动链路组装。qa_chain RetrievalQA.from_chain_type( llmchatglm_llm, retrieverretriever, return_source_documentsTrue, # 关键参数 promptqa_prompt ) result qa_chain({query: 公司报销流程是什么}) print(回答:, result[result]) for doc in result[source_documents]: print(f来源文件: {doc.metadata[source]}) print(f相关内容: {doc.page_content[:80]})加了出处之后问答系统就从黑匣子变成了可验证的工具。我一般会在WebUI的界面上把来源拼到答案的末尾用引用块展示这样答辩和实际使用都能一眼看出模型依据的是什么内容。这个技巧尤其适合处理AI幻觉的质疑——当答案有来源支撑时可信度会完全不同。6.2 先改写再检索用一个小Prompt把口语问题归一化知识库问答的检索失败有个常见原因用户的提问用词和文档里的表述不一致比如用户问怎么改密码文档里写的是密码重置流程。向量检索对同义改写是敏感的一个简单有效的技巧是在提问后先做一次query改写——用LLM把口语提问扩展成多个检索表达。rewrite_prompt PromptTemplate( input_variables[question], template基于以下原始问题扩展出3个等价的检索式表达用于知识库检索。 原始问题{question} 输出格式每行一个检索表达。 ) def search_with_rewrite(question: str): rewritten chatglm_llm(rewrite_prompt.format(questionquestion)) queries [question] [line.strip() for line in rewritten.splitlines() if line.strip()] docs [] for q in queries: docs.extend(retriever.get_relevant_documents(q)) # 合并去重保留相似度最高的 return deduplicate_docs(docs)这种做法不需要引入重排模型就能显著提升召回率。要注意的是改写消耗的token会被算进模型推理时间所以只对检索结果不理想的问题走改写逻辑正常问题直接检索就行。6.3 知识库更新的正确姿势只重建增量向量日常使用中知识库不可能一成不变。这个项目里的FAISS向量库是全量构建的如果每次加文档都要重建整个库文档一多时间不可接受。我一般会按目录划分向量库把每个文档目录独立生成一个索引文件查询时逐个索引检索后合并结果。这样新增一份文档只需要为它单独建一个索引不用动原来的库。这个思路代码改动不大但能解决加一次文档等十分钟的现实痛点。从那以后我每次改完配置都会先用cli.py强制跑三个用例一个知识库内的确定问题、一个换种说法提问的问题、一个明显不在库里的问题。三个用例过了再开WebUI基本不会再翻车。这套源码最大的价值也在这里——它是一个完整闭环你可以在上面反复做实验把RAG的每个组件都拆开看明白。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑