☰
ChatGLM私有化问答助理本地部署与排错指南
2026/11/2 10:09:04 网站建设 项目流程

简介:面向希望本地私有化搭建智能问答助理的开发者与机器学习学习者,这套示例项目以 Gradio 交互界面组织文档上传、知识库检索与对话流程,结合 FAISS 向量检索、句向量嵌入和 ChatGLM 实践,解决大量文档中快速定位内容并生成回答的问题,也可用于客户服务、技术支持与教育培训等场景验证,适合具备 Python 基础并关注数据隐私、希望验证本地问答流程的读者。压缩包共 18 个文件,约 336KB,含 5 个 Python 脚本、5 个文本说明、2 个 YAML 配置,以及依赖包、执行脚本、示例文档和证书等,覆盖代码运行、安装配置与测试资料。已有 140 人学习下载。目录保留主程序、模型和静态资源等模块,读者可据此梳理文件上传、向量建库、相似度检索到问答生成的链路,并参考多版本脚本与参考文档完成部署调试和二次定制。

1. ChatGLM 私有化问答助理本地部署:先别急着 pip install

上个月接手一台退役的图形工作站,任务是把它做成内部能用的私有化智能问答助理,模型选 ChatGLM。我一开始也是照着网上教程直接pip install,结果依赖冲突、权重加载失败、显存不够,连着撞了三天。这类机器学习和人工智能项目的安装,难点从来不在命令本身,而在版本对表和硬件摸底这两步。这篇笔记按我最后跑通的顺序写:先清点环境,再装依赖,再起服务,最后告诉你坏了怎么救。适合手上有一台带显卡的机器、想在内网把问答助理跑起来的人。

2. 环境清点:显卡、驱动与 Python 的版本对照表

动手之前先花二十分钟把机器的账算清楚,这一步省下来的时间,后面会成倍还给你。私有化问答助理和普通 Web 服务最大的区别,是它对三样东西同时敏感:显卡型号和显存、驱动支持的 CUDA 版本、以及 Python 与 transformers 的版本组合。任何一项对不上,表现都不是「报个错」,而是加载到一半卡死、推理输出乱码、或者干脆回退到 CPU 上跑,慢到你以为是模型不行。

我的习惯是先把这三件事写在纸上或记事本里,再去碰安装命令。尤其别在多人共用的机器上边装边试,装坏了影响的不只是你一个人。

2.1 用两条命令摸清机器底细

先查显卡和驱动,这是所有后续选择的起点。

# 1. 驱动版本、显存、当前占用,右上角那一行是重点 nvidia-smi # 2. 驱动装没装上都看不出来时,从 PCI 层确认卡在不在 lspci | grep -i nvidia # 3. 内存和磁盘,CPU 推理和权重落地都靠这两个 free -g df -h /data

nvidia-smi右上角的CUDA Version: 12.x是驱动支持的最高运行时版本,不是你装了 CUDA 12.x。很多人看到这行就以为 CUDA 已经就位,再去装一遍 Toolkit,纯属浪费时间。真正决定你能不能跑的是 PyTorch 的轮子——它自带运行时,跟驱动版本对得上就行。

显存那一列看MiB总数。6B 参数量的模型,FP16 精度大约要 13GB 以上显存,INT8 到 8GB 上下,INT4 压到 6GB 左右。所以 8GB 显存的卡不是不能跑,是只能走 INT4 这条路,而且别指望同时开很多并发。磁盘这块,权重文件加环境加缓存,我一般预留 30GB 以上,并且单独挂一个数据盘。

提示:如果nvidia-smi提示命令不存在,而lspci能看到 NVIDIA 设备,说明是驱动没装或者装崩了,先解决驱动,别往下走。

2.2 CUDA、PyTorch、Python 三者的版本对表

这一步是整个安装里最容易翻车的地方,我按实际跑通的组合列一张表,你照着对,别自己发挥。

组件推荐版本说明
GPU 驱动525 及以上低于这个版本,较新的 CUDA 轮子直接不认
Python3.10.x3.11 及以上部分依赖还没有预编译轮子,会现场编译,很折腾
PyTorch2.0.1 + cu118与 cu118 轮子绑定,驱动足够新也可以上 cu121
transformers4.33 左右高了会踩到接口变更,低了缺方法,别用 latest
sentencepiece0.1.99 及以上ChatGLM 的分词器依赖它,缺了在加载 tokenizer 时就报错
streamlit1.28 左右只做界面,版本容忍度相对高

三条铁律记住就行:驱动版本必须大于等于运行时 CUDA 版本;PyTorch 装带+cu118这类后缀的轮子,不要装默认包;Python 版本决定你有没有轮子可用,宁可降到 3.10。

# 版本自查,装之前和装之后各跑一次 python -V python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())" python -c "import transformers; print(transformers.__version__)"

第二行输出里的三样东西要同时合理:版本号带+cu118、CUDA 版本不是None、is_available()是True。只要最后一个是False,后面所有关于显存和速度的讨论都没有意义,先回去重装轮子。

2.3 权重文件与依赖的落地位置

权重不要放在家目录。家目录容易因为配额、清理策略或者权限问题出事,而且多人共用时你装的东西别人读不到。我统一放在/data/models下面,目录结构保持原样,别自己去改文件名。

# 统一的缓存与权重根目录,建议写进 /etc/profile.d/qa.sh export HF_HOME=/data/hf_cache export TRANSFORMERS_OFFLINE=1 # 权重备齐后打开,防止加载时联网重试卡住 export MODELS=/data/models # 期望的目录形态 # /data/models/chatglm-6b/ # config.json # tokenizer.model # tokenizer_config.json # pytorch_model-00001-of-00008.bin ... # modeling_chatglm.py tokenization_chatglm.py

TRANSFORMERS_OFFLINE=1这个开关值得单独说一句。默认情况下,transformers 在加载本地目录时仍可能尝试联网校验,网不通就反复重试,表面上像卡死,实际是在等超时。权重全部下载到位之后把它打开,加载时间能从几分钟掉到几十秒。同理,目录里那两个modeling_*.py和tokenization_*.py是权重自带的实现,必须跟着模型一起放好,缺了就会在trust_remote_code那一步报找不到类。

3. ChatGLM 安装实战:虚拟环境、依赖与首次推理

环境账算清楚以后,安装本身其实很短。真正花时间的是试错,所以这一章我把顺序固定下来:建环境、装轮子、锁依赖、写最小加载脚本、再调量化参数。每一步都有验证动作,不通就停下,不要带着问题往下装。

3.1 建一个干净的 conda 环境并锁死依赖

绝对不要装在系统 Python 里。这台机器后面可能还有别的服务,装混了谁都救不回来。

# 1. 建环境,Python 版本按表来 conda create -n glm python=3.10 -y conda activate glm # 2. 先单独装 PyTorch,索引换成和你驱动匹配的那个 cu118 / cu121 pip install torch==2.0.1 torchvision==0.15.2 \ --index-url https://download.pytorch.org/whl/cu118 # 3. 验证显卡可见,必须是 True python -c "import torch; print(torch.cuda.is_available(), torch.cuda.device_count())" # 4. 再装其余依赖,镜像源换成你手头能用的那个 pip config set global.index-url https://<你的镜像源>/simple pip install -r requirements.txt pip check

第 2 步和第 4 步的顺序不能反。我见过最典型的翻车是:requirements.txt里写了torch==2.0.1,pip install -r的时候把前面装好的 cu118 轮子覆盖成了默认包,is_available()直接变False,然后开始怀疑显卡坏了。所以 torch 永远单独装,依赖文件里只留一行注释说明。

# requirements.txt —— torch 单独装,不要写在这里,会被覆盖 transformers==4.33.2 tokenizers==0.13.3 sentencepiece==0.1.99 accelerate==0.21.0 streamlit==1.28.0 sentence-transformers==2.2.2

版本全部用==锁死,不要用>=。这类项目的依赖链条很长,transformers 一个小版本升级就可能导致权重里的自定义类加载失败,报的还不是「版本不对」,而是AttributeError或KeyError,排查起来很绕。pip check是用来确认依赖之间没有互相打架的,输出No broken requirements found才算这一步过关。

3.2 写一个最小可跑的加载脚本

先别急着做界面,用二十行代码确认模型能加载、能出话,这是后面所有工作的地基。

# /opt/qa/load_test.py from transformers import AutoTokenizer, AutoModel MODEL_DIR = "/data/models/chatglm-6b" tokenizer = AutoTokenizer.from_pretrained( MODEL_DIR, trust_remote_code=True # 权重目录自带 modeling/tokenization 实现,必须允许加载 ) model = AutoModel.from_pretrained( MODEL_DIR, trust_remote_code=True ).quantize(4).cuda() # 4bit 量化后再推到显卡,8G 显存靠这一步活下来 model = model.eval() # 关掉 dropout,推理必须,忘了会让输出飘 resp, history = model.chat(tokenizer, "用一句话解释什么是向量检索", history=[]) print(resp) print(history) # history 是 (问, 答) 元组列表,多轮对话要用

几个参数单独解释。trust_remote_code=True是这套权重的工作方式,它会在你本地执行模型目录里的 Python 文件,所以权重来源要自己确认清楚,别随便从不明渠道拿一份。quantize(4)是量化开关,取 4 或 8,只能在显卡上用,不写就是 FP16,显存需求直接翻倍。.cuda()指定单卡,多卡场景要在from_pretrained里改用设备映射,两者的写法不一样,别混着抄。

model.chat()返回两个值:回答文本和更新后的历史。历史必须由你自己保存并回传,模型本身是无状态的,这一点在写多轮界面的时候非常关键——忘了维护 history,用户会发现在第三轮开始模型就「失忆」了。

跑通这段,你会看到首次推理明显偏慢,几秒到几十秒都有可能,因为权重在从磁盘往显存搬。第二次开始就快了。这个现象后面还会提到,别把它当成故障。

3.3 量化参数怎么选:FP16、INT8 与 INT4 的显存账

同一个模型,三种精度,体验差别比想象中大。我按实测给个对照,你把显存数字套进去选。

精度显存占用(6B 量级)生成质量适用场景
FP1613GB 以上最好24G 显卡,人少、追求质量
INT88GB 左右接近 FP1612G~16G 显卡,小团队内部用
INT45~6GB略有损失,长回答偶有重复8G 显卡、试验机、演示环境
CPU(INT4)内存 16GB 以上与 INT4 相当没有显卡时保底,速度按字计

选完精度,生成参数也得跟着调,否则用户的第一印象会很差。

# 生成参数,按场景微调 resp, history = model.chat( tokenizer, "把下面这段运维记录整理成三条要点:……", history=[], max_length=1024, # 输入+输出的总长度上限,超过会被截断 top_p=0.7, # 采样范围,0.7 比 0.9 保守,内部问答更合适 temperature=0.3, # 温度越低越贴资料,做知识库问答就压低 repetition_penalty=1.1 # 抑制重复,INT4 下建议稍微调高一点 )

max_length是最容易被忽略的一项。给大了显存吃紧,给小了回答被拦腰截断,用户看到半句话会以为服务挂了。内部问答我一般设 1024,配合知识库检索时把上下文控制在 1200 字以内,剩下的留给输出。

还有个小细节:如果你用的是带微调权重的版本,路径和加载方式会有细微差别,别把两个目录的配置混着拷。装完之后可以把整个环境的依赖导出成快照,换机器时直接复现,省得重来一遍。

# 环境快照,换机器或回滚时用 conda list --export > /opt/qa/conda-glm.txt pip freeze > /opt/qa/pip-glm.txt

4. 问答界面与私有知识库:把模型变成能用的助理

命令行里能出话,离「助理」还差两件事:一个能点开就用的页面,以及它真的知道你们内部的资料。这一章把界面、检索和常驻服务串起来,做完之后同事只需要一个内网地址,不用登录服务器。

4.1 用 Streamlit 起一个内网问答页

界面层我选 Streamlit,原因是它不需要前端工作量,几十行就是一个像样的对话页。

# /opt/qa/app.py import streamlit as st from transformers import AutoTokenizer, AutoModel MODEL_DIR = "/data/models/chatglm-6b" @st.cache_resource # 整个进程只加载一次,避免每次对话重新搬 12GB 权重 def load_model(): tok = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True) m = AutoModel.from_pretrained(MODEL_DIR, trust_remote_code=True).quantize(4).cuda() return tok, m.eval() tokenizer, model = load_model() if "history" not in st.session_state: st.session_state.history = [] # 多轮上下文放会话里,刷新页面即重置 st.title("内部问答助理") question = st.chat_input("问点什么") if question: resp, _ = model.chat(tokenizer, question, history=st.session_state.history) st.session_state.history.append((question, resp)) for q, a in st.session_state.history: st.chat_message("user").write(q) st.chat_message("assistant").write(a)

@st.cache_resource是这一页最关键的一行。Streamlit 的模型是「每次交互重新执行脚本」,不加这个装饰器,用户每说一句话你就要重新加载一遍权重,一分钟都出不来话。session_state则是用来放多轮历史的,和上面说的history回传对应上。

启动命令行直接带上监听参数,不要写在代码里,方便后面交给服务管理:

streamlit run app.py --server.port 8501 --server.address 0.0.0.0

--server.address 0.0.0.0是让内网其他机器能访问。这一项写成127.0.0.1,你自己 curl 得通,同事一律打不开,这个坑我第一次就踩了。

4.2 接入本地文档:切片、向量化与检索拼接

模型本身不知道你们的制度、接口文档和会议纪要。最省事的做法不是微调,而是「先检索、再拼接」:把文档切块、算向量,问题来了取最相关的几块塞进提示词。

# /opt/qa/rag.py import glob from sentence_transformers import SentenceTransformer, util encoder = SentenceTransformer("/data/models/bge-small-zh") # 中文小模型,CPU 也能跑 CHUNKS = [] for f in glob.glob("/opt/qa/docs/*.md"): text = open(f, encoding="utf-8").read() for i in range(0, len(text), 400): CHUNKS.append(text[i:i + 480]) # 每块 480 字,块间重叠 80 字防答案被切断 VEC = encoder.encode(CHUNKS, normalize_embeddings=True) def build_prompt(question, topk=3, budget=1200): qv = encoder.encode(question, normalize_embeddings=True) hits = util.cos_sim(qv, VEC)[0].topk(topk).indices.tolist() ctx = "\n".join(CHUNKS[i] for i in hits)[:budget] # 6B 上下文只有 2K,必须截断 return ( f"已知资料:\n{ctx}\n\n" f"问题:{question}\n" "只根据已知资料回答;资料里没有的,直接回答“资料中没有相关信息”。" )

三个参数决定效果:400/480是切块粒度,太小语义不完整,太大检索不准还挤爆上下文;topk=3是取几块,文档几十份以内 3 块够用,多了反而稀释重点;budget=1200是硬截断,因为 6B 的上下文窗口只有 2K 左右,留给回答的空间必须提前留出来。

最后那句「资料中没有相关信息」不是客套话,是防止模型自由发挥的关键。私有化场景里,一个编造出来的制度条款比答不出来危险得多。

文档量真正上去(几百份以上)再考虑上向量库,几十份文件用这套内存里的写法完全够,还省掉一个组件。切片逻辑最好做成脚本定期重建索引,文档更新后重启一次服务即可。

4.3 用 systemd 常驻并把端口收进统一入口

命令行启动的进程,SSH 一断就没了。用 systemd 托管,顺便把环境变量和重启策略一起管起来。

# /etc/systemd/system/chatglm-qa.service [Unit] Description=chatglm-qa After=network-online.target [Service] Type=simple User=qa WorkingDirectory=/opt/qa Environment="CUDA_VISIBLE_DEVICES=0" Environment="HF_HOME=/data/hf_cache" Environment="TRANSFORMERS_OFFLINE=1" ExecStart=/opt/conda/envs/glm/bin/python -m streamlit run app.py --server.port 8501 --server.address 0.0.0.0 Restart=on-failure RestartSec=10 StandardOutput=append:/var/log/qa/app.log StandardError=append:/var/log/qa/app.err [Install] WantedBy=multi-user.target

几处要点:ExecStart里用 conda 环境的绝对路径,不要依赖 shell 里的conda activate,systemd 不认;CUDA_VISIBLE_DEVICES=0把这台机器的显卡独占给它,避免别人跑训练的时候你的服务被挤爆;Restart=on-failure加RestartSec=10让它在异常退出后自己起来,但别设 0 秒,否则模型加载失败会变成无限重启,日志被刷满。

sudo systemctl daemon-reload sudo systemctl enable --now chatglm-qa systemctl status chatglm-qa --no-pager journalctl -u chatglm-qa -n 100 --no-pager # 起不来先看这里 ss -lntp | grep 8501 # 端口确认在听

Streamlit 的界面配置建议单独放一份,别全部塞在命令行里:

# /opt/qa/.streamlit/config.toml [server] address = "0.0.0.0" port = 8501 enableXsrfProtection = true # 内网自用保持开启;只有当页面被嵌进内部门户的 iframe 才考虑关 [browser] gatherUsageStats = false # 内部部署关掉上报

enableXsrfProtection这一项值得记一笔。默认开着是对的,但如果你把它嵌到别的系统里,会表现为「点按钮没反应」或者整页空白,很多人第一反应是模型挂了,其实是这个开关。

5. 排查与避坑:六个把我按在键盘前的现场

这套环境装下来,报错基本集中在三个方向。下面按「现象 → 原因 → 解决」记,都是我自己撞过的。

5.1 显卡与显存:三条最容易误判的报错

现象一:torch.cuda.is_available()返回False,但nvidia-smi一切正常。原因:装的是不带 CUDA 的轮子,或者中途被别的pip install覆盖过。解决:pip uninstall torch torchvision卸干净,按第 3 章的索引地址重装,装完立刻验证,验证通过前不要继续装其他依赖。

现象二:torch.cuda.OutOfMemoryError: CUDA out of memory。原因:默认 FP16 要 13GB 以上,8G 卡必然爆;或者max_length给太大,或者有别人在同一张卡上占了显存。解决:先nvidia-smi看是谁在占,再给加载加quantize(4),把max_length降到 1024,用CUDA_VISIBLE_DEVICES独占卡。这三招按顺序上,通常第一招就够了。

现象三:第一次提问要等半分钟,后面就快了,看起来像时好时坏。原因:不是故障。首次推理要把权重从磁盘搬到显存,还要做一次性的算子初始化。解决:服务起来之后先发一条空请求预热,把等待转移给启动阶段。我一般在 systemd 启动后补一条curl打一下页面,让权重提前加载。

5.2 依赖与权重:版本对不上时的四个信号

现象四:加载权重时报AttributeError: 'ChatGLMForConditionalGeneration' object has no attribute 'quantize',或者KeyError: 'chatglm'。原因:transformers 版本和权重不匹配,高了接口变了,低了不认识自定义类。解决:按对照表锁版本,pip install transformers==4.33.2,不要用 latest。这类报错文字从来不会告诉你「版本错了」,只能靠锁版本排除。

现象五:卡在Loading checkpoint shards很久,日志里反复出现重试。原因:目录里的权重没备齐,或者没设离线模式,加载时在尝试联网校验。解决:清点pytorch_model-*.bin的份数与config.json里的分片数是否一致,设TRANSFORMERS_OFFLINE=1,路径一律用绝对路径。

现象六:回答重复、绕圈、或者突然断在半句。原因:max_length太小导致截断,或者temperature太高、repetition_penalty偏低,INT4 下尤其明显。解决:max_length抬到 1024,temperature压到 0.3,repetition_penalty提到 1.1,再观察长回答是否收敛。

5.3 服务与入口:页面打不开别先怀疑模型

现象七:systemctl status显示activating (auto-restart),一直在重启。原因:ExecStart用了相对路径或者python而不是 conda 环境的完整路径,systemd 找不到解释器;也可能是User=qa对模型目录没有读权限。解决:改成绝对路径,sudo -u qa ls /data/models/chatglm-6b验证一次读权限,再看journalctl -u chatglm-qa -n 100的具体报错。这个黑匣子日志比任何猜测都准。

现象八:服务器上curl 127.0.0.1:8501通,同事的电脑打不开。原因:监听地址写成了127.0.0.1,或者系统防火墙没放行 8501。解决:ss -lntp | grep 8501看监听地址,改成0.0.0.0;防火墙侧单独放行这个端口,别把整个网段开出去。这类问题排查顺序永远是:进程 → 端口 → 监听地址 → 防火墙,从内往外一层层来。

6. 装完怎么验证:四条命令确认这套问答助理真的通了

装完不等于跑通。我的验收分四步,从底层往上层走,任何一步不过就停在原地,不要往下看界面。

# 1. 显存里真的有进程在占,而不是回退到 CPU nvidia-smi --query-compute-apps=pid,used_memory --format=csv # 2. 端口在听,且监听地址是 0.0.0.0 而不是 127.0.0.1 ss -lntp | grep 8501 # 3. 页面本身活着,返回 200 curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8501/healthz # 4. 服务近半小时没有异常重启 systemctl show chatglm-qa -p NRestarts

第 1 步的输出里如果没有进程,说明模型加载失败但服务还在,属于最危险的状态——页面能打开,提问永远转圈。第 4 步的NRestarts如果是个不断增长的数字,回去看journalctl,别在界面上找原因。

再补一个端到端的冒烟脚本,它检查的不是「有没有回答」,而是「回答对不对」:

# /opt/qa/smoke_test.py from rag import build_prompt from transformers import AutoTokenizer, AutoModel tokenizer = AutoTokenizer.from_pretrained("/data/models/chatglm-6b", trust_remote_code=True) model = AutoModel.from_pretrained("/data/models/chatglm-6b", trust_remote_code=True).quantize(4).cuda().eval() # 第一问必须命中内部文档里的内容 resp, _ = model.chat(tokenizer, build_prompt("值班交接需要记录哪几项"), history=[], temperature=0.3) assert "交接" in resp, f"未命中知识库:{resp[:80]}" # 第二问超出文档范围,必须拒答而不是编造 resp2, _ = model.chat(tokenizer, build_prompt("明年预算批了多少"), history=[], temperature=0.3) assert "没有" in resp2 or "未" in resp2, f"存在编造风险:{resp2[:80]}" print("smoke ok")

两段断言对应两件事:知识库检索链路通不通,以及模型的拒答边界还在不在。第二问尤其重要,很多人只测「能不能答」,不测「会不会瞎答」,上线之后第一个出问题的就是这里。

最后的验收清单,我一般抄在工单里:

检查项命令/方式通过标准
显卡占用nvidia-smi有进程、显存占用接近所选精度预期
端口监听ss -lntp8501 监听在 0.0.0.0
页面存活curl /healthz返回 200
服务健壮性systemctl showNRestarts不再增长
知识命中冒烟脚本第一问断言通过
拒答边界冒烟脚本第二问断言通过

从那以后我每装一台新机器,都强制走一遍这套四步验收加两段断言,宁可多花十分钟,也不想在演示前十分钟才发现模型其实一直在 CPU 上跑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询