ARTICLE DETAIL

资讯详情

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

本地知识库自动问答实战:基于LangChain与ChatGLM-6B的RAG实现

本地知识库自动问答实战:基于LangChain与ChatGLM-6B的RAG实现 简介面向计算机、通信、人工智能、自动化等专业学生与从业者的毕业设计级项目基于LangChain与大语言模型ChatGLM-6B等系列LLM构建针对本地知识库的自动问答系统答辩评审曾获98分。项目代码经调试测试可运行适合作为课程设计、课程大作业或毕业设计参考也可供初学者学习LLM应用开发与进阶改造。压缩包共76个文件以Python源码12个py、pickle数据文件39个用于向量索引与模型缓存、Markdown/txt文档、Dockerfile部署配置及示例图片为主整体约17.96MB目录结构清晰。已有251人学习下载资源包含完整源码、使用说明、手册及离线部署配置其中源码分层清晰数据处理、嵌入、LLM调用、Web应用等便于理解RAG问答链路并替换模型或知识库实现个性化功能。1. 为什么说本地知识库自动问答难的不是大模型而是资料整理接到过这种活的人应该都懂产品经理丢过来几十份 PDF 和 docx说“你做个问答机器人让业务同事自己问”。真要动手才发现把文件喂给大模型容易让它每一条答案都出自这些文件、还能指出是哪份材料——这才是“基于 LangChain 和 ChatGLM-6B 等系列 LLM 的针对本地知识库的自动问答”这个项目要啃的硬骨头。这套方案背后的核心技术叫检索增强生成RAG离线把文档切块、向量化、建索引在线先拿用户问题去向量库召回相关段落再把段落拼进提示词交给 LLM 生成答案。它适合两类人一是不想把内部资料传给云端模型的团队二是手里只有一张 8GB 消费级显卡、却想体验完整大模型问答链路的技术人员。反直觉的点在于跑通 demo 只要半天把“答对问题”打磨到位却要两周。2. 先把链路画清楚LangChain、ChatGLM-6B 与向量库如何分工本地知识库自动问答不是单靠一个大模型就能完成的。标题里出现的 LangChain 和 ChatGLM-6B 分别负责编排与生成而常被忽略的第三类组件——嵌入模型与向量库决定了答案质量的上限。一个完整的本地问答链路可以拆成四段文档加载与切分文本向量化检索召回生成回答。前两段离线完成后两段在线发生。实践中很多翻车案例都不是模型不会说而是前三段里的某一步出了问题。2.1 LangChain 在自动问答里的职责它不是模型是流水线LangChain 的价值在于把上述四段流程封装成可以替换的标准件。你今天用 ChatGLM-6B明天改成 Qwen 或者通过 Ollama 拉下来的其他量化模型只需要换掉 LLM 这一层检索和切分逻辑不动。这对项目落地很重要因为本地 LLM 的格局变化太快一个方案如果被某个模型绑定死过半年就可能要重写。我一般会把问题拆成“加载与切分、向量化与索引、检索召回、生成回答”四部分再去看 LangChain 里对应的抽象。文档加载器有很多现成实现TextLoader、PyPDFLoader、UnstructuredMarkdownLoader 各有各的适用场景返回的都是统一的 Document 对象文本切分器 RecursiveCharacterTextSplitter 把长文档切成小块向量库接口统一FAISS 还是 Chroma 切换成本低最后 RetrievalQA 把检索器和 LLM 串成一条链。这套抽象真正的价值是排查问题方便答案不对时可以把检索结果单独打日志而不是对着最终输出猜黑匣子内部发生了什么。2.2 生成模型选型为什么大量项目用 ChatGLM-6B以及“等系列 LLM”意味着什么ChatGLM-6B 之所以大量出现在这类项目包里核心原因是显存门槛低。6B 参数在 INT8 量化后可以压进 6GB 显存一张 8GB 的消费级显卡就能跑起来中文效果在当时同尺寸开源模型里也排得上号。标题写的“等系列 LLM”意思就是这个位置并不唯一你可以换成 ChatGLM2、ChatGLM3、Qwen 系列或者通过 Ollama 这一层来加载各种量化模型。选型判断不应该只看宣传Open LLM Leaderboard 这类公开榜单能提供横向参考但最终要落到你自己的数据上验证。部署条件生成模型典型量化适用判断显存小于 6G4bit 量化模型或 OllamaINT4优先考虑 Ollama省去大量手工调参显存 8G 左右chatglm-6bINT8标题方案的标准舒适区纯 CPU 服务器小模型 / 量化模型无 / INT4推理很慢响应超过十秒就别硬撑可以连公有云任意大模型 API不需要效果最好但资料出站不一定合规注意一个容易忽略的许可问题ChatGLM-6B 早期版本对商用有限制如果你要做商业化产品上线前务必核对所用模型的 License。很多团队就是在 PoC 阶段没问题临上线才发现商用授权不满足被迫换模型重跑测试集。2.3 检索侧选型嵌入模型和向量库为什么决定答案上限进入检索侧之前先明确一个结论在 RAG 链路里检索召回的质量直接决定最终答案的上限。检索不出相关内容LLM 就只能靠它训练时的记忆硬编幻觉就是这么来的。嵌入模型我建议直接用中文语义模型比如 bge 系列或者 m3e而不是默认的英文模型。中文本就有语义层面的差异一个标点不同向量方向可能差出十万八千里用英文模型很容易让后面所有环节都白费。向量库的选择相对朴素。几十份文档、单次查询量不大的场景FAISS 就够用。FAISS 适合一次性建好索引、之后更新不频繁的知识库使用 faiss-cpu 版本在普通机器上也能跑Chroma 则适合需要频繁增删文档的场景。不要一上来就追求分布式向量数据库单机方案能解决的问题引入额外组件只会增加排查负担。这里把检索侧的主要参数列成表方便对照调整环节常见选型关键参数容易踩的坑文本切分RecursiveCharacterTextSplitterchunk_size200 到 400chunk_overlap40 到 80纯按字符切会切断中文句子嵌入模型bge-large-zh / m3enormalize_embeddingsTrue换模型后必须重建向量库向量库FAISS / Chroma维度与嵌入模型一致只复制部分文件导致索引加载失败生成模型chatglm-6b / Qwenmax_new_tokens512temperature0.3上下文塞太满输出超长这套配置的思路是每一层选成熟、可替换、文档多的组件而不是选看起来最强但难以调试的组件。接下来就从最小系统开始一步步把它们真正跑起来。3. 把最小系统跑起来装环境、加载模型、完成第一句问答直接进入实操。我默认你有一张 8GB 显存的 NVIDIA 显卡Linux 或者 Windows 本机都可以没有显卡也没关系后面会给出替代路径。很多 LangChain 入门教程喜欢把环境写得很复杂实际上单机本地问答的依赖并不需要那么多核心就是 PyTorch、Transformers、LangChain 和一个向量库。3.1 环境准备与最小依赖先建一个干净的虚拟环境。Python 版本我建议用 3.10兼容性比 3.11 遇到的那些依赖编译问题少。python3 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install torch torchvision pip install transformers sentence-transformers langchain langchain-community pip install langchain-huggingface chromadb faiss-cpu这里有两个点要说明。PyTorch 安装时要注意和你机器的 CUDA 版本匹配直接 pip 装到的默认版不一定能用 GPU安装后执行torch.cuda.is_available()确认返回 False 就回 PyTorch 官网按 CUDA 版本重新装。LangChain 现在拆成了多个包langchain-community里装着文档加载器和向量库langchain-huggingface里装着 HuggingFacePipeline老教程里那种一条pip install langchain全搞定的时代已经过去了。3.2 先验证 ChatGLM-6B模型能正常对话再接其他组件不要一上来就接 LangChain先把模型本身跑通。这一步的目的是把“模型问题”和“框架问题”分开。权重文件可以提前从模型仓库下载到本地目录避免每次启动都在线拉取路径以你实际存放的位置为准。from transformers import AutoModel, AutoTokenizer model_path ./chatglm-6b # 本地权重目录 tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModel.from_pretrained(model_path, trust_remote_codeTrue) model model.quantize(8) # INT8 量化显存能压到 6G 上下 model model.half().cuda() # 半精度上显卡 model.eval() response, _ model.chat(tokenizer, 什么是RAG) print(response)逻辑很直接加载分词器和模型ChatGLM-6B 因为依赖自定义代码必须设trust_remote_codeTrue。quantize(8)是 ChatGLM 自带的方法比通用 transforms 的load_in_8bit在这条链路上更省心。half()转半精度.cuda()上显卡。注意eval()必须调用否则模型不会进入推理模式。到这里能正常对话说明模型的部署环境没有问题接下来才轮到 LangChain 登场。如果你的机器是 CPU 运行把.cuda()去掉也能跑但响应会慢到让人怀疑程序卡死。纯 CPU 场景我更建议换 Ollama它会针对 CPU 推理做优化而且提供了 OpenAI 兼容接口后面接 LangChain 反而更方便。这一步是在为后面换模型留退路。3.3 把模型接进 LangChain用 HuggingFacePipeline 包装模型验证通过后用 LangChain 的 HuggingFacePipeline 把同一个模型包装成 LLM 接口。这样后续的所有链式调用就不需要再关心 transformers 的细节了。from transformers import pipeline from langchain_huggingface import HuggingFacePipeline pipe pipeline( text-generation, modelmodel, tokenizertokenizer, max_new_tokens512, temperature0.3, repetition_penalty1.05, ) llm HuggingFacePipeline(pipelinepipe) print(llm.predict(什么是本地知识库问答))代码里几个参数需要说明。max_new_tokens512限制生成长度本地知识库场景下答案通常不需要超过 300 字给 512 已经足够太大反而会拖慢响应并占显存。temperature0.3取值偏低让输出更稳定问答系统最怕同一个问题每次答案不一样。repetition_penalty1.05是中文环境下必须设置的不加的话模型容易在长回答里反复说同一句话。llm.predict只是验证包装是否成功真正项目里我们应该走后面的检索链。到这里最小系统已经建立模型能对话LangChain 能调用模型。但离“知识库自动问答”还有距离——必须把本地文档变成检索索引再把它和这个 LLM 串起来。4. 做厚核心问答链路文档切分、向量化与最终实现的各个参数最小系统只是骨架真正让答案靠谱的是资料处理。这一章把所有可调的参数摆出来讲透每一步都给出完整代码。本地知识库问答的血泪经验是指令和模型问题都好解决文档切分粒度不对调整起来最费时间。4.1 文档加载与切分策略先看文本形态再定 chunk_size直接拿一个真实场景举例知识库里有培训手册、产品 FAQ 和客户分析报告格式分别是 txt、pdf、md。先用统一的加载逻辑把它们读进来。from langchain_community.document_loaders import TextLoader, PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter docs [] for fp in [docs/培训手册.txt, docs/FAQ.pdf]: try: if fp.endswith(.txt): loader TextLoader(fp, encodingutf-8) else: loader PyPDFLoader(fp) docs.extend(loader.load()) except Exception as e: print(加载失败:, fp, e) splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap50, separators[\n\n, \n, 。, , , ., , ], ) chunks splitter.split_documents(docs) print(f切分得到 {len(chunks)} 个片段)chunk_size300的考虑是一个 300 字的文本块大约包含 6 到 8 个中文句子足够支撑一个完整的语义单元如果调大到 600检索召回时会把很多不相关的内容一起塞进上下文LLM 容易被噪声干扰。chunk_overlap50是为了让相邻两个块之间保留衔接信息避免某个关键结论正好被切在边界上。separators列表按优先级排列注意这里专门加了中文句号、叹号、问号和分号——这是中文切分和英文的最大区别。英文按空格和换行切基本不会把单词截断中文不指定标点模型只能按字符硬切检索回来的片段就可能全是残句。如果文档本身有章节结构比如 Markdown 的二级标题更稳妥的做法是先按章节切一次再把过长的章节切小块。这比直接全文件切块能保留更多上下文结构。4.2 向量化与索引中文嵌入模型怎么选、参数怎么设切分完成后每一段文本都要变成向量。这一步选嵌入模型重点不是追求榜单分数而是考虑中文效果和显存占用。bge-large-zh-v1.5 是目前在中小型知识库场景里综合表现稳定的选择维度合理、检索精度够用。from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS embeddings HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, model_kwargs{device: cpu}, encode_kwargs{normalize_embeddings: True}, ) vectorstore FAISS.from_documents(chunks, embeddings) vectorstore.save_local(faiss_index) print(已保存 faiss_index/index.faiss 与 index.pkl)嵌入模型我建议放在 CPU 上运行。嵌入计算是一次性工作几百份文档在 CPU 上几分钟就能完成没必要抢占 GPU 显存。normalize_embeddingsTrue很关键它会对向量做归一化使得后续用余弦相似度计算时结果更稳定。FAISS 建完索引后调用save_local会生成两个文件index.faiss存向量索引index.pkl存文档元数据。这两个文件后面必须一起用缺一个都会加载失败。这里要特别提醒一个后期必踩的坑更换嵌入模型后向量库必须重建。不同模型的输出维度不同旧索引文件和新模型算出来的向量根本对不上勉强加载只会得到空检索结果。所以不要试图在新模型上复用旧索引“换模型就重建索引”应该成为肌肉记忆。4.3 完整问答链路检索、Prompt 与生成向量库和 LLM 都就位了剩下的就是把它们串起来。这里用 RetrievalQA 完成任务重点在于 Prompt 模板的设计。from langchain.prompts import PromptTemplate from langchain.chains import RetrievalQA from langchain_community.vectorstores import FAISS vectorstore FAISS.load_local( faiss_index, embeddings, allow_dangerous_deserializationTrue, ) retriever vectorstore.as_retriever( search_typesimilarity, search_kwargs{k: 4}, ) template 你是本地知识库问答助手。 用户的问题是{question} 以下是从知识库检索到的材料 {context} 回答要求 1. 只基于上面材料回答找不到答案时直接说“知识库中没有相关内容” 2. 给出结论后在括号里注明材料来源文件名 3. 回答控制在300字以内。 回答 prompt PromptTemplate(templatetemplate, input_variables[question, context]) qa_chain RetrievalQA.from_chain_type( llmllm, retrieverretriever, promptprompt, return_source_documentsTrue, ) result qa_chain.invoke({query: 上个季度A客户为什么流失}) print(result[result]) for doc in result[source_documents]: print(doc.metadata[source], doc.page_content[:50])这段代码里的设计思路值得展开。Prompt 模板拆成三个层次第一层告诉模型它是谁、它的角色是本地知识库问答助手这样模型不会从通用理解模式出发瞎编第二层给出用户问题也就是模型正在查找的诉求第三层是回答约束明确告诉模型它能用哪些材料、不能做什么以及输出格式。这三段结构恰好对应了 Token 设计里常说的 key、query、value——角色是身份问题是目标约束是边界。缺失任何一层模型都有可能在边界外自由发挥。search_kwargs{k: 4}表示取最相似的 4 个片段进入上下文对 300 字的 chunk 来说4 个片段约 1200 字是本地 6B 模型生成答案时比较舒服的上下文长度。return_source_documentsTrue必须开启否则出问题时完全无法追踪答案来自哪份文档。最后用invoke而不是直接调用这是 LangChain 新版的推荐接口更利于后续加中间回调。到这里一个可用的本地知识库自动问答系统已经完整落地。但用起来你会发现问题并没有结束甚至才刚刚开始。5. 常见问题与排查答不对、OOM、乱码根子都在哪里跑本地知识库问答的人第一周几乎都在跟下面几类问题搏斗。遇到问题先别急着怀疑模型不行把链路拆成“数据、索引、检索、生成”四段逐段排查比对着最终答案猜原因高效得多。以下按“现象、原因、解决”整理成排查笔记覆盖单机部署现场的大多数情况。5.1 答案流畅但与知识库无关甚至答非所问现象系统回答出来句子通顺、语气自信但去原文里根本找不到依据有时还会把 A 客户的事情安到 B 客户头上。这是本地问答最典型也是最危险的幻觉场景。原因检索环节没把相关内容召回模型没看见该看的东西又不想说实话只能靠训练记忆硬编。提示词写得再严格也弥补不了检索结果的缺失。解决先打印retriever.get_relevant_documents(question)看返回片段是不是真的跟问题相关。如果返回为空或匹配对象错误多半是切分粒度问题把 chunk_size 调小、overlap 调大重新建库。如果检索正常但答案仍然乱编就将 k 值适当调大并在提示词中保留那句“找不到就说找不到”给模型一条体面的退路。这套组合能缓解大半幻觉但无法根治因为答案质量归根结底取决于检索结果本身。5.2 模型一加载就 OOM或者跑着跑着显存爆掉现象进程刚启动就报 CUDA out of memory或者前期正常多轮对话后突然卡死。原因多数情况不是显存真的不够用而是 GPU 被同时塞进了太多东西。嵌入模型、LLM、向量检索都默认想用 GPU再加上多轮对话历史不断累加显存就被挤爆了。解决嵌入模型明确指派到 CPU 运行向量检索在单机小库场景下也走 CPULLM 做好 INT8 或 INT4 量化加载后立刻model.eval()别让它停留在训练模式必要时在每次回答后调用torch.cuda.empty_cache()清理缓存。如果问题只发生在多轮对话场景先检查是不是把整段历史消息塞进了上下文控制历史轮数比加显存更有效。5.3 检索出来的文本全是残句开头没主语、结尾没句号现象向量库召回结果是对的用户问题但召回来的文本片段读起来像被刀切过一半句子缺失导致模型理解困难。原因切分器按字符长度硬切英文有空格天然分界中文没有切点正好落在句子中间一整句语义就被斩断成两半。解决在separators里加中文标点符号让切分器优先在句号、问号、叹号处断开chunk_overlap不要小于 50给切点附近留出衔接空间对表格类和标题类文档先按章节标题切一次再对长章节二次切分。中文语料必须单独调这些参数照搬英文教程的切分配置基本都会出问题。5.4 换嵌入模型后旧向量库加载失败或检索结果为空现象听说某新模型效果更好换掉之后重跑程序加载 faiss_index 时直接报错或者加载成功但检索结果为空。原因FAISS 索引的维度必须与嵌入模型输出维度一致。换模型后新旧维度不同旧索引文件里的向量序列跟当前模型算出来的向量在数学上根本配不上。解决换嵌入模型之后不要尝试在旧索引上打补丁直接重新跑一遍“切分、向量化、建库”流程。这里还有个附带提醒index.faiss和index.pkl两个文件必须一起备份和迁移最坏的情况是同事只拷了其中一个文件给你加载时怎么调都报错。LangChain 新版加载本地 FAISS 还需要加allow_dangerous_deserializationTrue这是安全策略变更不是程序 bug。5.5 升级 LangChain 后老脚本 import 直接报错现象几个月前还能跑的脚本这次启动直接 ImportError报错信息指向langchain.document_loaders之类的不存在模块。原因LangChain 0.2 之后做了包结构拆分文档加载器、向量库、嵌入模型实现都被迁移到langchain-communityHuggingFacePipeline 迁移到langchain-huggingface。老代码还在从langchain核心包引这些实现自然找不到。解决两条路选一条。要么把 import 统一改成新路径要么把 LangChain 版本锁在项目当初记录的那个版本。做项目维护时把requirements.txt严格锁住主版本号社区库升级太快依赖飘忽不定会把排查带到沟里。这里的排查顺序一定是从报错往上追包版本别先怀疑自己的代码逻辑。如果这几条都没命中你的问题最终手段是打开 LangChain 日志和模型输出日志看检索到的原始内容、模型生成的原始文本不要盯着最终界面。多数问题都藏在最后一步之前。6. 进阶用 LLM as Judge 和最小测试集做回归验证本地问答系统最容易被忽略的环节是验证。很多人调整参数靠手感改一次 chunk_size 就上线结果被真实问题打回来。更可靠的做法是建立一个小型测试集每次改动后让 LLM 当裁判自动打分用分数变化指导决策。测试集不需要大20 到 30 条就够。问题从真实业务场景里挖context 字段记录答案应该来自哪份文档test_set [ {question: A客户为什么流失, source: docs/客户分析.md}, {question: 报销流程需要几个签名, source: docs/手册.txt}, ]验证逻辑直接用同一个模型做裁判。虽然用同一个模型打分会偏乐观但我们要的不是绝对分数而是改动前后的相对对比def judge_answer(question: str, answer: str, model, tokenizer) - int: scoring_prompt ( f用户问题{question}\n f系统答案{answer}\n 请从忠实度、完整度、相关性三个维度打分1到5分5分最高。\n 只输出单个数字分数。 ) score, _ model.chat(tokenizer, scoring_prompt, history[]) return int(score.strip()[:1])每次修改切分参数、更换 LLM、调整 prompt 模板后把整个测试集跑一遍求平均分。分数低于上一次就回退高于上一次就保留。这套做法本质上就是基于 LLM 的单元测试它不能保证答案绝对正确但能防止系统一次改动就整体崩坏。我自己翻车最狠的一次是凭感觉把 chunk_size 从 300 调成 600直觉认为上下文长一点更准结果检索噪声变大答案整体漂移三天之后才被业务同事拿着原文怼回来。从那以后任何参数改动都先过测试集哪怕只有十条约问也能把大多数回归问题拦在发布之前。希望帮到你。本文还有配套的精品资源点击获取
返回列表