资讯详情

基于LangChain与ChatGLM-6B的本地知识库问答系统搭建指南

📅 2026/9/24 21:21:07 | 华诺云谱 👁 阅读
基于LangChain与ChatGLM-6B的本地知识库问答系统搭建指南
简介基于LangChain与ChatGLM-6B等大语言模型构建本地知识库自动问答系统是面向人工智能开发者与自然语言处理学习者的完整项目实践资源可解决私有知识检索与智能问答落地问题。资源围绕本地知识库问答场景涵盖语料切分、向量检索、大模型调用、网页交互展示等核心模块适合希望掌握检索增强生成及应用开发的中高级读者。压缩包共75个文件类型涵盖Python源码、序列化数据、说明文档、演示图片及容器化部署配置其中源码和依赖清单可直接用于环境搭建与二次开发文档与截图便于对照学习界面效果另有模型缓存与离线部署说明可辅助无网络环境使用。整体约17.77MB结构清晰、模块化程度高。已有935人学习下载内容覆盖从模型接入到前端交互的主要链路可为构建知识问答系统提供完整参考适合快速落地本地知识问答应用。1. 本地知识库问答系统为什么说模型选型只是这场实践的开胃菜公司内部存了几百份制度文档、验收报告和产品手册团队想把这些散落在不同目录里的资料变成一个能直接对话的智能问答系统这就是典型的基于LangChain和ChatGLM-6B等系列LLM的本地知识库自动问答。很多人第一眼看到的是“模型选哪个”实际把项目跑完一圈你会发现真正花时间的不是LLM本身而是文档切分、Embedding选型和检索调参这些看不见的环节。这个方案解决三个痛点文档敏感不能出内网、回答需要能溯源回原文、文档更新频繁但不想重训模型。适合谁想搭建本地部署的企业级知识库助手的开发者以及正在做私有化落地的项目团队。本文会把整条链路拆开讲清楚包括你的第一块显存该花在哪、哪些参数值得反复调、哪些坑我已经替你踩过了。2. RAG链路与基座选型项目的主轴为什么是LangChain加ChatGLM-6B2.1 先敲定路线RAG与微调怎么选企业知识库问答最常见的落地路线是RAG检索增强生成而不是微调。原因很直接知识库里的制度文档一周可能要改几次走微调的话每更新一次就要重训一轮成本高、周期长、效果还不可控。RAG把知识放在外部向量库里文档变了重建索引就行模型权重始终不动增量更新的代价被压到最低。RAG还有个天然优势是可溯源。回答能带出原始文档的章节位置这对企业内部背书很重要而微调后的模型是一个黑匣子用户问“这个结论哪来的”你只能回答“模型训练的时候学的”这在很多业务场景里是交不了差的。当然微调也有自己的位置比如需要模型固定输出某种公文格式、学习一批带业务专有名词的历史问答对那种场景微调更合适。但“本地知识库自动问答”本质是查资料RAG是从第一天就该选的路线。LangChain做的事情就是把这套RAG流水线串成标准组件加载器Loader、切分器Splitter、Embedding、向量库Vector Store、检索器Retriever、问答链QA Chain。它未必是性能最优的但生态最全、上手成本最低换组件也相对容易。如果从零开始自己写这套链路光处理PDF解析、编码问题、向量库持久化就得花掉两三天而且写出来的东西大概率没有社区踩过坑的版本健壮。2.2 基座LLMChatGLM-6B合适在哪“等系列”如何理解ChatGLM-6B是6B参数规模的中文对话模型FP16推理大约需要13GB显存量化到4bit之后能压到6GB左右个人工作站、二手游戏卡都能跑。在它发布的时间点这是开源中文模型里私有化部署最合理的选择之一中文语料占比高生成的句子通顺模型权重全部落在自己服务器上内部文档不需要出内网。对很多IT部门来说就凭这一点就能过审。“等系列LLM”值得展开理解RAG链路对底座模型的要求是“能读懂检索内容并按指令生成”并不锁定某个具体模型。LangChain对LLM做了抽象换底座经常只改模型名、tokenizer和加载参数这几行链路其他部分不用动。后续想换成ChatGLM2-6B、Qwen或者接一个OpenAI兼容接口的云端模型改动量都控制在可控范围。这就是为什么项目标题里写“等系列”它传递的设计意图是可替换。我一般会把“生成模型”和“Embedding模型”分开看。生成模型负责组织语言Embedding模型决定能不能把相关文档找回来。很多人纠结ChatGLM-6B生成质量不够好结果发现真正的问题是检索回来的内容完全不对——检索不准后面用什么模型都白搭。2.3 一条LangChain工作流要过六个站最典型的实现是一条标准LangChain工作流六个站每一站职责清晰文档加载读取PDF、Word、Markdown、TXT兼容企业里乱七八糟的格式。文本切分把长文档切成适合向量化的短块避免超长截断和主题混杂。向量化Embedding把每个文本块编码成向量语义相近的文本在向量空间里距离更近。向量库存储把向量和原文、元数据一起落库支持近似搜索。检索用户提问后把问题编码成向量在库里取最相似的top-k个文本块。生成把检索结果和问题拼进Prompt交给LLM生成带依据的回答。这六个站里第2步和第5步是决定体验的关键点也是最常翻车的地方。切分参数像玄学检索结果像黑匣子这两个环节值得花最多时间。第6步调Prompt当然也重要但体验上不去的时候先回头看切分和检索而不是反复改Prompt。3. 本地跑通环境显存评估、量化选型与依赖安装3.1 量化选型与显存预算先算账再动手。ChatGLM-6B的显存占用由模型权重、KV Cache和推理计算共同决定不同加载方式差别很大加载方式显存占用约效果适用场景FP1613GB以上最佳24GB显存及以上的卡INT88GB到9GB略降16GB显存效果与速度均衡INT46GB到7GB可用8GB到12GB大多数人的选择另外还要算上Embedding模型和向量库——它们可以跑在CPU上也可以占一小块显存。预算建议是16GB显存的卡直接上INT812GB以下老老实实INT4。我见过不少新手用FP16加载模型还没跑起来就把显存打满然后就陷入“OOM、重启、再OOM”的循环。注意先运行nvidia-smi确认显卡驱动正常、显存没被其他进程占用。这一步能帮你省掉一晚上的排错时间。3.2 拉取模型权重ModelScope与HuggingFace两种方式模型权重从哪拉国内网络环境下常见做法是直接用ModelScope下载速度比HuggingFace稳定很多。下面这段脚本会完整拉取ChatGLM-6B的模型文件# download_model.py from modelscope import snapshot_download model_dir snapshot_download( ZhipuAI/ChatGLM-6B, # 模型ID以ModelScope平台页面为准 local_dir./models/chatglm-6b ) print(f模型已下载到: {model_dir})这段代码的逻辑很简单snapshot_download会拉取模型仓库的全部文件到本地指定目录支持断点续传下载中断了重跑一遍即可。local_dir参数是把权重放到项目目录内方便后续加载时用相对路径引用。如果你更习惯用HuggingFace对应的仓库ID是THUDM/chatglm-6b下载逻辑完全一样。下载慢的时候换ModelScope往往比折腾其他方式省事得多。3.3 conda环境与Python依赖安装模型有了接下来创建干净的环境。用conda单独建一个环境是必须的否则本地系统Python里那些老包会把transformers版本冲掉conda create -n local-qa python3.10 -y conda activate local-qa pip install torch2.0 transformers4.30 accelerate pip install langchain chromadb sentence-transformers bitsandbytes pip install modelscope fastapi uvicorn说明一下这些包各管什么torch是深度学习底座版本跟随transformers的要求装太老或太新都可能碰上莫名其妙的兼容报错langchain是本项目的编排框架chromadb是向量库用来存和搜文本向量sentence-transformers负责加载Embedding模型bitsandbytes是INT8/INT4量化加载的依赖modelscope是下载通道fastapi和uvicorn用来把模型包成HTTP服务。注意Windows上bitsandbytes的安装偶尔会出问题装不上就查一下官方issue常见做法是换一个预编译版本。别在环境问题上耗太久值得直接放弃重装。3.4 把模型变成HTTP服务并验证连通性LangChain连本地模型常见做法是先起一个HTTP服务再让LangChain通过endpoint_url连过去。这样模型常驻内存问答不重复加载权重也方便多进程并发调用。新建server.py写入# server.py from fastapi import FastAPI from pydantic import BaseModel from transformers import AutoModel, AutoTokenizer import torch app FastAPI() tokenizer AutoTokenizer.from_pretrained( ./models/chatglm-6b, trust_remote_codeTrue ) model AutoModel.from_pretrained( ./models/chatglm-6b, trust_remote_codeTrue, load_in_4bitTrue, # 显存不足时改为INT4 device_mapauto # 自动分配到可用设备 ).eval() class Prompt(BaseModel): text: str max_new_tokens: int 2048 temperature: float 0.2 app.post(/generate) def generate(prompt: Prompt): with torch.no_grad(): response, _ model.chat( tokenizer, prompt.text, max_new_tokensprompt.max_new_tokens, temperatureprompt.temperature, ) return {response: response}逻辑说明model.chat是ChatGLM官方内置的对话方法内部已经处理了历史拼接和多轮格式比手工拼Prompt稳得多eval()切到推理模式关闭Dropout等训练行为torch.no_grad()避免计算图占用额外显存。启动服务uvicorn server:app --host 127.0.0.1 --port 8000启动后先用curl验证一下不要直接连LangChain。接口通了再往后走否则后面排查时模型服务和应用代码混在一起很难定位问题curl -X POST http://127.0.0.1:8000/generate \ -H Content-Type: application/json \ -d {text: 你好}如果返回正常的对话文本说明模型服务已经就绪。如果起服务就OOM回到3.1改量化方式或者把max_new_tokens调小。4. 实现本地知识库自动问答加载、切分、向量化与检索生成4.1 文档加载与切分chunk_size和chunk_overlap是第一个翻车点文档加载本身不复杂真正影响问答质量的是切分。加载器和切分器一般配套使用# split_docs.py from langchain_community.document_loaders import DirectoryLoader from langchain.text_splitter import RecursiveCharacterTextSplitter loader DirectoryLoader( ./docs, glob**/*.md, # 按需改成 *.pdf、*.txt show_progressTrue ) documents loader.load() text_splitter RecursiveCharacterTextSplitter( chunk_size300, # 每个文本块最大字符数 chunk_overlap60, # 相邻块重叠字符数 separators[\n\n, \n, 。, , , , ] ) chunks text_splitter.split_documents(documents) print(f文档数: {len(documents)}, 切分块数: {len(chunks)})逻辑说明DirectoryLoader遍历目录下所有匹配文件RecursiveCharacterTextSplitter按分隔符优先级逐级切分优先在段落边界断开段落太长才继续往下一级分隔符切尽量保证每个块语义完整。参数这块是血泪经验。chunk_size我一般取200到500中文信息密度高300字左右往往能覆盖一个完整论点太大则一个块里混了多个主题检索时按整块匹配精度下降。chunk_overlap取60到100目的是让跨块的长句在切分后仍保留上下文衔接避免块边界截断导致答案缺失。如果发现检索到的内容经常“差一口气”先调overlap而不是调Prompt。4.2 Embedding与向量库中文场景别拿英文模型凑合Embedding模型的选择直接决定检索命中率。默认的sentence-transformers/all-MiniLM-L6-v2对中文效果很差换中文语料预训练的模型是必须做的一步# build_index.py from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embeddings HuggingFaceEmbeddings( model_namemoka-ai/m3e-base, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) vector_store Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db ) vector_store.persist()逻辑说明HuggingFaceEmbeddings加载本地Embedding模型把上一步切好的chunks逐块编码成向量Chroma.from_documents负责建库并落盘到persist_directory。normalize_embeddings做向量归一化让后续相似度计算更稳定。这个环节常被忽略的细节是维度一致性。m3e-base输出768维向量一旦换了一个不同维度的Embedding模型旧向量库和新的向量库就没法兼容查询会直接报错或返回空结果。所以项目一开始就要定好Embedding模型后面不要频繁换。4.3 检索与Prompt拼接让模型按本地资料回答而不是闭卷猜检索方式决定模型能“看到”什么材料Prompt决定它怎么用这些材料。下面是核心的检索和生成部分# qa_chain.py from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate prompt PromptTemplate( input_variables[context, question], template你是一个企业知识库问答助手。请仅根据以下资料回答用户问题。 如果资料中没有相关内容请明确回答“在现有资料中未找到相关答案”。 资料 {context} 问题 {question} 请用中文回答 ) qa_chain RetrievalQA.from_chain_type( llmllm, # 指向本地模型服务 retrievervector_store.as_retriever( search_kwargs{k: 4} # 取最相似的4个文本块 ), chain_typestuff, # 把检索结果一次性拼入Prompt return_source_documentsTrue, # 保留来源便于溯源 chain_type_kwargs{prompt: prompt} ) result qa_chain(报销流程是什么) print(result[result])逻辑说明用户问题先被Embedding成向量在向量库中检索出最相似的多个文本块stuff方式把这些文本块和问题拼接进Prompt交给LLM生成回答。return_source_documentsTrue这个参数建议一直开着排查问题时你可以把实际检索到的原文打印出来看模型到底基于什么答的。Prompt里的“如果没有相关内容请明确回答未找到”这行话很重要它是抑制幻觉的第一道防线。ChatGLM-6B这类模型在资料不足时倾向于强行组织答案你不给它兜底话术它就会开始编。4.4 跑通最小闭环一个脚本串起全部环节把上面几段整合成一个完整脚本保存为local_qa.py这是本项目最精简的可运行版本# local_qa.py from langchain_community.llms import ChatGLM from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate from langchain_community.document_loaders import DirectoryLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # ---- 第1步加载并切分文档 ---- loader DirectoryLoader(./docs, glob**/*.md, show_progressTrue) documents loader.load() text_splitter RecursiveCharacterTextSplitter( chunk_size300, chunk_overlap60, separators[\n\n, \n, 。, , , , ] ) chunks text_splitter.split_documents(documents) # ---- 第2步Embedding与向量库 ---- embeddings HuggingFaceEmbeddings( model_namemoka-ai/m3e-base, model_kwargs{device: cuda}, encode_kwargs{normalize_embeddings: True} ) vector_store Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db ) # ---- 第3步连接本地模型服务 ---- llm ChatGLM( endpoint_urlhttp://127.0.0.1:8000, max_token2048, temperature0.2, top_p0.7, ) # ---- 第4步检索问答 ---- prompt PromptTemplate( input_variables[context, question], template你是一个企业知识库问答助手。请仅根据以下资料回答用户问题。 如果资料中没有相关内容请明确回答“在现有资料中未找到相关答案”。 资料 {context} 问题 {question} 请用中文回答 ) qa_chain RetrievalQA.from_chain_type( llmllm, retrievervector_store.as_retriever(search_kwargs{k: 4}), chain_typestuff, return_source_documentsTrue, chain_type_kwargs{prompt: prompt} ) result qa_chain(报销流程是什么) print(result[result])运行前提是第3章的模型服务已经在8000端口跑起来。脚本第一次运行会构建向量库之后每次启动都会检查./chroma_db是否存在已存在的库会被直接复用。5. 避坑记录这套问答系统最容易翻车的5个地方5.1 切分参数不对检索结果答非所问现象问“服务器IP是多少”模型答了一堆“网络架构说明”答案和问题完全不相关。原因chunk_size设得太大一个文本块里混进了多个主题检索时按整块匹配返回的块里包含目标信息但被大量无关内容淹没或者chunk_overlap太小目标答案恰好被切在块边界两边都不完整。解决把chunk_size降到300以内chunk_overlap提到60以上对结构化明显、标题分明的文档最好先按标题做一次粗切再对超长段落做细切。排查时把检索到的原文打出来一眼就能看出问题# 打印实际检索到的文本块 for doc in result[source_documents]: print(doc.metadata.get(source)) print(doc.page_content[:100])5.2 中文Embedding相似度低相关文档召不回来现象用户问“差旅费报销标准”检索返回的是“员工福利制度”语义上毫无关联。原因用了默认的英文Embedding模型。英文预训练模型对中文的编码能力很弱近义词、上下文语义都学不到位“报销”和“费用”这种语义关系根本体现不到向量距离上。解决换成中文语料训练过的Embedding模型m3e-base和text2vec-large-chinese都是常见选择效果差距明显。需要特别提醒的是更换Embedding模型后旧向量库必须删掉重建因为向量维度可能都变了。5.3 模型刚加载就OOM现象启动server.py后立刻报CUDA out of memory服务直接退出。原因默认以FP16加载ChatGLM-6B权重加计算缓存轻松超过10GB在8GB到12GB的卡上必炸。部分情况下还有显存碎片问题反复重启后显存没有完全释放。解决确认当前卡的实际显存按第3章表格选择量化方式。INT4加载是大多数人最终的选择加载参数固定写成model AutoModel.from_pretrained( ./models/chatglm-6b, trust_remote_codeTrue, load_in_4bitTrue, device_mapauto )同时养成习惯调试阶段重启服务前用nvidia-smi看一眼显存是否被残留进程占用遇到僵尸进程用kill -9结束掉。5.4 ChatGLM-6B回答变成复读机现象短问题正常长问题开始反复输出同一句话停不下来。原因6B规模模型在生成较长内容时采样随机性放大temperature太高会让模型在概率不明确的区域反复横跳repetition_penalty设太低则模型对重复内容没有惩罚最终陷入复读循环。毛刺问题在ChatGLM-6B上很常见。解决把temperature降到0.1到0.3之间repetition_penalty调到1.1左右这两个参数配合能显著缓解复读。temperature控制的是softmax概率分布的平滑度值越大输出越随机知识库问答场景不需要随机性求稳是第一位的。5.5 换机器后向量库查询为空现象本地开发一切正常代码部署到另一台机器后向量库加载不报错但检索结果为空或直接抛维度错误。原因旧机器上用了不同版本的Embedding模型或者persist_directory用了绝对路径导致新机器读到的是一个不完整的旧库。两个库的向量维度不一致时查询行为会很奇怪——有时报错有时返回空。解决把向量库目录固定为项目相对路径把Embedding模型版本写死在安装文档里换机器后不要复用旧库删掉./chroma_db重新构建几分钟的事但能避免一晚上玄学排障。6. 从“能跑”到“能用”给问答系统加来源、重排序与替换基座6.1 给回答附上参考资料return_source_documentsTrue已经保留了检索来源把这个信息展示出来问答系统的可信度会上一个台阶source .join({doc.metadata.get(source) for doc in result[source_documents]}) answer f{result[result]}\n\n参考资料{source} print(answer)这样用户看到答案时能自己核对原始文档系统答错了也能快速定位是检索问题还是生成问题。6.2 用重排序把检索结果再筛一遍向量检索是“粗召回”top-k里的顺序不一定准确。进阶做法是先取top-20再用CrossEncoder重排序模型精排取前5from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) pairs [(query, doc.page_content) for doc in top20] scores reranker.predict(pairs)重排序能明显改善“关键词匹配但语义不相关”的情况是预算有限时性价比最高的优化手段。6.3 更换基座模型与密钥管理LangChain的模型抽象让换基座很容易。团队没有本地显卡时走API也是合理选择但密钥必须用环境变量管理不要硬编码进脚本import os from langchain_openai import ChatOpenAI llm ChatOpenAI( modeldeepseek-chat, api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com )鉴权信息泄露是本地部署项目最容易忽视的安全问题只要代码里出现过明文密钥就默认它已经泄露尽早轮换。6.4 怎么验证系统真的“能用”准备20到30个测试问题分成三类文档内可直接回答的、需要跨文档综合的、文档里完全没有的。逐条记录模型是否基于检索内容作答、事实是否正确、文档外问题是否拒绝回答。一套可重复的测试集比临时试问几句更能说明系统靠不靠谱。我自己的习惯是把这些问题存成Markdown每次调参后跑一遍看整体通过率的变化而不是靠单次问答的运气。这趟做下来最深的感受是本地知识库问答系统的瓶颈从来不在模型名称而在你对文档结构和检索效果的把握。把切分和检索调顺比纠结换哪个最新模型划算得多。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。