从训练到上线: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_ids 和 attention_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. 进程管理
用 pm2 或 systemd 管理 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 | 高 | 很高 | 企业级、多模型管理 |
十一、小结
走完本篇,你完成了:
- ✅ 把 LoRA 模型封装成标准 HTTP 服务
- ✅ 定义了健康检查、单条、批量三个接口
- ✅ 解决了
shape报错、回复重复、首请求慢三个坑 - ✅ 给出了鉴权、反代、进程管理的线上方案
到这一步,你的微调模型已经是一台真正意义上的"线上服务"了——可以接前端、接公众号、接钉钉机器人、接任何能发 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 |
依赖锁定文件 |
如果这篇博客帮到了你,欢迎转发。有任何问题欢迎在评论区交流。