简介:面向具备 Python 基础、希望入门私有化知识库问答的开发者与机器学习学习者,这份示例项目围绕 Gradio 交互界面、FAISS 向量检索与句向量匹配,演示从文件上传、文档切分到知识库查询与回答生成的完整链路,可用于本地环境中的智能助理原型验证、课程设计或技术预研。压缩包共 18 个文件、约 336KB,包含 py 源码与运行脚本、txt 说明与配置、yaml 参数文件、whl 依赖包,以及 docx、xlsx、pdf 等示例文档和 pem 证书,便于对照目录理解工程结构与部署配置。已有 140 人浏览学习。读者可据此掌握私有化问答系统的基本组件、依赖安装思路、前后端交互方式与文档检索流程,并了解本地部署在数据隐私与可访问性上的优势,在此基础上替换行业资料、调整模型与检索参数,较快搭建可演示的本地智能问答助理。
1. 部署前先想清楚:这套私有化智能问答助理,为什么值得先在本地跑通
上周帮一位开发者处理一个部署失败的示例项目:机器有显卡,pip install也一路没报错,但from_pretrained到 60% 就崩了,日志里只有一行不常见的 OOM。本地部署私有化智能问答助理,翻车点几乎从不在“会不会装”,而在环境没对齐——ChatGLM 这类模型对显存、CUDA、transformers 版本三者很敏感,差一个版本组合,表现就是加载到一半退出、答非所问、首字延迟几十秒。这份示例项目把模型加载、接口封装、页面交互串成一条最小可用链路,适合手里有 8G 以上显存、想把问答能力放在自己机器上跑、不希望问答数据离开内网的开发者。下面按我实际装一遍的顺序写,能抄,也能看到哪里该改。
2. 环境与版本选型:显卡、Python 与 ChatGLM 权重怎么配
装之前先把机器的情况写清楚,比装到一半再回头查省事得多。这一步很多人跳过,直接进 conda,然后因为驱动版本和 torch 对不上,反复卸了装、装了卸。我的习惯是先列一张本地清单:显卡型号与显存、驱动版本、系统盘剩余空间、是否有外网访问能力。前三个决定你能跑哪个规格的权重,第四个决定依赖怎么拉。
2.1 显存与模型规格:先算清楚再下载权重
ChatGLM 系列 6B 规模的权重,是这套示例项目的默认底座。它的显存占用和加载精度强相关,粗略对应关系如下,这是我装了几次之后记下来的经验值,实际会随上下文长度和批大小浮动:
| 加载方式 | 权重显存占用 | 建议单卡显存 | 适用场景 |
|---|---|---|---|
| FP16 全精度 | 约 13GB | 16GB 及以上 | 追求回答质量,上下文较长 |
| INT8 量化 | 约 8~9GB | 12GB | 单机私有部署的主流选择 |
| INT4 量化 | 约 5~6GB | 8GB | 显卡紧张、只做轻量问答 |
| CPU 推理 | 依赖内存,约 12GB 起 | 无显卡 | 仅做验证,速度很慢 |
把这张表和自己机器对上,基本就能决定后面from_pretrained里写不写load_in_8bit或load_in_4bit。常见的错误是拿着 8G 卡直接按官方示例的 FP16 跑,加载到一半显存爆掉,日志还埋在几十行 warning 下面,看起来像“模型坏了”,其实是精度没降。我一般先用 INT4 把整条链路跑通,确认接口、页面都正常,再回头换精度对比效果。先验证链路、再谈质量,这个顺序能省掉大量返工。
另外要分清 6B 的几代权重:初代 6B 的上下文长度是 2K,2 代、3 代往上才扩到更长。示例项目里的问答场景如果只是单轮短问短答,初代就够;如果要做文档摘要素材的连续追问,直接上 2 代或 3 代,能少很多“答到一半被截断”的困惑。权重目录里一般会有config.json和一份说明,max_length的默认值写在那里,装之前翻一眼,比事后猜强。
磁盘也得提前规划。一个 6B 权重下来十几 GB,加上 pip 缓存、conda 环境、日志,系统盘剩个十几 G 很容易被吃干净。我通常把权重放在数据盘单独目录,环境变量里指过去,不放在用户主目录下。
# 查看显卡与驱动,确认 CUDA 驱动版本上限 nvidia-smi # 看系统盘和数据盘剩余空间,权重别放系统盘 df -h # 规划目录:模型、日志、代码分开,迁移时好打包 mkdir -p /data/models /data/logs /data/appsnvidia-smi右上角那行CUDA Version是驱动支持的最高运行时版本,不是本机装好的版本,这个区别后面定 torch 版本时要用到。df -h主要看权重落盘的位置够不够,/data这类单独挂载的盘优先。目录分开是血泪经验:模型权重和代码混在一个目录,迁移机器时打包几十 GB,很痛苦。
2.2 Python 与依赖矩阵:3.8 还是 3.10 的取舍
Python 版本这块,我的默认选择是 3.10。原因不复杂:新版 PyTorch 对 3.10 支持完整,很多依赖的 wheel 也都覆盖到 3.10;3.8 虽然兼容性最广,但部分新版本依赖已经开始放弃它,装到一半提示“没有匹配的分发版本”大多就是这个原因。3.11 及以上反而要小心,个别与量化相关的扩展编译不过去。
版本矩阵我按这个思路定,写出来供对照:
| 组件 | 我用的版本 | 说明 |
|---|---|---|
| Python | 3.10 | conda 单独环境,不污染系统 Python |
| PyTorch | 2.0.x | 与 CUDA 11.7/11.8 搭配验证较多 |
| CUDA 运行时 | 11.7 或 11.8 | 不高于nvidia-smi显示的上限 |
| transformers | 4.30 ~ 4.35 | 2 代权重偏 4.30,3 代要更高 |
| 量化相关扩展 | 按需 | 用 8bit/4bit 才装 |
这里最容易出错的是 transformers 版本。初代 6B 在 4.27 附近比较稳,2 代权重如果配了偏旧的版本,会报找不到模型类;3 代配旧版本同理。判断办法很直接:加载报KeyError或cannot import name之类,先怀疑版本不匹配,再去看权重目录里的说明。我一般会在环境装好后先python -c "import transformers; print(transformers.__version__)"确认一遍,避免装完忘了。
还有一个隐性依赖是protobuf和sentencepiece这类,版本冲突出现在装完某个库之后,原本能跑的推理突然报错。常见做法是把关键版本写进requirements.txt锁死,别用全局最新。
# requirements.txt,锁版本,避免“今天能跑明天不能” torch==2.0.1 transformers==4.30.2 sentencepiece==0.1.99 protobuf==3.20.3 accelerate==0.20.3 fastapi==0.100.0 uvicorn==0.22.0 gradio==3.39.0这份清单里每个版本都不是随便填的:transformers与accelerate之间有大版本耦合,accelerate太新会让老的加载代码走不同分支;gradio4.x 改了参数名,示例项目里用 3.x 写法的话,页面会直接起不来。锁版本这件事,看起来保守,实际是给未来的自己留后悔药。
2.3 权重目录与磁盘布局:别把模型塞进系统盘
权重目录的结构要固定下来,因为代码里要引用它。我习惯这么放:
/data/models/ └── chatglm-6b/ ├── config.json ├── tokenizer.model ├── pytorch_model-00001-of-00002.bin └── pytorch_model-00002-of-00002.bin多分片权重必须放同一目录,缺一片加载会报找不到文件,但报错信息往往只提最后一个分片名字,容易误以为下载不全就去重下。实际做法是先核对分片总数和pytorch_model.bin.index.json里的记录是否一致。
下载权重时能用断点续传的方式就用,十几个 GB 传到 90% 断了重来很折磨。拉完先做一次文件完整性检查,比对文件大小是否为 0,这一步比事后排查“模型加载异常”快得多。
3. 安装依赖:conda 环境、PyTorch 与 CUDA 的落地步骤
环境选型定完,剩下的是执行。这一章按我实际敲命令的顺序写,每步都有它的道理,不跳步。
3.1 conda 建环境与 PyTorch 安装
先用 conda 建一个干净环境,别在 base 里装。原因很实际:这类项目的依赖会拉进来一堆科学计算包,和系统里其他项目冲突之后很难摘干净,最后只能整套重装。
# 建独立环境,Python 固定 3.10 conda create -n qa-assistant python=3.10 -y conda activate qa-assistant # 确认当前环境,避免装到 base which pythonwhich python这一步看着多余,但换过 terminal 窗口之后忘了激活环境的情况太常见了,装完发现包不在预期位置。确认路径里带qa-assistant再往下走。
接着装 PyTorch。这里的关键是cu117还是cu118要与驱动上限对得上,同时不要用pip install torch这种不指定来源的写法,容易拉到 CPU 版本。CPU 版装完,代码跑起来不报错,但推理慢到怀疑人生,而且torch.cuda.is_available()返回 False。
# 明确指定 CUDA 版本,避免装成 CPU 版 pip install torch==2.0.1 torchvision==0.15.2 --index-url <与 cu117/cu118 对应的源> # 装完立刻验证 CUDA 是否可用 python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"输出里True和显卡型号都在,才算这步过了。如果is_available()是 False,先看驱动版本是否低于运行时要求,再看装的是不是 CPU wheel,最后看环境里有没有多个 torch 版本互相覆盖。不要急着改代码,问题一定在环境层。
3.2 装 transformers 等依赖并锁版本
PyTorch 就位后,按前面那份锁定的清单装其余依赖。我一般一次装完,装完再统一验证导入。
pip install -r requirements.txt # 逐个验证关键包能否导入,早发现问题 python -c "import transformers, accelerate, sentencepiece, fastapi, gradio; print('deps ok')"deps ok打出来只是一半,还要看有没有 warning 提示版本不兼容。有些 warning 当下不影响运行,但会让模型走不同分支,后面表现异常时不好定位,所以我在这一步会把明显的版本告警处理掉。
量化方案如果要用,还需要装对应的扩展包,装完一定要跑一次真实加载,因为这类扩展的编译错误常常在 import 时才暴露。装不上的情况我遇到过两种:一是编译工具链缺失,二是 Python 版本过新没有预编译包。前者的处理是补编译环境,后者直接退回 3.10。
3.3 半精度加载与一次最小推理脚本
依赖装完,先不碰接口和页面,用最短的脚本验证“模型能加载、能回答”。这一步是整个部署的分水岭,跑通了后面都是包装,跑不通就先别往下走。
# min_infer.py:最小可跑通的推理脚本 from transformers import AutoTokenizer, AutoModel MODEL_DIR = "/data/models/chatglm-6b" # trust_remote_code 是这类权重必须的,模型结构定义随权重下发 tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True) # 显存紧张时改成 load_in_4bit=True;12G 卡建议 8bit model = AutoModel.from_pretrained( MODEL_DIR, trust_remote_code=True, device_map="auto", # 单卡会自动放 cuda:0 load_in_8bit=True, # 量化开关,按显存选 ) model = model.eval() resp, history = model.chat(tokenizer, "用一句话说明什么是机器学习", history=[]) print(resp)几个参数的取舍说一下。trust_remote_code=True必须开,这类权重的模型类不在 transformers 主库里,关掉会直接报找不到。device_map="auto"在单卡机器上等价于放到 0 号卡,多卡时才体现价值。load_in_8bit是显存和质量的折中,8G 卡换 4bit,16G 以上可以关掉走 FP16 对比效果。model.eval()不能漏,推理模式下才会关掉 dropout,否则同一问题两次回答不一样,会让人误判成模型不稳定。
跑这个脚本时用nvidia-smi -l 1另开一个窗口盯着显存,能看到加载峰值和推理时的常驻占用,对自己机器的余量心里有数。第一次跑如果卡在加载阶段很久,多半是权重在从磁盘读,机械盘会明显拖慢,这不算故障。
4. 服务化配置:把本地模型包装成可调用的问答接口
最小脚本跑通只说明模型活着,离“问答助理”还差一步:别人得能用。这一章把模型包成 HTTP 接口和一个本地页面,方便内网其他系统对接,也方便自己测试多轮对话。
4.1 FastAPI 包一层问答接口
接口层我选 FastAPI,原因是它自带参数校验和文档页,联调时省事。核心是把模型在启动时加载一次,别放在请求处理里,否则每个请求都重新加载权重,显存直接崩。
# app.py:把模型挂成 HTTP 服务 from fastapi import FastAPI from pydantic import BaseModel from transformers import AutoTokenizer, AutoModel MODEL_DIR = "/data/models/chatglm-6b" app = FastAPI() tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True) model = AutoModel.from_pretrained( MODEL_DIR, trust_remote_code=True, device_map="auto", load_in_8bit=True ).eval() class AskBody(BaseModel): question: str history: list = [] # 多轮历史由调用方回传 @app.post("/ask") def ask(body: AskBody): # max_length 控制生成长度,过长会拖慢首字延迟 resp, new_history = model.chat( tokenizer, body.question, history=body.history, max_length=512 ) return {"answer": resp, "history": new_history}history交给调用方维护,而不是在服务端存会话,好处是无状态、可水平扩,坏处是调用方要自己带上下文,这一点在对接文档里必须写清楚。max_length是生成长度上限,设太大不只是慢,还会让回答跑题收不住。启动命令用uvicorn app:app --host 0.0.0.0 --port 8000,端口按内网规划改。
启动后第一件事是打一次接口,确认返回结构对得上。如果返回 500,先看服务端日志里的堆栈,通常是参数类型不对或者加载阶段就失败了。
4.2 Gradio 做本地交互页面
接口有了,再加一个页面,自己和同事验证效果时不用写脚本。Gradio 起一个带历史记录的对话界面,十来行就够。
# ui.py:本地对话页面 import gradio as gr from transformers import AutoTokenizer, AutoModel MODEL_DIR = "/data/models/chatglm-6b" tokenizer = AutoTokenizer.from_pretrained(MODEL_DIR, trust_remote_code=True) model = AutoModel.from_pretrained( MODEL_DIR, trust_remote_code=True, device_map="auto", load_in_8bit=True ).eval() def chat_fn(message, history): # history 是 gradio 传入的 [(问, 答), ...] 结构 resp, _ = model.chat(tokenizer, message, history=[]) return resp gr.ChatInterface(chat_fn).launch(server_name="0.0.0.0", server_port=7860)注意这里我把历史传了空列表,因为页面每次只做单轮验证;要做多轮得把history转成模型要的格式,转换的写法各代权重略有差别,照权重目录里的示例改最稳。server_name设成0.0.0.0才能被内网其他机器访问,默认只监听本机。
4.3 参数配置表:几个真正影响体验的开关
下面这几个参数是部署完最常被问到的,列出来对照着调:
| 参数 | 常用取值 | 影响 |
|---|---|---|
load_in_8bit/load_in_4bit | 按显存二选一 | 显存换质量,别两个都开 |
max_length | 256 ~ 1024 | 越大越慢,回答越长越容易跑题 |
top_p | 0.7 ~ 0.9 | 越低越保守,越高越发散 |
temperature | 0.1 ~ 0.9 | 问答类偏 0.1~0.5,创意类调高 |
device_map | auto | 多卡时自动分配,单卡无感 |
temperature这块有个实际问题:设成 0 不是“完全确定”,采样实现上仍有细微随机性,别拿它做严格一致的断言。私有化问答场景我一般给 0.2 左右,能稳住格式又不至于像在复读。
5. 避坑与排查:私有问答助理部署最容易翻车的五个地方
5.1 加载到一半显存溢出,日志只留一行 OOM
现象是from_pretrained跑到一半进程被杀,终端只剩Killed或者一段 CUDA out of memory。原因是按 FP16 加载,显存峰值超了卡的上限,而系统 OOM killer 直接把进程干掉,Python 层来不及打完整堆栈。解决是先降精度,8G 卡改 4bit、12G 卡改 8bit,同时用nvidia-smi -l 1盯着加载过程,确认峰值落在哪。如果降精度后仍然爆,检查是不是同一台机器上还有别的进程占着显存。
5.2 报找不到模型类,明明目录里文件都在
现象是KeyError或者提示模型类型未注册,权重文件却完整。原因是 transformers 版本与权重代次不匹配,或者trust_remote_code=True没写。解决是先打版本号,再对着权重目录里的说明核对,容器环境里尤其容易装成系统里那份旧版本。这类问题不看日志只看代码是找不到的,报错行往往指向 transformers 内部,跟自己的代码没关系。
5.3 服务起来了,但第一次请求等了几十秒
现象是接口能通,首个请求特别慢,之后就正常。原因是模型在第一次请求时才真正把权重搬上卡,或者被放在了请求处理函数里加载。解决是把加载提到模块级、服务启动时完成,再加一个健康检查接口预热一次推理。私有化部署里这个细节很影响体验,前端会以为服务挂了。
5.4 多轮对话越聊越慢,最后直接超时
现象是单轮正常,连续追问几轮后响应时间线性增长。原因是history无限制累积,上下文越来越长,注意力计算量跟着涨。解决办法是限制历史轮数,超过阈值就丢弃最早的一轮,或者在提示里做摘要压缩。常见做法是保留最近 3~5 轮,这个数值按你的问句长度定。
5.5 换了台机器,同样的代码跑不起来
现象是开发机正常,目标机器报驱动或 CUDA 相关错误。原因是环境靠手工装的,版本没记录,目标机器的驱动版本更低。解决办法是把requirements.txt和环境创建命令一起进版本库,部署时按文件重建,不用“复制一遍环境”的方式迁移。我自己吃过这个亏,后来所有涉及 CUDA 的项目都强制留一份环境快照。
6. 装完怎么验证跑通:三个层面的检查顺序
验证这件事,我按从下往上三层走,任何一层不过就别看下一层,否则问题会被上层现象掩盖。
第一层是环境层,只看三个返回值:
# 驱动与 CUDA 运行时是否匹配 nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv # PyTorch 是否真的能用上卡 python -c "import torch; print(torch.cuda.is_available(), torch.cuda.device_count())" # 关键依赖版本是否与锁定的清单一致 pip freeze | grep -Ei 'torch|transformers|accelerate|gradio'三个输出对上预期,环境层就算过了。torch.cuda.is_available()是 False 的情况下继续往下走没有意义,一定先回到第 3 章。
第二层是模型层,用固定问题跑两次,看回答是否稳定、显存占用是否在预期区间。我会记录三个数值:加载耗时、单次回答耗时、推理时常驻显存。这三个数和机器规格对得上,说明模型本身没问题。
| 检查项 | 预期表现 | 不合格时的方向 |
|---|---|---|
| 加载耗时 | 与磁盘类型相关,SSD 明显更快 | 权重是否在机械盘、是否有分片缺失 |
| 单次回答耗时 | 十几秒以内(6B、量化、短问) | 精度是否过高、max_length是否太大 |
| 常驻显存 | 与 2.1 的表格接近 | 量化没生效、有其他进程占卡 |
| 回答内容 | 切题、不重复、不截断 | max_length偏小、temperature过高 |
第三层是服务层,走一遍真实调用:先curl打接口,再用页面问三个不同类型的问题——事实类、需要连续追问的、需要长回答的。三类都正常,这套私有化问答助理才算真正落地。
# 接口连通性与返回结构检查 curl -X POST http://127.0.0.1:8000/ask \ -H "Content-Type: application/json" \ -d '{"question":"用一句话说明什么是人工智能","history":[]}'返回体里answer不为空、history结构完整,接口就算通。如果超时但服务日志显示推理完成,多半是客户端超时设置太短,把max_length和超时时间一起看。
从那以后我每次部署这类模型,都强制先跑一遍最小推理脚本再碰接口,这一步不过就绝不往下包装,省下来的排查时间比装环境本身还多。希望帮到你。
本文还有配套的精品资源,点击获取