
简介面向希望掌握RAG检索增强生成智能问答系统搭建的开发者资源整合LangChain、ChatGLM-6B与本地知识库解决开放域问答与特定领域知识结合的问题适用于企业知识库问答、智能客服等场景。压缩包共73个文件大小17.67MB主要包含Python源码12个py、pickle数据文件39个多为预训练模型缓存或向量索引、Markdown文档6个以及依赖配置、Dockerfile等既有可执行入口也有说明文档便于直接运行和二次开发。目前已有957人学习适合具备一定Python与LLM基础、希望快速落地RAG项目的读者。附完整源码和流程教程涵盖环境配置、知识库构建、模型调用等关键步骤并支持命令行与Web两种交互方式文档中另含部署指南、FAQ和更新日志可帮助排查常见问题。读者可按教程逐步复现也可替换或扩展本地知识库快速适配实际业务场景是优质的实战参考资料。1. RAG智能问答系统是什么把本地文档变成能提问的知识库企业内部最尴尬的场景不是没有大模型而是大模型不认识你的文档产品手册、合同、运维工单、历史方案全躺在文件服务器里直接问ChatGLM-6B它只会给你一段基于通用知识的漂亮废话。RAG检索增强生成就是让模型先“查资料再回答”用LangChain做流程编排把本地知识库的文档切块、向量化用户提问时先检索出最相关的片段再带着这些片段让ChatGLM-6B生成答案。这套系统适合有私域文档、不想把数据交给外部API的团队也是langchain入门到落地最直接的实战路线。整套流程按可复现顺序拆开讲环境、知识库构建、检索调优、踩坑排查。2. 搭建LangChainChatGLM-6B问答骨架从安装到跑通首条链路2.1 为什么用ChatGLM-6B和LangChain这套组合在开源中文模型里ChatGLM-6B是当时把“单卡可跑中文理解”平衡得最好的选择之一。6B参数量听起来不小但量化到int4之后一张12G左右的消费级显卡就能跑推理16G内存的M系列Mac也能通过CPU慢慢跑。相比API方案它最大的价值是数据不出内网这对企业知识库几乎是硬性要求。LangChain不是必须的但它把“加载文档-切块-向量化-检索-拼Prompt”这条链路抽象成了标准组件哪天想把ChatGLM换成Qwen或Llama把模型加载部分换掉就行检索和Prompt逻辑不动。所以我一般会把LangChain的代码和模型加载代码分层写出了问题也好定位。2.2 安装环境与依赖用conda隔离环境Python别选太新3.10最稳。ChatGLM-6B发布时官方要求的那一版transformers是4.30.2后面大版本改动很多直接装最新的容易翻车。我一般把LangChain固定在0.1.xembedding用sentence-transformers加载本地模型向量库先用Chroma单机演示阶段完全够用等文档量上了百万级再考虑Milvus这类分布式向量库。conda create -n rag python3.10 -y conda activate rag pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.30.2 langchain0.1.20 chromadb pip install sentence-transformers cpm-kernels sentencepiece accelerate pip install pypdf docx2txt这里需要注意torch的安装地址机器是NVIDIA显卡就装cu118如果只是CPU或Mac去掉--index-url参数直接装CPU版本就行后面加载模型会慢不少但能跑。“transformers4.30.2”是ChatGLM-6B官方依赖里明确锁过的版本不用这个版本模型加载时经常碰上各种兼容报错。LangChain用0.1.x是因为再往后的版本把不少chain接口改成了LCEL写法网上的中文教程大多按旧接口写照着做能少踩这种坑。Mac用户装好之后先用python -c import torch; print(torch.backends.mps.is_available())确认一下MPS后端返回True再继续。2.3 加载ChatGLM-6B的最小脚本先别急着接知识库第一步是确认模型能跑起来。权重从HuggingFace拉国内下载慢就用ModelScope的镜像仓库把模型目录换成你本地权重路径就行。下面这段是最小可用的加载代码顺便验证一下生成链路import torch from transformers import AutoTokenizer, AutoModel model_path ./chatglm-6b # 换成你本地权重目录 tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, ).half().cuda() # 显存紧张时改成 .half().quantize(4).cuda() model.eval() prompt 你好请一句话介绍你自己 inputs tokenizer.encode(prompt, return_tensorspt).to(model.device) outputs model.generate( inputs, max_new_tokens256, temperature0.7, top_p0.85, do_sampleTrue ) print(tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokensTrue))注意.quantize(4)只有显存紧张时才需要半精度float16要13G显存左右量化到int4之后6G出头就够。如果你的卡是24G不量化直接跑反而生成速度更快。加载时trust_remote_codeTrue是因为ChatGLM的模型定义走的是自定义代码路径不加这个参数会直接报错。这个最小脚本通过之后后面接RAG链路就只剩“把检索结果拼进prompt”这一件事。2.4 不接知识库先做一次“伪RAG”很多人一上来就搭完整链路结果出了问题没法判断是检索坏了还是生成坏了。我习惯先把检索这块逻辑单独验证从知识库里手工挑一段文字拼进prompt里让模型回答确认模型会“读材料”。这一步能提前暴露Prompt写法问题也能把“模型本身行不行”和“知识库行不行”两件事分开。context ( 根据公司《服务器巡检规范》每月最后一个工作日必须完成 所有线上服务器的CPU、内存、磁盘占用检查并生成巡检报告存档。 ) question 服务器巡检多久做一次 prompt f请根据下面资料回答问题\n{context}\n\n问题{question}\n答案 # 用2.3节加载的model和tokenizer生成 inputs tokenizer.encode(prompt, return_tensorspt).to(model.device) outputs model.generate(inputs, max_new_tokens128, temperature0.1, do_sampleFalse) print(tokenizer.decode(outputs[0][inputs.shape[1]:], skip_special_tokensTrue))这里把temperature压到0.1、do_sample关掉是为了让知识类问答尽量确定不冒创造性风险。模型如果回答“每月最后一个工作日”说明生成端没问题后面全力搞检索如果模型还在自作聪明地编就说明Prompt约束还不够第五章会讲怎么加“不知道就承认”的约束。这块跑通之后你会发现整个链路的瓶颈不在模型而在后面章节要讲的知识库构建质量这才是RAG系统真正的护城河。3. 构建本地知识库切分、Embedding选型与向量入库3.1 文档加载与切分策略知识库的质量决定了RAG的上限而知识库质量里最容易被忽略的是切分。直接把整个PDF丢给向量模型不现实模型对文本长度有限制而且一个块包含太多主题时检索出来也不精准。我一般用LangChain的DirectoryLoader读目录里的所有文件再用RecursiveCharacterTextSplitter按“先尝试按段落再按句子最后按字符”的优先级去切。from langchain.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter loader DirectoryLoader( ./docs, loader_clsTextLoader, loader_kwargs{encoding: utf-8} ) documents loader.load() splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , , ] ) chunks splitter.split_documents(documents) print(floaded {len(documents)} docs, split into {len(chunks)} chunks)chunk_size设512是经验值中文一个字符算一个token512字对应大概500多个token拼接上问题和历史后仍在ChatGLM-6B的上下文窗口内。overlap设64是为了避免一句话被从中间切断导致语义丢失。separators里把“。”和“”放进去是让中文文本尽量在句号处断开而不是像英文默认切分那样在空格处断。如果文档里全是表格或代码这套默认分隔符不够需要另写自定义splitter。3.2 中文Embedding选型别用英文模型RAG里文本向量化这步决定了检索召回率。很多刚入门的人直接抄英文教程用all-MiniLM-L6-v2对中文效果很差因为它在中文语料上训练不足。中文场景我常用两种一是text2vec-large-chinese二是BAAI/bge-large-zh-v1.5。后者在中文检索任务上更稳而且支持为检索任务加“为这个句子生成表示以便检索”这样的前缀能明显提升召回。模型中文效果显存占用备注all-MiniLM-L6-v2差小英文模型不推荐text2vec-large-chinese中上中老牌中文embeddingBAAI/bge-large-zh-v1.5好中中文检索/重排全家桶OpenAI text-embedding-3好无需要联网数据出内网选型看两条一是是否支持中文二是是否允许本地离线。OpenAI的embedding质量确实好但数据要出内网和选ChatGLM的初衷矛盾text2vec在短文本上有时候比bge更稳但长文档语义检索我实测bge整体表现更好。embedding模型跑起来只需要GPU显存里多占2G左右不存在ChatGLM那么大的压力所以我一般固定用bge系列。3.3 向量化入库Chroma持久化确定了embedding和切块大小把chunk向量化写进Chromafrom langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma embedding_model HuggingFaceEmbeddings( model_nameBAAI/bge-large-zh-v1.5, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) vectordb Chroma.from_documents( documentschunks, embeddingembedding_model, persist_directory./chroma_db, # 向量库落盘目录 collection_namecompany_docs # 一个目录可以放多个collection ) vectordb.persist()模型名这里是HuggingFace的仓库名首次运行会先下载权重。如果公司网络慢可以提前用ModelScope下载后改成绝对路径。persist_directory是向量库持久化目录这个参数比想象中容易出问题后面避坑章会专门讲。normalize_embeddings设True表示向量归一化配合余弦相似度检索可以让分数更可解释。入库完成后可以用一个简单查询验证retriever vectordb.as_retriever(search_kwargs{k: 3}) for doc in retriever.get_relevant_documents(服务器巡检多久一次): print(doc.page_content[:100]) print(------)这段会打印出最相关的三个文档块。看到“巡检”相关的内容说明检索通如果打印出来的东西毫不相关优先怀疑embedding模型和切分粒度不要急着调生成。到这里本地知识库的构建闭环已经完成但真正上线还要处理增量更新的问题。3.4 知识库维护增删改与重建知识库不是建完就不动的。Chroma支持add_documents追加也支持delete删除但要维护一个“文档块和源文件路径”的映射否则你根本不知道删的是哪段。我一般会维护一个简单的更新脚本# 每条chunk补充文档名方便追踪来源 for chunk in new_chunks: chunk.metadata[doc_name] chunk.metadata.get(source, unknown) vectordb.add_documents(new_chunks) vectordb.persist()常见的错误操作是源文件改了直接重新query发现回答还是老的。因为向量库里旧chunk还在。简单做法是把整个collection删掉重新建规模大了再用元数据过滤按文件更新。顺便说一句纯向量检索方式是“语义相近”它回答不了“查询所有供应商名称”这种结构化问题这类需求要考虑kg知识库/结构知识库把实体关系抽出来存图数据库属于RAG的进阶形态。再往上就是ontology rag的概念级建模用本体定义概念间关系让系统能回答关系型问题但那是后话。知识库的版本管理也容易被忽略。我会把向量库的collection和切分参数一起记下来比如目录名就叫chunk512_overlap64_bge改版对比效果时直接切collection不用重新全量建库。记住一条铁律embedding模型换掉向量库必须重建旧向量没有比较价值。4. 检索优化与Prompt调优把RAG效果从能用拉到好用4.1 RAG瓶颈通常在检索而不是生成很多人让模型一本正经地胡说第一反应是“这模型不行”。我做过的RAG系统里八成的问题出在检索环节搜回来的片段和问题不相关或者相关片段被切得只剩一半。排查方法很简单——把检索到的chunk直接打印出来看不要看生成的答案。这一步能筛掉绝大多数“幻觉”问题的锅。# 打检索结果确认召回是否合理 question 设备采购合同中的付款条款是什么 docs retriever.get_relevant_documents(question) for i, d in enumerate(docs): print(f--- chunk {i} ---) print(d.metadata.get(source), d.page_content[:200])如果打印出来的chunk里根本没有“付款”相关内容那问题就是检索如果chunk里明显有答案但模型答错了才轮到调Prompt。这个判断顺序建议写成团队规范任何问答效果问题先把检索结果留档再谈下一步。我见过太多人把时间花在换大模型上结果换完照样错因为检索回来的材料本身就是错的。4.2 TopK和重排召回先宽后严检索参数里最直接的是top_k。设太小比如k1相关文档一旦切分得不好就漏召回设太大比如k10噪声片段混进来ChatGLM-6B的注意力会被无关内容带偏。我的经验是先k5跑一轮看召回质量再调。更进一步的做法是加一层重排rerank先用embedding取回20个候选再用cross-encoder模型逐条打分只保留前3个进Prompt。交叉编码器比双塔embedding更准但慢只用来精排是划算的。from sentence_transformers import CrossEncoder # 先用embedding召回20个候选 question 设备采购合同中的付款条款是什么 candidate_docs retriever.get_relevant_documents(question) # 再用cross-encoder精排保留前3个 reranker CrossEncoder(BAAI/bge-reranker-v2-m3) pairs [(question, d.page_content) for d in candidate_docs] scores reranker.predict(pairs) top_docs sorted(zip(candidate_docs, scores), keylambda x: x[1], reverseTrue)[:3] for doc, score in top_docs: print(round(score, 4), doc.page_content[:120])加了重排之后召回质量会有可感知的提升代价是每次查询多几十到几百毫秒单机问答场景完全能接受。注意重排模型也要中文模型bge-reranker-v2-m3是中文环境下我测下来比较稳的。重排之后返回的文档顺序就是最终进Prompt的顺序这段顺序本身也很重要。4.3 Prompt模板让模型不硬编生成这一步的核心是Prompt。很多人觉得ChatGLM是中文模型就不会乱编真用起来发现它一样会脑补。给RAG写的Prompt必须加两个硬约束一是只准使用提供的资料二是资料里没有答案就直说不知道。下面这个模板一直在用from langchain.prompts import PromptTemplate prompt_template 你是一个严谨的知识库问答助手。 请只根据下面提供的【资料片段】回答用户问题不要使用资料以外的知识。 如果资料中没有相关依据请直接回答“知识库中未找到相关信息”不要编造。 【资料片段】 {context} 【用户问题】 {question} 【回答】 qa_prompt PromptTemplate( templateprompt_template, input_variables[context, question] )注意占位符必须和最终拼接的变量名一致LangChain不会帮你校验这个拼错了运行时才报错。生成参数也要同步收敛temperature调到0.05甚至0关掉do_samplemax_new_tokens设置成100到300之间防止生成长篇废话。知识类问答不是创作类任务随机性越低越好。除了Prompt约束还有个容易被忽略的点上下文顺序。把最相关的chunk放在prompt的最前面让模型先看到重点。重排后的结果通常就是按分数排序的我一般会在自己的检索逻辑里保证分数最高的块在第一段实测能减少模型“被后面的无关块带偏”的概率。4.4 必调参数速查参数推荐值调整方向chunk_size256~512块太小容易打断实体块太大检索精度下降chunk_overlap64~128段落主题连续时适当加大top_k5先5后按召回质量加减rerank top_n3重排后保留数越大上下文越长temperature0.01~0.1知识问答压到接近0max_new_tokens100~300按答案长度需求调整这些参数的联动关系是chunk_size变大语义完整但检索粒度变粗top_k变大召回全但噪声多加了rerank之后可以把top_k放心调大。我用这套顺序做优化先调chunk再看检索chunk内容最后调Prompt和生成参数。这套组合拳打下来RAG的调参就不再是玄学而是每一步都有验证依据。5. 本地部署ChatGLM-6B与RAG避坑指南5条血泪排查记录RAG加本地模型的坑和普通Web开发完全不是一个套路。报错信息不会直接告诉你是检索的问题还是生成的问题很多问题要等到上线被用户问倒才暴露。这里按“现象-原因-解决”拆开五类我实际踩过的问题。5.1 显存溢出OOM直接把进程杀掉现象加载ChatGLM-6B时直接OOM或者生成到一半报CUDA out of memory。原因默认以float16加载ChatGLM-6B的权重加激活层需要13G左右显存12G的卡就会爆。解决加载时加.quantize(4)做int4量化显存降到6G左右再不行把max_new_tokens调小并限制batch_size1。量化后生成质量损失在可接受范围内尤其是知识问答这种答案依赖检索内容的场景模型的“创意”本来就不需要太多。如果做批量测试建议写一个循环每跑完一条就torch.cuda.empty_cache()避免显存碎片累积。5.2 中文检索效果差召回结果毫不相干现象问“服务器巡检”检索出“年会活动安排”词面上没有任何重叠但语义上也不是一回事。原因用了all-MiniLM-L6-v2这类英文embedding模型在中文上只会按字面匹配。解决换成BAAI/bge-large-zh-v1.5并记得在查询时添加“为这个句子生成表示以便检索”的指令前缀。换完embedding之后如果之前已经入库了必须重建向量库否则新旧向量维度不同会直接报错或检索错乱。这是一个反复出现的坑换模型不重建库等于让旧房子装新水管水还是出不来。5.3 模型一本正经地编答案不认检索资料现象知识库里根本没有的内容模型也能流畅地给出一段像模像样的回答。原因Prompt里没有约束“资料中没有就拒绝回答”而且temperature开得偏高模型把知识问答当成了开放闲聊。解决把4.3节的Prompt约束加上把temperature设成0.05并把检索结果为空的分支单独处理当检索chunk数量为0或重排分数过低时直接回复“知识库中未找到相关信息”不再调用模型生成。这是最省钱又最有效的兜底逻辑也是RAG系统对抗幻觉的最后防线。5.4 LangChain版本升级后API全变了现象网上教程里的代码复制过来第一步from langchain.vectorstores import Chroma就报ModuleNotFoundError。原因LangChain从0.1到0.2做了不少接口调整向量库和检索器改到了langchain_community包老教程没跟上。解决在项目里锁版本搭环境时直接用pip install langchain0.1.20避免用最新版。如果项目已经用了新版就把导入路径改成from langchain_community.vectorstores import Chroma其余的chain逻辑大体还是兼容的。这类“接口迁移”问题没什么技术含量纯靠版本锁定来规避别在网上随手抄一段不知道哪个版本的代码。5.5 在Mac上搭RAG知识库跑不起来现象M1/M2 Mac加载ChatGLM-6B时提示mps算子不支持或者CPU跑一步生成要几十秒。这类问题在“怎么在mac上搭建rag知识库”的需求里特别常见。原因ChatGLM-6B的自定义CUDA算子cpm-kernels在MPS后端上没有完整实现transformers在MPS上会退回CPUApple Silicon的CPU跑6B模型确实吃力。解决建议M系列Mac把ChatGLM-6B换成int4量化版本跑CPU推理同时把embedding也切成CPU模式并限制torch线程避免把系统拖死。如果只是做检索链路验证也可以先用ChatGLM-6B-int4做阶段测试生产部署再换GPU服务器。Mac本地更适合用来调试切分和检索而不是做最终推理这点想明白能省很多时间。提示这5条不是全部坑但按顺序排查能覆盖大多数RAG新手前两周遇到的问题。遇到新报错时优先看traceback里的第一个ModuleNotFoundError或CUDA error再决定是从环境还是从代码入手。先定位再动手改能省不少时间。6. 进阶先建评估集再上线RAG系统RAG系统的上线标准不是“能回答几个问题”而是“真实问题的命中率稳定”。我吃过亏第一版系统演示效果不错业务同事问了自己文档里几个刁钻问题答案错得离谱。后来发现根本不是模型问题是那几个问题的相关文档在切分时被切散了检索根本没召回。所以我现在的习惯是任何RAG项目上线前一定先花半天时间做两件事——建评估集、记参数。建评估集不需要多复杂从知识库里挑30到50个真实问题每个问题标注一个期望召回文档ID或关键词。跑一遍检索统计前5个结果里包含期望文档的比例这就是检索命中率。低于70%不要上线回去调chunk_size和top_k。回答质量再单独抽20条人工打分重点看答案是否忠于检索片段。这个“先测检索再测生成”的顺序能让你把有限的时间花在真正影响效果的地方。下一步如果系统已经稳定再考虑从RAG走向Agent。LangChain、Dify、CrewAI这些框架各有侧重LangChain最灵活但代码量大Dify适合快速拖配置CrewAI更适合多角色协作任务。我的建议是RAG阶段不要惦记Agent检索都还没做准给模型加工具只会放大错误。等知识库问答的命中率稳了再让LangChain去调用查询工具、写报告脚本扩展成能主动干事的系统复杂度一次加一点出问题也容易定位。我自己现在做知识库问答一定会给每个回答附上“参考来源”把命中的文档块原文展示在答案下面。这个习惯让排查效率提升了一个量级用户说答案不对直接把来源贴出来看是哪段召回错了而不是互相对着屏幕猜。最后再啰嗦一句先小规模验证再扩展文档量先单机跑通再谈高并发。希望帮到你。本文还有配套的精品资源点击获取