内网用 ChatGLM3 做文档问答,纠结模型权重和向量库放一台还是两台,通常先看两边的瓶颈在哪:推理侧卡的是显卡显存和算力,检索侧卡的是内存容量和磁盘。两台机器分工后,换模型不动索引、重建索引不动模型,停机范围小;但多一跳内网调用,就多一处需要做超时和失败兜底。文档量不大、显存也宽裕时,合一部署能跑,只是扩容和替换会互相牵制,要提前想清楚。
判断可以简化成三句:推理机器看显存和算力,检索机器看内存和磁盘 IO,两台之间只走内网 HTTP。显存能装下 ChatGLM3 及其上下文、切片量也不大时,合一部署可行;一旦要换更大模型或重建索引,分开的停机范围更小。无论合一还是分开,都建议先确认离线依赖与模型文件已落到目标机器,并用一次本地调用验证通路,再去接业务侧。
先定哪台机器跑模型、哪台存向量
分工不需要一开始就做得很细,按三条依据先判断就够了。
- 推理侧看显存与算力。ChatGLM3 权重加载后要常驻显存,还要留出上下文长度对应的 KV 空间。用
nvidia-smi看空闲显存,用加载脚本能否成功跑通一轮问答来确认,不能只看型号。显存不宽裕时,可以考虑半精度加载或更短的上下文上限。 - 检索侧看内存与磁盘。向量索引是否全量驻内存、原始切片要占多少磁盘,决定了检索机器的配置。用
free -g看可用内存,用df -h看数据盘余量,索引文件目录和原始文档目录建议单独挂盘,方便单独扩容。 - 两者能否合一,看边界条件。如果显存加载模型后仍有余量、内存能放下索引、磁盘和显存都留了扩容空间,合一部署是可接受的;只要其中一项已经吃紧,或者后续要换更大的模型、重建索引频率较高,就建议按两台机器拆开。
资源核对表可以按这个顺序过一遍,每项都落到具体命令上:推理机器核对 nvidia-smi、驱动版本、模型目录大小;检索机器核对 free -g、df -h、索引目录文件数和索引版本号;两台都要核对能否互相 ping 通、目标端口是否可达。核对结果记在部署文档里,换机器时可以直接比对。
把模型服务暴露成内网HTTP接口
推理侧封装成一个 HTTP 服务,检索侧和业务侧都用同一个入口调用,后续换模型或换量化方式时,调用方不用改代码。服务只监听内网地址,例如 `--host` 10.0.0.21 `--port` 8000,不要图省事绑 0.0.0.0 再靠防火墙兜底,绑定错误的网卡会让排查方向完全跑偏。
请求体字段和返回字段可以先按下面这个骨架约定,字段名可替换成团队已有规范,关键是两边一致:
POST /v1/chat/completions
{
"question": "用户问题原文",
"context": ["检索片段1", "检索片段2"],
"max_tokens": 512,
"request_id": "调用方生成的追踪ID"
}
200 OK
{
"answer": "生成的回答",
"finish_reason": "stop | length | error",
"request_id": "与请求一致",
"elapsed_ms": 0
}
接口写完后先用一次本地调用验证通路,不要等业务侧接入才发现问题:
curl -s http://10.0.0.21:8000/health
curl -s -X POST http://10.0.0.21:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"question":"测试","context":[],"max_tokens":64,"request_id":"local-001"}'
健康检查能过、上面这条 curl 能返回 answer 字段,就算通路通了。如果返回里 request_id 和请求不一致,说明网关或中间层做了改写,需要先修掉再往上接。
检索那台机器只做检索
检索服务不参与生成,只负责把问题变成片段列表,这样召回和生成可以各自扩容、各自替换。输入输出字段建议固定下来:
POST /v1/retrieve
{ "query": "用户问题原文", "top_k": 5, "filters": {} }
200 OK
{
"hits": [
{ "chunk_id": "doc-000123#p7", "score": 0.0, "text": "片段内容" }
],
"index_version": "索引版本号"
}
top_k 建议在服务端设一个硬上限,超过上限就截断并在日志里记一条警告,避免业务侧传一个大值把上下文塞爆、把生成侧拖慢。chunk_id 用“文档ID#页码或块序号”这种可回溯的格式,方便人工核对命中片段是否对得上原文。index_version 每次重建索引时更新,排查“昨天答得对今天答错了”时能直接看出索引换没换。
断网环境下确认依赖已预置
离线环境的坑通常不在装不上,而在部署当天才发现少了一个包或少了模型文件。需要提前备齐的东西大致三类:Python 离线安装包(wheel 目录加 requirements 文件)、模型权重文件(含配置文件与分词器文件)、运行库(显卡驱动、CUDA 相关库、常见系统依赖如 libgomp 之类)。
建议在一台同架构、临时有源的机器上先把 requirements 装进一个干净的 venv,确认无冲突后再导出 wheel 目录带进内网;直接在内网机器上试装,失败了很难定位是包缺还是版本冲突。落到内网后逐项检查:
nvidia-smi
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
python -c "import transformers, sentence_transformers; print('ok')"
ls -l /data/models/chatglm3-6b/ | head
python -c "import os; print(os.path.isfile('/data/models/chatglm3-6b/config.json'))"
pip install `--no-index` `--find-links`=/data/wheels -r requirements.txt
ldd $(python -c "import torch, os; print(os.path.dirname(torch.__file__))")/lib/libtorch_cuda.so | grep 'not found'
上面每条命令都要有明确输出,grep 'not found' 输出为空才算通过。任何一条失败,先回到有源机器上补齐对应文件,不要在内网机器上反复重装。
加一层超时与失败返回
检索失败时让模型凭已有知识作答,是内网文档问答里最容易埋雷的地方——答案看着流畅,但和文档无关。建议在检索侧和生成侧的调用链上加一层约定,把三类情况分开处理。
- 检索超时。给检索调用设一个超时值,超过就按“检索未完成”处理,不再把空上下文交给模型。超时值可以先设得保守一些,再按实际响应情况调整。
- 返回为空。
hits为空数组时,直接返回固定话术,不进入生成阶段。日志里记下 query 和 index_version,方便判断是索引没建好还是问题确实没匹配到。 - 服务不可达。连接被拒或域名解析失败时,按“检索服务不可用”返回固定话术,同时触发健康检查,连续失败可以考虑让生成侧直接短路,避免请求堆积。
对外返回可以统一成这类结构,让业务侧能从 finish_reason 区分正常回答和降级回答:
{ "answer": "当前没能取到相关文档片段,请稍后重试或换一种问法。",
"finish_reason": "retrieve_failed",
"request_id": "local-002" }
日志建议每条至少记这些字段:request_id、阶段(retrieve 或 generate)、耗时、命中片段数、错误类型、目标地址。request_id 从入口一路带到返回里,排查时能把一次问答的两段日志串起来。这套约定只是兜底,超时值和话术都需要结合内网实际环境确认后再定稿。