企业内网用ChatGLM3做文档问答 / 模型权重和向量库分两台机器放

文章导读
内网用 ChatGLM3 做文档问答,纠结模型权重和向量库放一台还是两台,通常先看两边的瓶颈在哪:推理侧卡的是显卡显存和算力,检索侧卡的是内存容量和磁盘。两台机器分工后,换模型不动索引、重建索引不动模型,停机范围小;但多一跳内网调用,就多一处需要做超时和失败兜底。文档量不大、显存也宽裕时,合一部署能跑,只是扩容和替换会互相牵制,要提前想清楚。
📋 目录
  1. 先定哪台机器跑模型、哪台存向量
  2. 把模型服务暴露成内网HTTP接口
  3. 检索那台机器只做检索
  4. 断网环境下确认依赖已预置
  5. 加一层超时与失败返回
A A

内网用 ChatGLM3 做文档问答,纠结模型权重和向量库放一台还是两台,通常先看两边的瓶颈在哪:推理侧卡的是显卡显存和算力,检索侧卡的是内存容量和磁盘。两台机器分工后,换模型不动索引、重建索引不动模型,停机范围小;但多一跳内网调用,就多一处需要做超时和失败兜底。文档量不大、显存也宽裕时,合一部署能跑,只是扩容和替换会互相牵制,要提前想清楚。

判断可以简化成三句:推理机器看显存和算力,检索机器看内存和磁盘 IO,两台之间只走内网 HTTP。显存能装下 ChatGLM3 及其上下文、切片量也不大时,合一部署可行;一旦要换更大模型或重建索引,分开的停机范围更小。无论合一还是分开,都建议先确认离线依赖与模型文件已落到目标机器,并用一次本地调用验证通路,再去接业务侧。

先定哪台机器跑模型、哪台存向量

分工不需要一开始就做得很细,按三条依据先判断就够了。

  • 推理侧看显存与算力。ChatGLM3 权重加载后要常驻显存,还要留出上下文长度对应的 KV 空间。用 nvidia-smi 看空闲显存,用加载脚本能否成功跑通一轮问答来确认,不能只看型号。显存不宽裕时,可以考虑半精度加载或更短的上下文上限。
  • 检索侧看内存与磁盘。向量索引是否全量驻内存、原始切片要占多少磁盘,决定了检索机器的配置。用 free -g 看可用内存,用 df -h 看数据盘余量,索引文件目录和原始文档目录建议单独挂盘,方便单独扩容。
  • 两者能否合一,看边界条件。如果显存加载模型后仍有余量、内存能放下索引、磁盘和显存都留了扩容空间,合一部署是可接受的;只要其中一项已经吃紧,或者后续要换更大的模型、重建索引频率较高,就建议按两台机器拆开。

资源核对表可以按这个顺序过一遍,每项都落到具体命令上:推理机器核对 nvidia-smi、驱动版本、模型目录大小;检索机器核对 free -gdf -h、索引目录文件数和索引版本号;两台都要核对能否互相 ping 通、目标端口是否可达。核对结果记在部署文档里,换机器时可以直接比对。

把模型服务暴露成内网HTTP接口

推理侧封装成一个 HTTP 服务,检索侧和业务侧都用同一个入口调用,后续换模型或换量化方式时,调用方不用改代码。服务只监听内网地址,例如 `--host` 10.0.0.21 `--port` 8000,不要图省事绑 0.0.0.0 再靠防火墙兜底,绑定错误的网卡会让排查方向完全跑偏。

请求体字段和返回字段可以先按下面这个骨架约定,字段名可替换成团队已有规范,关键是两边一致:

企业内网用ChatGLM3做文档问答 / 模型权重和向量库分两台机器放
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 和请求不一致,说明网关或中间层做了改写,需要先修掉再往上接。

检索那台机器只做检索

检索服务不参与生成,只负责把问题变成片段列表,这样召回和生成可以各自扩容、各自替换。输入输出字段建议固定下来:

企业内网用ChatGLM3做文档问答 / 模型权重和向量库分两台机器放
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' 输出为空才算通过。任何一条失败,先回到有源机器上补齐对应文件,不要在内网机器上反复重装。

企业内网用ChatGLM3做文档问答 / 模型权重和向量库分两台机器放

加一层超时与失败返回

检索失败时让模型凭已有知识作答,是内网文档问答里最容易埋雷的地方——答案看着流畅,但和文档无关。建议在检索侧和生成侧的调用链上加一层约定,把三类情况分开处理。

  • 检索超时。给检索调用设一个超时值,超过就按“检索未完成”处理,不再把空上下文交给模型。超时值可以先设得保守一些,再按实际响应情况调整。
  • 返回为空。hits 为空数组时,直接返回固定话术,不进入生成阶段。日志里记下 query 和 index_version,方便判断是索引没建好还是问题确实没匹配到。
  • 服务不可达。连接被拒或域名解析失败时,按“检索服务不可用”返回固定话术,同时触发健康检查,连续失败可以考虑让生成侧直接短路,避免请求堆积。

对外返回可以统一成这类结构,让业务侧能从 finish_reason 区分正常回答和降级回答:

{ "answer": "当前没能取到相关文档片段,请稍后重试或换一种问法。",
  "finish_reason": "retrieve_failed",
  "request_id": "local-002" }

日志建议每条至少记这些字段:request_id、阶段(retrieve 或 generate)、耗时、命中片段数、错误类型、目标地址。request_id 从入口一路带到返回里,排查时能把一次问答的两段日志串起来。这套约定只是兜底,超时值和话术都需要结合内网实际环境确认后再定稿。