一、前言
做本地大模型部署调试这段时间,直观感受真的是一山更比一山高。最开始落地医疗场景AI项目,我一直用的是ChatGLM3-6B,日常的语义对话、简单信息解读都很流畅,当时觉得这个小模型的语义理解能力,完全能满足本地业务需求了。
直到后续部署适配4090 24G显卡的Qwen3-32B 4bit量化模型,我才明显感受到大小参数量模型的垂直业务能力鸿沟。尤其在高血压风险筛查这种严谨、重规则、靠权威指南支撑的医疗场景下,两款模型的实战表现天差地别。以往只看通用对话,很难发现小模型的短板,但落地到医疗筛查这种专业规则场景,ChatGLM3-6B知识储备不足、规则理解薄弱、细节判读缺失的问题彻底暴露。本次项目就基于真实落地场景,对两款模型搭建统一的OpenAI兼容API服务,使用完全相同的患者数据、相同提示词模板做对照测试。

二、核心概念定义
1. 本地大模型推理
本地大模型推理,是指不依赖云端API接口,将开源大模型权重文件下载至本地服务器/显卡设备,通过推理框架完成模型加载、计算生成、结果输出的全过程。
核心优势集中在医疗场景:数据不出本地、无网络延迟、无调用次数限制、隐私安全性拉满,完美适配医疗数据合规要求。本次采用vLLM、Transformers两大主流框架完成推理部署。
2. 模型量化技术
量化是大模型本地部署的核心关键,简单说就是压缩模型权重、降低显存占用,让超大参数量模型能在消费级显卡运行。
本次实战采用两种落地方案:
- Qwen3-32B:4bit bitsandbytes量化,大幅压缩显存,适配4090 24G显卡
- ChatGLM3-6B:FP16半精度量化,轻量化部署,无需重度压缩
3. 高血压筛查业务
本次筛查严格依据《中国高血压防治指南2023》、《中国2型糖尿病防治指南2020》、《慢性肾脏病筛查防治指南2022》三大权威指南,固定11项高血压易患因素。
核心逻辑是大模型读取患者体检、基础信息,逐条匹配风险因素,输出标准化JSON结果,包含匹配状态、判定原因。
4. 兼容OpenAI API
两款模型均封装统一的OpenAI格式接口,实现接口标准化。上层业务无需适配不同模型,仅需调用固定接口,即可快速切换模型,降低业务改造成本。
三、完整执行过程
本次医疗筛查业务流程标准化、可复用,所有垂直规则类AI场景均可套用,全程分为6个核心步骤,简单清晰、落地性极强。

1. 环境依赖部署
- 统一搭建Python3.11运行环境,安装核心依赖库,覆盖模型下载、推理、接口开发、跨域配置等全需求。
- 核心依赖包含vllm、fastapi、uvicorn、transformers、modelscope等,为模型加载和接口服务提供基础支撑。
2. 模型本地缓存
- 通过modelscope自动下载模型权重,缓存至本地固定目录。
- 首次运行自动下载,后续启动直接读取本地缓存,无需重复拉取,大幅提升启动效率,同时保证模型版本稳定统一。
3. 模型加载初始化
- 根据模型特性配置参数:大参数量Qwen3-32B开启4bit量化、拉满显存利用率;
- 轻量化ChatGLM3-6B采用auto自动设备映射、FP16精度加载,适配硬件性能。
4. 提示词模板封装
- 固化医疗筛查专属提示词模板,绑定权威指南、11项筛查指标、输出格式要求。
- 支持动态传入患者数据和自定义筛查因子,实现通用化筛查能力,约束模型输出规范,杜绝自由发挥。
5. 接口服务启动
- 基于FastAPI搭建服务,封装四大核心接口:对话接口、高血压筛查接口、模型列表查询、服务健康检查。
- 配置全局跨域,支持前端、后端业务自由调用。
6. 推理与结果输出
- 接收业务传入的患者数据,动态拼接提示词,模型完成AI推理,自动清洗无效标签、修复不完整JSON;
- 标准化输出筛查结果,同时记录推理速度、token消耗等数据。
四、核心逻辑说明
1. 本地推理框架逻辑
两款模型采用不同推理框架,底层逻辑差异明显,直接影响部署难度和推理效果。
- Qwen3-32B-vLLM框架:基于PagedAttention分页注意力机制,核心优势是高吞吐、低延迟、支持动态显存调度。通过禁用v1引擎,规避4bit量化兼容bug,适配大参数量模型的稳定推理,显存利用率最高可达97%,充分挖掘4090显卡性能。
- ChatGLM3-6B-Transformers框架:原生轻量化推理框架,适配小参数量模型,部署简单、兼容性强。采用auto设备映射,自动分配CPU、GPU资源,无需复杂调参,稳定性极高,但吞吐性能低于vLLM。
2. 量化技术底层原理
量化的核心是降低模型权重数值精度,减少显存占用,以极小的精度损失换取可落地的部署能力。
- 4bit量化(Qwen3-32B):将原本32位浮点权重压缩为4位,显存占用大幅降低,让32B超大模型可在24G单卡运行。同时通过bfloat16计算精度,最大限度减少医疗场景的判定误差。
- FP16半精度(ChatGLM3-6B):将32位浮点转为16位,压缩比例适中,几乎无精度损失,适配6B小参数量模型,无需重度压缩即可流畅运行。

3. 医疗提示词工程逻辑
本次筛查效果的核心关键,不是模型能力,而是标准化、约束化的提示词设计,也是垂直AI落地的核心。
- 模板核心约束:绑定权威医疗指南、固定筛查条目、明确匹配规则、限制理由字数、强制JSON格式输出。
- 同时加入/no_think指令,关闭模型冗余思考过程,避免输出无效文本,保证结果规整可直接被业务系统解析。
4. 结果容错修复逻辑
实战中模型常出现JSON截断、标签残留、条目缺失等问题,代码内置多层容错机制,保障业务可用性:
- 自动过滤<think>冗余思考标签、markdown代码块
- 检测截断JSON,自动补全闭合括号,修复残缺数据
- 映射模型输出序号与业务编码,统一数据格式
- 自动补全未返回的筛查条目,缺失数据默认标记为未检出
五、应用实践对比
1. Qwen3-32B应用实践
在单卡RTX 4090上通过vLLM+4bit量化部署Qwen3-32B大模型,对外提供OpenAI兼容Chat接口及基于《中国高血压防治指南》的11项易患因素智能筛查API进行慢病高危筛查。
python
import os
# 禁用vLLM v1引擎(v1对bitsandbytes支持不完善,回退v0稳定引擎)
os.environ["VLLM_USE_V1"] = "0"
# 屏蔽vLLM INFO/WARNING日志,只显示ERROR及以上
os.environ["VLLM_LOGGING_LEVEL"] = "ERROR"
from vllm import LLM, SamplingParams
from modelscope import snapshot_download
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import List, Literal
import subprocess
import time
import uuid
import re
import json
import uvicorn
# ===================== 1. 配置参数 =====================
model_name = "qwen/Qwen3-32B"
cache_dir = "/home/model"
# ===================== 2. 下载模型到本地 =====================
print("正在下载/校验模型缓存...")
local_model_path = snapshot_download(
model_name,
cache_dir=cache_dir,
revision="master"
)
print(f"模型已就绪,本地路径:{local_model_path}")
# ===================== 3. vLLM加载模型(4bit量化) =====================
print("正在加载模型(vLLM + bitsandbytes 4bit量化)...")
llm = LLM(
model=local_model_path,
trust_remote_code=True,
# 4bit量化(运行时量化FP16权重)
quantization="bitsandbytes",
load_format="bitsandbytes",
dtype="bfloat16",
# 显存利用率(拉满,32B-4bit单卡显存紧张)
gpu_memory_utilization=0.97,
# 最大上下文长度(筛查场景需容纳11项JSON输出)
max_model_len=4096,
# 无flash-attn时使用eager模式(如已装flash-attn可去掉此行)
enforce_eager=True,
)
# 获取tokenizer(后续Chat接口复用)
tokenizer = llm.get_tokenizer()
# 输出模型加载后显存占用(vLLM在子进程中分配显存,需通过nvidia-smi读取)
import subprocess
def get_gpu_memory():
result = subprocess.run(
["nvidia-smi", "--query-gpu=memory.used,memory.total", "--format=csv,noheader,nounits"],
capture_output=True, text=True
)
used, total = result.stdout.strip().split(", ")
return float(used) / 1024, float(total) / 1024 # 转GB
used_mem, total_mem = get_gpu_memory()
print(f"\n--- 模型加载后显存(nvidia-smi) ---")
print(f"已使用:{used_mem:.2f} GB / 总计:{total_mem:.2f} GB | 剩余:{total_mem - used_mem:.2f} GB")
# ===================== 4. FastAPI Chat接口 =====================
app = FastAPI(title="Qwen3-32B Chat API", description="OpenAI兼容Chat接口")
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ===================== 高血压风险筛查提示词模板 =====================
HTN_SCREENING_PROMPT_TEMPLATE = """依据《中国高血压防治指南2023》《中国2型糖尿病防治指南2020》《慢性肾脏病筛查防治指南2022》,基于患者客观数据逐条比对,禁止虚构。
筛查COUNT项高血压易患因素:
FACTOR_LINES
match=1:有明确数据证实;match=0:未提及/否定/数据正常
reason须简短(≤30字)。仅返回JSON,无多余文字:
[JSON_EXAMPLE]
患者信息:PATIENT_DATA_PLACEHOLDER"""
def build_screening_prompt(factor_list, patient_data):
"""根据外部传入的因素编码和名称动态构建提示词"""
count = len(factor_list)
factor_lines = "\n".join(
f"{i+1}.{f['name']}" for i, f in enumerate(factor_list)
)
# 用第一个因素构造JSON示例,i字段用编码
json_example = json.dumps(
{"i": factor_list[0]["code"], "t": factor_list[0]["name"], "m": 0, "r": "简要原因"},
ensure_ascii=False
)
prompt = HTN_SCREENING_PROMPT_TEMPLATE.replace("COUNT", str(count))
prompt = prompt.replace("FACTOR_LINES", factor_lines)
prompt = prompt.replace("JSON_EXAMPLE", json_example)
prompt = prompt.replace("PATIENT_DATA_PLACEHOLDER", patient_data)
return prompt
# 请求/响应模型
class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
model: str = "qwen3-32b"
messages: List[ChatMessage]
temperature: float = Field(default=0.7, ge=0, le=2)
top_p: float = Field(default=0.8, ge=0, le=1)
max_tokens: int = Field(default=512, ge=1, le=1024)
stream: bool = False # 暂不支持流式,预留字段
@app.post("/v1/chat/completions")
async def chat_completions(req: ChatRequest):
"""OpenAI兼容的Chat接口"""
# 格式化对话(messages → chat_template)
# 追加 /no_think 关闭Qwen3思考模式,避免输出大段<think...</think)>
msg_dicts = [{"role": m.role, "content": m.content} for m in req.messages]
if msg_dicts and msg_dicts[-1]["role"] == "user":
msg_dicts[-1]["content"] += " /no_think"
prompt = tokenizer.apply_chat_template(msg_dicts, tokenize=False, add_generation_prompt=True)
sampling_params = SamplingParams(
temperature=req.temperature,
top_p=req.top_p,
max_tokens=req.max_tokens,
)
start_time = time.time()
outputs = llm.generate([prompt], sampling_params)
elapsed = time.time() - start_time
completion = outputs[0].outputs[0]
# 过滤残留的<think...</think)>标签(双重保险)
text = re.sub(r"<think.*?</think\s*>", "", completion.text, flags=re.DOTALL).strip()
prompt_tokens = len(outputs[0].prompt_token_ids)
new_tokens = len(completion.token_ids)
# OpenAI兼容响应格式
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:12]}",
"object": "chat.completion",
"created": int(time.time()),
"model": req.model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": new_tokens,
"total_tokens": prompt_tokens + new_tokens,
},
"speed_tokens_per_sec": round(new_tokens / elapsed, 2) if elapsed > 0 else 0,
}
@app.get("/v1/models")
async def list_models():
"""模型列表接口"""
return {
"object": "list",
"data": [{"id": "qwen3-32b", "object": "model", "owned_by": "local"}]
}
@app.get("/health")
async def health():
"""健康检查接口"""
used, total = get_gpu_memory()
return {"status": "ok", "gpu_used_gb": round(used, 2), "gpu_total_gb": round(total, 2)}
# ===================== 高血压易患因素筛查接口 =====================
# 默认因素列表(当外部未传入factorList时使用)
DEFAULT_FACTORS = [
{"code": "HTN_F01", "name": "正常高值血压(SBP120-139/DBP80-89mmHg)"},
{"code": "HTN_F02", "name": "年龄≥45岁"},
{"code": "HTN_F03", "name": "超重(BMI≥24)或腹型肥胖(男腰围≥90/女≥85cm)"},
{"code": "HTN_F04", "name": "高血压家族史(一二级亲属)"},
{"code": "HTN_F05", "name": "高盐饮食(日盐>6g)"},
{"code": "HTN_F06", "name": "长期大量饮酒(男日乙醇≥40g/女≥20g)"},
{"code": "HTN_F07", "name": "吸烟(含被动)"},
{"code": "HTN_F08", "name": "缺乏体力劳动(周运动<150min/久坐)"},
{"code": "HTN_F09", "name": "长期精神紧张"},
{"code": "HTN_F10", "name": "血脂异常(TC≥5.18/LDL-C≥3.37/TG≥1.70mmol/L或HDL-C降低)"},
{"code": "HTN_F11", "name": "糖尿病(FPG≥7.0/HbA1c≥6.5%/已诊断)"},
]
class FactorItem(BaseModel):
code: str # 因素编码
name: str # 因素名称
class PatientDataRequest(BaseModel):
chatContent: str
factorList: List[FactorItem] = [] # 外部传入的因素编码和名称,为空时使用默认列表
@app.post("/api/htn-screening")
async def htn_screening(req: PatientDataRequest):
"""高血压易患因素筛查接口"""
# 使用外部传入的因素列表,若为空则使用默认列表
factor_list = [f.model_dump() for f in req.factorList] if req.factorList else DEFAULT_FACTORS
# 构建编码→名称映射(用于结果替换)
code_map = {f["code"]: f["name"] for f in factor_list}
# 动态构建提示词
prompt = build_screening_prompt(factor_list, req.chatContent)
messages = [
{"role": "user", "content": prompt + " /no_think"},
]
formatted = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# 计算剩余可用token:总上下文 - 输入prompt = 最大输出
prompt_token_ids = tokenizer.encode(formatted)
prompt_len = len(prompt_token_ids)
max_output = max(64, 4096 - prompt_len) # 至少保留64 tokens输出
sampling_params = SamplingParams(temperature=0.1, top_p=0.8, max_tokens=max_output)
outputs = llm.generate([formatted], sampling_params)
completion = outputs[0].outputs[0]
text = re.sub(r"<think.*?</think\s*>", "", completion.text, flags=re.DOTALL).strip()
# 去掉markdown代码块包裹
text = re.sub(r"^```json\s*", "", text)
text = re.sub(r"\s*```$", "", text)
# 截断修复:如果输出被max_tokens截断,JSON不完整,尝试补全
if completion.finish_reason == "length" and "[" in text and "]" not in text:
# 去掉最后一个不完整的对象,补上闭合括号
last_brace = text.rfind("},")
if last_brace > 0:
text = text[:last_brace + 1] + "\n]"
else:
last_brace = text.rfind("{")
if last_brace > 0:
text = text[:last_brace] + "]"
try:
json_match = re.search(r'\[.*\]', text, re.DOTALL)
if json_match:
screening_list = json.loads(json_match.group())
else:
screening_list = json.loads(text)
# 构建序号→编码映射(LLM可能返回数字序号或字符串序号1,2,...而非编码)
idx_to_code = {i + 1: f["code"] for i, f in enumerate(factor_list)}
# 短字段名映射为完整字段名(逐条处理,单条异常不影响其他)
result_list = []
for item in screening_list:
try:
raw_i = item.get("i", "")
try:
raw_i_int = int(raw_i)
except (ValueError, TypeError):
raw_i_int = None
if raw_i_int is not None and raw_i_int in idx_to_code:
code = idx_to_code[raw_i_int]
elif raw_i in code_map:
code = raw_i
else:
code = str(raw_i)
match_val = item.get("m", 0)
result_list.append({
"code": code,
"name": item.get("t", code_map.get(code, "")),
"match": match_val,
"reason": item.get("r", ""),
})
except Exception as e:
print(f"单条解析异常:{e}, item={item}")
result_list.append(item)
return {"status": 200, "data": result_list, "truncated": completion.finish_reason == "length"}
except json.JSONDecodeError as e:
print(f"JSON解析异常:{e}\n原始返回:{text}")
return {"status": 500, "data": [], "error": f"模型返回格式异常:{str(e)}", "raw": text}
except Exception as e:
print(f"接口异常:{e}")
return {"status": 500, "data": [], "error": str(e)}
# ===================== 5. 启动 =====================
if __name__ == "__main__":
print("\n模型加载完成!启动Chat API服务...")
print(f"Chat接口:POST http://0.0.0.0:8000/v1/chat/completions")
print(f"高血压筛查:POST http://0.0.0.0:8000/api/htn-screening")
print(f"模型列表:GET http://0.0.0.0:8000/v1/models")
print(f"健康检查:GET http://0.0.0.0:8000/health")
uvicorn.run(app, host="0.0.0.0", port=8000)
服务启动:

Postman接口调试:

输出结果:
{
"status": 200,
"data": [
{
"code": "HTN_F01",
"name": "正常高值血压(SBP120-139/DBP80-89mmHg)",
"match": 1,
"reason": "收缩压130mmHg,符合正常高值血压标准"
},
{
"code": "HTN_F02",
"name": "年龄≥45岁",
"match": 1,
"reason": "年龄55岁,符合标准"
},
{
"code": "HTN_F03",
"name": "超重(BMI≥24)或腹型肥胖(男腰围≥90/女≥85cm)",
"match": 0,
"reason": "BMI为22.6,腰围78cm,均未超标"
},
{
"code": "HTN_F04",
"name": "高血压家族史(一二级亲属)",
"match": 1,
"reason": "父亲有高血压病史,发病年龄约55岁"
},
{
"code": "HTN_F05",
"name": "高盐饮食(日盐>6g)",
"match": 0,
"reason": "未提及饮食盐摄入量"
},
{
"code": "HTN_F06",
"name": "长期大量饮酒(男日乙醇≥40g/女≥20g)",
"match": 0,
"reason": "未提及饮酒情况"
},
{
"code": "HTN_F07",
"name": "吸烟(含被动)",
"match": 0,
"reason": "日吸烟量为0支"
},
{
"code": "HTN_F08",
"name": "缺乏体力劳动(周运动<150min/久坐)",
"match": 0,
"reason": "未提及运动情况"
},
{
"code": "HTN_F09",
"name": "长期精神紧张",
"match": 0,
"reason": "未提及精神状态"
},
{
"code": "HTN_F10",
"name": "血脂异常(TC≥5.18/LDL-C≥3.37/TG≥1.70mmol/L或HDL-C降低)",
"match": 0,
"reason": "甘油三酯1.5mmol/L,未超标"
},
{
"code": "HTN_F11",
"name": "糖尿病(FPG≥7.0/HbA1c≥6.5%/已诊断)",
"match": 0,
"reason": "空腹血糖3mmol/L,未达糖尿病标准"
}
],
"truncated": false
}
2. ChatGLM3-6B部署实践
使用ChatGLM3-6B搭建本地GPU推理服务,提供OpenAI兼容Chat对话接口及基于《中国高血压防治指南》的11项高血压易患因素智能筛查API进行慢病高危筛查。
python
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from transformers import AutoTokenizer, AutoModel, AutoConfig
from pydantic import BaseModel, Field
from typing import List, Literal
import torch
import uvicorn
from modelscope import snapshot_download
import time
import uuid
import re
import json
import warnings
warnings.filterwarnings("ignore")
# --- 全局变量 ---
model = None
tokenizer = None
model_name = "ZhipuAI/chatglm3-6b"
cache_dir = "/home/model"
# 2. 初始化FastAPI应用
app = FastAPI(title="ChatGLM3-6B Chat API", description="OpenAI兼容Chat接口 + 高血压筛查")
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 3. 加载模型和Tokenizer
print("正在下载/校验模型缓存...")
local_model_path = snapshot_download(model_name, cache_dir=cache_dir)
print(f"正在加载模型,路径: {local_model_path}")
tokenizer = AutoTokenizer.from_pretrained(local_model_path, trust_remote_code=True)
# 修复ChatGLM配置兼容性问题
config = AutoConfig.from_pretrained(local_model_path, trust_remote_code=True)
if not hasattr(config, 'max_length'):
config.max_length = config.seq_length if hasattr(config, 'seq_length') else 8192
# 使用AutoModel加载
try:
if not torch.cuda.is_available():
raise RuntimeError("未检测到GPU,请确保CUDA环境配置正确")
print(f"检测到GPU: {torch.cuda.get_device_name(0)}")
print(f"GPU显存: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB")
model = AutoModel.from_pretrained(
local_model_path,
config=config,
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=torch.float16,
device_map="auto"
)
model.eval()
device = next(model.parameters()).device
print(f"模型加载成功!当前设备: {device}")
# 显存统计
alloc_mem = torch.cuda.memory_allocated() / 1024**3
reserv_mem = torch.cuda.memory_reserved() / 1024**3
total_mem = torch.cuda.get_device_properties(0).total_memory / 1024**3
print(f"已分配:{alloc_mem:.2f} GB | 已保留:{reserv_mem:.2f} GB | 剩余:{total_mem - reserv_mem:.2f} GB / 总计:{total_mem:.2f} GB")
except Exception as e:
print(f"加载失败: {e}")
model = None
# ===================== 高血压风险筛查提示词模板 =====================
HTN_SCREENING_PROMPT_TEMPLATE = """依据《中国高血压防治指南2023》《中国2型糖尿病防治指南2020》《慢性肾脏病筛查防治指南2022》,基于患者客观数据逐条比对,禁止虚构。
筛查{count}项高血压易患因素:
{factor_lines}
match=1:有明确数据证实;match=0:未提及/否定/数据正常
reason须简短(≤30字)。仅返回JSON,无多余文字:
[{json_example}]
患者信息:PATIENT_DATA_PLACEHOLDER"""
def build_screening_prompt(factor_list, patient_data):
"""根据外部传入的因素编码和名称动态构建提示词"""
count = len(factor_list)
factor_lines = "\n".join(
f"{i+1}.{f['name']}" for i, f in enumerate(factor_list)
)
# 用第一个因素构造JSON示例,i字段用编码
json_example = json.dumps(
{"i": factor_list[0]["code"], "t": factor_list[0]["name"], "m": 0, "r": "简要原因"},
ensure_ascii=False
)
prompt = HTN_SCREENING_PROMPT_TEMPLATE.replace("{count}", str(count))
prompt = prompt.replace("{factor_lines}", factor_lines)
prompt = prompt.replace("{json_example}", json_example)
prompt = prompt.replace("PATIENT_DATA_PLACEHOLDER", patient_data)
return prompt
# ===================== 请求/响应模型 =====================
class ChatMessage(BaseModel):
role: Literal["system", "user", "assistant"]
content: str
class ChatRequest(BaseModel):
model: str = "chatglm3-6b"
messages: List[ChatMessage]
temperature: float = Field(default=0.7, ge=0, le=2)
top_p: float = Field(default=0.8, ge=0, le=1)
max_tokens: int = Field(default=512, ge=1, le=2048)
stream: bool = False
class FactorItem(BaseModel):
code: str # 因素编码
name: str # 因素名称
class PatientDataRequest(BaseModel):
chatContent: str
factorList: List[FactorItem] = [] # 外部传入的因素编码和名称,为空时使用默认列表
# ===================== Chat对话接口 =====================
@app.post("/v1/chat/completions")
async def chat_completions(req: ChatRequest):
"""OpenAI兼容的Chat接口"""
if model is None:
raise HTTPException(status_code=500, detail="模型未加载,请检查服务器启动日志")
# 拼接对话历史为ChatGLM格式
history = []
user_query = ""
for m in req.messages:
if m.role == "user":
user_query = m.content
elif m.role == "assistant" and user_query:
history.append((user_query, m.content))
user_query = ""
# 取最后一条user消息作为当前输入
if not user_query:
user_query = req.messages[-1].content
start_time = time.time()
try:
response, _ = model.chat(tokenizer, user_query, history=history)
except Exception as e:
raise HTTPException(status_code=500, detail=f"推理出错: {str(e)}")
elapsed = time.time() - start_time
# 粗略token统计(ChatGLM无精确prompt_token_ids)
prompt_tokens = len(tokenizer.encode(user_query))
completion_tokens = len(tokenizer.encode(response))
speed = completion_tokens / elapsed if elapsed > 0 else 0
return {
"id": f"chatcmpl-{uuid.uuid4().hex[:12]}",
"object": "chat.completion",
"created": int(time.time()),
"model": req.model,
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": response},
"finish_reason": "stop",
}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
},
"speed_tokens_per_sec": round(speed, 2),
}
# ===================== 模型列表 =====================
@app.get("/v1/models")
async def list_models():
return {
"object": "list",
"data": [{"id": "chatglm3-6b", "object": "model", "owned_by": "local"}]
}
# ===================== 健康检查 =====================
@app.get("/health")
async def health():
if torch.cuda.is_available():
alloc_mem = torch.cuda.memory_allocated() / 1024**3
total_mem = torch.cuda.get_device_properties(0).total_memory / 1024**3
return {"status": "ok", "gpu_used_gb": round(alloc_mem, 2), "gpu_total_gb": round(total_mem, 2)}
return {"status": "ok"}
# ===================== 高血压易患因素筛查接口 =====================
# 默认因素列表(当外部未传入factorList时使用)
DEFAULT_FACTORS = [
{"code": "HTN_F01", "name": "正常高值血压(SBP120-139/DBP80-89mmHg)"},
{"code": "HTN_F02", "name": "年龄≥45岁"},
{"code": "HTN_F03", "name": "超重(BMI≥24)或腹型肥胖(男腰围≥90/女≥85cm)"},
{"code": "HTN_F04", "name": "高血压家族史(一二级亲属)"},
{"code": "HTN_F05", "name": "高盐饮食(日盐>6g)"},
{"code": "HTN_F06", "name": "长期大量饮酒(男日乙醇≥40g/女≥20g)"},
{"code": "HTN_F07", "name": "吸烟(含被动)"},
{"code": "HTN_F08", "name": "缺乏体力劳动(周运动<150min/久坐)"},
{"code": "HTN_F09", "name": "长期精神紧张"},
{"code": "HTN_F10", "name": "血脂异常(TC≥5.18/LDL-C≥3.37/TG≥1.70mmol/L或HDL-C降低)"},
{"code": "HTN_F11", "name": "糖尿病(FPG≥7.0/HbA1c≥6.5%/已诊断)"},
]
@app.post("/api/htn-screening")
async def htn_screening(req: PatientDataRequest):
"""高血压易患因素筛查接口"""
if model is None:
raise HTTPException(status_code=500, detail="模型未加载,请检查服务器启动日志")
# 使用外部传入的因素列表,若为空则使用默认列表
factor_list = [f.model_dump() for f in req.factorList] if req.factorList else DEFAULT_FACTORS
# 构建编码→名称映射(用于结果替换)
code_map = {f["code"]: f["name"] for f in factor_list}
# 动态构建提示词
prompt = build_screening_prompt(factor_list, req.chatContent)
# ChatGLM3用model.chat推理
start_time = time.time()
try:
response, _ = model.chat(
tokenizer,
prompt,
history=[],
)
except Exception as e:
return {"code": 500, "screeningList": [], "error": f"推理出错: {str(e)}"}
elapsed = time.time() - start_time
# 过滤残留标签
text = re.sub(r"<think.*?</think\s*>", "", response, flags=re.DOTALL).strip()
# 去掉markdown代码块包裹
text = re.sub(r"^```json\s*", "", text)
text = re.sub(r"\s*```$", "", text)
# 截断修复:JSON不完整时尝试补全
if "[" in text and "]" not in text:
last_brace = text.rfind("},")
if last_brace > 0:
text = text[:last_brace + 1] + "\n]"
else:
last_brace = text.rfind("{")
if last_brace > 0:
text = text[:last_brace] + "]"
try:
json_match = re.search(r'\[.*\]', text, re.DOTALL)
if json_match:
screening_list = json.loads(json_match.group())
else:
screening_list = json.loads(text)
# 构建序号→编码映射(LLM可能返回数字序号1,2,...而非编码)
idx_to_code = {i + 1: f["code"] for i, f in enumerate(factor_list)}
# 短字段名映射为完整字段名
matched_codes = set()
for item in screening_list:
raw_i = item.pop("i", "")
# 如果是数字序号,映射为对应编码;如果已经是编码则直接使用
if isinstance(raw_i, int) and raw_i in idx_to_code:
code = idx_to_code[raw_i]
elif raw_i in code_map:
code = raw_i
else:
code = str(raw_i)
item["code"] = code
item["title"] = item.pop("t", code_map.get(code, ""))
item["match"] = item.pop("m", 0)
item["reason"] = item.pop("r", "")
matched_codes.add(code)
# 补全:LLM未返回的因素自动填充 match=0
for f in factor_list:
if f["code"] not in matched_codes:
screening_list.append({
"code": f["code"],
"title": f["name"],
"match": 0,
"reason": "未检出相关数据",
})
return {"code": 200, "screeningList": screening_list, "truncated": False}
except json.JSONDecodeError as e:
print(f"JSON解析异常:{e}\n原始返回:{text}")
return {"code": 500, "screeningList": [], "error": f"模型返回格式异常:{str(e)}", "raw": text}
except Exception as e:
print(f"接口异常:{e}")
return {"code": 500, "screeningList": [], "error": str(e)}
# ===================== 启动 =====================
if __name__ == "__main__":
print("\n模型加载完成!启动Chat API服务...")
print(f"Chat接口:POST http://0.0.0.0:8000/v1/chat/completions")
print(f"高血压筛查:POST http://0.0.0.0:8000/api/htn-screening")
print(f"模型列表:GET http://0.0.0.0:8000/v1/models")
print(f"健康检查:GET http://0.0.0.0:8000/health")
uvicorn.run(app, host="0.0.0.0", port=8000)
服务启动:

Postman接口调试:

输出结果:
{
"code": 200,
"screeningList": [
{
"code": "HTN_F01",
"title": "正常高值血压(SBP120-139/DBP80-89mmHg)",
"match": 0,
"reason": "患者存在正常高值血压,需注意控制饮食、增加运动,定期检查血压"
},
{
"code": "HTN_F02",
"title": "年龄≥45岁",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F03",
"title": "超重(BMI≥24)或腹型肥胖(男腰围≥90/女≥85cm)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F04",
"title": "高血压家族史(一二级亲属)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F05",
"title": "高盐饮食(日盐>6g)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F06",
"title": "长期大量饮酒(男日乙醇≥40g/女≥20g)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F07",
"title": "吸烟(含被动)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F08",
"title": "缺乏体力劳动(周运动<150min/久坐)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F09",
"title": "长期精神紧张",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F10",
"title": "血脂异常(TC≥5.18/LDL-C≥3.37/TG≥1.70mmol/L或HDL-C降低)",
"match": 0,
"reason": "未检出相关数据"
},
{
"code": "HTN_F11",
"title": "糖尿病(FPG≥7.0/HbA1c≥6.5%/已诊断)",
"match": 0,
"reason": "未检出相关数据"
}
],
"truncated": false
}
3. 模型实战表现对比
本次采用完全相同的患者数据、相同提示词模板、相同接口逻辑,仅替换模型,实测结果差异极具参考价值,精准体现大小参数量模型在垂直规则场景的能力差距。
3.1 实测患者核心数据
患者55岁,收缩压130mmHg,BMI22.6,腰围78cm,父亲有高血压病史,无吸烟饮酒史,空腹血糖、血脂指标均正常。核心风险点:高龄、正常高值血压、高血压家族史。
3.2 Qwen3-32B筛查优势表现
32B大参数量模型的核心优势是精准匹配、严格判读、无主观偏差,完全贴合医疗业务需求。
- 精准识别三大阳性风险:年龄≥45岁、正常高值血压、高血压家族史,全部标记匹配成功
- 阴性指标判定严谨,依据患者具体数据给出明确否定理由,不笼统概括
- 输出格式100%标准,无缺失、无冗余、无格式错误,无需二次修复
- 严格遵守规则,不主观推演、不夸大风险,完全基于客观数据判定
3.3 ChatGLM3-6B筛查短板表现
6B轻量化模型存在明显的规则理解薄弱、细节识别缺失问题,无法满足严谨医疗筛查需求。
- 核心风险全部漏判:高龄、血压异常、家族史三大关键风险均标记为不匹配
- 统一套用"未检出相关数据"笼统理由,不会结合患者具体数据分析
- 存在逻辑错误:识别到血压异常数据,却未标记阳性,判定逻辑矛盾
- 规则执行力弱,无法精准区分"有数据阴性"和"无数据未检出"

3.4 模型综合能力差异总结
- 精准度层面:Qwen3-32B远优于ChatGLM3-6B,大参数量模型对复杂规则、细节数据的理解能力碾压小模型,医疗严谨场景必须优先选用大模型。
- 部署成本层面:ChatGLM3-6B轻量化、低显存、启动快、零调参,适合简单问答、模糊筛查;Qwen3-32B需4bit量化、高显存利用率,部署门槛更高,但业务价值更高。
- 容错能力层面:两款模型均需代码容错,但大模型输出稳定性更强,小模型极易出现漏判、误判、格式错乱问题。
- 医疗知识库储备层面:两款模型权威医疗指南知识储备差距悬殊,直接决定垂直医疗业务的判定根基。

4. 医疗权威知识库专项实测
为进一步验证模型底层医疗能力,针对筛查依托的三大核心权威指南,对两款模型进行统一知识问答测试,请求参数、提问内容、输出token上限完全一致,精准对比专业知识储备与提炼能力。
4.1 详细测试实例
Qwen3-32B请求:
{
"model": "qwen3-32b",
"messages": {"role": "user", "content": "你是否充分具备以下指南的完整知识,并100字分别概述:《中国高血压防治指南2023》、《中国2型糖尿病防治指南2020》、《慢性肾脏病筛查防治指南2022》"},
"temperature": 0.7,
"top_p": 0.8,
"max_tokens": 512
}
Qwen3-32B指南回复:
{
"id": "chatcmpl-473781e335e6",
"object": "chat.completion",
"created": 1782181988,
"model": "qwen3-32b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "是的,我具备以下指南的核心知识,并可分别进行简要概述:\n\n---\n\n**《中国高血压防治指南2023》** \n该指南强调高血压的早期筛查与分级管理,推荐130/80 mmHg为起始干预阈值,倡导生活方式干预与个体化药物治疗相结合,并注重高危人群(如糖尿病、慢性肾病患者)的综合管理。\n\n---\n\n**《中国2型糖尿病防治指南2020》** \n指南提出以"早筛、早诊、早治"为核心,强调生活方式干预为基础,结合个体化药物治疗。关注血糖、血压、血脂综合控制,并提出分阶段治疗策略,注重并发症预防与患者教育。\n\n---\n\n**《慢性肾脏病筛查防治指南2022》** \n该指南强调CKD的早期筛查(如尿蛋白、eGFR检测),提出分阶段管理策略,并重视高血压、糖尿病等高风险人群的肾脏保护。倡导多学科协作治疗,延缓疾病进展并减少终末期肾病发生。\n\n---\n\n如需更详细内容或具体章节解读,可进一步提问。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 65,
"completion_tokens": 241,
"total_tokens": 306
},
"speed_tokens_per_sec": 11.45
}
Postman请求示例:

ChatGLM3 请求:
{
"model": "chatglm3-6b",
"messages": [
{"role": "user", "content": "你是否充分具备以下指南的完整知识,并100字分别概述:《中国高血压防治指南2023》、《中国2型糖尿病防治指南2020》、《慢性肾脏病筛查防治指南2022》"}
],
"temperature": 0.7,
"top_p": 0.8,
"max_tokens": 512,
"stream": false
}
ChatGLM3 指南回复:
{
"id": "chatcmpl-8a60f445f765",
"object": "chat.completion",
"created": 1782183305,
"model": "chatglm3-6b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "我不是医生,但我可以为你提供一些基本信息。\n\n《中国高血压防治指南2023》是一份关于高血压的防治指南,包含了高血压的诊断、治疗和预防等方面的内容。\n\n《中国2型糖尿病防治指南2020》是一份关于2型糖尿病的防治指南,包含了2型糖尿病的病因、病理、诊断、治疗和预防等方面的内容。\n\n《慢性肾脏病筛查防治指南2022》是一份关于慢性肾脏病的筛查和防治指南,包含了慢性肾脏病的病因、病理、诊断、治疗和预防等方面的内容。\n\n这些指南都是针对特定疾病的防治措施和指导医生实践的工具,旨在提高医生的诊断和治疗水平,从而更好地服务于患者。"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 51,
"completion_tokens": 156,
"total_tokens": 207
},
"speed_tokens_per_sec": 33.14
}
Postman请求示例:

4.2 Qwen3-32B指南知识表现
Qwen3-32B具备扎实的垂直医疗知识库储备,可精准提炼三大指南核心干货内容,贴合临床筛查标准:
- 《中国高血压防治指南2023》:精准掌握新版干预阈值、高低危人群分层、生活方式+药物联合干预核心规则,贴合本次筛查判定依据
- 《中国2型糖尿病防治指南2020》:明确早筛早治核心原则,掌握糖代谢、血压血脂综合管控、并发症预防的核心逻辑
- 《慢性肾脏病筛查防治指南2022》:熟知核心筛查指标、分阶段管理方案,明确三高人群肾脏保护核心要点
整体回答精炼专业、贴合实战、无冗余废话,完全匹配高血压联合筛查的业务底层逻辑,为精准判读提供扎实知识支撑。
4.3 ChatGLM3-6B指南知识短板
ChatGLM3-6B存在专业医疗知识储备匮乏、无细节、无干货、泛化回答严重的核心问题:
- 无法提炼三大指南的核心更新点、核心筛查及治疗标准,仅能输出"包含诊断、治疗、预防"等通用空泛话术
- 无任何指南专属核心知识点,无法区分三份指南的适用场景、核心侧重点差异
- 缺乏医疗垂直领域深度知识,仅具备基础通识认知,不支撑专业筛查判定
4.3 知识储备差异核心结论
高血压筛查的精准度,本质依托模型内置的权威医疗知识库。
- Qwen3-32B凭借海量垂直医疗数据训练,具备精准、细分、落地的指南解读能力;
- ChatGLM3-6B仅具备通用通识能力,对细分权威医疗指南无深度认知,这也是其出现大规模漏判、误判的底层核心原因。小
模型通用知识尚可,但垂直专业知识库深度不足,无法支撑严谨医疗AI业务。
六、总结
通过本次Qwen3-32B与ChatGLM3-6B的高血压筛查全流程实战,我们能清晰看透本地大模型落地垂直行业的核心逻辑:模型参数量决定业务上限,量化技术决定落地门槛,提示词工程决定输出精度,容错机制决定服务稳定性。
小模型性价比高、适配特定场景,但在医疗、金融、政务等严谨规则类场景中,小模型的细节缺失、规则漏判问题会直接导致业务不可用。而通过4bit量化技术,超大参数量模型已经可以适配消费级显卡,让高精度本地AI落地从理想变成现实。本地大模型落地不是简单的模型部署,而是硬件适配、量化调优、提示词设计、接口封装、结果容错的整套工程化体系。