
简介这份资源面向希望动手实践RAG智能问答系统的开发者与AI学习者基于LangChain、ChatGLM-6B与本地知识库搭建完整项目解决单一生成模型在特定领域问答中准确度不足的问题适合具备一定Python与深度学习基础、想深入理解检索增强生成技术的人群。压缩包共73个文件约17.67MB以39个pickle模型缓存、12个Python源码、6个Markdown文档为主另含配置文件、依赖清单与Dockerfile覆盖从模型加载、文本切分、向量嵌入到服务部署的完整链路。目前已有956人学习下载。项目提供可直接运行的源码与流程教程读者能据此复现系统掌握LangChain接口调用、ChatGLM-6B推理及本地知识库检索增强的实现方法并可按需修改扩展快速应用于实际问答场景。1. 从零搭一套 RAG 智能问答为什么 LangChain ChatGLM-6B 本地知识库是当前最稳的起步组合如果你手里有一堆内部文档、产品手册、运维记录想让它们变成一个能对话的问答系统又不希望数据离开自己的机器那 RAG 智能问答系统就是最直接的答案。RAG 的全称是检索增强生成核心思路不复杂用户提问后系统先从本地知识库里检索出最相关的片段再把片段拼进提示词交给大模型生成回答。这样做的好处是模型不需要记住你的私有数据回答有据可查更新知识只需要更新文档不用重新训练。LangChain 负责把文档加载、切分、向量化、检索、生成这条链路串起来ChatGLM-6B 作为本地可跑的中文对话模型负责最后一步生成本地知识库则用向量数据库承载语义检索。这套组合之所以适合起步是因为三个组件都能在单机或单卡环境跑通社区资料多出问题容易定位。适合谁适合有 Python 基础、手头有 8GB 以上显存或愿意用 CPU 推理的工程师想先跑通再优化而不是一上来就追求生产级吞吐。2. 把链路拆开看LangChain 的 RAG 流程与 ChatGLM-6B 的接入方式2.1 文档加载、切分与向量化RAG 检索质量的第一道关很多人第一次搭 RAG把文档一股脑塞进去结果检索出来的片段要么太长要么太碎生成答案时模型抓不住重点。常见做法是先把文档统一转成纯文本再按语义边界切分。LangChain 提供了多种 Loader比如TextLoader、UnstructuredFileLoader、DirectoryLoader对 PDF 和 Word 建议先用外部工具转成 txt 或 markdown避免解析层引入噪声。切分用RecursiveCharacterTextSplitter中文场景下分隔符要调整默认的[\n\n, \n, , ]对中文段落不够友好建议加上中文句号、问号、分号。from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.document_loaders import DirectoryLoader, TextLoader # 加载本地 knowledge 目录下的所有 txt 文件 loader DirectoryLoader( ./knowledge, glob**/*.txt, loader_clsTextLoader, loader_kwargs{encoding: utf-8} ) docs loader.load() # 中文场景下调整分隔符优先按段落和句子切 splitter RecursiveCharacterTextSplitter( chunk_size500, # 每段最大字符数 chunk_overlap80, # 相邻段重叠防止语义被切断 separators[\n\n, \n, 。, , , , , , ], length_functionlen ) chunks splitter.split_documents(docs) print(f原始文档 {len(docs)} 篇切分后 {len(chunks)} 段)这段代码的逻辑是先批量加载再切分。chunk_size设 500 是中文场景的常用起点太大检索精度下降太小上下文不完整。chunk_overlap设 80 左右保证跨段落的句子不会被硬切。分隔符顺序很重要LangChain 会优先用靠前的分隔符切所以把\n\n和中文句号放在前面能尽量保持段落完整。如果文档里有表格或代码建议单独处理不要和正文混在一起切。2.2 向量库选型与检索器配置Chroma 够用但参数要调本地知识库的载体是向量数据库。常见选择有 Chroma、FAISS、Milvus。单机起步我一般用 Chroma因为它自带持久化、API 简单、和 LangChain 集成度高。嵌入模型可以用text2vec-base-chinese或bge-small-zh后者在中文语义相似度上表现更稳。检索器用similarity_search或as_retriever关键参数是k和score_threshold。k是返回片段数设 3 到 5 比较平衡score_threshold用来过滤低相关片段设太高会漏设太低会引入噪声。from langchain.embeddings import HuggingFaceEmbeddings from langchain.vectorstores import Chroma # 使用本地嵌入模型避免调用外部 API embedding HuggingFaceEmbeddings( model_nameBAAI/bge-small-zh-v1.5, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) # 持久化到本地目录下次直接加载 vectordb Chroma.from_documents( documentschunks, embeddingembedding, persist_directory./chroma_db ) vectordb.persist() # 检索器配置 retriever vectordb.as_retriever( search_typesimilarity, search_kwargs{k: 4, score_threshold: 0.35} )normalize_embeddingsTrue让向量归一化余弦相似度计算更稳定。persist_directory指定后下次启动不用重新嵌入。score_threshold设 0.35 是经验值低于这个分数的片段通常和问题关系不大。如果发现检索结果总是差一点先把k调到 6 看看召回是否改善再决定是否换嵌入模型。Chroma 在数据量超过十万段后检索会变慢那时候再考虑 FAISS 或 Milvus。2.3 ChatGLM-6B 本地加载与 LangChain 包装ChatGLM-6B 是清华开源的对话模型6B 参数FP16 下显存占用约 13GBINT4 量化后约 6GB。单卡 8GB 显存建议用 INT4 量化加载。LangChain 没有内置 ChatGLM 的 LLM 类需要自己写一个包装类实现_call和_llm_type方法。加载时注意trust_remote_codeTrue因为 ChatGLM 的模型代码不在标准 transformers 库里。from langchain.llms.base import LLM from transformers import AutoTokenizer, AutoModel from typing import Optional, List class ChatGLMLLM(LLM): tokenizer: AutoTokenizer None model: AutoModel None def __init__(self, model_path: str, device: str cuda): super().__init__() self.tokenizer AutoTokenizer.from_pretrained( model_path, trust_remote_codeTrue ) self.model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, device_mapdevice, load_in_4bitTrue # 8GB 显存用 INT4 ).eval() def _call(self, prompt: str, stop: Optional[List[str]] None) - str: response, _ self.model.chat( self.tokenizer, prompt, history[], max_length2048, temperature0.7 ) return response property def _llm_type(self) - str: return chatglm-6bload_in_4bitTrue是显存不够时的后悔药代价是推理速度下降约 30%。max_length控制生成总长度设 2048 对问答够用。temperature设 0.7 让回答不至于太死板但知识问答场景可以降到 0.3 提高确定性。包装类必须实现_callLangChain 的 Chain 会调用它。如果加载时报CUDA out of memory先确认没有其他进程占卡再试device_mapauto让 transformers 自动分配。2.4 用 RetrievalQA 串起完整链路LangChain 的RetrievalQA把检索器和 LLM 拼在一起输入问题直接返回答案。提示词模板要改默认模板偏英文中文场景建议自定义明确要求模型只根据检索到的上下文回答不要编造。from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate prompt_template 基于以下已知信息回答用户问题。如果无法从中得到答案请说根据现有资料无法回答不要编造。 已知信息 {context} 用户问题{question} 回答 PROMPT PromptTemplate( templateprompt_template, input_variables[context, question] ) qa_chain RetrievalQA.from_chain_type( llmChatGLMLLM(model_path./chatglm-6b), chain_typestuff, retrieverretriever, return_source_documentsTrue, chain_type_kwargs{prompt: PROMPT} ) result qa_chain({query: 产品的保修期是多久}) print(result[result]) print(来源, [doc.metadata for doc in result[source_documents]])chain_typestuff是最简单的拼接方式把所有检索片段塞进一个提示词。片段多的时候会超长可以换map_reduce或refine但速度会慢。return_source_documentsTrue让结果带回来源片段方便排查检索是否准确。如果回答总是根据现有资料无法回答先检查检索器返回的片段是否真的包含答案再检查提示词是否把上下文截断了。3. 避坑与排查RAG 智能问答系统最常见的 5 个翻车现场3.1 检索结果和问题无关答案全靠模型编现象问报销流程是什么检索出来的却是考勤制度模型基于错误上下文生成了一段看似合理但完全不对的回答。原因通常是切分粒度太粗一个片段里混了多个主题或者嵌入模型对中文语义区分度不够。解决先把chunk_size降到 300 试试再换bge-base-zh或text2vec-large-chinese嵌入模型。如果文档本身主题混杂建议按文档类型分库检索时指定库。3.2 ChatGLM-6B 加载报 trust_remote_code 错误现象AutoModel.from_pretrained抛出ValueError: trust_remote_code。原因是 ChatGLM 的模型定义不在 transformers 标准库必须显式允许远程代码。解决两个地方都要加trust_remote_codeTruetokenizer 和 model 各一次。如果公司网络限制下载提前把模型文件拉到本地目录model_path指向本地路径。3.3 显存溢出INT4 也救不了现象加载模型时CUDA out of memory即使load_in_4bitTrue。原因可能是显卡被其他进程占用或者max_length设太大导致推理时 KV cache 爆显存。解决先nvidia-smi确认没有残留进程再把max_length从 2048 降到 1024temperature不影响显存。如果还是不够用 CPU 推理速度慢但能跑适合验证流程。3.4 检索速度越来越慢Chroma 查询超过 2 秒现象知识库加到几万段后每次检索要等两三秒。原因是 Chroma 默认用暴力搜索数据量大时线性增长。解决建索引时指定collection_metadata{hnsw:space: cosine}启用 HNSW 近似搜索或者换 FAISS 的 IVF 索引。如果数据量超过五十万段单机 Chroma 就不合适了考虑 Milvus 或 Qdrant。3.5 回答总是根据现有资料无法回答但资料里明明有现象检索器返回了正确片段模型却说无法回答。原因通常是提示词里上下文被截断或者score_threshold设太高把正确片段过滤了。解决先把score_threshold降到 0.2 看是否恢复再检查chunk_size和max_length的关系确保拼接后的提示词不超过模型上限。如果用的是stuff模式且片段多换refine模式逐段精炼。4. 进阶技巧用多路召回和重排序把 RAG 命中率再提一截跑通基础链路后最值得投入的优化方向是检索命中率。单一向量检索对短查询和专有名词不敏感常见做法是加一路 BM25 关键词召回再用重排序模型合并结果。LangChain 的EnsembleRetriever支持多路召回配合CrossEncoderReranker做精排。BM25 用rank_bm25库重排序用bge-reranker-base。from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain_community.retrievers import BM25Retriever # 向量检索器 vector_retriever vectordb.as_retriever(search_kwargs{k: 6}) # BM25 关键词检索器 bm25_retriever BM25Retriever.from_documents(chunks) bm25_retriever.k 6 # 多路召回合并权重各半 ensemble_retriever EnsembleRetriever( retrievers[vector_retriever, bm25_retriever], weights[0.5, 0.5] ) # 重排序先召回 12 段精排后取前 4 cross_encoder HuggingFaceCrossEncoder(model_nameBAAI/bge-reranker-base) reranker CrossEncoderReranker(modelcross_encoder, top_n4) compression_retriever ContextualCompressionRetriever( base_compressorreranker, base_retrieverensemble_retriever ) # 替换原来的 retriever qa_chain.retriever compression_retrieverEnsembleRetriever的weights控制两路召回占比向量强在语义BM25 强在关键词各 0.5 是稳妥起点。CrossEncoderReranker的top_n4表示精排后只留 4 段给模型减少噪声。重排序模型比嵌入模型慢但只在召回后的少量片段上跑整体延迟增加可控。如果发现 BM25 对中文分词效果差先装jieba做分词再喂给 BM25。这套组合在实测中能把命中率从 60% 左右提到 80% 以上代价是每次查询多 200 到 400 毫秒。验证方法很简单准备 20 到 30 个有标准答案的问题跑一遍看检索片段是否包含答案再跑一遍看最终回答是否正确。两个指标分开看检索对了但回答错问题在提示词或模型检索就错了回去调切分和嵌入。我自己的习惯是每次改完参数都跑一遍这个测试集记录命中率变化避免凭感觉调参。希望帮到你。本文还有配套的精品资源点击获取