LangChain+ChatGLM-6B:构建本地知识库自动问答实战
发布时间:2026/9/28 21:38:13来源:尧图网络
简介这是一套面向AI开发者与NLP初学者的本地知识库问答系统项目实践资料基于LangChain与ChatGLM-6B等系列大模型实现自动问答适合需要构建私有化知识问答场景的读者。压缩包共75个文件包含Python源码、pickle数据文件、Markdown文档、镜像文件与配置文件等其中39个pickle用于预训练模型与索引缓存12个py覆盖模型封装、文本切分、命令行交互等核心模块整体大小仅17.77MB。内容中可见langchain-ChatGLM应用实现、PaddlePaddle与ModelScope接入、Dockerfile部署配置以及详细中文文档能够帮助读者理解从文档加载、向量化到LLM问答的完整链路并掌握本地知识库的构建方法。目前已有935人学习下载适合用于课程设计、毕业设计或企业知识库问答系统的快速原型参考。1. 本地知识库自动问答是什么LangChain与ChatGLM-6B的组合解决什么你是不是也遇到过这种场景几百页产品手册躺在硬盘里遇到问题得先回忆资料放在哪翻到文件还要再CtrlF好几轮。基于LangChain和ChatGLM-6B构建的本地知识库自动问答就是把这个过程替换成“直接问一句话它给你带出处的一段答案”。它先用向量检索把本地文档的相关片段找出来再交给ChatGLM-6B这类开源LLM组织成回答数据不出内网部署成本也只有一台带显卡的工作站。适合企业内部资料问答、个人知识库整理也适合第一次接触RAG的开发者。想把它跑通核心就三件事文档切分、向量检索、LLM生成。2. 把本地知识库切成向量之前文档加载、中文切块与Embedding选型2.1 最短链路从PDF到本地FAISS索引的一段可复现代码做本地知识库问答最忌讳一上来就调prompt。检索这一步的质量决定了后面的答案上限检索出来的内容本身就是错的LLM怎么组织都救不回来。常见的做法是先把文档目录里的文件加载进来按语义切成小块每一块用Embedding模型转成向量存进本地向量库。这样后面每一次提问都只需要在这堆向量里找最相似的几个片段。下面这段代码就是这条链路的最短写法直接用LangChain自带的组件拼起来from langchain.document_loaders import DirectoryLoader, PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import FAISS # 1. 加载 docs 目录下所有 PDF 文件 loader DirectoryLoader( ./docs, glob**/*.pdf, loader_clsPyPDFLoader, show_progressTrue, ) docs loader.load() # 2. 切块优先按段落边界切切不了再按句子切 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , , , ] ) chunks splitter.split_documents(docs) # 3. 中文向量化 embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5 ) vector_store FAISS.from_documents(chunks, embeddings) vector_store.save_local(./faiss_index)这里要解释几个关键点。DirectoryLoader不认PDF格式必须用loader_clsPyPDFLoader指定如果目录里还有docx可以再加一个Docx2txtLoader或者干脆把不同格式分开加载再合并。第二行的RecursiveCharacterTextSplitter会按照separators里的顺序尝试切分优先保住段落再退到句子中文场景一定要把句号、问号加进去否则它按英文句点切中文长句会被切得支离破碎。chunk_size500指的是字符数不是token数。中文一句话平均二三十个字符500个字符能装下大约十几句话足够表达一个完整含义。chunk_overlap80让相邻块有少量重叠避免问题引用的内容恰好被切在边界上。Embedding我推荐先用bge-large-zh-v1.5它针对中文检索做过优化效果比通用的多语言模型稳定得多。save_local会把索引存到磁盘下次启动不用重新建库。提示PDF加载依赖pypdf向量化依赖sentence-transformers和faiss-cpu。安装时不要只装langchain这三个库缺一个都会在运行时报出很奇怪的ImportError。2.2 参数怎么设chunk_size、chunk_overlap、k值与Embedding模型的取舍同样的文档切法不同检索结果完全不同。这个环节是最值得花时间调的也是LangChain入门教程里经常被略过的地方。下面这张表是我调本地知识库时的默认起点参数典型范围影响我的建议chunk_size200-1000字符太小则信息碎片化太大则一个片段里塞满无关内容中文文档先用500效果不好再往两端试chunk_overlap0-150字符跨块语义被截断取chunk_size的15%-20%即80-100k3-5太少容易漏太多prompt变长、噪声变多先设4再看检索结果调Embedding模型bge系列 / m3e决定检索的“语义”是否贴合中文纯中文用bge-large-zh中英混合用m3e-base为什么chunk_size不能拍脑袋定我遇过一种翻车现场把文档按5000个字符大块切提示词塞进去倒是很全但问题问的是某个具体参数向量检索出来的这块内容里可能同时讲了十几个参数LLM分不清该引用哪个最后答非所问。反过来块切得太小检索出来的片段只有一两句话回答丢上下文尤其是涉及“这个机制的前提条件”这种问题基本必挂。k这个参数同样容易被忽视。它在vector_store.as_retriever(search_kwargs{k: 4})里控制每次检索召回多少个片段。k太小正确答案不在召回里k太大每个片段按顺序拼进prompt前面的信息容易被后面的干扰。如果你发现回答里总带着不相干的内容先别急着骂模型把return_source_documentsTrue打开看看召回的4个片段里是不是混进了2个无关块。至于Embedding模型别默认选text-embedding-ada-002这类英文向云接口。本地知识库场景下一个支持中文的Embedding对效果的影响比换一个更大的LLM还明显。我一般先装一个bge-large-zh-v1.5跑通再拿自己的问题集做对比如果召回的命中率没差就继续用。3. 把ChatGLM-6B接进LangChain检索链本地LLM问答最小实现3.1 为什么选ChatGLM-6B而不是云API隐私、成本、可控很多人在这一步会犹豫既然LangChain支持各种大模型为什么非要费劲在本地部署一个ChatGLM-6B直接调云API不是更省事吗这个问题的答案取决于数据性质。企业内部的技术手册、项目文档、客户反馈往往不能出内网云API意味着每次提问都要把检索到的片段发给第三方这在合规上就可能过不去。本地部署的ChatGLM-6B所有数据都留在自己机器上这是最直接的动机。成本也是实打实的。一个面向内部几十人的问答服务如果每天几千次请求云API按token计费一个月下来不是小数而本地的一张消费级显卡只需要付一次电费。ChatGLM-6B这类开源LLM的算力需求不算高量化后可以在单张8GB显存的卡上跑这对很多团队来说是现有的硬件条件。第三个原因是对 prompt 的控制力。云API通常有内容审核和输入输出限制你没法在 prompt 里随意指定“必须用表格输出”“只许回答三步以内的结论”这类个性要求。用本地模型prompt 怎么写完全由自己说了算。等以后想换更强的Qwen、BaichuanLangChain封装层几乎不用改只要换模型加载路径。这也是“等系列LLM”在标题里被单独点出来的原因。3.2 用HuggingFacePipeline RetrievalQA跑通的最少代码向量库建好之后第二步就是把大模型接进检索问答链。常见做法是用transformers先构造一个文本生成pipeline再用LangChain的HuggingFacePipeline把它包装成标准的LLM接口最后用RetrievalQA把“检索生成”串起来。from langchain.llms import HuggingFacePipeline from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain.vectorstores import FAISS from langchain.embeddings import HuggingFaceEmbeddings from transformers import AutoTokenizer, AutoModel, pipeline # 1. 加载本地向量索引注意要传同一个embedding embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-large-zh-v1.5) vector_store FAISS.load_local(./faiss_index, embeddings) # 2. 加载ChatGLM-6B8bit量化可以压到9G显存左右 tokenizer AutoTokenizer.from_pretrained( THUDM/chatglm-6b, trust_remote_codeTrue, ) model AutoModel.from_pretrained( THUDM/chatglm-6b, trust_remote_codeTrue, load_in_8bitTrue, ) model model.eval() pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens512, temperature0.2, top_p0.8, repetition_penalty1.1, ) llm HuggingFacePipeline(pipelinepipe) # 3. 构造检索问答链 prompt PromptTemplate.from_template( 请仅根据以下资料片段回答问题资料中没有的内容就说不知道。 资料 {context} 问题{question} 回答 ) qa RetrievalQA.from_chain_type( llmllm, retrievervector_store.as_retriever(search_kwargs{k: 4}), chain_typestuff, return_source_documentsTrue, chain_type_kwargs{prompt: prompt}, ) result qa({query: 这个产品的重试机制怎么配置}) print(result[result]) print(来源, [d.metadata.get(source) for d in result[source_documents]])这套代码最需要注意的是load_in_8bitTrue。ChatGLM-6B在FP16精度下显存占用约13GB8bit量化大约9GB如果你的卡只有8GB显存可以再改成load_in_4bitTrue一般能压到6GB左右。显存实在不够的话别硬上6B换1.5B级别的小模型更实际推理速度和稳定性都好得多。max_new_tokens512控制生成答案最长的长度。本地知识库问答的答案通常不需要长编大论设成512足够设太大只会让推理时间变长。temperature0.2和top_p0.8是给问答场景一个偏保守的采样设置减少模型自由发挥。repetition_penalty1.1是对中文生成里常见的复读问题做兜底尤其在模型自主生成内容时很有用。chain_typestuff表示把检索到的所有片段一次性塞进prompt适合k值小、文档短的场景。如果你把k调到10以上stuff方式会撑爆ChatGLM-6B有限的上下文窗口那时就要换refine或map_reduce但代价是推理次数变多、回答时间成倍上涨。我的习惯是优先控制chunk_size和k让stuff方式始终能撑住。3.3 换其他系列LLM时这套代码哪里要动标题里写的是“ChatGLM-6B等系列LLM”等不等于只能死磕这一个模型。这套代码的替换成本很低。换成Qwen或者Baichuan时只需要把AutoModel和AutoTokenizer的路径换成对应模型目录pipeline部分几乎不动。真正要留意的是模型的上下文长度和prompt格式。ChatGLM-6B的旧版本上下文大约在2K token级别所以chunk_size和k都要克制。换成上下文更长的模型后可以顺手把chunk_size调大到700-800让每个片段包含更完整的背景。另一个差异是chat模板新模型通常要求用tokenizer.apply_chat_template把用户消息包一层旧ChatGLM直接拼字符串也能跑但如果换新模型忘了适配chat模板回答质量会明显下降。4. 从离线脚本到可用问答服务FastAPI封装、流式输出与多轮会话的取舍4.1 把离线脚本变成HTTP接口FastAPI一步到位离线脚本只能自己跑要给同事用就得包成一个HTTP服务。这块我一般直接用FastAPI自带参数校验和交互式文档省去写一堆路由模板的时间。接口设计上除了返回答案一定要把召回来源一起返回否则用户看到答案没法判断靠不靠谱。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() qa build_qa() # 复用上一节构造RetrievalQA的逻辑 class AskRequest(BaseModel): query: str history: list [] app.post(/ask) def ask(req: AskRequest): result qa({query: req.query}) return { answer: result[result], sources: [ { source: d.metadata.get(source, ), page: d.metadata.get(page, 0), preview: d.page_content[:100], } for d in result[source_documents] ], }代码里qa build_qa()是在服务启动时一次性加载模型和索引避免每次请求都重复加载否则第一次请求会等上几十秒。sources里我只截了前100个字符做预览不要把整个召回片段返回给前端不然接口体积会很大前端也不方便展示。启动命令也很简单uvicorn server:app --host 0.0.0.0 --port 8000注意这个服务默认是同步阻塞的FastAPI会把它放到线程池里跑。如果同时进来多个请求LLM推理又慢GPU显存很快会被占满。我一般会在推理函数外面加一个信号量限制并发为1宁可让后面的请求排队也别把显存打爆。4.2 流式输出先想清楚首字延迟再决定做不做很多产品经理一上来就要求“像ChatGPT那样一个字一个字蹦出来”。这个需求本身合理但放在本地6B模型上要冷静评估。流式输出解决的是“首字之后”的体验真正让用户感觉卡的是首字延迟——也就是从提问到模型吐出第一个token的耗时。ChatGLM-6B在消费级显卡上的首字延迟经常要两三秒之后输出速度也不快流式只能让用户感觉“在动”不能缩短等待。技术上的做法是用LangChain的StreamingStdOutCallbackHandler或者FastAPI的StreamingResponse把HuggingFacePipeline的streamTrue打开。但在我自己做的内部工具里这个功能优先级一直排在最后原因是本地模型生成速度有限流式输出的价值更多是心理安抚。与其费劲改流式不如先优化检索和prompt把答案质量提上去。4.3 多轮会话历史记录不该直接塞给检索器本地知识库问答最容易翻车的是第二轮问题。用户第一轮问“这个产品的重试次数怎么配”第二轮紧接着问“改成10次会怎样”这里的“它”和“改成”都依赖上下文。LangChain自带的ConversationBufferMemory可以记住对话但它存的是问答历史检索器并不会基于历史改写提问所以第二轮拿到一个残缺的问题照样检索不到正确答案。我一般不做复杂的状态管理就在FastAPI层把最近几轮问答拼成一个重写提示先让LLM把当前问题补全再拿去检索def rewrite_query(query, history): if not history: return query last_q, last_a history[-1] context f用户{last_q}\n助手{last_a}\n新问题{query} return f请把新问题补全为完整问题\n{context}\n补全结果这段逻辑不直接交给向量检索而是先调用一次LLM拿到类似“这个产品的重试次数改成10次会产生什么影响”的完整问题再走检索问答链。代价是多一次推理调用但对多轮准确率的提升非常明显。如果团队用的是Dify这类带知识库流水线的平台它们内部已经把这类查询改写做了自己用LangChain组合时就别忘了这个环节。5. 排查与避坑跑通容易用得好难五个现场复盘5.1 检索结果与提问完全无关先看source_documents再谈prompt现象问题问的是“重试机制配置”返回的召回片段却是一段日志说明LLM回答得驴唇不对马嘴。 原因八成本科不在prompt而在检索。要么是PDF加载后文本乱码切出来的chunk本来就是符号堆要么是Embedding模型不支持中文把语义都编码丢了要么是切块太小一个完整知识点被拆得七零八落。 解决先打印几条召回结果。打开result[source_documents]看前200个字符是不是可读的中文。如果是乱码换PDF加载方案比如先转成文本再处理。如果不是乱码但不相关换bge-large-zh-v1.5再试。如果相关但不完整调大chunk_size和chunk_overlap。这一步能用掉一个下午但值得。5.2 ChatGLM-6B推理慢到没法用显存、量化和并发三个瓶颈叠加现象单次回答要等20秒两三个人同时用就开始报显存错误。 原因FP16加载吃满13GB显存max_new_tokens又设得过大模型生成时占用的显存会随batch上涨。FastAPI接口同时接收多个请求时多个推理任务同时在GPU上跑显存直接爆掉。 解决先用8bit或4bit量化把模型压到10GB以内把max_new_tokens从默认的1024降到256-512给推理加锁限制并发数为1。如果并发需求确实高别用HuggingFacePipeline硬扛把模型接到vLLM这类推理框架上LangChain侧仍然走openai兼容协议代码改动不大。5.3 回答一本正经地“编”知识幻觉不是调prompt就能根治的现象回答里出现资料里没有的数字、日期和结论而且措辞非常肯定。 原因生成式LLM在信息不足时会“脑补”。stuff方式把多个召回片段堆在一起模型分不清哪句来自资料哪句是它自己编的。prompt里没有明确禁止补充时幻觉尤其严重。 解决先在prompt里加一句“不允许回答资料片段之外的内容找不到就回答‘资料中未找到’”这也是最便宜的一招。再把temperature降到0.1-0.2top_p在0.7-0.8之间。最后通过return_source_documents人工核对如果发现幻觉都来自某类问题说明那块语料在知识库里压根没覆盖到需要补文档而不是改模型。5.4 新文档加进目录后索引没更新增量更新和重建的取舍现象向docs目录里放了新版本手册重建索引后重新提问新知识仍然答不出来。 原因FAISS.save_local保存的是建库那一刻的向量之后新文档的chunk不会自动进入索引。很多人直接跑建库脚本全量重建但重建后又忘了load_local时传入同一个Embedding对象导致加载的向量维度对不上。 解决把建库脚本拆成add_to_index(file_path)这样的函数每次新增文档只对新文档做切块和向量化然后调用vector_store.add_documents(new_chunks)再save_local。注意load_local时必须传入和建库时相同的Embedding实例否则会报错。文档量不大的话定期全量重建也不是坏事能顺便清理已失效的旧版本内容。5.5 同类文档在Dify里效果好自己用LangChain变差先别怪框架现象同一批知识库放进Dify回答质量很正常换成自己写的LangChain链答非所问。 原因Dify这类知识库流水线在处理文档时做了额外的分段清洗和索引策略检索阶段还可能有查询改写和重排。而自己用LangChain时通常只是“加载-切块-一次检索-直接生成”中间少了几个优化环节。 解决先把Dify里那份知识库的分段策略导出来看它每段多长、重叠多少、有没有过滤空行和页眉页脚在自己的脚本里对齐。然后在LangChain侧加MultiQueryRetriever或多路召回让同一个问题生成几个不同问法的查询再合并结果。这一步对答案质量的提升比换更大的模型明显得多。6. 上线前给检索质量打分三十分钟验证法以及一条更进阶的改造路线6.1 三十分钟手动打分法用hit rate找召回问题模型部署完成、接口能回答之后别急着宣布上线。我习惯先做一轮检索质量打分从真实使用场景里抽30个问题每个问题记下至少一个“应该在文档里出现的关键词或句子”然后用脚本把每个问题检索出来的前4个片段打印出来人工判断答案是否在召回里。questions [ (重试机制怎么配置, retry), (支持哪些部署方式, docker), ] hit 0 for q, marker in questions: docs vector_store.similarity_search_with_score(q, k4) joined .join(d.page_content for d in docs) if marker.lower() in joined.lower(): hit 1 print(fhit rate: {hit / len(questions):.2%})这个脚本只做粗筛关键是让团队成员都能看得见“哪个问题没召回到”。如果hit rate低于70%调prompt毫无意义回去调切块和Embedding。如果高于90%说明检索链路没问题剩下的差异由LLM生成负责。6.2 一条更进阶的改造路线从单链到查询改写和图编排当检索质量已经稳定再往上是把链路从“单次检索单次生成”改成有状态、可分支的流程。LangChain本身提供的是链式的组件拼装而LangGraph更偏有向图的编排能在检索前加一个查询改写节点在检索后加一个重排节点必要时还能做多轮检索。不用一开始就上整个图结构。最划算的改造是多查询检索把用户问题拆成几个不同表述分别检索后再合并去重。我手上好几个内部问答系统最后都停在这一步就够用了。至于要不要上LangGraph取决于你是否需要处理“先判断问题类型再决定走哪条检索路径”这类复杂分支。我保留到现在的习惯是每次调完一个参数都把“问题-召回片段-最终答案”一起存档下来。这样哪次效果变差了翻回去看历史就能定位是检索变了还是模型变了不用靠脑子记。希望这套路径能帮你把本地知识库问答从能跑到好用少走几段弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网