把“通用大模型”改造成“更懂你业务/更像你风格”的专用模型
三者不是互斥关系,最佳实践通常是Prompt(控制输出)+ RAG(补充事实)+ 微调(固化能力/风格/行为)。
预训练模型学到的是语言统计规律 + 世界知识的粗映射。微调则更像是在特定分布上做“再训练”, 让模型在你关心的任务/风格/约束上更可靠。
用指令-回答(Instruction → Response)数据教模型“该怎么答”。 目标通常是:更听话、更结构化、更像业务助手。
就像给新客服一本标准话术手册。手册里有各种问题和标准答案,新员工照着手册回答,就能保持服务一致性。 SFT 就是把这本"话术手册"写进模型参数里。
场景:某电商平台每天收到10万+用户咨询,需要自动分类路由到不同部门
训练数据:5万条真实用户问题 + 人工标注的分类(售前/售后/投诉/其他)
微调前:通用模型经常把"退货"误判为"售前",准确率只有65%
微调后:准确率提升到92%,能稳定输出JSON格式:{"category": "售后", "confidence": 0.95}
业务价值:人工审核工作量减少70%,响应时间从5分钟降到30秒
不更新全部参数,而是在模型关键层插入小的可训练模块。 典型代表:LoRA(低秩适配),QLoRA(量化+LoRA)。
想象一个万能工具箱(基础模型),你不用重新买整个工具箱,只需要添加专用徽章(LoRA适配器): 加上"客服徽章"就变成客服工具,加上"医生徽章"就变成医疗工具。 LoRA 就是这些可插拔的"技能徽章",成本低、切换快。
场景:一个AI客服系统服务100+不同行业的客户(电商/金融/教育/医疗)
挑战:每个客户需要不同的行业术语和话术风格
解决方案:使用同一个7B基础模型 + 100个不同的LoRA适配器
训练成本:每个LoRA只需2小时训练,显存占用8GB(vs 全量微调需48GB)
部署效果:切换客户时只需加载对应LoRA(3秒),推理延迟不变
商业价值:服务器成本降低80%,新客户上线时间从2周缩短到2天
SFT 解决“会不会答”,对齐解决“更偏好怎么答”。 常见做法:偏好数据(chosen / rejected),用 DPO 直接优化偏好。
员工已经会说话(SFT教会基本回答),但需要学会更得体的表达。 通过对比"好的回答"vs"不好的回答",让员工明白:同样意思,不同说法,效果完全不同。 DPO 就是让模型学会这种"说话的艺术",更懂用户喜好。
场景:银行内部AI助手需要回答员工关于风险合规的问题
SFT后问题:模型能答对,但经常过于冗长、语气生硬、缺乏风险提示
偏好数据:1万组对比,人工标注"更优回答"vs"较差回答"
• chosen:"建议您先检查客户KYC评级,再决定是否需要人工复核。如需帮助,请联系风控部门。"
• rejected:"直接查KYC就行了。"
DPO效果:回答简洁度提升40%,合规提示覆盖率从30%提升到95%
业务价值:员工满意度提升60%,合规风险事件减少45%
| 维度 | SFT(监督微调) | PEFT(LoRA/QLoRA) | 对齐(DPO/RLHF) |
|---|---|---|---|
| 优化目标 | 学会"怎么答" | 低成本学会"怎么答" | 学会"更偏好怎么答" |
| 数据格式 | 指令-回答对 | 指令-回答对 | 偏好对比(chosen/rejected) |
| 参数更新 | 全量更新 | 只更新适配器(1-5%) | 基于SFT模型微调偏好 |
| 显存需求 | 高(全参数) | 低(QLoRA最低) | 中等(基于SFT模型) |
| 适用场景 | 任务能力、结构化输出 | 多业务、资源受限 | 风格优化、安全对齐 |
第一步:明确目标
第二步:评估资源
第三步:组合策略
微调成败很大程度取决于数据。建议把数据当成产品资产:有规范、有版本、有评估、有回滚。
{
"messages": [
{"role": "system", "content": "你是某电商平台的售后客服,回答必须输出 JSON。"},
{"role": "user", "content": "我想退货,包裹已经拆了还能退吗?"},
{"role": "assistant", "content": "{\"answer\": \"可以退\", \"policy\": \"7天无理由…\", \"next_step\": \"…\"}"}
]
}{
"prompt": "用户问:如何查询订单物流?",
"chosen": "你可以在【我的订单】→ 选择订单 → 点击【查看物流】…",
"rejected": "自己去网上查吧。"
}这一节把“微调”落到工程:怎么装环境、怎么下载模型、LoRA 到底在改什么、SFT/LoRA/QLoRA 怎么选。 你可以把它当成一份“从 0 到能跑”的操作手册。
把“字段齐全、类型正确、拒答一致”当成硬指标(可自动评测)。
减少长 Prompt,靠“参数”固化口吻与表达结构。
分类/抽取/路由/意图识别:靠指标评测,不靠主观感觉。
用更短 prompt 达到同等质量,减少 token 与延迟。
微调像做项目交付:如果你不先说清楚“交付件是什么”,最后很容易变成“感觉好像更聪明了,但上线不稳定”。 结构化输出/分类抽取这类目标之所以适合入门,是因为你可以像做质检一样:写一组样例,一键回归对比“微调前 vs 微调后”。
目标是:优先把流程跑通(能下载模型、能训练、能保存 adapter、能复现结果),再逐步放大规模。
| 显存 | 推荐方案 | 适合做什么 | 注意事项 |
|---|---|---|---|
| 8GB | 优先 QLoRA / 更短序列长度 | 跑通流程、小数据试验 | 更容易 OOM,需要更保守的 batch/seq_len |
| 12GB | QLoRA 或 LoRA(保守) | 7B/8B 级别微调入门 | 建议开启混合精度、合理 seq_len |
| 16GB | LoRA(最舒适起点) | 7B/8B LoRA 更好迭代 | 能跑更长 seq_len/更大 batch |
| 24GB+ | LoRA / 多卡 / 更大模型 | 更大参数、更大数据、更长上下文 | 评估/数据闭环同样重要,别只堆规模 |
# 建议:创建独立环境(conda/venv 二选一即可)
# conda create -n ft python=3.10 -y
# conda activate ft
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
# 核心库:训练与微调
pip install "torch" "transformers" "datasets" "accelerate" "peft" "trl"
# QLoRA 需要:4-bit 量化训练
pip install "bitsandbytes"量化可以理解为:把模型权重从常见的 FP16/BF16(16 位)压缩成更低位宽(如 8-bit / 4-bit)。 位数越低,占用的显存越少,但会引入一定数值误差。
你可以把 FP16/BF16→8-bit/4-bit 理解成:把一张高清图片从“无损/高码率”压成“更小的文件”。 文件更小(更省显存),但会带来一定细节损失/噪点。 所以 QLoRA 的工程直觉就是:先把 base “压缩着加载”(省显存),再训练那块小小的 LoRA 增量(adapter)。
# 检查 torch 是否能看到 GPU
python -c "import torch; print('cuda:', torch.cuda.is_available()); print('device:', torch.cuda.get_device_name(0) if torch.cuda.is_available() else None)"
# accelerate 配置(单卡也建议做)
accelerate config如果你是 Mac(M 系列)或纯 CPU 环境,也能做小模型或少量数据的实验,但主流 LoRA/QLoRA 训练体验一般还是以 CUDA 为主。
from transformers import AutoTokenizer, AutoModelForCausalLM
model_id = "your-base-model"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
print("loaded")首次运行会自动下载到本机缓存目录(可通过环境变量指定缓存路径)。
# Hugging Face(示例)
pip install -U huggingface_hub
# 预先登录(可选)
# huggingface-cli login
# 下载到指定目录(示意)
huggingface-cli download your-base-model --local-dir ./models/your-base-model --local-dir-use-symlinks False工程里更推荐预下载,避免线上机器跑脚本时临时拉模型。
训练时再临时下载模型,就像出门前不收拾行李,到了机场才发现充电器没带——一旦网络/权限/代理出问题,整个流程会卡死。
预下载到 models/base 的好处是:你把“外部不确定性”提前消化掉,训练/推理就能稳定复现。
在 Transformer 里,核心是大量线性层(比如注意力中的 Q/K/V 投影)。 全量微调会更新这些层的整个权重矩阵 W。 LoRA 的思想是:不直接改 W,而是学一个低秩增量 ΔW。
真实模型里一层的 W 通常是几千维(非常大)。这里用 4×4 做一个更接近“真实结构”的例子。 LoRA 选 r=2 时:A(2×4)、B(4×2),所以 ΔW = B·A 仍然是 4×4。
| 1 | 0 | 2 | -1 |
| 0 | 1 | 0 | 2 |
| -2 | 1 | 1 | 0 |
| 1 | -1 | 0 | 1 |
| 1 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 |
| 1 | 0 |
| 0 | 1 |
| -1 | 0 |
| 0 | -1 |
直觉:r=2 表示用两条“方向”(2 个通道)来表达增量。
因为 A 是两行、B 是两列,所以: ΔW = B[:,1] ⊗ A[1,:] + B[:,2] ⊗ A[2,:] (也就是两个 rank-1 外积叠加)。这就是“低秩”的核心直觉。
| 1 | 0 | 1 | 0 |
| 0 | 1 | 0 | 1 |
| -1 | 0 | -1 | 0 |
| 0 | -1 | 0 | -1 |
| 2 | 0 | 3 | -1 |
| 0 | 2 | 0 | 3 |
| -3 | 1 | 0 | 0 |
| 1 | -2 | 0 | 0 |
训练时只更新 A、B,W 冻结不动,因此显存和训练开销大幅下降。
你可以把 LoRA 理解成:在不动底座权重 W 的前提下,额外挂了一条“小支路”去学习增量 ΔW。 r、alpha、dropout 本质上是在控制:这条“小支路”有多宽(能学多少)、声音有多大(影响多强)、以及会不会死记硬背(过拟合)。
| 方案 | 更新哪些参数 | 显存/成本 | 优点 | 风险/缺点 | 适用场景 |
|---|---|---|---|---|---|
| SFT(全参) | 更新大部分/全部参数 | 最高(显存与训练时间) | 理论上最强,改造力度最大 | 成本高、易遗忘、维护多版本困难 | 数据量大、算力充足、需要强改造 |
| LoRA(PEFT) | 冻结 base,仅训练 LoRA 适配器 | 中低(工程最常用) | 训练快、显存省、多版本易管理 | 能力上限受限于 base 与 r(但多数业务足够) | 企业落地首选:话术/抽取/分类/结构化输出 |
| QLoRA | base 4-bit 量化 + 训练 LoRA 适配器 | 最低(最省显存) | 单卡更容易跑起来,适合快速试验 | 量化带来数值/速度/兼容性差异,需要更多踩坑经验 | 显存紧张(8GB/12GB)或先试后放大 |
上面 1) 环境与安装、2) 下载模型、3) 数据、4) 训练与选择 已经把原理与坑点讲清楚了。
这里把它们串成一次可执行的“验收流程”:你按顺序做完,最终能拿到 outputs/adapter 并完成推理验证。
# 建议新建一个独立目录(示例名称:ft-demo)
ft-demo/
data/
train.jsonl
eval.jsonl
scripts/
train_lora.py
infer.py
models/
base/ # 预下载的 base 模型(推荐,避免训练时临时下载失败)
outputs/
adapter/ # 训练后产物(LoRA 适配器)
logs/ # 训练日志(可选)./models/base)models/base → 训练产出 outputs/adapter → 推理验证这样训练时不会“临时下载失败/断网”,也更符合工程可复现。
预下载的意义是:把模型文件落到你项目里的 models/base/。
从这一步开始,你后续训练/推理就不再需要写 HuggingFace 模型名(例如 Qwen/xxx),而是直接把 BASE_MODEL
指向本地目录:models/base。
Transformers 的 from_pretrained() 支持读取“本地目录”,所以会直接从磁盘加载,不会再联网下载。
# 示例:先选一个能跑通流程的小模型(你也可以换成任何可用模型)
export BASE_MODEL="Qwen/Qwen2.5-1.5B-Instruct"
# 可选:把 HF 缓存放到项目目录,方便迁移与复现
export HF_HOME="$(pwd)/.hf"
pip install -U huggingface_hub
# 下载模型到 models/base
huggingface-cli download "$BASE_MODEL" --local-dir ./models/base --local-dir-use-symlinks False
# 下载完快速自检
ls -lh models/base | head
python -c "from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('models/base'); print('tokenizer ok')"# 训练/推理时使用“本地目录”作为 base(重点看这里)
BASE_MODEL="models/base" python scripts/train_lora.py
BASE_MODEL="models/base" python scripts/infer.py
# 这等价于代码里的:
# AutoTokenizer.from_pretrained("models/base")
# AutoModelForCausalLM.from_pretrained("models/base")
在 data/train.jsonl 与 data/eval.jsonl 中,每行一个 JSON:包含 messages(system/user/assistant)。
# data/train.jsonl(示例,至少几条就能跑通)
{"messages": [{"role": "system", "content": "你是售后助手,输出必须是 JSON"}, {"role": "user", "content": "我想退货"}, {"role": "assistant", "content": "{\"label\":\"售后\",\"next_step\":\"引导用户在订单页点击退货\"}"}]}
{"messages": [{"role": "system", "content": "你是售后助手,输出必须是 JSON"}, {"role": "user", "content": "怎么查物流"}, {"role": "assistant", "content": "{\"label\":\"查询\",\"next_step\":\"引导用户在订单页查看物流\"}"}]}# data/eval.jsonl(示例,放 2-5 条也能先跑通流程)
{"messages": [{"role": "system", "content": "你是售后助手,输出必须是 JSON"}, {"role": "user", "content": "包装拆了还能退吗"}, {"role": "assistant", "content": "{\"label\":\"售后\",\"next_step\":\"解释规则并引导申请\"}"}]}
你把下面这段完整复制到 scripts/train_lora.py,然后只需要改 model_id 即可。
import os
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
def main():
# 1) 选择 base 模型(最关键:你只需要改这里)
# model_id 支持 2 种写法:
# - HuggingFace 模型名(会自动下载/走缓存)
# 示例:"Qwen/Qwen2.5-1.5B-Instruct"
# - 本地模型目录(推荐先预下载到本地,更稳定/更可复现)
# 示例:"./models/base" 或 "/data/models/Qwen2.5-1.5B-Instruct"
#
# 你可以在命令行里用环境变量覆盖:
# BASE_MODEL="Qwen/Qwen2.5-1.5B-Instruct" python scripts/train_lora.py
# BASE_MODEL="models/base" python scripts/train_lora.py
model_id = os.environ.get("BASE_MODEL", "Qwen/Qwen2.5-1.5B-Instruct")
# 2) 读 tokenizer
# tokenizer 负责把文本变成 token id。
tokenizer = AutoTokenizer.from_pretrained(model_id)
# 一些模型没有 pad_token(尤其是类 LLaMA 系),训练/对齐 batch 时可能报错。
# 简单做法:把 pad_token 设成 eos_token。
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# 3) 读 base 模型(两条路线:LoRA / QLoRA)
# LoRA:普通加载(显存够用时推荐)
# device_map="auto":让 transformers 自动把模型放到可用 GPU 上。
model = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
# QLoRA:如果显存不够,改用 4-bit 加载(需 bitsandbytes)
# 你只需要:把上面那行 LoRA 加载注释掉,然后启用下面这一行。
# model = AutoModelForCausalLM.from_pretrained(model_id, load_in_4bit=True, device_map="auto")
# model = AutoModelForCausalLM.from_pretrained(model_id, load_in_4bit=True, device_map="auto")
# 4) 配置 LoRA(核心:你训练的就是这里的 A/B,不动 base 的 W)
# target_modules:决定“哪些线性层”加 LoRA。入门阶段可以先只加 attention 的投影层。
# r/alpha/dropout:先用默认即可;效果不够再调大 r。
lora_config = LoraConfig(
r=8, # rank(秩/容量):越大越“能学”,但参数量与显存/算力也越高;入门常用 8/16
lora_alpha=16, # 缩放系数:控制 ΔW 的“强度”(常见取 16/32);通常与 r 搭配一起调
lora_dropout=0.05, # dropout:只作用在 LoRA 分支上,防过拟合;数据少时可稍微调大
bias="none", # 是否训练 bias:none 最常见;如果你不清楚就保持默认
task_type="CAUSAL_LM", # 任务类型:自回归语言模型(大多数开源 LLM 微调都是这个)
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # 注入 LoRA 的层:注意力投影层是最常见起点
)
# 把 LoRA 适配器“挂”到模型上:从此以后训练的参数主要就是 LoRA 的那部分。
model = get_peft_model(model, lora_config)
# 5) 读数据
# 这里读的是 JSONL(每行一个 JSON)。示例格式:
# {"messages": [{"role":"system","content":"..."},{"role":"user","content":"..."},{"role":"assistant","content":"..."}]}
dataset = load_dataset(
"json",
data_files={"train": "data/train.jsonl", "eval": "data/eval.jsonl"},
)
# 6) 训练
# SFTTrainer:把 messages 格式的数据组装成模型可训练的文本序列。
# max_seq_length:越大越吃显存;如果 OOM,先把它降到 512/256。
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset["train"],
eval_dataset=dataset["eval"],
max_seq_length=1024,
)
trainer.train()
# 7) 保存产物:LoRA adapter
# 注意:这里保存的是 adapter(增量权重),不是完整 base。
# 推理时要用:base + adapter 组合加载。
os.makedirs("outputs/adapter", exist_ok=True)
trainer.save_model("outputs/adapter")
print("\n✅ saved adapter to: outputs/adapter")
if __name__ == "__main__":
main()# 方式A:直接运行(最简单)
BASE_MODEL="your-base-model" python scripts/train_lora.py
# 方式B:用 accelerate(更贴近工程,多卡/混合精度更容易扩展)
BASE_MODEL="your-base-model" accelerate launch scripts/train_lora.pyaccelerate 像“导演场记”。
saved adapter to: outputs/adapteroutputs/adapter/ 下会出现 adapter 权重与配置(通常包括 adapter_model / adapter_config 等文件)
你可以把 adapter 理解为:在不改动底座权重 W 的前提下,LoRA 学出来的那部分增量参数(也就是 A/B,间接形成 ΔW)。
所以 outputs/adapter 里只保存“增量”,体积通常远小于 base 模型。
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("models/base", device_map="auto")
tokenizer = AutoTokenizer.from_pretrained("models/base")
# 把 LoRA adapter 挂到 base 上(这一步不会改 base 文件)
model = PeftModel.from_pretrained(base, "outputs/adapter")# ✅ 可执行方案:把 LoRA adapter 合并到 base,并导出一个“单模型目录”
# 保存为:scripts/merge_adapter.py
import os
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
def main():
# 1) base 模型路径(建议用本地目录,例如 models/base)
base_dir = os.environ.get("BASE_MODEL", "models/base")
# 2) LoRA adapter 目录(训练产物)
adapter_dir = os.environ.get("ADAPTER_DIR", "outputs/adapter")
# 3) 合并后的输出目录
out_dir = os.environ.get("MERGED_DIR", "outputs/merged_model")
# 注意:merge 需要 base 是“正常精度”(fp16/bf16/fp32) 的权重。
# 如果你当初是 QLoRA(4-bit)训练的:合并时仍然要用“非 4-bit”的 base 来加载并 merge。
base = AutoModelForCausalLM.from_pretrained(
base_dir,
device_map="auto",
torch_dtype=torch.float16,
)
tokenizer = AutoTokenizer.from_pretrained(base_dir)
model = PeftModel.from_pretrained(base, adapter_dir)
# 把 LoRA 增量合并回 base 权重,并卸载 LoRA 结构
merged = model.merge_and_unload()
os.makedirs(out_dir, exist_ok=True)
merged.save_pretrained(out_dir)
tokenizer.save_pretrained(out_dir)
print(f"✅ merged model saved to: {out_dir}")
if __name__ == "__main__":
main()# 在 ft-demo 目录下
BASE_MODEL="models/base" ADAPTER_DIR="outputs/adapter" MERGED_DIR="outputs/merged_model" \
python scripts/merge_adapter.py
# 之后推理就直接加载 merged_dir(不再需要 adapter)
python -c "from transformers import AutoModelForCausalLM, AutoTokenizer; m=AutoModelForCausalLM.from_pretrained('outputs/merged_model', device_map='auto'); t=AutoTokenizer.from_pretrained('outputs/merged_model'); print('ok')"
把下面代码保存为 scripts/infer.py,运行后对比“加载 adapter 前/后”的输出差异。
import os
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
def generate(model, tokenizer, prompt: str):
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=200)
return tokenizer.decode(out[0], skip_special_tokens=True)
def main():
model_id = os.environ.get("BASE_MODEL", "your-base-model")
tokenizer = AutoTokenizer.from_pretrained(model_id)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
base = AutoModelForCausalLM.from_pretrained(model_id, device_map="auto")
prompt = "你是售后助手,输出必须是 JSON。用户:我想退货"
print("\n=== Base only ===")
print(generate(base, tokenizer, prompt))
model = PeftModel.from_pretrained(base, "outputs/adapter")
print("\n=== Base + LoRA adapter ===")
print(generate(model, tokenizer, prompt))
if __name__ == "__main__":
main()# 运行推理验证
BASE_MODEL="your-base-model" python scripts/infer.py推理验证跑通后,下一步就是把“底座模型 + LoRA 适配器”变成一个线上可访问的推理服务。 下面给你一套按步骤可落地执行的方案:用 vLLM 起一个OpenAI 兼容接口(其它服务直接用 HTTP 调用)。
models/base(或你线上机器的同等路径)outputs/adapter(训练输出)线上部署有两条路: A)推理时加载 LoRA(最快上线、方便切换多个业务 adapter); B)把 LoRA 合并进模型权重(更像“一个完整模型目录”,迁移更直观)。
如果你希望部署时只拿一个目录(不额外带 adapter),可以执行“合并并导出”。 合并后的目录可以直接给 vLLM/Transformers 加载。
# scripts/merge_lora.py
import os
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
BASE_MODEL = os.getenv("BASE_MODEL", "models/base")
ADAPTER = os.getenv("ADAPTER", "outputs/adapter")
OUT_DIR = os.getenv("OUT_DIR", "outputs/merged_model")
def main():
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True,
)
model = PeftModel.from_pretrained(model, ADAPTER)
model = model.merge_and_unload() # 把 ΔW 合并进 W
os.makedirs(OUT_DIR, exist_ok=True)
model.save_pretrained(OUT_DIR)
tokenizer.save_pretrained(OUT_DIR)
print(f"merged model saved to: {OUT_DIR}")
if __name__ == "__main__":
main()# 执行合并
BASE_MODEL="models/base" ADAPTER="outputs/adapter" OUT_DIR="outputs/merged_model" python scripts/merge_lora.py
vLLM 是一个专门为大模型推理加速设计的开源框架,核心特点是:
PagedAttention(显存管理优化)+ OpenAI 兼容接口。
对外提供 /v1/chat/completions 等 OpenAI 风格接口,所以无论你部署什么模型,调用方代码几乎不用改。
答案:接口一样(OpenAI 兼容),但 model 字段要填你在启动时 --served-model-name 的值。
你可以用以下命令快速查看你自己的服务到底暴露了哪些接口:
# 1) 查服务元信息(返回 OpenAI 风格的 info)
curl http://127.0.0.1:8000/v1
# 2) 列出可用模型
curl http://127.0.0.1:8000/v1/models
# 3) 查 OpenAPI 文档(如果 vLLM 版本支持)
# 浏览器打开:http://127.0.0.1:8000/docs
curl http://127.0.0.1:8000/docsGET /v1/models – 列出可用模型POST /v1/chat/completions – 对话(最常用)POST /v1/completions – 补全(旧式)POST /v1/embeddings – 嵌入(如果模型支持)GET /v1 – 服务元信息调用示例(完全兼容 OpenAI SDK):
# 对话接口(最常用)
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-ft-model",
"messages": [{"role":"user","content":"用一句话解释 LoRA"}],
"temperature": 0.2
}'
答案:可以。LangChain 的 ChatOpenAI 类只要把 base_url 指向你的 vLLM 服务,就能像调用 OpenAI 一样调用。
关键是把 openai_api_key 设成“随便填”(因为 vLLM 默认不鉴权,除非你自己加了)。
# LangChain 调用示例(Python)
+from langchain_openai import ChatOpenAI
+# 把 base_url 指向你的 vLLM 服务,api_key 随便填(vLLM 默认不鉴权)
llm = ChatOpenAI(
+ model="my-ft-model", # 对应 --served-model-name
+ base_url="http://127.0.0.1:8000/v1",
+ api_key="not-used", # vLLM 默认不鉴权,随便填
+ temperature=0.2
+)
+# 直接用 LangChain 的链路调用
+from langchain_core.messages import HumanMessage
+response = llm.invoke([HumanMessage(content="用一句话解释 LoRA")])
+print(response.content)/chat/completions,所以你只要写 http://host:port/v1。model 必须等于你启动时 --served-model-name 的值。api_key 就要填对应 token。
+ 一句话总结:把 ChatOpenAI 的 base_url 指向你的 vLLM,就能无缝接入 LangChain 生态。
下面示例用 vLLM 启动一个 HTTP 服务(默认 8000 端口),对外提供 /v1/chat/completions 等接口。
其它服务只要会发 HTTP 请求就能调用。
# 1) 在服务器创建虚拟环境并安装 vLLM(建议 Python 3.10/3.11 + CUDA)
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
# 2) 安装 vLLM(CUDA 机器上)
pip install "vllm>=0.4"# 3) 启动服务(路线 A:merged_model)
vllm serve outputs/merged_model \
--host 0.0.0.0 --port 8000 \
--served-model-name my-ft-model# 列出模型
curl http://127.0.0.1:8000/v1/models
# 发一次对话请求(OpenAI ChatCompletions 兼容)
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "my-ft-model",
"messages": [{"role": "user", "content": "用一句话解释 LoRA"}],
"temperature": 0.2
}'这条路线的目标是:同一个 base 模型不动,按需加载不同的 adapter(例如不同客户/行业一套 LoRA)。 你需要确保 vLLM 版本支持 LoRA(不同版本参数名可能略有差异)。
# 路线 B:启动 base,并启用 LoRA
# 说明:--lora-modules 的写法是“逻辑名=adapter目录”,逻辑名用于 API 调用时的 model 字段。
vllm serve models/base \
--host 0.0.0.0 --port 8000 \
--served-model-name my-base \
--enable-lora \
--lora-modules my-ft=outputs/adapter
核心原则只有一个:给每个 adapter 起一个唯一“逻辑名”,并把它映射到对应的 adapter 目录。
之后你在请求里通过 model 字段切换逻辑名,就能切换到不同的 adapter。
models/base/
adapters/
ecommerce/ # 电商话术 LoRA
finance/ # 金融风控 LoRA
medical/ # 医疗问诊 LoRA# ✅ 思路:my-base 作为 base;多个 adapter 用不同逻辑名
# 注意:不同 vLLM 版本对 --lora-modules 的“多值写法”可能不同
# - 有的版本支持重复传参
# - 有的版本支持逗号分隔
# 你务必以 `vllm serve --help` 输出为准。
vllm serve models/base \
--host 0.0.0.0 --port 8000 \
--served-model-name my-base \
--enable-lora \
--lora-modules ecommerce=adapters/ecommerce \
--lora-modules finance=adapters/finance \
--lora-modules medical=adapters/medical# 调用 base
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"my-base","messages":[{"role":"user","content":"用一句话解释 LoRA"}]}'
# 调用挂载了 LoRA 的版本(model=你在 --lora-modules 里写的逻辑名)
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"my-ft","messages":[{"role":"user","content":"用一句话解释 LoRA"}],"temperature":0.2}'
调用侧不需要理解 LoRA 的细节,你只要把 model 当成“版本号/客户号”即可:
同一个 URL,通过 model=ecommerce / model=finance 选择不同 adapter。
# 电商 adapter
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"ecommerce","messages":[{"role":"user","content":"我想退货,怎么走流程?"}]}'
# 金融 adapter
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"finance","messages":[{"role":"user","content":"这笔交易风险点有哪些?"}]}'
# 医疗 adapter
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"medical","messages":[{"role":"user","content":"持续发烧三天应该注意什么?"}]}'现实里确实会遇到:某些版本/框架不支持在同一端口下用请求参数切换 LoRA。 这时最稳的工程解法是:一个 adapter 一个端口(或一个容器/进程),再用 Nginx/网关做路由。
# 启动 3 个实例(示意):每个实例固定一个 adapter
vllm serve models/base --port 8001 --served-model-name ecommerce --enable-lora --lora-modules ecommerce=adapters/ecommerce
vllm serve models/base --port 8002 --served-model-name finance --enable-lora --lora-modules finance=adapters/finance
vllm serve models/base --port 8003 --served-model-name medical --enable-lora --lora-modules medical=adapters/medical# Nginx 路由(示意):按路径或子域名分流到不同端口
location /ecommerce/ { proxy_pass http://127.0.0.1:8001/; }
location /finance/ { proxy_pass http://127.0.0.1:8002/; }
location /medical/ { proxy_pass http://127.0.0.1:8003/; }这种方式的缺点是实例数会变多;优点是最兼容、最容易排障、隔离性最好(一个 adapter 出问题不影响其它)。
vllm --help 以当前版本为准。你至少需要做三件事: (1)进程守护(systemd/pm2/supervisor), (2)Nginx 反代(统一域名与 HTTPS), (3)鉴权与限流(避免裸奔被刷爆)。
# systemd 示例:/etc/systemd/system/vllm.service
[Unit]
Description=vLLM Inference Server
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/ft
Environment=PATH=/home/ubuntu/ft/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
ExecStart=/home/ubuntu/ft/.venv/bin/vllm serve /home/ubuntu/ft/outputs/merged_model --host 0.0.0.0 --port 8000 --served-model-name my-ft-model
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
# 启用与启动
sudo systemctl daemon-reload
sudo systemctl enable vllm
sudo systemctl start vllm
sudo systemctl status vllm# Nginx 反代示例(HTTPS 建议用 certbot 配好证书)
# /etc/nginx/sites-enabled/ft-model.conf
server {
listen 80;
server_name ft-api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 最低限度:加一个简单的 token(更严谨可用网关/鉴权服务)
# if ($http_authorization != "Bearer YOUR_TOKEN") { return 401; }
}
}
sudo nginx -t
sudo systemctl reload nginx# 任何语言都行:只要能发 HTTP POST
curl https://ft-api.yourdomain.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"model": "my-ft-model",
"messages": [
{"role": "system", "content": "你是企业内部知识助手"},
{"role": "user", "content": "把这段文本提取成 JSON:..."}
],
"temperature": 0.2
}'journalctl -u vllm -f)max_seq_length、减小 batch(SFTTrainer 默认也可能占用较大)、或改用 QLoRA 的 4-bit 加载BASE_MODEL 指向本地路径选择一个你熟悉的业务(电商/教育/医疗/金融任意),定义一个结构化输出任务(如:工单分类、字段抽取、风险标签)。写清:
针对你上面的任务,说明为什么需要(或不需要)微调,并写出替代方案与风险: