ARTICLE DETAIL

资讯详情

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

LangChain 与 ChatGLM-6B 搭建本地知识库问答:RAG 实战与避坑指南

LangChain 与 ChatGLM-6B 搭建本地知识库问答:RAG 实战与避坑指南 简介该资源为基于LangChain与ChatGLM-6B等系列大语言模型的本地知识库自动问答项目定位面向计算机、人工智能、自动化等专业学生及从业者适用课程设计、毕业设计或个人进阶实践。项目以Python为主要开发语言围绕文档加载、文本分割、向量检索与大模型调用等模块展开代码经过完整调试测试可快速运行部署。压缩包共包含76个文件主要涵盖pickle缓存数据、py源码脚本、md说明文档、jpg示意图、txt配置文件及readme、toml配置、Dockerfile等辅助内容整体体积约17.96MB。包内附有详细README、FAQ、部署说明、更新历史及操作手册并有demo演示图辅助理解整体流程便于从零搭建或二次开发。目前已有251人学习下载具备较高参考价值适合希望掌握本地知识库问答系统实现思路与LangChain加ChatGLM应用范式的中高级学习者。1. 本地知识库问答到底是什么LangChain ChatGLM-6B 这条链值不值得走一个很现实的场景内部文档堆了几百份 PDF 和 Markdown新人问“项目部署的默认端口在哪”老员工自己也要翻半天共享盘。基于 LangChain 和 ChatGLM-6B 等系列 LLM 的针对本地知识库的自动问答就是用来解决这种“资料有但找不到”的问题。它的做法是把本地文档切块、向量化存进本地向量库用户提问时先检索出最相关的片段再让大模型用这些片段组织成答案。整个过程数据不出内网模型权重、向量库全部在自己机器上适合有私有化部署需求的技术团队也适合想折腾本地 RAG 做自动问答的个人。我按实际搭过的路径写先是选型理由再给最小可跑代码最后是参数设置和避坑记录。2. 先想清楚再动手RAG 架构与模型选型为什么是 LangChain 而不是裸调模型2.1 本地知识库自动问答的底层逻辑检索增强生成标题里“针对本地知识库的自动问答”说白了就是 RAGRetrieval-Augmented Generation检索增强生成。为什么不把整本手册直接塞给大模型因为 ChatGLM-6B 这类开源模型的上下文窗口有限默认也就 2048 个 token塞不下几百页文档强行塞进去回答速度慢且成本高。RAG 的思路很直接你问“门禁卡丢了怎么办”先从向量库里找出“门禁卡补办流程”那几段原文再把问题和这几段原文拼到一起交给模型生成答案。模型不需要记住整本手册只需要阅读相关片段。我把这条链路拆成五步文档加载把 PDF、Word、Markdown 解析成纯文本、文本分割按段落和句号切块避免一块内容横跨多个主题、向量化用 Embedding 模型把文本变成向量、相似度检索用余弦相似度找最相关的 top_k 块、生成回答把检索结果拼进 Prompt 让模型组织语言。第 3 章的代码就是按这条路写的第 4 章的参数也分别对应这几个步骤的坑。很多人第一次搭的时候跳过检索直接让模型回答结果就是一本正经地胡说八道。RAG 最大的价值是“可解释”模型答完你可以把引用的原文片段一并打出来用户能自己核对。内部知识库场景里这一点特别重要否则业务部门问你“这答案是从哪来的”你答不上来这项目就算白做了。2.2 ChatGLM-6B 等系列 LLM选开源模型的硬件账与效果账标题特意写了“ChatGLM-6B 等系列 LLM”说明这不是唯一选择。ChatGLM-6B 是清华系开源的中英双语对话模型6B 参数在消费级显卡上经过 INT4/INT8 量化后能跑起来。我第一套方案就用它原因是它处理“报销流程”“权限申请”这类中文业务表述比同体量的国外开源模型更稳。后来我也试过 Qwen-7B、Baichuan2-7B结论是如果你的卡是 24GB 显存的 3090/4090直接用 FP16 的 7B 模型效果最好如果只有 16GB老老实实用 6B 的 INT4或者换 4B 的小模型。显存和效果是一条递减曲线没有白嫖的选项。还有一笔账容易忽略模型下载体积。ChatGLM-6B 的权重文件 FP16 约 12GB量化版约 6GB。先确认你的磁盘和网速撑不撑得住再决定用哪个。我一般建议第一次跑就用 INT4 量化版先把流程走通再回头换更重的模型因为调通链路比追求单次效果更重要。选型时还要注意 LLM 和 Embedding 模型的匹配。常见组合是 ChatGLM 系列搭配 m3e-base 或 text2vec-large-chinese这两个中文 Embedding 在开源里算第一梯队。别小看这一步Embedding 模型选差了后面检索全跑偏再好的 LLM 也白搭。我见过有人用英文 Embedding 索引中文文档检索出来的东西驴唇不对马嘴这不是模型笨是前期选型翻车。2.3 LangChain 在这里扮演的角色把文档加载、向量库、Prompt 串成流水线有朋友问我自己写脚本调用模型、搞向量库不行吗行但你会被各种边缘情况拖死。LangChain 的价值在于把文档加载、文本分割、向量化、检索、Prompt 组装、模型调用这些步骤统一封装成标准组件。想从 PDF 换成 Markdown 文档换一个 Loader向量库想从 Chroma 换成 FAISS换一个类名。我习惯用langchain_community.document_loaders和langchain.text_splitter这套接口代码写起来很直接。from langchain.chains import RetrievalQA from langchain_community.vectorstores import Chroma # vectorstore 是已经建好的本地向量库实例 qa RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 3}), chain_typestuff ) answer qa.run(门禁卡丢失了怎么办)这段代码把“检索 组装 Prompt 调用 LLM”串成一行。search_kwargs{k: 3}控制每次检索取回几个片段。chain_typestuff表示把所有检索结果直接拼进 Prompt适合片段少的场景。LangChain 不是银弹它版本迭代快API 经常变网上教程三天两头过期。我的经验是锁定一个大版本跑通后再考虑升级。还有LangChain 只是编排框架不改变模型能力上限回答好不好最终还是取决于检索质量、Prompt 和模型本身别把它当黑匣子出了问题要能跳到底层去看。3. 最小可跑系统从 PDF 文档到能回答问题的完整命令与代码3.1 环境准备Python 虚拟环境与依赖安装先创建独立的虚拟环境避免把系统 Python 搞乱。我用 Python 3.10PyTorch 版本根据显卡驱动装对应版本然后安装 LangChain 和文档解析、向量库相关依赖。记得锁定 LangChain 的 0.1.x 大版本因为 0.2 之后 API 变动较大很多网上教程都不兼容。python -m venv venv source venv/bin/activate # Windows 上执行 venv\Scripts\activate pip install langchain0.1.* langchain-community chromadb pip install pypdf docx2txt sentence-transformers pip install transformers accelerate逻辑说明langchain-community是 0.1 版本之后社区组件所在的包Loader、向量库实现大多在这chromadb是本地向量库默认持久化到磁盘pypdf用于解析 PDFdocx2txt用于解析 Wordsentence-transformers负责加载 Embedding 模型transformersaccelerate负责加载 ChatGLM-6B 并进行设备分发。如果你的机器访问 Hugging Face 很慢设置HF_ENDPOINThttps://hf-mirror.com环境变量再下载权重能省不少时间。3.2 加载并向量化本地文档文本分割与 Embedding 的选择把文档放进docs/目录下面这段代码读取 PDF按固定块大小切分然后用中英文 embedding 模型转成向量写入本地数据库。注意切分规则里我指定了中文标点作为分隔符这对中文文档很重要。from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载 PDF 文档 loader PyPDFLoader(docs/员工手册.pdf) documents loader.load() # 2. 切分文档chunk_size 是单块最大字符数 splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , , ] ) chunks splitter.split_documents(documents) print(f切分成 {len(chunks)} 个文本块) # 3. 加载 Embedding 模型并写入向量库 embeddings HuggingFaceEmbeddings(model_namem3e-base) vectorstore Chroma.from_documents( chunks, embeddings, persist_directory./chroma_db )逻辑说明PyPDFLoader按页加载RecursiveCharacterTextSplitter会先按两个换行切再按单个换行、句号、感叹号逐级降级切分这样能保留语义完整的句子。chunk_size300对中文大约是 300 字对应 300 个左右的 token配合 ChatGLM-6B 的上下文够用。chunk_overlap50让相邻块有 50 字重叠避免某个关键句恰好被从中间切断。HuggingFaceEmbeddings会从 Hugging Face 下载 m3e-base 模型国内用镜像源更稳。首次运行后./chroma_db目录会保存向量数据下次直接加载不用重新切分。3.3 接入 ChatGLM-6B 并实现检索问答核心脚本这一步把 ChatGLM-6B 包装成 LangChain 能识别的 LLM 对象。ChatGLM-6B 官方推荐用model.chat()生成所以我不走通用的pipeline路径而是写一个轻量的自定义 LLM 类把_call方法接到model.chat()上。from langchain.llms.base import LLM from langchain.chains import RetrievalQA from transformers import AutoModel, AutoTokenizer from typing import Optional, List tokenizer AutoTokenizer.from_pretrained(THUDM/chatglm-6b, trust_remote_codeTrue) model AutoModel.from_pretrained( THUDM/chatglm-6b, trust_remote_codeTrue, device_mapauto, load_in_4bitTrue ) class ChatGLMLLM(LLM): model: AutoModel tokenizer: AutoTokenizer property def _llm_type(self) - str: return chatglm def _call(self, prompt: str, stop: Optional[List[str]] None) - str: response, _ self.model.chat(self.tokenizer, prompt) return response llm ChatGLMLLM(modelmodel, tokenizertokenizer) # 加载本地向量库并构建 QA 链 from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings embeddings HuggingFaceEmbeddings(model_namem3e-base) vectorstore Chroma(persist_directory./chroma_db, embedding_functionembeddings) qa RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 3}), chain_typestuff, return_source_documentsTrue ) while True: query input(提问: ) if query.lower() in [exit, quit]: break result qa.invoke({query: query}) print(答案:, result[result]) print(来源:, [d.metadata.get(source, unknown) for d in result[source_documents]])代码说明load_in_4bitTrue用 4 比特量化加载显存占用约 6GB16GB 显卡能跑如果你的显存足够把这一行去掉用 FP16效果更好。device_mapauto让模型自动分配到可用设备。ChatGLMLLM类必须实现_llm_type和_callLangChain 在执行qa.invoke()时会先把检索到的片段和问题拼成 Prompt然后调用_call把 Prompt 传给model.chat()。return_source_documentsTrue会返回被引用的原文块方便你核对答案是否有出处。3.4 跑通后的验证问几个问题确认链路正常运行上面脚本先问一个明确能从文档里找到答案的问题比如“门禁卡丢失后多久可以补办”。如果答案里出现了文档里的原话说明链路正常。再问一个需要跨多个片段才能回答的问题比如“报销单需要哪几个领导签字”看k3的检索是否把相关片段都带回来了。验证时我会做三个动作第一看返回的来源里是不是自己刚放进docs/的文件如果不是检查向量库是否用了旧的persist_directory第二把来源片段打印出来确认切分后的文本可读性如果片段全是半句话说明chunk_size或separators设置不合理第三连续问几个不同主题的问题确认没把不同文档的内容混在一起。这三个动作能过滤掉八成搭建期的问题。如果模型答非所问优先怀疑检索结果而不是模型能力——打印检索到的片段是最直接的排查手段。4. 调参让问答从“能用”到“好用”分割、检索、Prompt 与模型参数4.1 文本分割参数chunk_size 与 chunk_overlap 怎么定文本切分是 RAG 的地基。chunk_size太大一块文本里包含多个话题检索时噪音大太小语义不完整模型看不到上下文。中文场景我一般从chunk_size300起步然后看切出来的块是否都是完整句子。chunk_overlap我建议设为 10%20%50 字是个不出错的起始值。注意不要让 overlap 包含完整句子否则会出现大量重复内容浪费上下文窗口。我用一个很土但有效的验证方法把切分后的块按顺序拼接看是否能还原成原文的 90% 以上。如果丢了很多句子说明分隔符挑得不合适。比如文档是表格类内容就要把\t加进 separators。如果文档是代码仓库的 Markdown那优先按标题层级切而不是按固定长度。LangChain 的MarkdownHeaderTextSplitter就专门干这个比长度切分更符合文档结构。参数推荐值效果影响chunk_size300500太小语义碎太大检索噪音高chunk_overlap50100太小丢边界信息太大重复冗余separators按 。 优先中文场景避免切破句子切分策略RecursiveCharacterTextSplitter逐级降级切分兼容各种排版4.2 检索参数top_k、相似度阈值与重排的取舍top_k控制取多少个片段进入 Prompt。k3是 ChatGLM-6B 上下文下的保守值如果文档片断很短可以加到k5。k太大Prompt 里塞满无关片段模型容易抓不住重点。k太小答案可能缺关键信息。我一般先k3测一轮然后看来源片段是否覆盖了答案所需的要点再决定增减。向量检索默认返回相似度最高的几个但“最高”不等于“相关”。我常见的问题是问“年假怎么算”检索回来的是“年假”出现在表格标题里的碎片真正讲计算规则的那段反而没进 top_k。解决方法是打开retriever.search_kwargs里的score_threshold只保留相似度超过阈值的片段。阈值在 m3e-base 上我通常取 0.50.6太低没过滤效果太高容易一个片段也留不下。更彻底的做法是用重排rerank模型比如 bge-reranker-base把向量检索的前 20 个结果精排成 3 个效果提升明显但会多花几十毫秒到几百毫秒的时间。4.3 Prompt 模板如何让模型只依据检索结果回答不写 Prompt 模板LangChain 会用一个默认模板效果一般。我常用的模板是强制指定“只根据下面的上下文回答不要瞎编”并把检索片段放在问题前面。中文场景下模板语言用中文模型输出的中文会更自然。from langchain.prompts import PromptTemplate prompt_template 根据下面的上下文内容回答问题。 如果你不知道答案直接说“文档中没有找到相关信息”不要编造。 上下文 {context} 问题{question} 答案 QA_PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) qa RetrievalQA.from_chain_type( llmllm, retrievervectorstore.as_retriever(search_kwargs{k: 3}), chain_typestuff, chain_type_kwargs{prompt: QA_PROMPT}, return_source_documentsTrue )逻辑说明{context}是检索到的片段拼接结果{question}是用户输入。chain_type_kwargs里传入自定义 Prompt 后LangChain 不会再套默认模板。我还在模板里加了“不要编造”的约束实测能减少一部分幻觉。如果你发现答案总在复述上下文而不是总结可以把模板改成“请概括并组织语言”但注意不要过于约束否则模型会变成复读机。4.4 模型生成参数temperature、max_length 与量化级别temperature控制随机性。知识库问答属于确定性任务我把它设成 0.10.3避免同一个问题每次答案不一样。如果设成 0输出最稳定但偶尔会卡在某些词上0.3 是比较舒服的平衡点。max_length控制生成长度上限默认 2048 对问答来说太长我通常设 512够用而且能防止模型跑题。注意这里的max_length是在加载模型时的生成参数里设不是 LangChain 的max_tokens。我在第 3 章的自定义类里没有传生成参数实际使用可以通过model.chat方法的max_length和temperature传参或者把参数加到RetrievalQA的llm调用上。量化级别是另一组参数。load_in_4bitTrue让显存占用降到 6GB 左右但回答质量会有一点损失尤其长文本生成时更明显。8 比特量化占用约 8GB质量损失小一些。如果你的卡是 24GB直接用 FP16省去量化那层玄学。我的建议先上 4bit 跑通再用 8bit 对比同一个测试问题的答案质量肉眼可见差异不大就维持 4bit差异明显就升级到 8bit 或 FP16。这个决策不用拍脑袋用第 6 章的测试集打分就能定量比较。5. 避坑指南本地知识库问答最常见的 5 个翻车现场5.1 现象模型总是“一本正经胡说八道”检索结果根本没进上下文原因最常见的是检索结果为空或太少系统把空白上下文和问题一起丢给了模型。我排查过多起大多是向量库persist_directory指向了旧的空目录或者as_retriever的k设成了 0。还有一种情况是自定义 LLM 的_call方法只接收了prompt但没有把 Prompt 打印出来看结果发现 LangChain 传进去的是英文默认模板模型根本没理解上下文。解决把_call方法里的prompt打印出来确认检索到的内容是否在字符串里。如果{context}是空的回头查vectorstore的文档数量。另外设置return_source_documentsTrue看系统答完返回的来源片段只要来源越多答案就越有依据。记住一句话答案质量先取决于检索质量其次才是模型能力。5.2 现象启动 ChatGLM-6B 直接 OOM连加载都过不去原因在 8GB 显存的卡上用 FP16 加载 6B 模型光权重就需要 12GB不虚爆才怪。我见过有人报错 OOM 后第一反应是调小max_length完全没用问题在权重加载阶段。另一常见原因是device_mapauto没生效模型全塞到 GPU 0。解决先用load_in_4bitTrue或load_in_8bitTrue加载4bit 下 6GB 显存就能放权重。再不行就换更小的模型比如 ChatGLM-3-1.5B、Qwen-1.8B。如果你的机器只有 CPU内存 32GB 也能跑 6B 的 4bit但速度会很慢单次回答可能要一两分钟可以接受就先跑通验证效果。OOM 时还可以看torch.cuda.memory_summary()确认到底是权重占满还是激活值占满后者可以开启torch.utils.checkpoint减少显存压力。5.3 现象向量库检索回来的全是不相关内容查了才发现 Embedding 用错了原因用了英文 Embedding 模型处理中文文档或者模型名称写错导致加载了不合适的模型。还有可能是文档编码问题PDF 解析出来是乱码向量化之后自然检索不到。我踩过一次用默认的all-MiniLM-L6-v2跑中文检索结果完全不对后来换成m3e-base立刻改善。解决切换 Embedding 模型时必须把./chroma_db目录删除重建因为原向量库里的向量维度可能与新模型不匹配强行加载会报错不报错也会因为向量空间不同而检索失效。另外中文文档解析后先打印前几百字确认可读性再进切分流程。判断 Embedding 模型是否合适可以拿几个典型问题去做相似度搜索看返回的片段是不是你预期的那几段这一步比调任何 Prompt 都重要。5.4 现象单次问答要等半分钟不是模型慢而是管道重复初始化原因很多人把模型加载和向量库加载写在每次请求里。比如用 FastAPI 提供接口时每个请求都重新执行AutoModel.from_pretrained那不是问答是在反复重启模型。模型加载一次就要十几秒所有请求叠在一起速度自然惨不忍睹。解决模型和向量库的初始化放在全局进程启动时加载一次后续请求复用同一个llm实例。如果必须用多进程也要用进程安全的共享方式而不是每个 worker 都加载一份模型。还想更快的话把模型精度从 FP16 降到 8bit推理速度能快一点或者用 vLLM 这类推理引擎部署 ChatGLM-6B吞吐能提升不少但项目复杂度也上去了。先确认你的延迟是加载时间还是生成时间再决定要不要上推理引擎。5.5 现象中文问答效果差英文反而好先看分词和 Prompt 语言原因如果英文提问效果好而中文差多半是 Embedding 模型对中文支持不够或者切分时没有把中文标点加入分隔符导致中文文本块边界混乱。另外默认 Prompt 模板可能是英文的英文指令和中文文档混在一起模型容易被带偏。解决先换成 m3e-base 或 text2vec 系列中文 Embedding再调整recursive_text_splitter的separators把。放进去。Prompt 模板全部改成中文并明确要求“用中文回答”。换完通常立竿见影。如果还是差检查文档本身有没有散落的大段英文比如技术术语堆那就要考虑是否需要在切分前做语言归一化。我还会做一个小测试问一个需要从句子里挖数字的问题比如“报销上限是 5000 元还是 50000 元”看模型能不能准确说出来这是中文数字理解的高频翻车点。6. 更进一步用 LLM as Judge 评估问答质量并留住对话记忆6.1 用 LLM as Judge 给你的问答系统做体检调参调得差不多如何知道改得好不好靠感觉不行。我习惯准备 20 条标准问答手工写好参考答案然后让另一个更强大的模型比如用 API 的大模型或者本地跑一个训练充分的 13B 模型当裁判逐条打分。这样能避免自己主观偏袒某版配置。judge_prompt 给定一个问题、参考答案和候选答案请打分0-10。 标准内容是否与参考答案一致是否有幻觉是否完整。 问题{question} 参考答案{reference} 候选答案{answer} 返回分数只输出数字。每改一个参数用同一批问题跑一遍记录平均分。我自己的教训是千万不要把 20 条答案肉眼扫一遍就下结论模型生成的文本看似差不多实际关键数字可能已经变了。把评估脚本固化下来以后换模型、换分割参数都能立刻知道是变好还是变坏。6.2 从独立问答到多轮对话Memory 怎么接单轮问答只能回答“报销流程是什么”如果用户追问“那需要多长时间呢”系统已经忘了前文指的是报销。LangChain 里的ConversationBufferWindowMemory可以把最近几轮对话塞进下一轮 Prompt但要注意历史对话占用上下文窗口ChatGLM-6B 只有 2048 token塞太多历史会导致检索片段没地方放。我的做法是只保留最近两轮对话且把历史压缩成“用户前一轮问过 X我回答过 Y”的摘要而不是原样拼接。实际实现时可以把memory参数传给ConversationChain但RetrievalQA和memory结合比较绕我一般直接在外部维护一个历史列表手动拼进 Prompt。这个阶段最容易出 bug 的是历史里带着上一个查询的答案导致模型分不清当前问题是什么。多轮记忆不是越多越好够用就行毕竟知识库问答的重心始终在检索不在闲聊。最后说点我的血泪经验第一次搭这套系统我花了两天调 Prompt结果发现问题是 Embedding 模型选错了。后来我养成一个习惯——每次换知识库先打印检索到的原始片段再让模型回答。检索对了Prompt 才有意义模型生成质量才有保障。希望帮到你。本文还有配套的精品资源点击获取
返回列表