LangChain+ChatGLM-6B本地知识库问答实战:从RAG构建到调优
发布时间:2026/9/11 19:50:43 锦皓数字建站

简介面向计算机、通信、人工智能、自动化等相关专业学生及从业者的一套完整毕业设计项目基于LangChain和ChatGLM-6B等主流LLM解决针对本地知识库的自动问答问题。该项目为个人毕业设计代码经调试测试确保可运行作者答辩评审分达98分既适合作为课程设计、大作业或毕业设计参考也方便进阶用户在其基础上扩展功能。压缩包共76个文件、仅17.96MB其中39个pickle缓存文件用于存放词向量或模型数据12个Python源文件覆盖中文文本切分、嵌入模型封装、ChatGLM接口调用、命令行与Web展示等关键模块另有Markdown说明文档、部署配置与依赖清单等辅助文件整体结构清晰便于按模块学习。目前已有251人学习下载内容还包含离线部署手册、常见问题记录与更新日志可帮助快速理解项目结构和复现问答流程整体具有较高的学习借鉴价值。1. 本地知识库自动问答为什么选 LangChain 和 ChatGLM-6B 这条技术路线任何一个在政务、金融或企业内部做过知识管理的人都会遇到同一个问题资料都放在本地文档成百上千份但检索靠关键词、问答靠人工文档利用率极低。把大语言模型接到本地文档上做自动问答成了这两年最实在的落地方向。常见做法是拿 LangChain 搭流程ChatGLM-6B 等开源 LLM 做推理配合向量数据库完成“语义检索 生成回答”。这个链路之所以受欢迎是因为它不需要微调模型、不需要昂贵的算力还能保证文档不出内网。本文就沿着这套方案从原理到指令把每个环节的参数、代码和坑讲清楚。适合正在选型、或者已经上了 LangChain 但回答质量和召回率不理想的工程师。2. 问答系统的核心链路从 LangChain 架构到 RAG 增强检索2.1 先理解 LangChain 在链路里的角色LangChain 是一个编排框架本身不具备大模型能力。它把“对话、检索、记忆、工具调用”这些能力标准化成组件开发者通过链式组合来搭建应用。很多刚接触 LangChain 的人会误以为它是一个现成的问答产品其实它更像是一个将流程串起来的中间层。在本地知识库自动问答这个场景中LangChain 负责的工作是把本地文档读入内存做格式清洗再切割成合适大小的块将文本块调用 Embedding 模型转成向量写入向量库用户提问时先将问题向量化再到向量库做相似度检索取出最相关的文档片段把检索结果和用户问题拼装成 Prompt交给 LLM 生成最终回答这里最关键的思维转变是LLM 不是直接“读”你的文档而是通过检索把文档片段作为上下文喂给它。这就是热词里反复出现的 RAGRetrieval-Augmented Generation—— 检索增强生成。用户问题 ↓ 向量化 → 向量库相似度检索 → Top-K 相关文档 ↓ 拼装 Prompt问题 文档片段 记忆 ↓ ChatGLM-6B 等 LLM 生成回答这个链路里每个环节的失误都会被放大文档切得太碎语义就断了向量检索召回太差模型再强也答不对Prompt 拼装不合理模型会被无关信息带偏。后面几章会逐一展开。2.2 为什么首选 ChatGLM-6B 而不是 GPT 系列选择 ChatGLM-6B 有几个直接原因。首先是部署门槛低6B 参数的模型用 int4 量化后显存占用约 6GB 左右一张消费级显卡就能跑起来即便是纯 CPU 推理速度慢一些但也能用。其次是中文理解能力在同等参数规模的模型中表现靠前这对本地知识库这种中文文档为主的场景非常关键。从 LangChain 的角度来看ChatGLM-6B 是标准 OpenAI 兼容接口通过 HuggingFace Pipeline 或 FastAPI 封装后可以直接接入 LangChain 的ChatGLM或HuggingFacePipeline类不需要二次开发。应该避开一个误导不要拿 ChatGLM-6B 和 GPT-4 比生成质量这没有意义。6B 模型的优势是私有化部署和可控性而不是绝对效果。如果你手里的文档数量少、格式规范用 ChatGLM-6B 够用如果文档量特别大且问题偏专业后续可以无缝切换到 ChatGLM-130B 或者 Qwen-14BLangChain 这层封装恰好让这种切换成本变低。2.3 RAG 与微调的选择边界除了 RAG本地知识库问答的另一种方案是微调。但这两个路线的本质完全不同微调是把新知识“写进”模型权重代价是需要构造训练集、做训练且每次变更文档都要重新训练RAG 是“外挂知识”文档的增删改只影响向量库模型本身不动在本地知识库场景下RAG 的优势在于文档变更频繁、需要可追溯的回答来源且团队可能不具备持续的微调工程能力。RAG 也保留了模型的通用能力 —— 微调一个 6B 模型如果数据和参数没控好很容易发生“灾难性遗忘”模型反而变笨了。提示如果业务文档动辄几千份且格式高度统一比如全是合同模板可以考虑“RAG 少量微调”结合先向量检索再让微调后的模型按固定格式输出。3. 动手搭建最小可用系统LangChain ChatGLM-6B 向量库3.1 环境准备与依赖安装以下环境是在 Linux 服务器上验证过的组合。GPU 显存最低 8GB如果显存只有 6GB把加载量化等级改成bitsandbytes的 4bit 加载即可。# Python 3.9 或 3.10 均可 pip install langchain langchain-community langchain-chroma chromadb pip install transformers accelerate sentencepiece bitsandbytes pip install pypdf docx2txt这里用langchain-chroma而不是通用chromadb是因为新版 LangChain 把向量库集成拆成了独立包避免版本冲突。pypdf用于解析 PDFdocx2txt则读取 Word。另外需要说明一个热词对应的场景很多人搜“langchain 过时了吗”是因为看到了 LangGraph 的出现。LangChain 本身没死只是把复杂 Agent 的编排能力逐渐移交给了 LangGraph。本文的问答场景是一个相对线性的 RAG 流仍然用 LangChain 的LCELLangChain Expression Language语法最合适不需要引入 LangGraph。3.2 文档加载与切分决定问答效果的第一道关口所有 RAG 应用的第一个成功要素都是切分策略。切得太大检索出来的片段会混入太多无关内容被 LLM 当成事实切得太小语义又被割裂模型拿不到完整的逻辑。from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter def load_documents(file_path: str): if file_path.endswith(.pdf): loader PyPDFLoader(file_path) elif file_path.endswith(.docx): loader Docx2txtLoader(file_path) else: loader TextLoader(file_path, encodingutf-8) return loader.load() # 切分参数chunk_size 与 chunk_overlap 是召回效果的关键 text_splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , ., , ], ) docs load_documents(data/xxx.pdf) chunks text_splitter.split_documents(docs)chunk_size300是一个相对稳妥的经验值适应多数中文文档。chunk_overlap50是为了避免一句话被拦腰切断后检索时丢失上下文。separators里按优先顺序把换行符、句号放在前面保证拼音和英文混排的文档也能切到自然边界。3.3 配置 Embedding 模型中文场景下的选型要点向量化的质量直接决定“能不能检索到”。当前最有名的通用 Embedding 模型是text-embedding-ada-002但本地部署场景首先排除它因为数据要出网。中文开源模型里text2vec-large-chinese是经过大量社区验证的常用选择显存占用不高检索效果稳定。from langchain_huggingface import HuggingFaceEmbeddings embedding_model HuggingFaceEmbeddings( model_nameGanymedeNil/text2vec-large-chinese )注意langchain_huggingface是一个独立包需要用pip install langchain-huggingface单独安装。如果你手里的机器内存紧张也可以选用shibing624/text2vec-base-chinese参数少一半效果差距不大。3.4 写入向量库与构建检索器接下来将切分好的文本块写入 Chroma完成知识库索引构建。from langchain_chroma import Chroma # 构建向量库并持久化 vector_store Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db ) # 相似度检索 Top-5 retriever vector_store.as_retriever( search_typesimilarity, search_kwargs{k: 5} )persist_directory参数让向量库落盘服务重启后不需要重新构建节省时间。k5的含义是每次问答从库里取出最相关的 5 个片段。这个数值需要根据文档质量和模型能力动态调整太高会把噪声带进来太低则召回不全。3.5 加载 ChatGLM-6B 并封装成 LangChain LLM这是整条链路中最容易版本踩坑的地方。ChatGLM-6B 通过 Transformers 加载后要包装成 LangChain 的HuggingFacePipeline才能被链调用。import torch from transformers import AutoTokenizer, AutoModel, pipeline from langchain.llms.huggingface_pipeline import HuggingFacePipeline tokenizer AutoTokenizer.from_pretrained(THUDM/chatglm-6b, trust_remote_codeTrue) model AutoModel.from_pretrained( THUDM/chatglm-6b, trust_remote_codeTrue, load_in_4bitTrue, # 显存不足 8GB 时开启 torch_dtypetorch.float16, device_mapauto ) pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens512, temperature0.7, top_p0.9, repetition_penalty1.1 ) llm HuggingFacePipeline(pipelinepipe)因为 ChatGLM-6B 使用trust_remote_codeTrue加载自定义代码请务必确认模型文件来源可信。max_new_tokens512设定回答的最大 token 长度太长会增加显存压力太短则没法给出完整步骤类答案。3.6 组装问答链检索、Prompt、LLM 三者拼接LangChain 从 0.1 版本开始推荐 LCEL 写法语法的组合性和类型提示都比旧的RetrievalQA链更清晰也更容易 debug。from langchain.prompts import PromptTemplate from langchain.schema import StrOutputParser from langchain.schema.runnable import RunnablePassthrough template 请根据以下资料片段回答问题。 资料片段由 doc 和 /doc 包裹其中可能存在无关信息请忽略。 doc {context} /doc 问题{question} 如果资料片段没有包含答案所需的信息请直接说“根据提供的资料无法回答”。 请用中文回答。 prompt PromptTemplate.from_template(template) def format_docs(docs_list): return \n\n.join(doc.page_content for doc in docs_list) qa_chain ( { context: retriever | format_docs, question: RunnablePassthrough() } | prompt | llm | StrOutputParser() ) answer qa_chain.invoke(公司请假的审批流程是什么) print(answer)retriever | format_docs是 LCEL 的管道操作符拖取检索结果后送入 Prompt 模板。RunnablePassthrough把原始问题原样传给模板。StrOutputParser将 LLM 返回的对象转成纯文本字符串。整条链的执行逻辑是用户问题 → 向量检索 → 拼装上下文 → 交给 ChatGLM-6B 生成 → 输出答案。这也是热词里的“langchain 架构”在最小规模下的完整样例。4. 回答质量调优参数怎么摆、效果怎么验证4.1 检索参数k 值、相似度阈值与重排玩熟基础链路后下一步就是调参和做效果对比。当前链路中检索参数的调整优先级最高因为生成环节再强力检索错了什么都白费。先看k值的一个实用调法。切分后的文本块若是 300 字左右检索到 5 个块相当于向模型提供约 1500 字的上下文。可以按问题复杂度来做初步建议指标含义建议值说明chunk_size200-600小值适合法律法规等逐条表述文档chunk_overlap10%-20% 的 chunk_size避免切分导致语义断点k检索块数4-8根据答案长度和文档噪声调整score_threshold0.5-0.7低于该分数视为不相关直接丢弃score_threshold在as_retriever中的配置方法是换用search_typesimilarity_score_threshold配合search_kwargs传入阈值。还有一个被很多文章忽略的操作重排Rerank。Chroma 这类向量库做的是初召Recall重排则是对召回结果做一次精排。常见的做法是用bge-reranker-large做交叉编码器二次打分能明显把最相关的文档排到前面。LangChain 中通过ContextualCompressionRetriever接入。from langchain.retrievers import ContextualCompressionRetriever from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers.document_compressors import CrossEncoderReranker encoder HuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-large) compressor CrossEncoderReranker(modelencoder, top_n3) compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverretriever )重排的代价是多一次模型推理耗时但它带来的精度提升非常明显尤其在候选块数量超过 10 个的时候。top_n3的意思是从初召结果中再挑选 3 个最相关的片段进 Promot。4.2 生成参数temperature 与 top_p 的取值ChatGLM-6B 在HuggingFacePipeline里暴露了标准的采样参数但很多人习惯网上抄一份配置不理解每个参数在回答质量上的作用。只需锚定一个事实温度越高生成越有创造力也越容易胡扯。参数推荐值场景说明temperature0.1 - 0.3知识库问答强调精准尽量偏低top_p0.8 - 0.9低温度下起补充作用repetition_penalty1.1 - 1.15超过 1.2 则输出会变得机械max_new_tokens256 - 1024长文档摘要类任务取上限本地知识库问答是最接近“闭卷考”的场景理想答案是直接源自文档内容不需要模型发挥。所以temperature0.2左右是合理的起点。4.3 调试工具LangSmith 与自定义中间层打印调参不能靠“看回答感觉对不对”必须有观察中间结果的手段。国外的 LangSmith 在本地部署或私有化场景下不好接入更实际的做法是直接打印检索结果检查问题有没有被匹配到正确的文档。def debug_retrieval(question: str): results retriever.invoke(question) for i, doc in enumerate(results): print(f--- 第 {i1} 个片段来源: {doc.metadata.get(source, 未知)} ---) print(doc.page_content[:200]) print()把上一步的 debug 输出和最终回答并列对比就能定位问题到底出在“检索不到”还是“生成了错误内容”。另外需要留意 LangChain 版本变化。langchain.llms.HuggingFacePipeline在 0.3 版本之后迁移到了langchain_huggingface如果你用的是更早些的代码示例很可能遇到“ModuleNotFoundError”或参数不兼容的报错。处理方式是先查看当前版本的 API 文档再对照改导入路径。这也是热词里“langchain 菜鸟教程”和“langchain 面试题”中最常考的细节。5. 本地化部署的实战避坑指南5.1 先解决 CPU 推理速度问题如果只有 CPU 可用ChatGLM-6B 的单次推理可能要数十秒甚至更久。常见的优化是使用llama.cpp的 GGUF 格式模型配合 CPU 推理或者降级使用 CPU 友好的小模型。这里给两条路线任选其一即可# 路线一使用 llama.cpp 的 GGUF 模型 pip install llama-cpp-python # 下载 chatglm-6b 的 GGUF 量化文件然后加载如果坚持使用 Transformers 加载则需要torch.set_num_threads()控制 CPU 线程数并通过model AutoModel.from_pretrained(..., low_cpu_mem_usageTrue)控制内存占用。不过坦白说6B 模型在 CPU 上做实时问答体验有限生产环境建议至少配一张显存 12GB 以上的 GPU。5.2 解决向量库持久化和增量更新的问题生产环境的知识库不是一次性建好的而是持续追加。Chroma 的from_documents每次执行都会重置库内数据这属于新手的第一个坑。正确做法是先判断路径是否存在再做增量写入import os vector_store None if os.path.exists(./chroma_db) and os.listdir(./chroma_db): vector_store Chroma( persist_directory./chroma_db, embedding_functionembedding_model ) else: vector_store Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db ) # 增量添加新文档 vector_store.add_documents(new_chunks)注意Chroma类的add_documents方法只负责索引新内容文档更新后原有的旧向量依然存在。要保证更新效果需要按metadata里的文档 ID 做先删除再写入。# 删除旧版本后再写入新内容 vector_store.delete(where{source: data/xxx.pdf}) vector_store.add_documents(new_chunks)where条件可以按文档来源字段过滤这个设计在上线时尤其重要。5.3 Embedding 模型与 LLM 的分工还有一个常见的认知误区集中在“Embedding 模型能不能用同一个 LLM 来充当”上。很多初学者以为向量化也用 ChatGLM-6B 就行实际是不同的模型职责完全不同Embedding 模型将文本映射为稠密向量输出向量目标是让语义相近的文本在向量空间里距离更近LLM 是生成模型输出文字目标是理解上下文并生成回答有些项目会把 LLM 的最后一层隐藏向量取出来当文本向量用不否认这样也能工作但维度大、推理耗时高且效果不如专门的 Embedding 模型。所以不要在这个环节省事按前面推荐的模型即可。5.4 Prompt 层级拆解清晰界定 RAG 的三段式结构一个高质量的知识库问答 Prompt 应该尽量保持三段结构清晰可见角色设定、资料片段、问题。前面给出的模板已经是一个基础版本下面做一次增强目标是提升回答的鲁棒性template 你的任务是基于给定的资料片段回答用户的问题。 回答必须满足以下要求 1. 优先使用资料片段中的原话或原意 2. 当资料片段信息不足时明确说明“资料中未找到相关内容”不得编造 3. 如果问题涉及操作步骤请按步骤分条列出 4. 回答末尾标注信息来源格式为 [来源:文件名]。 资料片段 {context} 用户问题{question} 你的回答 prompt PromptTemplate.from_template(template)这里的第 4 条容易引起争议实际操作中可以配合Document对象的metadata字段来追加来源。方法是在format_docs函数中读取doc.metadata[source]并手写到模板里。def format_docs_with_source(docs_list): formatted [] for doc in docs_list: source doc.metadata.get(source, 未知) formatted.append(f[来源:{source}]\n{doc.page_content}) return \n\n.join(formatted)有了来源标注用户可以直接打开原文档核对这在企业知识库场景中对信任建立非常关键。5.5 从原型到生产四个必须补齐的工程缺口原型系统上线之前一般还需要解决四个问题它们直接影响系统能否稳定运行在当前环境里。第一个缺口是接口封装。不要直接通过 Python 脚本交互建议用 FastAPI 封一层 HTTP 服务from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QARequest(BaseModel): question: str class QAResponse(BaseModel): answer: str sources: list[str] app.post(/qa, response_modelQAResponse) def answer_question(req: QARequest): result qa_chain.invoke(req.question) return {answer: result, sources: []}第二个缺口是会话记忆。LangChain 的ConversationSummaryBufferMemory可以保留最近的对话摘要避免多轮问答中用户追问“那第二步呢”时模型丢失上文。不过在最早版本里先不用急着加记忆加了反而可能把无关的历史话题带入当前上下文。第三个缺口是并发控制。如果系统被多个用户同时使用要限制最大并发数防止显存溢出。常规做法是在模型加载层用信号量限制并发或在服务层做排队。ChatGLM-6B 的推理是串行的并发高了会出现 OOM 或排队时间过长。第四个缺口是日志与监控。至少需要记录每轮问答的问题、回答、检索到的文档来源、耗时用于后续分析回答质量。埋点可以很简单只需要在qa_chain.invoke前后打印时间戳和检索结果即可。6. 进阶验证用 8 组测试样本给问答系统打分既然系统已经跑通最后分享一组成本低但有效的验证方法。不要只看两三个例子就说“效果还可以”而是固定一套测试问题集在每次参数调整后反复运行用统一的维度打分才能知道改动到底是变好了还是变坏了。制定一个 8 条问题的黄金测试集覆盖四类基本情况2 条直接能从文档唯一位置找到答案的问题验证基础召回2 条需要综合两个以上段落才能回答的问题验证跨文档理解2 条文档里没有答案的问题验证模型是否不乱编2 条答案在文档中但不使用原话的问题验证语义泛化每次调整参数后记录三个数字回答正确数、来源标注正确数、平均响应耗时。如果调了参数正确数不升反降则回滚参数再尝试其它方向。额外建议记录一个“幻觉率”指标。做法是在每次回答后人工检查回答内容是否都来源于检索到的文档片段。这个指标其实比正确率更关键 —— 一个答错但能看出是依据文档答错的系统比一个胡编的系统更容易继续调优。最后提一个适合熟手的进阶方向。当文档量变大到一定程度单一向量检索会开始出现语义死角此时可以从“一步检索”演化为“多路召回”同时用关键词检索BM25和向量检索再把两路结果合并去重后一起喂给 RAG。LangChain 中EnsembleRetriever就是干这个的它的优势在于向量检索擅长语义匹配、关键词检索擅长精确匹配二者互补后准确率往往有明显提升。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。