简介:这份资源面向希望动手实践RAG智能问答系统的开发者与AI学习者,基于LangChain、ChatGLM-6B与本地知识库搭建完整项目,解决单一生成模型在特定领域问答中准确度不足的问题。压缩包共73个文件,约17.67MB,以39个pickle模型缓存、12个Python源码、6个Markdown文档为主,另含配置文件、依赖清单与Dockerfile,覆盖从模型加载、文本切分、向量检索到对话生成的完整链路。已有956人学习下载,说明其在中文RAG实战方向具备一定参考价值。读者可获得可直接运行的源码、分步流程教程与部署说明,理解LangChain如何串联ChatGLM-6B与本地知识库,掌握检索增强生成的关键实现与排错思路,并据此改造出适配自身业务场景的问答系统。
1. 从一份能跑起来的 RAG 源码包说起:LangChain + ChatGLM-6B + 本地知识库到底解决了什么
很多人第一次接触 RAG,是在搜索框里敲下「如何用 AI 搭建本地部署的企业级知识库助手」,然后被一堆概念砸晕:向量库、Embedding、检索器、重排、生成。概念都懂,真到动手,发现连一个能跑通的 demo 都拼不出来。这份资源就是冲着这个痛点来的——它是一套完整的 RAG 智能问答系统源码包,技术栈是 LangChain + ChatGLM-6B + 本地知识库,附带了从环境搭建到离线部署的流程教程。拆开压缩包,能看到app.py、chatglm_llm.py、chinese_text_splitter.py、paddle_embedding.py、config.py这些核心文件,还有docs目录下的deploy.md、faq.md、OfflineDeploy.md,以及Dockerfile和Dockerfile.Base。它不是那种只给你一个 notebook 的玩具,而是一个带 CLI、带 Web 界面、带 Docker 部署路径的工程化项目。适合谁?适合已经知道 RAG 是什么、想找一个能直接复现的 rag 项目实战来拆解的人;也适合手里有本地文档、想搭一个不依赖外部接口的问答系统的开发者。下面我按「这东西怎么跑起来 → 每个模块在干什么 → 哪里容易翻车 → 怎么验证效果」的顺序,把这份源码包拆一遍。
2. 把源码包跑起来:环境、依赖与 ChatGLM-6B 的加载路径
2.1 依赖清单与 Python 环境的选择
拿到压缩包,第一件事不是急着python app.py,而是先看requirements.txt和pyproject.toml。这个项目同时提供了 pip 和 poetry 两套依赖管理方式,说明作者考虑过不同人的习惯。从文件结构看,根目录有一份requirements.txt,modelscope子目录下还有一份,说明模型加载部分可能有独立的依赖需求。
我一般会先建一个干净的 conda 环境,Python 版本选 3.8 到 3.10 之间。为什么不是 3.11?因为 ChatGLM-6B 依赖的transformers、torch以及paddlepaddle在某些版本组合下对高版本 Python 支持不完整,这是血泪经验。命令如下:
conda create -n rag-chatglm python=3.9 conda activate rag-chatglm pip install -r requirements.txt如果requirements.txt里没有锁死版本,建议手动确认几个关键包的版本:torch要和你机器的 CUDA 版本匹配,transformers建议用 4.27 到 4.33 之间的版本,langchain的版本决定了后面检索链的 API 写法。项目里出现了paddle_embedding.py,说明 Embedding 用的是 PaddleNLP 的模型,所以paddlepaddle和paddlenlp也要装对。
提示:如果你只有 CPU,ChatGLM-6B 的推理速度会非常慢,6B 参数在 CPU 上跑一轮对话可能要几十秒。建议至少有一张 8GB 显存以上的卡,或者用项目里提到的量化加载方式。
2.2 ChatGLM-6B 的加载与chatglm_llm.py的角色
chatglm_llm.py是这个项目里封装语言模型的核心文件。它做的事情不复杂:加载 ChatGLM-6B 的 tokenizer 和 model,提供一个生成接口给 LangChain 调用。但这里有几个参数直接决定你能不能跑起来。
# chatglm_llm.py 中常见的加载逻辑示意 from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained( "THUDM/chatglm-6b", trust_remote_code=True ) model = AutoModel.from_pretrained( "THUDM/chatglm-6b", trust_remote_code=True ).half().cuda() # 半精度加载,显存占用约 13GB model = model.eval() response, history = model.chat(tokenizer, "你好", history=[])trust_remote_code=True是必须的,因为 ChatGLM-6B 的模型代码不是 transformers 内置的,需要从模型仓库拉取自定义代码。.half()表示半精度,能把显存占用从约 24GB 降到 13GB 左右。如果你的卡显存不够,项目里可能提供了quantize相关的参数,或者你可以用model.half().quantize(4)做 4-bit 量化,但量化后生成质量会下降,这是取舍。
modelscope_hub.py和modelscope目录的存在说明作者也支持从 ModelScope 拉模型。国内网络环境下,从 ModelScope 下载往往比从 HuggingFace 更顺畅。如果你在chatglm_llm.py里看到模型路径写的是本地路径,比如./model_cache,那就需要先把模型文件下载到那个目录。model_cache这个文件夹名在文件列表里出现了,大概率就是放模型权重的地方。
2.3 启动方式:CLI 与 Web 界面两条路
项目提供了cli.py和app.py两个入口。cli.py是命令行交互,适合快速验证模型能不能正常回答;app.py是 Web 界面,通常基于 Gradio 或 Streamlit,适合演示和实际使用。
# 先跑 CLI 验证模型加载 python cli.py # 确认没问题后再启动 Web 服务 python app.py先跑 CLI 的好处是,如果模型加载失败,报错信息会直接打在终端上,比在 Web 界面里看日志更直观。常见报错包括:CUDA out of memory(显存不够)、ModuleNotFoundError(依赖没装全)、OSError: Can't load tokenizer(模型路径不对)。这三个错误基本覆盖了 80% 的首次启动失败。
app.py启动后,通常会监听一个本地端口,浏览器打开就能看到对话界面。项目文件里有demo.jpg、demo_new.jpg、demo_hf.jpg、demo_ms.jpg几张截图,分别对应不同模型来源的演示效果,说明作者测试过 HuggingFace 和 ModelScope 两种加载路径。
3. 本地知识库的构建:文档切分、Embedding 与检索链的组装
3.1chinese_text_splitter.py:中文文档怎么切才不丢语义
RAG 系统里,文档切分是最容易被忽视、但影响检索命中率最大的环节。英文场景下,LangChain 自带的RecursiveCharacterTextSplitter按空格和标点切就行,但中文没有空格,直接套用会把一句话切成两半。chinese_text_splitter.py就是解决这个问题的。
# chinese_text_splitter.py 的核心逻辑示意 import re from langchain.text_splitter import TextSplitter class ChineseTextSplitter(TextSplitter): def __init__(self, chunk_size=250, chunk_overlap=50): self.chunk_size = chunk_size self.chunk_overlap = chunk_overlap self.separators = ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] def split_text(self, text): # 按中文标点逐级切分,保证句子完整性 splits = re.split(r'(?<=[。!?;])', text) chunks = [] current = "" for s in splits: if len(current) + len(s) <= self.chunk_size: current += s else: chunks.append(current) current = s if current: chunks.append(current) return chunkschunk_size控制每个片段的最大字符数,chunk_overlap控制相邻片段的重叠字符数。为什么要有 overlap?因为一个问题可能跨越两个片段的边界,如果完全不重叠,检索时可能两个片段都只命中一半信息。250 字左右的 chunk 是我比较常用的值,太小会导致检索到的上下文不完整,太大则会引入无关噪声,稀释关键信息。
separators列表的顺序很重要:先按段落切,再按句子切,最后才按标点切。这个优先级保证了切出来的片段尽量是语义完整的段落,而不是断句。
3.2paddle_embedding.py:为什么用 PaddleNLP 而不是 OpenAI Embedding
这个项目的一个显著特点是 Embedding 用了 PaddleNLP,而不是调用外部 API。paddle_embedding.py封装的就是本地 Embedding 模型。这样做的好处很直接:完全离线,不依赖外部服务,数据不出本地。对于企业知识库场景,这一点往往是硬需求。
# paddle_embedding.py 的封装示意 from paddlenlp import Taskflow class PaddleEmbedding: def __init__(self, model_name="ernie-3.0-base-zh"): self.model = Taskflow("feature_extraction", model=model_name) def embed_documents(self, texts): return [self.model(text)["features"][0] for text in texts] def embed_query(self, text): return self.model(text)["features"][0]Taskflow("feature_extraction")是 PaddleNLP 提供的统一接口,底层加载的是 ERNIE 系列的模型。embed_documents用于批量处理知识库文档,embed_query用于处理用户提问。两者必须用同一个模型,否则向量空间不对齐,检索结果会完全乱掉。
这里有个坑:PaddleNLP 的Taskflow首次调用会自动下载模型,如果网络不通会卡住。建议提前把模型下载到本地,或者配置好 ModelScope 的缓存路径。项目里的model_cache目录可能也承担了这个角色。
3.3 检索链的组装:LangChain 在这里做了什么
LangChain 在这个项目里的角色是「胶水」:把文档加载、切分、向量化、检索、生成这几个步骤串成一条链。核心逻辑通常在chatllm.py或app.py里。
from langchain.vectorstores import FAISS from langchain.chains import RetrievalQA from langchain.embeddings.base import Embeddings # 假设 embedding 和 llm 已经初始化 vectorstore = FAISS.from_documents(documents, embedding) retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, return_source_documents=True ) result = qa_chain({"query": "你的问题"}) print(result["result"]) print(result["source_documents"])search_kwargs={"k": 3}表示检索时返回最相似的 3 个片段。这个数字需要根据你的文档密度调整:文档多、问题细,可以调到 5;文档少、问题泛,2 到 3 就够。chain_type="stuff"是最简单的策略,把所有检索到的片段拼成一个 prompt 塞给模型。如果片段总长度超过模型上下文窗口,就需要换成map_reduce或refine,但那样会增加推理次数,速度变慢。
return_source_documents=True是个好习惯,它让你能看到模型是基于哪些片段生成的答案。调试阶段一定要打开,否则你无法判断是检索错了还是生成错了。
4. 避坑与排查:从显存溢出到检索答非所问的五个真实问题
4.1 启动就报 CUDA out of memory
现象:python cli.py或python app.py刚加载模型就崩,终端打印RuntimeError: CUDA out of memory。
原因:ChatGLM-6B 全精度加载需要约 24GB 显存,半精度约 13GB,如果你的卡是 8GB 或 12GB,直接加载必然溢出。另外,如果之前有残留进程占用显存,也会导致可用显存不足。
解决:先nvidia-smi确认没有僵尸进程。然后修改chatglm_llm.py里的加载方式,改成 4-bit 或 8-bit 量化加载。常见做法是用bitsandbytes库:
model = AutoModel.from_pretrained( "THUDM/chatglm-6b", trust_remote_code=True, load_in_8bit=True, device_map="auto" )8-bit 量化后显存占用约 7GB,4-bit 约 4GB,但生成质量会有所下降。如果量化后还是不够,就只能换卡或改用更小的模型。
4.2 检索结果和问题完全不相关
现象:问「公司的报销流程是什么」,检索出来的片段却是「员工考勤制度」的内容。
原因:大概率是 Embedding 模型和向量库不匹配,或者文档切分粒度太粗。如果paddle_embedding.py用的模型和建库时用的不是同一个,向量空间就不一致。另一个可能是chunk_size设得太大,一个片段里混了多个主题,检索时匹配到了错误的主题词。
解决:确认建库和查询用的是同一个 Embedding 实例。把chunk_size从 500 降到 250 左右,增加chunk_overlap到 50。如果还不行,可以在检索后加一个重排步骤,用交叉编码器对候选片段重新打分。
4.3 模型回答「我不知道」或答非所问
现象:检索到了正确的片段,但 ChatGLM-6B 生成的答案要么说「我不知道」,要么答得驴唇不对马嘴。
原因:prompt 模板没写好。LangChain 的RetrievalQA默认 prompt 是英文的,直接套在中文场景下,模型可能理解不了「基于以下上下文回答问题」这个指令。另外,如果检索到的片段太长,超出了模型的上下文窗口,模型会截断或忽略部分内容。
解决:自定义 prompt 模板,用中文明确指令。常见做法是:
from langchain.prompts import PromptTemplate prompt_template = """基于以下已知信息,简洁和专业地回答问题。 如果无法从中得到答案,请说"根据已知信息无法回答该问题"。 已知信息: {context} 问题:{question} 答案:""" PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", retriever=retriever, chain_type_kwargs={"prompt": PROMPT} )同时控制检索片段的总长度,k不要设太大,或者用RecursiveCharacterTextSplitter的chunk_size限制单片段长度。
4.4 PaddleNLP 模型下载卡住或报网络错误
现象:运行paddle_embedding.py时卡在「Downloading model...」,或者报ConnectionError。
原因:PaddleNLP 默认从外部源下载模型,网络不通时会一直重试或直接失败。
解决:提前手动下载模型到本地缓存目录,然后在代码里指定本地路径。或者配置 ModelScope 作为下载源,项目里的modelscope_hub.py可能已经做了这件事。检查config.py里有没有模型路径的配置项,把它改成你本地的实际路径。
4.5 Docker 构建失败或容器内模型加载慢
现象:docker build到一半报错,或者容器启动后加载模型要等好几分钟。
原因:Dockerfile.Base和Dockerfile的分层设计如果没做好,每次构建都会重新下载依赖和模型。另外,容器内如果没有正确挂载 GPU,模型会回退到 CPU 推理。
解决:确认Dockerfile里安装了nvidia-container-toolkit相关的运行时,启动容器时加--gpus all。模型文件不要打进镜像,而是通过 volume 挂载model_cache目录。OfflineDeploy.md里应该写了离线部署的步骤,照着走一遍,把模型和依赖提前准备好。
5. 验证 RAG 效果与进阶调优:从命中率到多路召回
5.1 怎么判断你的 RAG 系统是真的能用
跑通不等于能用。我一般会用三个指标来快速判断一套 RAG 系统的健康度:检索命中率、答案忠实度、响应延迟。
检索命中率(rag hit rate)是最直观的。准备 20 到 30 个你已知答案的问题,看检索到的前 3 个片段里有没有包含正确答案。如果命中率低于 70%,说明切分或 Embedding 有问题,先别急着调生成模型。
答案忠实度是指模型生成的答案是否严格基于检索到的上下文,而不是自己编造。ChatGLM-6B 在中文场景下幻觉相对可控,但如果 prompt 没写好,它仍然会「自由发挥」。验证方法是把return_source_documents=True打开,人工比对答案和源片段。
响应延迟方面,6B 模型在单卡上的首 token 延迟通常在 1 到 3 秒,完整回答 5 到 15 秒。如果超过 30 秒,检查是不是检索的k太大,或者模型跑在了 CPU 上。
5.2 多路召回与重排:让检索更稳
单一向量检索在遇到同义词、缩写、领域术语时容易漏召回。进阶做法是加一路关键词检索(比如 BM25),然后把两路结果合并去重,再用重排模型精排。LangChain 里的EnsembleRetriever就是干这个的。
from langchain.retrievers import EnsembleRetriever from langchain.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(documents) bm25_retriever.k = 3 ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vectorstore.as_retriever(search_kwargs={"k": 3})], weights=[0.4, 0.6] )weights控制两路召回的权重,向量检索给 0.6,关键词给 0.4,是我在中文知识库场景下比较常用的配比。如果文档里有大量专有名词和编号,可以把 BM25 的权重调高到 0.5。
5.3 一个我踩过的坑:去重逻辑不能省
多路召回合并后,同一个片段可能被两路都召回。如果不做去重,prompt 里会出现重复内容,浪费上下文窗口,还可能让模型过度关注重复信息。LangChain 的EnsembleRetriever内部有去重逻辑,但如果你自己写合并代码,一定要按文档 ID 或内容哈希去重。
seen = set() unique_docs = [] for doc in combined_results: doc_hash = hash(doc.page_content) if doc_hash not in seen: seen.add(doc_hash) unique_docs.append(doc)这个去重逻辑看起来简单,但少了它,检索结果里可能出现三四个完全一样的片段,模型生成时会被误导。从那以后我每次搭 RAG 流程,都会在检索和生成之间强制走一遍去重,不管用的是框架自带还是自己写的。
5.4 离线部署的检查清单
项目里的OfflineDeploy.md和Dockerfile指向的是离线部署场景。离线环境下,所有依赖和模型都必须提前准备好。我一般会按这个顺序检查:Python 依赖是否全部打进镜像、ChatGLM-6B 权重是否放在model_cache并正确挂载、PaddleNLP 的 Embedding 模型是否已下载到本地、config.py里的路径是否指向容器内的实际路径。任何一项没对齐,启动时都会报错。
希望这份拆解能帮你少走点弯路,把这份源码包真正跑成自己的东西。
本文还有配套的精品资源,点击获取