简介:本地部署私有化智能问答助理示例项目,面向对ChatGLM等大模型和机器学习应用感兴趣、希望构建离线知识库问答系统的开发者。项目基于深度学习技术,以Gradio搭建交互界面,结合FAISS完成大规模向量的相似性检索,配合Sentence-Transformers生成文本嵌入,实现文件上传、文档解析与自动应答,可快速落地客户服务、技术支持、内部知识管理等场景。资源包共18个文件、约336KB,包含多个Python主程序脚本、TXT配置与说明文档、YAML部署配置、Shell执行与更新脚本、依赖清单、WHL离线安装包、PEM安全证书以及DOCX/XLSX/PDF示例文档,能覆盖从环境准备到运行调优的完整流程。目前已有140人学习下载;多版本代码、依赖清单与执行命令便于对照修改和排查问题,尤其适合在学习中快速试错和二次开发,能帮助读者掌握私有化问答系统的构建思路与完整部署方案。
1. 私有化智能问答助理:ChatGLM本地部署的第一步怎么迈
我手头有一台只有一块显卡的普通工作站,要做的是一个只在内网跑的问答助理。数据不能出网络,云端API基本不用考虑,剩下的路就是本地部署一个开源模型。这个示例项目的核心,是把ChatGLM这一档模型完整拉起来,从环境检查、模型权重、量化加载,到后端接口和Web问答页,一步步串成一条能用的链路。
适合两类人:一类是刚接触本地私有化部署,想先跑通一个最小可用的智能问答助理;另一类是已经在做机器学习相关开发,想把内部文档问答收回到自己机器上控制的人。它解决的核心问题就一句话:让AI问答只发生在内网里,数据不过外网,回答质量还够看。如果你是后者,直接翻到第三章开始搭环境就行。
2. 选型与部署架构:为什么私有化用ChatGLM、模型跑在什么机器上
做私有化智能问答,第一步不是写代码,而是想清楚选型。云端大模型API用起来确实省事,但一旦落到“私有化”三个字,很多常规选择就失效了。这一章先把选型逻辑讲透,再给出一套最小架构和资源预估,后面所有命令都跑在这套决策之上。
2.1 云端API与本地模型的取舍:三个硬条件
云端API调用方便、回答质量高,但一个内部工具把每条提问都送到外部服务,大多数团队接受不了。我在动手前会先问三个问题:数据能不能出内网?外部服务出现波动时,工具是不是直接瘫痪?高频调用之下成本怎么算?这三个问题里只要有一个答案是否定的,落到本地部署基本就是唯一选择。
用一张表格把云端API和本地模型的关键差异摊开看,会比较直观:
| 对比维度 | 云端API | 本地部署模型 |
|---|---|---|
| 网络依赖 | 必须能访问外部服务 | 完全不依赖外部网络 |
| 数据出网 | 内容会传到外部服务 | 不出内网 |
| 单次成本 | 按调用量计费,高频成本高 | 一次硬件投入,后续无调用费 |
| 离线可用 | 服务挂了就不可用 | 内网独立运行 |
| 回答质量 | 整体较高,模型档位更高 | 受本机模型档位限制 |
| 硬件门槛 | 没有额外硬件要求 | 需要一台带GPU的机器 |
这个对比做完,结论基本清楚:只要对数据边界敏感,或者调用频次高到按量计费肉疼,本地部署就是更划算的路。选型上,ChatGLM这一档开源的模型权重比较适中,transformers接口对新手友好,量化方案也成熟,是本地部署入门最顺手的起点之一。后面就算换更大参数量的模型,整个部署骨架不会变,只是资源估算要重新做。
2.2 最小可用的架构:模型服务、后端接口与前端页面
这个示例项目采用的是一套三层的极简架构。模型推理层负责加载ChatGLM权重、接收文本并生成回复;接口层用FastAPI把模型推理包成HTTP服务;前端层是一个简单的Web页面,负责收集用户输入和展示回答。
各层职责和选型如下:
| 层 | 职责 | 组件 |
|---|---|---|
| 模型推理层 | 加载权重、执行对话生成 | ChatGLM + transformers |
| 后端接口层 | 暴露HTTP接口、管理并发请求 | FastAPI + uvicorn |
| 前端页面层 | 用户输入、展示回答 | HTML + JavaScript |
三层跑在同一台机器上,数据和请求都在内网走,模型权重加载一次后常驻内存,后端接口进程与模型进程放一起,避免重复加载带来的资源浪费。这个设计谈不上复杂,但足够把一个私有化问答助理从零跑通。先跑通链路,再谈优化,是这个项目最核心的推进方式。
2.3 显存、内存与量化档位:动手前的资源估算
动手之前先算账。ChatGLM这一档模型的原始权重在FP16精度下约占12GB磁盘空间,推理时显存占用大概在14GB左右,普通显卡扛不住。于是量化档位的选择直接决定了你要准备什么机器。
以下是经验参考值,实际数值会随上下文长度浮动:
| 加载精度 | 显存占用参考 | 适用场景 |
|---|---|---|
| FP16 | 约14GB | 显卡显存充足,追求回答质量 |
| int8 | 约8GB | 显存中等,质量和资源较平衡 |
| int4 | 约6GB | 显存吃紧,先把链路跑通 |
内存建议16GB起步,硬盘预留20GB左右。没有独立显卡也能用CPU跑,但回答速度会降到每秒几个字,体验很打折扣。我一般不建议零GPU新手直接上FP16,显存一溢出就容易一脸懵。最省心的路线是先按int4把整条链路跑通,之后资源有余量再往上调精度。
3. 动手前的环境准备:把GPU、驱动、Python依赖一次配齐
环境问题在这个项目里占了一半的翻车率。很多人在权重加载阶段报错,回头查才发现是CUDA没配对、Python版本太新、tokenizer文件不完整。这一章把环境准备做成一次能通过的操作清单,照着做能把后续排查成本压到最低。
3.1 GPU与CUDA检查:先跑一条命令确认能不能用
拿到机器先别急着装依赖,跑三条命令确认底子:
# 检查显卡是否被系统识别,以及驱动和显存大小 nvidia-smi # 查看CUDA编译版本,确认与后续安装的torch匹配 nvcc --version # 确认Python版本,推荐3.10或3.11 python --version命令背后要看的不是它有没有输出,而是输出里的关键值。nvidia-smi能看到驱动版本、显存总量和当前占用,显存量直接决定你能用FP16还是int4。nvcc --version输出的是CUDA Toolkit版本,后文安装torch时要按这个版本选对应的安装命令。如果nvcc找不到,不代表不能用GPU,驱动装好且torch能识别即可,只是排查时少一个信息源。
Python版本我建议直接锁定在3.10。torch对3.10的预编译wheel最全,避免为Python 3.12等新版本去等兼容包或者自己编译,白白浪费时间。
3.2 创建conda环境与安装依赖:版本锁定的必要性
环境隔离是这类部署必备的习惯。模型项目里的依赖版本敏感,直接装在系统Python里,日后其他项目一升级依赖,这个项目就会被拖垮。
# 创建独立环境,避免污染系统Python conda create -n rag_assistant python=3.10 -y # 激活环境 conda activate rag_assistant # 安装深度学习与推理相关依赖 pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install transformers accelerate sentencepiece protobuf pip install fastapi uvicorn其中torch的安装命令需要和你本机CUDA版本对应,上面示例是CUDA 11.8的索引地址,如果你的环境是其他CUDA版本,去PyTorch官网生成对应的安装命令替换即可。transformers负责加载模型和tokenizer,accelerate负责显存自动分配,sentencepiece和protobuf是ChatGLM这类中文模型依赖的分词组件,不能省。
装完依赖后,我建议把核心版本记到一个requirements.txt里:
torch>=2.0 transformers>=4.30,<4.45 accelerate>=0.20 sentencepiece>=0.1.99 protobuf>=3.20 fastapi>=0.100 uvicorn>=0.23transformers的版本范围特别说一下。ChatGLM加载依赖trust_remote_code=True来执行模型自带的远程代码,而transformers新版本对旧自定义代码的兼容性有变更,版本太新或太老都可能触发加载报错。把版本锁在这个区间内,是本地部署这类模型比较稳妥的做法。
3.3 模型权重目录结构:下载后的校验与软链接
权重文件准备好了,组织方式也不能乱。这个示例项目按三层目录来划分,模型权重、后端服务、前端页面分开管理:
# 规划目录结构 mkdir -p models/chatglm-6b mkdir -p server mkdir -p web # 模型权重存放在其他磁盘时,用软链接避免重复占用空间 # ln -s /mnt/data/models/chatglm-6b models/chatglm-6b从公开模型仓库把权重拿下来后,先检查目录下是否有这些关键文件:config.json、tokenization_chatglm.py、tokenizer.model、pytorch_model.bin或分片权重文件。tokenizer.model是中文分词关键,缺了它会出现回答乱码的怪问题,这一点后面排查章节会单独讲。
磁盘空间也要顺手确认一下。模型权重解压后通常超过10GB,如果所在分区剩余不足,加载到一半会报磁盘写入错误。我习惯在解压后马上跑一遍du -sh models/chatglm-6b,确认实际大小和预期相符再进入下一步。
4. 跑通第一条问答链路:模型加载、量化启动与接口验证
环境就绪后,进入核心环节:把模型加载起来,封装成接口,再通过页面访问到。这个过程是“黑匣子”最集中的一段,每一步都要留一个验证手段,确认服务真正可用了再往下一层走。
4.1 启动模型推理服务:从命令行到FastAPI接口
把模型推理包成一个HTTP服务,常见做法是用FastAPI写一个推理脚本,启动后常驻内存。下面是最小可用的服务端代码:
# server/chat_server.py from fastapi import FastAPI from pydantic import BaseModel from transformers import AutoModel, AutoTokenizer # 加载tokenizer,trust_remote_code=True是执行ChatGLM自定义代码的开关 tokenizer = AutoTokenizer.from_pretrained( "models/chatglm-6b", trust_remote_code=True ) # 加载模型,device_map="auto"让transformers自动分配显存 model = AutoModel.from_pretrained( "models/chatglm-6b", trust_remote_code=True, device_map="auto" ).eval() app = FastAPI() class Query(BaseModel): prompt: str history: list = [] @app.post("/chat") def chat(query: Query): # model.chat()内部完成了多轮历史拼接和模板处理 response, new_history = model.chat( tokenizer, query.prompt, history=query.history ) return {"response": response, "history": new_history}这段代码有几个参数要特别解释。trust_remote_code=True是ChatGLM特有要求,模型目录里的自定义模型文件会被transformers执行,没有这个开关会直接报加载错误。device_map="auto"是accelerate提供的自动分配机制,单卡或多卡环境下都能把模型的各层分到显存中,省去手动指定设备的麻烦。.eval()把模型切到推理模式,避免训练模式下额外的内存开销。
启动服务:
# 在server目录下执行,0.0.0.0允许内网其他机器访问 uvicorn chat_server:app --host 0.0.0.0 --port 8000--host 0.0.0.0表示监听所有网卡接口,这样内网其他机器可以通过服务器IP访问;如果只在本机测试,改成127.0.0.1即可。--port 8000是服务端口,被占用时换成其他端口。
首次启动会加载权重,耗时取决于磁盘读取速度,几秒到几十秒不等。日志停在一行没有任何报错,不代表死掉了,可能还在读模型文件。这时候最忌讳反复Ctrl+C重启,耐心等日志出现Application startup complete再操作。
4.2 接入Web问答界面:请求参数与返回结构
服务端接口就位后,需要一个前端页面把问答交互搭起来。这里用一个极简的静态页面演示请求和响应的结构:
<!-- web/index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>本地问答</title> </head> <body> <textarea id="input" rows="3" placeholder="输入你的问题"></textarea> <button onclick="ask()">发送</button> <div id="answer"></div> <script> async function ask() { const prompt = document.getElementById("input").value; const res = await fetch("http://<服务器IP>:8000/chat", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({prompt: prompt, history: []}) }); const data = await res.json(); document.getElementById("answer").innerText = data.response; } </script> </body> </html>这段页面里,fetch把用户输入以JSON格式POST到后端/chat接口,后端处理完返回{"response": "...", "history": [...]},页面把回答渲染到answer容器。请求体里的history字段是对话历史数组,传空数组表示首轮提问,传上次接口返回的history则可以进行多轮对话。
注意<服务器IP>要替换成启动服务那台机器的实际内网IP,不能写localhost。页面是在浏览器里跑的,浏览器所在的机器可能和服务端不是同一台,写localhost会请求到浏览器本机,必然失败。
4.3 首轮验证:用curl和一段测试脚本确认服务可用
页面写好后,先不急着打开,用curl做一次最直接的接口验证:
# 在能访问到服务器的机器上执行 curl -X POST http://127.0.0.1:8000/chat \ -H "Content-Type: application/json" \ -d '{"prompt": "你好,请介绍你自己", "history": []}'如果服务正常,会返回一段包含response字段的JSON。这一步能同时验证服务是否起来、模型是否加载完成、接口参数是否匹配。curl通了,再打开页面;curl都不通,页面调不通是必然的,别把时间花在浪费前端排查上。
接口通了以后,我建议再跑一个批量测试脚本,把几个固定问题一次性打进去,确认回答内容不是复读机:
import requests questions = [ "什么是机器学习?", "用一句话解释人工智能", "写一句产品欢迎语", ] for q in questions: r = requests.post( "http://127.0.0.1:8000/chat", json={"prompt": q, "history": []}, timeout=120, ) text = r.json()["response"] print(f"问:{q}\n答:{text}\n")这个脚本的作用是冒烟测试,确认模型在不同类型问题上都有正常输出。timeout=120是因为首次推理时模型的预填充会比较慢,尤其是量化模型,短超时会把正常请求误判为失败。回答里如果出现明显跑题或英文混杂,先记下来,这类问题会在第五章给出排查方向。
5. 常见问题排查:显存溢出、中文乱码、接口超时的处理记录
复现这类项目,几乎不可能一次走通。这一章按我实际踩过的顺序记录四个高频问题,每条都是“现象、原因、解决”三步走。你照着前面的步骤操作,如果服务没起来或者回答不对,大概率落在这四个坑里。
5.1 现象一:加载模型就报CUDA out of memory
现象:执行模型加载代码后,终端抛出CUDA out of memory,模型进程直接退出。
原因:显存不足以容纳当前精度下的模型权重。FP16权重在单卡显存不到14GB的机器上很容易触发,尤其后台还有其他进程占着显存时,情况更严重。
解决:先把后台无关的显存进程清掉,再用nvidia-smi确认可用显存。如果可用显存仍然不够,把加载精度降到int4,这是最直接的后悔药。示例项目里建议的量化加载方式如下:
from transformers import AutoModel, AutoTokenizer from transformers import BitsAndBytesConfig import torch # int4量化加载,bitsandbytes库需要提前安装 quant_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, ) tokenizer = AutoTokenizer.from_pretrained( "models/chatglm-6b", trust_remote_code=True ) model = AutoModel.from_pretrained( "models/chatglm-6b", trust_remote_code=True, quantization_config=quant_config, device_map="auto" ).eval()这里bitsandbytes是量化加载的底层库,安装缺失会直接报ModuleNotFoundError。量化后显存占用会明显下降,但回答质量也会有肉眼可见的波动,这是精度换资源的必然代价。注意:显存溢出不只是加载阶段会发生,回答很长的问题时也可能在生成中途爆显存,因为生成长文本需要缓存更多中间状态。
5.2 现象二:回答全是英文或中文乱码
现象:模型推理正常,但输出文本夹杂大量英文,或者中文变成“口口”样式的乱码。
原因:tokenizer相关文件不完整,最常见的是tokenizer.model缺失或损坏。ChatGLM这类中文模型依赖专用的词表文件完成中文分词,文件不在,模型就只能退回到通用token的切分方式,中文表现自然崩。
解决:检查模型目录下的关键文件是否齐全:
# 查看模型目录文件,重点确认tokenizer相关文件 ls -lh models/chatglm-6b/重点看tokenizer.model和tokenization_chatglm.py是否存在。缺失就从原模型仓库重新下载这两个文件补回去。另外,不要手动编辑config.json里的auto_map字段,这个字段指向模型自定义代码文件,一旦改错,加载时连错误信息都会变得很隐晦,远不如改回原样省事。
5.3 现象三:前端请求一直转圈直到超时
现象:浏览器里点击发送按钮后,页面一直pending,几十秒后报网络错误。
原因:这个现象通常有三个来源。一是后端服务根本没起来,curl进接口都失败;二是前端fetch地址写成了localhost,而页面和模型不在同一台机器;三是FastAPI没有配置CORS,浏览器在跨源请求时直接拦截了响应,前端拿不到结果。
解决:按顺序排查,不要跳步。先在服务器本机执行一次curl,确认服务可用;再检查前端代码里的地址是否替换成了实际服务器IP;最后确认CORS配置。FastAPI最简单的跨域处理如下:
from fastapi.middleware.cors import CORSMiddleware # 放行所有来源,适合纯内网Demo阶段 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], )这个配置加到app = FastAPI()之后即可。allow_origins=["*"]表示允许任何来源访问,在隔离内网里问题不大,但如果有安全要求,应该改成具体的前端来源地址,别图省事一直放开。
5.4 现象四:int4量化后回答质量明显下降
现象:换上int4量化后,模型能跑通,但回答明显变得逻辑断裂,甚至出现自相矛盾的句子。
原因:量化精度损失在低比特下更明显,特别是生成长文本时更容易跑偏。这不是代码bug,而是资源与质量的固有矛盾。
解决:如果机器的显存有余量,优先上int8或FP16。资源没法变的时候,从生成参数上补救:把temperature调低到0.2以下,top_p控制在0.7左右,减少随机性。ChatGLM推理时的采样参数默认偏发散,降低这两个值能让输出更贴近训练时的分布中心。
提示:量化这个环节有一定的玄学成分。同一套int4配置,换一个话题方向,质量浮动可能很大。判断质量不要靠一两个问题的印象,要用固定的测试集对比。
6. 进阶与验证:用提示词模板与检索增强把问答从能用变好用
链路跑通后,下一步是把回答质量从“demo水平”拉到“内部能用”。两个手段性价比最高:提示词模板让模型知道自己的边界,检索增强解决它瞎编资料的问题。
6.1 提示词模板:把问答限定在业务边界内
私有化问答最常见的失控场景是模型回答超范围,用户问内部资料里没有的东西,它也能编出一套答案。改进方式是给每轮提问拼上一段固定的提示词:
system_prompt = """你是一个内部问答助手。 回答时只基于给定资料,不要编造。 如果资料里没有相关内容,直接回答“资料中未找到”。 请使用中文回答。""" def build_full_prompt(query: str) -> str: return f"{system_prompt}\n问题:{query}"把拼好的build_full_prompt(query)作为prompt传给模型接口,而不是把用户原始输入直接丢进去。实际操作中,temperature设置在0.1到0.3之间,top_p保持0.7左右,回答会更收敛,减少发散式生成。
6.2 检索增强与生成参数:回答正确率和可控性的调整方向
内部文档问答里,直接让模型凭训练记忆回答,很多内容它没学过。常见做法是把相关资料切片后放进prompt,让模型基于上下文作答:
def build_prompt_with_docs(query: str, docs: list) -> str: context = "\n".join(docs[:3]) return f"{system_prompt}\n相关资料:{context}\n问题:{query}"这里的docs可以从内部文档里按关键词切出来,排序取最相关的前几段拼进去。加资料之后,模型瞎编的概率会明显下降,代价是输入变长、回答速度变慢,需要在上线前实测延迟是否可接受。
从那以后,我每次换量化档位或者换模型权重,都会先把同一组十条测试问题跑一遍,记录耗时时长和回答是否自洽,再决定要不要上线。这成了我部署私有化问答的习惯。希望帮到你。整套部署脚本和配置整理成了可直接复现的示例项目,按这篇文章的步骤从环境到接口,两小时能跑通基础版本。
本文还有配套的精品资源,点击获取