ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangChain+ChatGLM-6B:本地知识库RAG问答系统的构建与调优

LangChain+ChatGLM-6B:本地知识库RAG问答系统的构建与调优 简介基于 LangChain 与 ChatGLM-6B 等系列大语言模型的支持中英文双语的本地知识库自动问答项目面向人工智能开发者、自然语言处理初学者及有私有化部署需求的技术人员解决从文档解析、文本分块、向量化、语义检索到答案生成的全流程搭建问题。压缩包共75个文件核心为12个Python脚本另有39个pickle数据、6个Markdown说明、3个文本说明及Dockerfile、TOML配置、演示图片等整体仅17.77MB目录结构紧凑清晰。已有936人学习下载项目成熟度与参考价值得到初步验证。内容覆盖chatglm_llm.py、paddle_embedding.py、chinese_text_splitter.py等关键模块实现模型接入、PaddlePaddle向量化与中文文本切分同时提供命令行与网页界面两种交互方式附依赖清单和Docker部署文件可对照文档快速复现环境适合直接作为毕业设计、课程设计或企业内网知识库问答的起步模板。1. 基于LangChain和ChatGLM-6B做本地知识库问答系统这套组合到底解决了什么问题当你手里有一堆产品手册、内部规范、历史项目文档想让大家随时问“这个流程怎么走”“那个接口参数是什么”而团队又不想把文档传到外部服务时基于LangChain和ChatGLM-6B等系列LLM的本地知识库自动问答就派上用场了。它本质上是一条RAG检索增强生成管线把本地文档切成小块、向量化存进向量库用户提问时先检索最相关的片段再让ChatGLM-6B根据这些片段组织答案。这样既保住了私有数据不出内网又能让大模型在特定领域里“实话实说”不用靠训练干预知识。我最早做这个方向是被一个问题逼的——几十个txt和Word文件里躺着核心经验新人不翻完根本找不到关键信息。当时花了一个周末把最小链路跑通从此这类项目成了团队内部最常用的AI工具。这套方案适合手里有私有文档、需要快速搭建内部问答的开发者也适合刚接触LangChain、想跑通一条完整RAG链路的学习者。接下来我会按从架构到落地的顺序把每一步怎么做、参数怎么调、坑在哪都讲清楚。2. 整体架构与核心环节LangChain怎么把ChatGLM-6B和向量库串成一条问答链路2.1 从文档到答案一条完整的本地知识库问答管线由哪几个环节组成基于LangChain的本地知识库自动问答核心是一条五段式RAG管线每个环节都有自己的职责和坑。第一段是文档加载把本地的PDF、Word、Markdown、TXT读成纯文本第二段是文本切分把长文档切成适合检索和塞进上下文的小块第三段是向量化用Embedding模型把每个文本块变成向量并写入向量库第四段是检索用户提问时把问题也变成向量在库里做相似度搜索取回最相关的几个片段第五段是生成把检索到的片段和用户问题拼进Prompt交给ChatGLM-6B生成最终答案。我一般用这样的数据流来理解整条链路原始文档 - 文本块 - 向量索引 - 检索结果 - Prompt上下文 - 答案。这里面最容易被忽略的是文本切分和Prompt模板设计很多人能在半天内跑通demo但答案质量差问题几乎都出在这两处而不是模型本身。举个例子假设你有一份50页的产品手册如果不切分直接整个塞进Prompt先不说上下文长度限制检索阶段也无法定位到具体章节。切分太粗每块内容太长检索回来的是好几页文字重点被稀释切分太细又会切断段落之间的逻辑关系模型读到的片段缺乏上下文。这是整套系统里第一个需要反复实验的环节后面会展开讲参数选取。2.2 为什么选ChatGLM-6B这类本地模型而不是直接调用云端API选择ChatGLM-6B系列模型做本地问答核心考量是数据合规和可控性。金融、医疗、政企类场景对文档外流极度敏感直接把内部文档发给第三方API等于把核心资产交给了别人这个风险大多数团队不敢冒。而ChatGLM-6B是可以在内网服务器上部署的开源模型推理过程不经过外部网络在线索和响应上都掌握在自己手里。另一个理由是成本结构。云端API按token计费如果团队内部每天有几十个人高频提问一个月下来是一笔不小的开销。本地部署是典型的固定成本一台带GPU的服务器撑住日常使用边际成本几乎为零。当知识库规模不大、检索到的上下文能控制在几百到两千token时6B级别模型的能力已经足够覆盖内部文档问答这类相对垂直的需求。当然本地模型也有明显代价。ChatGLM-6B的中文基础能力不错但和更大规模的商用模型相比在复杂推理和长文本理解上存在差距。做这个方案前要有明确预期它擅长的是“从给定文档片段中找到答案并组织语言”而不是“凭模型自身知识解答开放性问题”。如果知识库里的文档本身质量差、信息不完整本地模型不会比云端模型更好。选型建议是——先跑通最小链路、用真实文档测试效果再决定是否值得投入GPU资源做完整部署。3. 环境准备与最小可运行链路从下载ChatGLM-6B到LangChain加载模型全流程3.1 硬件评估与模型下载ChatGLM-6B的量化版本到底怎么选在动手写代码前先把硬件底账算清楚。ChatGLM-6B原始FP16权重占用约12GB显存加上推理时的激活值开销单卡16GB的GPU勉强能跑。如果你手里只有消费级显卡比如8GB显存的RTX 3060/3070常见做法是使用4bit量化版本比如通过bitsandbytes的load_in_4bit参数加载把显存占用压到6GB左右。我的建议很直接——先从量化版本跑验证流程没问题后再上更高精度。模型下载需要访问HuggingFace或国内的ModelScope镜像站。以ChatGLM-6B为例模型仓库名是THUDM/chatglm-6b在装有git-lfs的环境里可以用git clone完整拉取权重文件总量接近12GB。如果你的网络访问HuggingFace不稳定优先从ModelScope下载国内速度通常更快。下载完成后确认目录下包含config.json、pytorch_model.bin或分片后的pytorch_model-00001-of-00008.bin和tokenization_chatglm.py这几个关键文件缺失任何一个都会导致加载失败。# 检查模型目录完整性 import os model_dir ./models/chatglm-6b required_files [config.json, tokenization_chatglm.py] bin_files [f for f in os.listdir(model_dir) if f.startswith(pytorch_model)] print(配置文件存在:, all(f in os.listdir(model_dir) for f in required_files)) print(权重文件数量:, len(bin_files))这段代码帮你快速排查下载是否完整。config.json定义了模型结构参数tokenization_chatglm.py是ChatGLM专用分词器权重文件可能以单文件或分片形式存在。3.2 搭建LangChain环境关键依赖版本与最小验证代码这一节的目标是让ChatGLM-6B成功被LangChain加载并完成一次对话这是后面所有工作的地基。创建一个虚拟环境Python版本建议3.9或3.10安装langchain、transformers、torch、accelerate、bitsandbytes这几个核心依赖。版本搭配很有讲究LangChain对上层接口频繁变动transformers版本太新反而可能和ChatGLM-6B的代码冲突我建议直接参考官方仓库requirements.txt中锁定的版本组合比如transformers 4.30.x配合torch 2.0.x这一档。from transformers import AutoTokenizer, AutoModel from langchain.llms import HuggingFacePipeline from transformers import pipeline import torch # 加载量化模型 model_path ./models/chatglm-6b tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, load_in_4bitTrue, # 4bit量化显存不够时打开 device_mapauto, # 自动分配到可用设备 torch_dtypetorch.float16 ) model model.eval() pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens256, # 限制生成长度防止无限输出 temperature0.7, top_p0.85, do_sampleTrue ) # 包装成LangChain可用的LLM对象 llm HuggingFacePipeline(pipelinepipe) # 验证一次对话 resp llm(用一句话介绍什么是本地知识库问答系统) print(resp)这段代码里的参数直接影响生成质量。max_new_tokens256控制单次回答的最大长度问答场景不需要长文输出设太大会让服务响应变慢temperature0.7给了一点随机性但知识库问答更偏向确定性输出后面可以降到0.3以下device_mapauto在只有单卡时会自动把全部模型放上GPU如果加入CPU卸载需要额外配置。如果这一步能跑通并给出合理回复说明模型链路是通的。接下来要做的就是把文档导入管线让模型从“凭记忆回答”变成“基于知识库回答”。4. 文档加载与向量化存储把本地知识库变成模型能检索的索引4.1 文档加载与文本切分PDF、Word、Markdown怎么统一处理一个实际知识库文件夹里往往混着多种格式——PDF制度文件、Word技术方案、Markdown开发文档、TXT日志摘要。LangChain自带多种DocumentLoader常见做法是写一个统一入口函数根据文件后缀分派到对应的加载器最后合并成一个Document列表。PDF加载推荐PyPDFLoader它对扫描版PDF无能为力但作为文本层提取工具足够用Word文档用Docx2txtLoader会丢失页眉页脚对正文内容影响不大纯文本和Markdown直接用TextLoader注意指定编码为utf-8否则Windows环境下经常出现乱码。from langchain.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader def load_documents(file_paths): docs [] for path in file_paths: if path.endswith(.pdf): loader PyPDFLoader(path) elif path.endswith(.docx): loader Docx2txtLoader(path) else: loader TextLoader(path, encodingutf-8) docs.extend(loader.load()) return docs # 递归获取目录下所有待处理的文件 import os target_dir ./knowledge_base all_files [] for root, _, files in os.walk(target_dir): for f in files: if f.endswith((.pdf, .docx, .md, .txt)): all_files.append(os.path.join(root, f)) documents load_documents(all_files) print(f加载完成共 {len(documents)} 个文档片段)loader.load()返回的每个Document包含page_content文本内容和metadata来源路径、页码等metadata在后期展示“答案出自哪份文档”时很有用。如果PDF加载后中文文字出现乱码或空格错位优先检查PDF本身是否为扫描版或者尝试更换加载器版本。加载完原始文档后下一步是切分。我见过很多人直接用默认参数跑结果检索回来一堆相互割裂的片段。针对中文文档我推荐使用RecursiveCharacterTextSplitter它的切割策略是按优先级逐级查找分隔符对段落完整性相对友好。from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个文本块的最大字符数 chunk_overlap50, # 相邻块之间的重叠字符数 separators[\n\n, \n, 。, , , , , ] ) split_docs text_splitter.split_documents(documents) print(f切分完成共得到 {len(split_docs)} 个文本块)chunk_size500是一个面向中文的折中值百度、知乎等平台的实践经验也落在这个区间。500字左右的中文文本既能保持段内语义完整又不至于超过Embedding模型的最大输入长度通常是512 token。chunk_overlap50让相邻块共享一小段上下文避免一个问题被切断在边界上导致检索不到。4.2 Embedding模型和向量库选型从text2vec到FAISS的落地对比文本切分完成后需要把每块文本变成向量。Embedding模型的选择直接决定检索质量中文场景下最常用的几个选项是text2vec-base-chinese、m3e-base、bge-small-zh三者都是开源模型体积在几百MB到1GB之间本地完全跑得动。我的选择逻辑是先试bge-small-zh它在中文语义匹配任务上表现均衡而且体积小、加载快如果检索效果不满意再换m3e-base对比。from langchain.embeddings import HuggingFaceEmbeddings # text2vec和bge都通过HuggingFaceEmbeddings加载 embeddings HuggingFaceEmbeddings( model_name./models/bge-small-zh, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) # 验证相同语义的句子应该得到相近的向量 query_vec embeddings.embed_query(报销流程是什么) doc_vec_a embeddings.embed_query(差旅费用如何申请报销) doc_vec_b embeddings.embed_query(今天天气很好) from sklearn.metrics.pairwise import cosine_similarity import numpy as np sim_a cosine_similarity([query_vec], [doc_vec_a])[0][0] sim_b cosine_similarity([query_vec], [doc_vec_b])[0][0] print(f相关文本相似度: {sim_a:.4f}, 无关文本相似度: {sim_b:.4f})normalize_embeddingsTrue会把向量归一化归一化后的余弦相似度等价于内积计算在向量检索时可以直接用点积算分配合FAISS的IndexFlatIP索引类型能提升检索性能。向量库的选择相对简单。数据量在几万条文本块以内FAISS足够数据量更大或需要服务化部署再上Milvus或Elasticsearch。对绝大多数团队内部知识库的场景FAISS是性价比最高的方案它以单文件形式落盘随程序启动加载不依赖独立数据库服务。from langchain.vectorstores import FAISS # 创建并持久化向量索引 vectorstore FAISS.from_documents( documentssplit_docs, embeddingembeddings ) vectorstore.save_local(./faiss_index) # 模拟线上环境加载已有索引 vectorstore FAISS.load_local( ./faiss_index, embeddings, allow_dangerous_deserializationTrue ) # 检索测试 docs vectorstore.similarity_search_with_score(差旅费报销标准, k3) for doc, score in docs: print(f来源: {doc.metadata}, 相似度: {score:.4f}) print(doc.page_content[:100])similarity_search_with_score返回的结果里带有距离分数FAISS默认返回的是L2距离数值越小越相似。在使用时注意区分不同向量库的分数含义FAISS的L2距离与余弦相似度方向相反写阈值判断时容易搞反。索引建立起来以后实现自动问答的最后一步就是组合检索结果和Prompt交给模型生成答案。5. 避坑与排查本地知识库问答系统最常见的8个翻车点5.1 模型加载阶段显存溢出和transformers版本冲突现象1加载ChatGLM-6B时报CUDA out of memory模型还没开始推理就崩了。原因是硬编码了加载整个FP16权重没有启用量化。解决方法是使用4bit量化加载并且把torch_dtype设为float16device_map设为auto让模型动态分配到卡上。另外检查一下同时运行的其他进程有时候不是模型放不下而是显存被别的任务占了。现象2加载模型时报错“tokenizer class not found”或“quantization config error”。原因是transformers版本与ChatGLM-6B的trust_remote_code机制不兼容。ChatGLM依赖远程代码执行需要trust_remote_codeTrue。如果已经加了还是报错查一下transformers版本过新或过旧的版本都可能导致ChatGLM专用代码无法正常执行直接安装官方推荐版本最稳妥。5.2 文档处理阶段中文内容加载出来是乱码或全挤在一行现象3加载Word或PDF文档后中文乱码或者所有段落挤成一个超长字符串。原因往往是编码不匹配或加载器没有正确识别段落边界。Word文档在Windows下可能是GBK编码如果TextLoader指定utf-8就会报错或乱码。解决方法是统一在加载前用encodingutf-8配合错误处理或者先批量转码PDF乱码则多为扫描件或字体嵌入问题先确认PDF本身能复制出文字。现象4切分后检索回来的片段内容破碎明明在文档里是一段完整的话检索出来却只有半句。原因是chunk_size设得太小或者分隔符列表里没有中文字符切分符。RecursiveCharacterTextSplitter默认分隔符全英文标点中文文本不换行时它会一直切到空的兜底分隔符导致在字符边界硬切。解决方法是把。加入separators列表让切分优先发生在语义完整的位置。5.3 检索与生成阶段答案质量差和慢得离谱现象5用户问的问题在文档里明明有明确答案模型却说“不知道”。原因分两种一是chunk_overlap太小问题对应的内容刚好被切分边界切开检索时两边都匹配不到二是top_k太小正确的片段排在第三第四条却被截掉了。解决方法是把top_k先提到5到10配合search_kwargs调参同时检查文档加载环节是不是漏了文件——实际项目中这种低级错误出现频率很高。现象6模型回答内容明显超出知识库范围开始一本正经地编造制度条款。原因是Prompt模板没有强调“只依据给定上下文回答”。ChatGLM-6B在对话模式下倾向于自由发挥必须要用系统提示词把它的行为约束住。我的常见做法是在Prompt里明确写上“你是企业内部知识助手只能根据以下资料回答用户问题资料中没有的信息请明确告知用户无法回答”。现象7回答速度慢到无法接受一个简单的报销问题等了一分钟。原因通常是传入的上下文过长多个检索结果加起来几千字模型生成时要处理大量输入。解决方法是调小chunk_size、限制max_new_tokens并且只把检索回来的前两三条片段拼进Prompt。另外一个常见做法是引入历史会话摘要机制全程对话拼接会让输入长度越来越大。现象8之前跑通的功能换了一台服务器就启动失败报错不可复现。原因是环境依赖版本不一致尤其是torch和transformers的组合被操作系统和CUDA版本影响。解决方法是把整个运行环境打包成Docker镜像或者写死requirements.txt里的所有依赖版本。提示以上8个坑是我在搭建同类系统时亲身踩过的。其中文本切分和Prompt约束的问题最隐蔽所用文档的写作风格不同最终效果都需要在真实数据上验证不要迷信任何一组默认参数。6. 调参、验证与进阶让自动问答从“能说”到“说得准”6.1 三个必调的检索参数chunk_size、top_k与相似度阈值跑通链路只是起点真正的工作量在调参。我按影响程度排序优先调chunk_size这个全局参数。知识库文档大多是说明性文字500到600字切分通常表现最稳定。如果文档是大量短条目并列的清单比如命令列表、规格参数需要把chunk_size降到300以下防止多个不相关条目被塞进同一个块如果文档是长段落论述比如技术方案、调研报告可以升到800保证段内逻辑完整。第二个必调参数是top_k它决定检索后把多少个片段交给模型。top_k3是个保守的起步值上下文短、模型注意力集中但容易漏答案top_k6到8时覆盖率上来了但碎片信息可能互相干扰模型容易被次要内容带偏。结合FAISS返回的分数做判断找到“答案基本覆盖且无冗余”的临界值。第三个参数是相似度阈值过滤。FAISS的L2距离不能直接当置信度看但可以通过实验找到合理截断点。跑一批已知答案的测试问题记录每条正确检索结果的分数范围然后设定一个阈值低于阈值距离大的结果直接不参与生成这一步能显著减少模型胡说八道的概率。6.2 验证方法构建一份固定测试集来量化问答效果调参不能靠感觉建一套固定测试集是成本最低的验证方式。收集50到100个真实业务问题每题带上“预期答案来源”的关键句子然后跑一遍完整链路记录回答正确率。测试集一旦建好就不要改每次调整参数后对比同一份测试集的结果。你可以在测试脚本里为每个问题设定一个关键词列表只要回答中包含这些关键词就算初步通过这个方法可以快速排除大面积劣化。# 简易评测脚本检查回答是否命中期望关键词 def evaluate_qa(qa_chain, questions_with_keywords): total len(questions_with_keywords) pass_count 0 for question, keywords in questions_with_keywords: answer qa_chain(question) hit all(kw in answer for kw in keywords) print(f问题: {question[:30]}... 命中: {hit}) pass_count int(hit) print(f通过率: {pass_count}/{total} {pass_count/total:.1%}) questions [ (报销流程需要哪些材料, [发票, 审批]), (年假可以累积到下一年吗, [年假, 累计]), # 更多真实业务问题 ]这套方法虽然粗糙但能让你在半小时内判断一次参数调整是变好还是变差。全部调优完成后对Survey式回答的质量评估可以引入人工抽查——让文档作者判断回答是否准确这是最终客观依据。进阶方向上如果知识库规模很大建议把FAISS升级为Milvus支持超过百万级向量的检索如果模型能力成为瓶颈组件结构不变只需要把HuggingFacePipeline换成接入更大规模模型的接口即可。架构解耦带来的收益在模型迭代时最能体现——从ChatGLM-6B切换到新模型时只需重写模型加载函数前面的文档处理、索引建立和检索逻辑完全不用动。做一个本地知识库问答系统的核心思路是“检索负责找到答案模型负责读懂答案”。我在做过几个类似项目后最大的教训是把Prompt模板设计放到了和模型选型同等重要的位置——一个好的模板能最大限度发挥模型能力而一个差的模板会让正确的检索结果也答得稀烂。建议你跑通最小链路后先用50个真实问题跑一遍再进入参数调优环节。希望帮到你。本文还有配套的精品资源点击获取
返回列表