从训练到上线:Qwen3-0.6B + LoRA FastAPI 推理服务部署实战

从训练到上线:Qwen3-0.6B + LoRA FastAPI 推理服务部署实战

从训练到上线:Qwen3-0.6B + LoRA FastAPI 推理服务部署实战

摘要:这是「无显卡微调 Qwen3-0.6B」系列的部署篇。上一篇完成了 CPU 上的 QLoRA 微调并验证了 LoRA 生效,本篇将把微调后的模型封装成一个常驻内存的 HTTP 推理服务,支持单条/批量对话、健康检查、Token 统计,并给出线上鉴权与反代的基本方案。所有代码可直接复制运行。

标签大模型部署 FastAPI Qwen3 LoRA 推理服务 PyTorch 博客园实战


一、写在前面

训练完模型只是第一步。模型不提供服务,就只是一堆躺在磁盘上的权重文件。

本篇要解决的问题只有一个:

如何让微调后的 Qwen3-0.6B + LoRA,像一个真正的"后端 API"那样,接收用户请求、返回模型生成结果。

最终效果:

  • 启动一次,模型常驻内存
  • POST /chat 接收 JSON,返回回复文本
  • GET /health 健康检查,方便监控
  • POST /chat/batch 批量推理
  • 本地能跑,低配云服务器也能跑

二、服务架构概览

┌──────────────────────────────────────────────────┐
│                  客户端 / 前端                    │
│          (curl / requests / 浏览器 / App)         │
└────────────────────┬─────────────────────────────┘│ HTTP JSON▼
┌──────────────────────────────────────────────────┐
│              FastAPI (uvicorn)                    │
│  ┌────────────────────────────────────────────┐   │
│  │ GET  /health        → 健康检查             │   │
│  │ POST /chat          → 单条对话             │   │
│  │ POST /chat/batch    → 批量对话             │   │
│  └────────────────────────────────────────────┘   │
│                     │                             │
│                     ▼                             │
│  ┌────────────────────────────────────────────┐   │
│  │   Tokenizer (chat_template)                │   │
│  │   PeftModel (Base + LoRA adapter)          │   │
│  │   model.generate() → reply                 │   │
│  └────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────┘

三、环境准备

安装依赖

python -m pip install fastapi uvicorn pydantic
作用
fastapi Web 框架,定义路由和请求体
uvicorn ASGI 服务器,真正跑 HTTP
pydantic 请求参数校验和类型提示

训练时已装的 torch / transformers / peft 继续复用,无需重装。


四、完整服务代码

保存为 fastapi_server.py

"""
Qwen3-0.6B + LoRA FastAPI 推理服务(CPU 低配版)
启动:uvicorn fastapi_server:app --host 0.0.0.0 --port 8000
测试:curl http://localhost:8000/chat -d "{\"message\":\"我的快递三天没动了\"}" -H "Content-Type: application/json"
"""import torch
from fastapi import FastAPI, Request, Header, HTTPException
from pydantic import BaseModel
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel# ============================================================
# 配置区(改成你的路径)
# ============================================================
MODEL_PATH = r"E:\ai\Qwen3-0.6B"
LORA_PATH  = r"E:\ai\qwen_lora\qwen3_lora_output"# ============================================================
# 启动时加载模型(只加载一次,常驻内存)
# ============================================================
print("⏳ 正在加载 Tokenizer ...")
tok = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
if tok.pad_token is None:tok.pad_token = tok.eos_tokenprint("⏳ 正在加载 Base 模型 ...")
base = AutoModelForCausalLM.from_pretrained(MODEL_PATH,torch_dtype=torch.float32,trust_remote_code=True,device_map=None,
)print("⏳ 正在加载 LoRA 适配器 ...")
model = PeftModel.from_pretrained(base, LORA_PATH)
model.eval()print("✅ 模型加载完成,服务就绪")# ============================================================
# FastAPI 应用
# ============================================================
app = FastAPI(title="Qwen3-0.6B LoRA 客服助手", version="1.0")# ---------- 请求体模型 ----------
class ChatRequest(BaseModel):message: strsystem: str | None = None          # 可选:覆盖系统提示max_new_tokens: int = 256          # 可选:控制回复长度temperature: float = 0.7           # 可选:采样温度# ---------- 健康检查 ----------
@app.get("/health")
def health():return {"status": "ok", "model": "Qwen3-0.6B + LoRA"}# ---------- 核心接口:单条对话 ----------
@app.post("/chat")
def chat(req: ChatRequest, x_api_key: str = Header(None)):# 简易鉴权(可选,生产环境建议换成 JWT)# if x_api_key != "your-secret-key":#     raise HTTPException(status_code=401, detail="Unauthorized")# 构造消息messages = []if req.system:messages.append({"role": "system", "content": req.system})messages.append({"role": "user", "content": req.message})# 编码inputs = tok.apply_chat_template(messages,return_tensors="pt",padding=True,add_generation_prompt=False,)input_ids = inputs["input_ids"]# 生成with torch.no_grad():out = model.generate(input_ids=input_ids,max_new_tokens=req.max_new_tokens,do_sample=True,temperature=req.temperature,pad_token_id=tok.pad_token_id,)# 只解码新增部分(去掉输入)new_tokens = out[0][input_ids.shape[1]:]reply = tok.decode(new_tokens, skip_special_tokens=True)return {"reply": reply.strip(),"usage": {"prompt_tokens": int(input_ids.shape[1]),"completion_tokens": int(len(new_tokens)),},}# ---------- 批量接口 ----------
@app.post("/chat/batch")
def chat_batch(messages: list[ChatRequest]):results = []for m in messages:msgs = []if m.system:msgs.append({"role": "system", "content": m.system})msgs.append({"role": "user", "content": m.message})inputs = tok.apply_chat_template(msgs, return_tensors="pt", padding=True, add_generation_prompt=False)input_ids = inputs["input_ids"]with torch.no_grad():out = model.generate(input_ids=input_ids,max_new_tokens=m.max_new_tokens,do_sample=True,temperature=m.temperature,pad_token_id=tok.pad_token_id,)new_tokens = out[0][input_ids.shape[1]:]reply = tok.decode(new_tokens, skip_special_tokens=True)results.append(reply.strip())return {"replies": results}

五、启动与测试

启动服务

cd /d E:\ai\my_code
uvicorn fastapi_server:app --host 0.0.0.0 --port 8000

成功标志:

⏳ 正在加载 Tokenizer ...
⏳ 正在加载 Base 模型 ...
⏳ 正在加载 LoRA 适配器 ...
✅ 模型加载完成,服务就绪
INFO:     Uvicorn running on http://0.0.0.0:8000

测试方式一:curl

curl http://localhost:8000/chat -d "{\"message\":\"我的快递三天没动了\"}" -H "Content-Type: application/json"

测试方式二:Python requests

import requests
r = requests.post("http://localhost:8000/chat",json={"message": "我的订单什么时候发货?"}
)
print(r.json()["reply"])

测试方式三:浏览器 Swagger UI

启动后访问:

http://localhost:8000/docs

FastAPI 自动生成交互式文档,点 /chat → Try it out → 填内容 → Execute。


六、接口文档

GET /health

字段 类型 说明
status string 服务状态,ok 表示正常
model string 当前加载的模型标识

响应示例:

{"status": "ok", "model": "Qwen3-0.6B + LoRA"}

POST /chat

请求体:

字段 类型 必填 说明
message string 用户输入
system string 覆盖系统提示,切换角色
max_new_tokens int 最大生成长度,默认 256
temperature float 采样温度 0~1,默认 0.7

请求示例:

{"message": "我的快递三天没动了","system": "你是礼貌简洁的中文客服助手","max_new_tokens": 128,"temperature": 0.5
}

响应示例:

{"reply": "非常抱歉给您带来不便,我帮您查询一下物流状态,请稍等。","usage": {"prompt_tokens": 42,"completion_tokens": 38}
}

POST /chat/batch

请求体:JSON 数组,每个元素同 /chat 请求体。

响应:

{"replies": ["非常抱歉给您带来不便...","您好,退货流程如下...","营业时间为早9点到晚6点..."]
}

七、代码设计要点

设计点 为什么这么做
模型在模块顶层加载 服务启动时加载一次,常驻内存,避免每次请求重加载(否则每次几秒到十几秒)
model.eval() 关闭 dropout 等训练模式,确保推理输出稳定可复现
torch.no_grad() 推理时不计算梯度,省内存、提速约 20%
只解码 new_tokens 不把输入原样吐回,只返回模型新生成的部分
pad_token_id 显式传入 CPU 模式下生成时若不指定,可能报 padding 相关错误
system 可选字段 不改代码即可切换角色(客服 / 翻译 / 摘要 / 代码助手)
usage 字段 模仿 OpenAI 接口格式,方便前端展示 Token 消耗

八、踩坑记录

坑 1:AttributeError: shape

现象:直接把 apply_chat_template() 的返回值丢给 generate()

原因apply_chat_template() 返回的是 dict(含 input_idsattention_mask),不是 tensor。

解决:显式取 input_ids

inputs = tok.apply_chat_template(messages, return_tensors="pt", padding=True)
input_ids = inputs["input_ids"]   # ✅ 关键

坑 2:回复里包含输入原文

现象:模型生成的文本把用户的提问也重复了一遍。

原因:解码了整个 output_ids,包含输入部分。

解决:只解码新增 token:

new_tokens = out[0][input_ids.shape[1]:]
reply = tok.decode(new_tokens, skip_special_tokens=True)

坑 3:首次请求特别慢

现象:服务启动后第一条请求耗时 10~30 秒,后续正常。

原因:首次推理触发了 PyTorch 的算子编译和内存分配。

解决:服务启动后发一条预热请求:

# 在模型加载完成后加一句预热
with torch.no_grad():_ = model.generate(input_ids=torch.tensor([[1]]), max_new_tokens=1)

九、线上部署注意事项

1. 鉴权(防裸奔)

取消注释代码中的鉴权段,或换成 JWT:

@app.post("/chat")
def chat(req: ChatRequest, x_api_key: str = Header(None)):if x_api_key != "your-secret-key":raise HTTPException(status_code=401, detail="Unauthorized")...

调用时带 Header:

curl http://localhost:8000/chat -H "X-API-Key: your-secret-key" -d "..."

2. 用 nginx 反代 + HTTPS

公网不要直接暴露 8000 端口。nginx 配置示例:

server {listen 443 ssl;server_name your-domain.com;location / {proxy_pass http://127.0.0.1:8000;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;}
}

3. 资源预估

配置 能否跑
本地 8GB 内存 ✅ 能跑,0.6B 常驻约 1.5~2GB
云服务器 2核4G ✅ 勉强,并发限 1~2
云服务器 4核8G ✅ 推荐,稳定
有 GPU(16GB+) 🚀 可换 vLLM,并发大幅提升

4. 进程管理

pm2systemd 管理 uvicorn 进程,防止意外退出:

# /etc/systemd/system/qwen.service
[Unit]
Description=Qwen3 LoRA FastAPI Service
After=network.target[Service]
ExecStart=/usr/bin/python -m uvicorn fastapi_server:app --host 0.0.0.0 --port 8000
WorkingDirectory=/path/to/your/code
Restart=on-failure[Install]
WantedBy=multi-user.target

十、与其他部署方案对比

方案 门槛 并发 适合场景
FastAPI(本文) 低~中 学习、内部工具、低配云
Ollama + GGUF 无 GPU 服务器、一键部署
vLLM 中(需 GPU) 生产环境、高并发
Triton Inference Server 很高 企业级、多模型管理

十一、小结

走完本篇,你完成了:

  1. ✅ 把 LoRA 模型封装成标准 HTTP 服务
  2. ✅ 定义了健康检查、单条、批量三个接口
  3. ✅ 解决了 shape 报错、回复重复、首请求慢三个坑
  4. ✅ 给出了鉴权、反代、进程管理的线上方案

到这一步,你的微调模型已经是一台真正意义上的"线上服务"了——可以接前端、接公众号、接钉钉机器人、接任何能发 HTTP 请求的系统。

后续可以探索:

  • 多轮对话(累积 messages 历史)
  • 流式输出(SSE / WebSocket,打字机效果)
  • Ollama GGUF(脱离 Python 依赖,部署更轻)
  • vLLM(有 GPU 后无缝切换)

附录:完整文件清单

文件 用途
fastapi_server.py FastAPI 推理服务主文件
train_qlora_cpu.py QLoRA 训练脚本
train_data.json Alpaca 格式训练数据
qwen3_lora_output/ 训练产物(LoRA 适配器)
requirements.txt 依赖锁定文件

如果这篇博客帮到了你,欢迎转发。有任何问题欢迎在评论区交流。