本地部署私有化智能问答助理:ChatGLM+向量检索实战 简介这份资源面向希望本地搭建私有化智能问答系统的开发者与AI学习者基于Gradio、FAISS与Sentence-Transformers构建文件上传与知识库检索问答示例解决数据隐私保护、离线部署与文档高效检索的问题。压缩包共18个文件约336KB以Python脚本与txt配置说明为主辅以yaml参数文件、whl依赖包、sh执行脚本以及docx、xlsx、pdf等示例文档和pem证书文件覆盖模型配置、依赖安装与运行入口等模块。已有140人学习下载。通过该示例可掌握向量相似度检索与文本嵌入的落地方式理解私有化部署中数据隔离与资源控制的实现思路并参考目录结构快速复现文件上传、知识库查询与对话交互的完整流程适合作为智能问答项目的入门实践与二次开发起点。1. 从一台吃灰的旧笔记本说起为什么我要把问答助理搬进本地去年收拾工位翻出一台 16G 内存的旧笔记本卖不上价扔了可惜。当时正好在做一个内部知识库的问答需求数据敏感不能往外发请求我就琢磨着能不能把模型塞进这台机器里跑起来。试了一圈最后落在一个叫「本地部署私有化智能问答助理」的示例项目上——它把 ChatGLM 这类对话模型、向量检索和一层简单的 Web 界面串成了一条完整链路下载下来改改配置就能跑。这篇文章就是我把这套东西拆开、跑通、又动手改过之后的记录适合两类人一类是想在自己机器上搭一个不联网问答服务的开发者另一类是手里有私有语料、想找个能二次开发的底座的人。下面从它能干什么讲到怎么改中间该踩的坑我都标出来了。2. 项目定位与技术选型这套东西到底由哪几块拼起来2.1 它解决的是什么问题一句话概括把「大模型对话能力」和「你自己的文档」绑在一起全部跑在本地。用户在前端输入问题后端先从你喂进去的资料里检索出相关片段再把片段和问题一起交给模型模型基于这些片段生成回答。整个过程不依赖外部接口模型权重、向量库、文档全在本地磁盘上。这个定位决定了它的技术选型不会太花哨。模型层用 ChatGLM 系列原因是它对中文支持好、量化后显存占用可控6G 显存就能跑起来检索层用向量相似度做召回常见做法是 sentence-transformers 类的编码模型把文本转成向量存进 FAISS 或 Chroma 这类本地向量库服务层用 FastAPI 起一个 HTTP 接口前端用轻量的页面调它。这套组合的好处是每一层都能单独替换——你嫌 ChatGLM 慢可以换别的嫌 FAISS 不好维护可以换 Chroma互不影响。2.2 目录结构长什么样下载下来解压典型的目录大概是这样不同版本会有出入以你手里的为准project/ ├── app/ │ ├── main.py # FastAPI 入口注册路由 │ ├── api/ │ │ ├── chat.py # 对话接口 │ │ └── knowledge.py # 知识库上传/检索接口 │ ├── core/ │ │ ├── config.py # 配置读取 │ │ └── llm.py # 模型加载与推理封装 │ └── rag/ │ ├── embedder.py # 文本向量化 │ └── retriever.py # 向量检索 ├── web/ # 前端静态页面 ├── data/ │ ├── docs/ # 放你的原始文档 │ └── vector_store/ # 向量库持久化目录 ├── models/ # 模型权重放这里 ├── requirements.txt └── config.yaml # 模型路径、端口、检索参数这个结构的关键在于core/llm.py和rag/两个目录前者管模型怎么加载、怎么生成后者管文档怎么进库、怎么被检索出来。二次开发基本都围绕这两块动刀。2.3 一次问答请求的完整链路理解链路比记目录重要。用户在页面输入「报销流程是什么」请求打到/api/chat后端做的事按顺序是把问题用编码模型转成一个向量拿这个向量去向量库里找最相似的 N 个文档片段N 就是常说的 top_k把这些片段拼成一段上下文和原始问题一起塞进 prompt 模板把拼好的 prompt 交给 ChatGLM 生成回答把回答返回前端同时可以带上引用的片段来源。第 2 步和第 3 步是这套系统区别于「裸模型」的核心。裸模型问它报销流程它只能编有了检索它答的是你文档里真实写的内容。所以调参的重点也在这两步top_k 取多少、片段切多大、prompt 怎么拼直接决定回答质量。2.4 环境准备与依赖安装先把 Python 环境隔离出来别污染系统环境# 建议 3.9 或 3.10太新的版本部分依赖轮子还没跟上 conda create -n qa-local python3.10 -y conda activate qa-local # 装依赖torch 按你的显卡选对应版本 pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install -r requirements.txtrequirements.txt里通常会有fastapi、uvicorn、transformers、sentence-transformers、faiss-cpu或faiss-gpu、pyyaml这些。装的时候最容易翻车的是 torch 和 faiss 的版本匹配显卡驱动新、CUDA 版本对不上torch 装完torch.cuda.is_available()返回 False模型就只能跑 CPU慢到没法用。装完先跑一句验证import torch print(torch.cuda.is_available(), torch.cuda.device_count())输出True 1才算 GPU 可用。如果只有 CPU也能跑但 ChatGLM 的 6B 模型在 CPU 上生成一句话要等十几秒体验很差建议至少找张 6G 显存的卡。3. 核心模块拆解模型加载、向量检索和接口怎么串起来3.1 模型加载与推理封装core/llm.py是整个项目最吃资源的地方。它要做的事就一件把模型权重读进显存暴露一个chat()方法给上层调。常见写法是这样# app/core/llm.py from transformers import AutoTokenizer, AutoModel import torch class LLMEngine: def __init__(self, model_path: str, device: str cuda): # 加载分词器和模型trust_remote_code 是 ChatGLM 系列必须的 self.tokenizer AutoTokenizer.from_pretrained( model_path, trust_remote_codeTrue ) self.model AutoModel.from_pretrained( model_path, trust_remote_codeTrue ).to(device) # 半精度能省一半显存6G 卡跑 6B 模型基本靠它 self.model self.model.half().eval() self.device device def chat(self, prompt: str, historyNone, max_length: int 2048): # 关掉梯度计算推理阶段不需要反向传播 with torch.no_grad(): response, new_history self.model.chat( self.tokenizer, prompt, historyhistory or [], max_lengthmax_length, temperature0.7, ) return response, new_history几个参数值得说清楚。model_path指向models/下你放权重的目录路径写错会直接报找不到 config。half()把权重转成 fp16显存占用砍半代价是极少数情况下数值精度略降对话场景基本无感。max_length控制的是输入加输出的总长度设太大显存会爆设太小长文档塞不进去2048 是个稳妥的起点。temperature越高回答越发散做知识问答建议调到 0.1 到 0.3让它老老实实按检索到的内容答。chat()方法里的history是对话历史多轮对话靠它维持上下文。注意历史越长占的显存越多长会话要定期截断否则跑着跑着就 OOM。3.2 文档向量化与入库检索质量的上限在入库这一步就定死了。rag/embedder.py负责把文档切成片段、转成向量# app/rag/embedder.py from sentence_transformers import SentenceTransformer class Embedder: def __init__(self, model_name: str BAAI/bge-small-zh): # 中文场景优先选中文编码模型别用英文的 self.model SentenceTransformer(model_name) def split_text(self, text: str, chunk_size: int 500, overlap: int 50): # 按固定长度切overlap 让相邻片段有重叠避免答案被切断 chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) start end - overlap return chunks def encode(self, texts): # normalize 后余弦相似度等价于内积检索更快 return self.model.encode(texts, normalize_embeddingsTrue)chunk_size和overlap是两个最需要调的参数。切太碎一个完整答案被拆到好几个片段检索出来拼不回去切太大一个片段里混了好几个主题检索精度下降。500 字配 50 字重叠是中文文档的常见起点如果你的文档是表格或条款得按结构切不能硬按字数。编码模型选bge-small-zh这类中文模型用英文模型编码中文检索结果会差得让你怀疑人生。入库流程是读data/docs/下的文件 → 切片段 → 编码 → 存进向量库。这一步通常写成一个脚本改完文档重新跑一遍即可。3.3 检索与 prompt 拼接rag/retriever.py把问题向量和库里向量做比对取最相似的几个# app/rag/retriever.py import faiss import numpy as np class Retriever: def __init__(self, index_path: str): self.index faiss.read_index(index_path) def search(self, query_vector, top_k: int 3): # query_vector 形状要是 (1, dim)返回距离和下标 distances, indices self.index.search( np.array([query_vector]).astype(float32), top_k ) return indices[0], distances[0]top_k取 3 到 5 比较常见。取太少可能漏掉真正相关的片段取太多无关内容混进 prompt模型容易被带偏还白占显存。检索回来的片段要拼进 prompt模板大致是基于以下资料回答问题资料里没有的信息不要编造。 资料 {检索到的片段} 问题{用户问题} 回答这句「资料里没有的信息不要编造」很关键能明显降低模型胡编的概率。模板怎么措辞直接影响回答风格二次开发时这是最值得反复打磨的地方。3.4 FastAPI 接口层app/main.py把上面几块串起来对外暴露 HTTP 接口# app/main.py from fastapi import FastAPI from pydantic import BaseModel from app.core.llm import LLMEngine from app.rag.retriever import Retriever from app.rag.embedder import Embedder app FastAPI() llm LLMEngine(model_pathmodels/chatglm) embedder Embedder() retriever Retriever(index_pathdata/vector_store/index.faiss) class ChatRequest(BaseModel): question: str top_k: int 3 app.post(/api/chat) def chat(req: ChatRequest): # 1. 问题转向量 q_vec embedder.encode([req.question])[0] # 2. 检索相关片段 indices, _ retriever.search(q_vec, top_kreq.top_k) context \n.join(load_chunks(indices)) # 3. 拼 prompt 交给模型 prompt f基于以下资料回答问题\n{context}\n问题{req.question}\n回答 answer, _ llm.chat(prompt) return {answer: answer, sources: indices.tolist()}启动命令是uvicorn app.main:app --host 0.0.0.0 --port 8000。--host 0.0.0.0让局域网内其他机器也能访问只在本机用的话写127.0.0.1更安全。接口返回里带上sources前端就能显示「答案来自哪几个片段」这对知识问答场景很重要用户能自己核对。4. 跑起来从零到能对话的完整步骤4.1 模型权重怎么放项目本身不含模型权重得自己下。ChatGLM 的权重放到models/chatglm/下目录里应该有config.json、tokenizer.model、若干个.bin或.safetensors文件。放好后改config.yamlmodel: path: models/chatglm device: cuda max_length: 2048 retrieval: top_k: 3 chunk_size: 500 embed_model: BAAI/bge-small-zh server: host: 0.0.0.0 port: 8000路径用相对路径时注意启动命令的工作目录要对否则会找不到模型。我一般直接写绝对路径省得来回折腾。4.2 灌入你的文档把要问答的资料丢进data/docs/支持 txt、md、pdf 都行pdf 需要额外的解析库。然后跑入库脚本python -m app.rag.build_index --docs data/docs --out data/vector_store这个脚本内部就是遍历文件、切片段、编码、写 FAISS 索引。文档多的时候这一步会跑几分钟编码模型在 CPU 上跑更慢有 GPU 会快很多。跑完data/vector_store/下会多出index.faiss和对应的片段映射文件。4.3 启动服务并验证uvicorn app.main:app --host 0.0.0.0 --port 8000浏览器打开http://localhost:8000能看到对话页面。先问一个文档里明确写了的问题比如文档里有「年假 15 天」就问「年假多少天」看它答得对不对。再问一个文档里没有的看它会不会老实说不知道。这两个测试能快速判断检索和 prompt 有没有配好。用 curl 直接测接口也行curl -X POST http://localhost:8000/api/chat \ -H Content-Type: application/json \ -d {question: 年假多少天, top_k: 3}返回的 JSON 里answer是回答sources是命中的片段下标。如果sources是空的或者明显不相关问题出在检索层回去查编码模型和切片参数。5. 避坑与排查这几个问题我几乎每次都遇到5.1 显存不够跑着跑着就崩现象启动时报 CUDA out of memory或者聊几轮之后突然崩掉。原因通常是模型没做量化、max_length设太大或者对话历史没截断越积越多。解决先确认用了half()还不行就上 4bit 量化load_in_4bitTrue6G 卡跑 6B 模型基本靠量化把max_length降到 1024在chat()里限制 history 长度超过就丢掉最早的几轮。5.2 回答和文档对不上模型在编现象问文档里的内容模型答得头头是道但和原文不符。原因多半是检索没召回正确片段模型只能靠自己「记忆」编。排查顺序先看sources命中的片段是不是相关的不相关就是检索问题——检查编码模型是不是中文的、chunk_size是不是切碎了、top_k是不是太小。如果片段相关但回答还是偏就是 prompt 模板没约束好把「不要编造」那句加粗强调或者把temperature降到 0.1。5.3 中文检索效果差现象明明文档里有答案检索就是召不回来。原因基本是编码模型用了英文的比如默认的all-MiniLM-L6-v2它编码中文的效果很差。解决换成BAAI/bge-small-zh或text2vec-base-chinese这类中文模型换完重新跑一遍入库脚本——注意换了编码模型旧索引必须重建否则向量空间对不上检索全是乱的。5.4 改了文档但回答没变现象往data/docs/加了新文件问答还是老样子。原因是入库脚本没重跑向量库里还是旧数据。解决每次改完文档都重新执行build_index。如果文档量大、只想增量更新得自己改脚本支持追加写入FAISS 原生支持add但要注意片段映射文件同步更新。5.5 端口被占或局域网访问不了现象uvicorn启动报Address already in use或者本机能访问、别的机器访问不了。原因端口被别的进程占了或者--host写成了127.0.0.1。解决换端口--port 8001或者lsof -i:8000找到占用进程杀掉局域网访问必须用0.0.0.0同时确认防火墙放行了对应端口。6. 二次开发把示例改成你自己能长期用的东西示例项目能跑通只是起点真要长期用得动几处地方。我按改动价值从高到低说。第一处是 prompt 模板。这是投入产出比最高的改动。示例里的模板很朴素你可以按业务加约束要求回答带出处、要求用固定格式输出、要求遇到不确定就明确说不知道。我一般会把模板抽到config.yaml里改措辞不用动代码。模板改完拿一组固定问题回归测一遍看回答有没有变好别凭感觉。第二处是检索策略。示例用的是纯向量检索对短问题够用但遇到专有名词、编号这类精确匹配向量检索经常召不回来。常见做法是加一路关键词检索BM25两路结果融合排序也就是常说的混合检索。改动集中在retriever.py加一个 BM25 索引检索时两路各取 top_k按分数加权合并。这块代码量不大但效果提升明显。第三处是文档解析。示例对 pdf、word 的支持通常很粗糙直接按字符切会把表格和标题切烂。如果你的资料以 pdf 为主建议在入库前加一层结构化解析把标题、段落、表格分开处理表格单独转成文本再切。这一步做扎实检索质量能上一个台阶。第四处是并发和缓存。示例是单进程同步处理多人同时问会排队。生产用的话模型推理那层要加锁或者用队列避免多个请求同时进模型导致显存爆掉高频问题可以加一层结果缓存相同问题直接返回省一次推理。验证改动有没有效果别只看单次回答。我习惯准备一组 20 到 30 个问题的测试集覆盖「文档里有的」「文档里没有的」「需要跨片段拼的」三类每次改完跑一遍记录答对率。凭单次对话感觉调参很容易越调越偏。最后说个我自己的教训。早期我图省事改完 prompt 直接上线结果某天发现模型开始把检索到的片段原文大段抄进回答用户看着像复读机。从那以后我每次改完 prompt 模板都强制走一遍那组测试集确认回答风格没跑偏才敢用。这套流程麻烦但比事后被用户投诉强。希望帮到你。本文还有配套的精品资源点击获取