🚀 从 0 到 1:基于 QLoRA 微调你的第一个考试题库 AI 专家

本教程记录了如何利用轻量级的 Qwen1.5-1.8B-Chat 模型和 QLoRA 技术,在有限的 GPU 资源(如 6GB 显存的 RTX 3060 Laptop)上,成功微调出一个能够精确、专业地回答特定领域题库的 AI 专家。

🎯 一、项目目标与技术选型

1. 目标

将一个通用的大型语言模型(LLM)定制化,使其能够像专业的教师或题库系统一样,以结构化、高准确率的方式回答我们提供的 几千道特定领域的考试题目

2. 技术选型

  • 基础模型 (Base Model): Qwen/Qwen1.5-1.8B-Chat
    • 原因: 体积小(18亿参数),对 VRAM 友好;基于 Qwen 结构,中文处理能力强,同时支持 Chat 功能。
  • 微调方法 (Fine-tuning): QLoRA (Quantized Low-Rank Adaptation)
    • 原因: 在 4-bit 量化模型上进行 LoRA 训练,极大地降低了 VRAM 占用,使 6GB 显存的 GPU 也能进行训练。

🛠️ 二、环境与数据准备

1. 基础环境配置

请确保您的环境已安装以下关键库:

1
2
3
4
5
6
7
# 安装 pytorch (带 CUDA 支持)
conda install pytorch torchvision torchaudio pytorch-cuda=11.8 -c pytorch -c nvidia

# 安装 transformers, peft, accelerate, bitsandbytes (QLoRA 核心依赖)
pip install transformers peft accelerate bitsandbytes
# 安装 huggingface_hub 和 datasets (数据处理与模型下载)
pip install datasets huggingface_hub pandas sqlalchemy pyarrow

注意: 如果下载模型遇到 404 错误,请在终端执行 huggingface-cli login 进行登录认证。

2. 数据格式化 (1_data_prep.py)

我们的目标是将数据库中的题库格式化为 LLM 微调所需的 指令微调 (Instruction Tuning) 格式:{instruction, input, output}

  • instruction (指令): 固定的任务描述,如“请根据提供的题目信息,给出正确答案以及详细的解析。”
  • input (输入): 题目的全部信息,包括科目、类型、题干、所有选项。
  • output (输出): 标准答案和详细解析。

最终生成的文件是 instruction_data.jsonl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 核心代码片段 (1_data_prep.py)
def export_and_format_data(db_url, output_file):
# ... (数据库连接和读取代码) ...
for index, row in df.iterrows():
input_text = f"科目:{row['p_subjects']}\n题目:{row['p_question']}\n\n"
# ... (动态添加选项) ...

output_text = f"【正确答案】:{row['p_answer']}\n"
output_text += f"【详细解析】:{row['p_explanation'] or '暂无解析'}"

formatted_data.append({
"instruction": INSTRUCTION,
"input": input_text.strip(),
"output": output_text.strip()
})
# ... (写入 jsonl 文件) ...

⚙️ 三、QLoRA 模型微调 (2_qlora_finetune.py)

1. QLoRA 核心配置

为适应 6GB 显存,我们做了以下关键优化:

  • 模型: Qwen/Qwen1.5-1.8B-Chat
  • 量化: BitsAndBytesConfig 启用 4-bit NF4 量化。
  • 序列长度: MAX_LENGTH = 384 (最大输入序列长度,减少 VRAM 占用)。
  • 训练参数 (TrainingArguments):
    • per_device_train_batch_size=1: 单步 Batch Size 降至最低。
    • gradient_accumulation_steps=8: 通过梯度累积,实现等效 Batch Size 为 8,保证训练效果。

2. 训练脚本 (2_qlora_finetune.py) 运行

运行脚本开始训练:

1
& C:/Users/zhaoz/anaconda3/python.exe c:/Users/zhaoz/Desktop/大模型训练/2_qlora_finetune.py
指标 训练初期表现 优化效果
VRAM 占用 约 5GB / 6GB QLoRA 成功,VRAM 安全。
训练速度 约 4-5 秒/步 (Step) 比 7B 模型加速 10 倍以上,总训练时长降至约 1.5 小时内。
GPU 状态 功率跑满 (如 99W/115W) GPU 资源得到充分利用。

🧪 四、模型调用与效果测试 (3_inference.py)

训练完成后,LoRA 权重被保存到 ./qwen1.5_1.8b_lora 目录。我们通过 PeftModel 将 LoRA 权重注入到量化后的基础模型中。

1. 核心调用逻辑

推理脚本的关键在于使用 PeftModel.from_pretrained(model, OUTPUT_DIR)

1
2
3
4
5
6
7
8
# 核心代码片段 (3_inference.py)
# 1. 加载量化后的基础模型
model = AutoModelForCausalLM.from_pretrained(...)
# 2. 注入 LoRA 适配器
model = PeftModel.from_pretrained(model, OUTPUT_DIR)
model.eval() # 切换到评估模式
# 3. 构造与训练时完全一致的 Prompt 进行生成
prompt = f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"

2. 效果测试示例

通过交互式测试,我们验证了微调后的效果:

测试指令: 什么是马克思主义?马克思主义具有哪些鲜明的特征?
Input 文本: (空)

微调模型响应:

【一】马克思主义是关于无产阶级和人类解放的政治经济学。
【二】马克思主义具有以下三个显著特点:

  1. 马克思主义是一个有机的整体,它是马克思恩格斯创立的科学社会主义理论体系;
  2. 马克思主义是一个完整的理论体系…
  3. 马克思主义是一个不断发展和完善的过程…

结论: 模型能够精确地遵循训练数据中的结构化格式(使用 【一】【二】1. 等),并给出专业、高相关性的答案。


💡 五、总结与心得体会

QLoRA 微调的真正价值

你的模型 (微调后) Ollama 通用模型 (未微调)
专家化: 成为特定题库知识的权威专家。 通用性: 知识广博,但特定领域不权威。
结构化输出: 强制遵循训练数据中的格式(如:【正确答案】、【详细解析】)。 自由输出: 回答格式多样、灵活,缺乏一致性。
知识权重强化: 优先、准确地调用训练数据中的标准答案。 知识分散: 回答可能包含非标准或过时的信息。
推理速度: 模型更小(1.8B)且回答收敛快,速度更快 推理速度: 模型较大(通常 > 7B)且需要更多“思考”步骤,速度较慢。

微调的意义在于:它教会了模型“如何”使用这些知识,以及“如何”以你期望的专业格式进行回复。 这不仅仅是简单的“记住答案”,而是实现了行为和知识的对齐

经验总结

  1. 模型选择至关重要: 在资源有限的情况下,选择 1.8B 级别的轻量模型(如 Qwen1.5-1.8B-Chat)是成功的关键。
  2. Prompt 格式一致性: 训练(2_qlora_finetune.py)和推理(3_inference.py)中的 Prompt 模板(如 ### Instruction:\n...)必须完全一致,否则模型将无法理解你的指令。
  3. 网络稳定性挑战: 即使是正确的模型 ID,下载时也可能因网络或 SSL 问题遭遇顽固的 404 错误。遇到问题时,应优先重开终端、清理环境变量,或使用稳定镜像源。

1_data_prep.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import pandas as pd
from sqlalchemy import create_engine
import json
import os

# --- 配置 ---
# 数据库连接字符串:根据您的数据库类型修改
# 例如:SQLite: 'sqlite:///your_database_name.db'
# 例如:MySQL: 'mysql+pymysql://user:password@host/db_name'
DATABASE_URL = "sqlite:///everything.db"
OUTPUT_FILE = "instruction_data.jsonl"
INSTRUCTION = "请根据提供的题目信息,给出正确答案以及详细的解析。"
# ----------------

def export_and_format_data(db_url, output_file):
"""从数据库读取题库,并格式化为指令微调所需的 JSONL 格式。"""

print(f"尝试连接数据库:{db_url}")
engine = create_engine(db_url)

# 假设您的表名为 practice_questions
df = pd.read_sql("SELECT * FROM practice_questions", engine)
print(f"成功读取 {len(df)} 条题目数据。")

formatted_data = []

for index, row in df.iterrows():
# 1. 构建 INPUT 字段 (包含所有题目和选项信息)
input_text = f"科目:{row['p_subjects']}\n"
input_text += f"类型:{row['p_type']}\n"
input_text += f"章节:{row['p_chapter'] or 'N/A'}\n"
input_text += f"题目:{row['p_question']}\n\n"

# 动态添加选项,处理可能的 None/NaN 值
options = {
'A': row.get('p_option_a'), 'B': row.get('p_option_b'),
'C': row.get('p_option_c'), 'D': row.get('p_option_d'),
'E': row.get('p_option_e'), 'F': row.get('p_option_f'),
'G': row.get('p_option_g'),
}

valid_options = [f"选项{k}{v}" for k, v in options.items() if pd.notna(v) and v is not None]
input_text += "\n".join(valid_options)

# 2. 构建 OUTPUT 字段 (标准答案和解析)
output_text = f"【正确答案】:{row['p_answer']}\n"
output_text += f"【详细解析】:{row['p_explanation'] or '暂无解析'}"

# 3. 构造最终的 JSON 格式
formatted_data.append({
"instruction": INSTRUCTION,
"input": input_text.strip(),
"output": output_text.strip()
})

# 将数据写入 JSONL 文件
with open(output_file, 'w', encoding='utf-8') as f:
for item in formatted_data:
f.write(json.dumps(item, ensure_ascii=False) + '\n')

print(f"\n数据格式化完成,已保存到 {output_file},共 {len(formatted_data)} 条记录。")

if __name__ == "__main__":
# 在运行前,请确保您已修改 DATABASE_URL
export_and_format_data(DATABASE_URL, OUTPUT_FILE)

2_qlora_finetune.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
BitsAndBytesConfig,
)
from datasets import load_dataset
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# --- 基础参数 ---
BASE_MODEL = "Qwen/Qwen1.5-1.8B-Chat" # ✅ 改用轻量版
DATA_PATH = "instruction_data.jsonl"
OUTPUT_DIR = "./qwen1.5_1.8b_lora"

# LoRA 参数
LORA_R = 16
LORA_ALPHA = 32
LORA_DROPOUT = 0.05

# 关键显存优化参数
MAX_LENGTH = 384 # ✅ 再次降低序列长度
MAX_VRAM_GB = 5.5

# ----------------------------------------------------
# 1. 数据预处理
# ----------------------------------------------------

def format_prompt(sample):
text = f"### Instruction:\n{sample['instruction']}\n\n### Input:\n{sample['input']}\n\n### Response:\n{sample['output']}"
return {"text": text}

print("加载并格式化数据集...")
dataset = load_dataset("json", data_files=DATA_PATH)
dataset = dataset.map(format_prompt, batched=False)

dataset = dataset["train"].train_test_split(test_size=0.05)
train_data = dataset["train"]
val_data = dataset["test"]
print(f"训练集大小: {len(train_data)}, 验证集大小: {len(val_data)}")

# ----------------------------------------------------
# 2. 加载模型 (QLoRA)
# ----------------------------------------------------

print(f"正在加载基础模型: {BASE_MODEL} (4-bit 量化)...")

bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)

max_memory = {
0: f'{int(MAX_VRAM_GB * 1024)}MiB',
'cpu': '30GiB',
}

model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
max_memory=max_memory,
low_cpu_mem_usage=True,
)

tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
tokenizer.pad_token = tokenizer.eos_token

# ----------------------------------------------------
# 3. 分词与标签
# ----------------------------------------------------

def tokenize_function(sample):
tokens = tokenizer(
sample["text"],
max_length=MAX_LENGTH,
truncation=True,
padding="max_length",
)
return {
"input_ids": tokens["input_ids"],
"attention_mask": tokens["attention_mask"],
"labels": tokens["input_ids"].copy(),
}

print("应用分词器...")
train_data = train_data.map(
tokenize_function,
batched=False,
remove_columns=["text", "instruction", "input", "output"]
)
val_data = val_data.map(
tokenize_function,
batched=False,
remove_columns=["text", "instruction", "input", "output"]
)

model = prepare_model_for_kbit_training(model)

lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "v_proj"],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# ----------------------------------------------------
# 4. 训练参数
# ----------------------------------------------------

training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=1, # ✅ 单 batch,降低显存
gradient_accumulation_steps=8, # ✅ 梯度累积弥补小 batch
learning_rate=2e-5,
num_train_epochs=3,
logging_steps=20,
save_strategy="epoch",
fp16=True,
optim="paged_adamw_8bit",
report_to="none",
remove_unused_columns=False,
gradient_checkpointing=False, # ✅ 关闭以避免过度显存占用
)

# ----------------------------------------------------
# 5. 启动训练
# ----------------------------------------------------

trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_data,
eval_dataset=val_data,
tokenizer=tokenizer,
)

trainer.train()

trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)

print("\n🎉 微调完成!模型与LoRA权重已保存到", OUTPUT_DIR)


3_inference.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel

# --- 配置参数 (请确保与训练时的配置一致) ---
# 基础模型(Qwen/Qwen1.5-1.8B-Chat)
BASE_MODEL = "Qwen/Qwen1.5-1.8B-Chat"
# 训练结果保存目录(LoRA 权重)
OUTPUT_DIR = "./qwen1.5_1.8b_lora"

# ----------------------------------------------------
# 1. 模型加载配置
# ----------------------------------------------------

# 4-bit 量化配置(必须与训练时一致)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True,
)

# 严格限制 VRAM 占用
MAX_VRAM_GB = 5.5
max_memory = {
0: f'{int(MAX_VRAM_GB * 1024)}MiB',
'cpu': '30GiB'
}

print(f"正在加载基础模型: {BASE_MODEL}...")

# 1. 加载基础模型(带量化)
# 注意:确保有足够的 GPU 内存
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
quantization_config=bnb_config,
device_map="auto",
max_memory=max_memory,
low_cpu_mem_usage=True,
)

# 2. 加载 LoRA 适配器权重
print(f"正在加载 LoRA 权重: {OUTPUT_DIR}...")
model = PeftModel.from_pretrained(model, OUTPUT_DIR)
model.eval() # 切换到评估模式

# 3. 加载分词器
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
tokenizer.pad_token = tokenizer.eos_token

print("模型和 LoRA 权重加载完成。")

# ----------------------------------------------------
# 2. 推理/测试函数
# ----------------------------------------------------

def generate_response(instruction, input_text=""):
"""
根据指令和输入生成模型的响应。
请确保 prompt 格式与训练时使用的 `format_prompt` 一致。
"""

# !!! 警告:请检查并修改 Qwen 模型实际使用的 Prompt 格式 !!!
# 当前格式:
prompt = f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"

# 编码输入
# 假设 CUDA 可用
device = "cuda" if torch.cuda.is_available() else "cpu"
inputs = tokenizer(prompt, return_tensors="pt", truncation=True).to(device)

# 生成设置
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=256, # 最大生成长度
do_sample=True, # 启用采样(更自然的文本)
top_p=0.9, # Top-p 采样
temperature=0.7, # 温度
pad_token_id=tokenizer.eos_token_id
)

# 解码结果并清理
response_text = tokenizer.decode(outputs[0], skip_special_tokens=True)

# 提取实际的 Response 部分
try:
# 找到 ### Response: 之后的内容
start_index = response_text.index("### Response:\n") + len("### Response:\n")
response = response_text[start_index:].strip()
except ValueError:
# 如果模型没有严格遵循格式,则返回整个解码结果
response = response_text.strip()

return response

# ----------------------------------------------------
# 3. 交互式测试循环
# ----------------------------------------------------

print("\n--- 启动交互式测试(输入 'exit' 或 'quit' 结束) ---")

while True:
# 接收指令
user_instruction = input("\n[请输指令 (或 exit/quit 退出)]: ").strip()

# 检查退出指令
if user_instruction.lower() in ['exit', 'quit']:
print("--- 退出交互式测试 ---")
break

# 接收可选的输入文本
user_input = input("[请输 Input 文本 (可选,直接回车跳过)]: ").strip()

print("--- 正在生成响应... ---")

try:
# 生成响应
response = generate_response(user_instruction, user_input)

# 打印结果
print("\n**模型响应:**")
print("=" * 20)
print(response)
print("=" * 20)

except Exception as e:
print(f"\n[错误]: 在生成响应时发生错误:{e}")
# 如果出现错误,继续循环而不是直接退出

您提出的问题非常关键:如何将目前在 Python 脚本中运行的模型,转换为像 Ollama 那样,可以通过网络 API 端口(如 11434)调用的服务。

当前您使用的 LoRA 模式和 Ollama 的原生使用方式存在一个技术障碍,但我们有完美的解决方案。

🎯 核心障碍与解决方案

技术障碍 解决方案
LoRA 模式: 您的模型是 Base Model (4bit 量化) + LoRA 适配器 分离运行的。 模型合并 (Merge): 将 LoRA 权重与基础模型的权重永久合并,生成一个全新的、可以直接加载的 全量模型
Ollama 支持: Ollama 主要设计用于加载标准的 Hugging Face 或 GGUF 格式的全量模型,它不直接支持加载分离的 LoRA 权重。 部署合并后的模型: 将合并后的模型转换成 GGUF 格式 或直接使用 FastAPI/Flask 搭建 API 服务

🛠️ 步骤一:合并 LoRA 适配器 (Merge and Unload)

这是将您的微调成果固化的关键一步。合并后,您将不再需要基础模型和 LoRA 文件夹,只需加载新的合并模型即可。

1. 创建合并脚本 (4_merge_model.py)

请在您的项目目录下创建 4_merge_model.py 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

# --- 配置参数 (请确保与训练时的配置一致) ---
BASE_MODEL = "Qwen/Qwen1.5-1.8B-Chat"
LORA_DIR = "./qwen1.5_1.8b_lora"
MERGED_OUTPUT_DIR = "./qwen1.5_1.8b_merged"
# ----------------------------------------------------

print(f"--- 1. 正在加载基础模型: {BASE_MODEL} (非量化) ---")
# ⚠️ 注意:合并时需要加载基础模型为全精度 (torch.float16),否则会导致精度丢失!
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float16,
device_map="auto",
low_cpu_mem_usage=True,
# 合并时不需要量化配置
)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)

print(f"--- 2. 正在加载 LoRA 权重: {LORA_DIR} ---")
# 使用 PeftModel 包装基础模型
model = PeftModel.from_pretrained(model, LORA_DIR)

print("--- 3. 正在执行 LoRA 权重合并 (Merge and Unload) ---")
# 核心操作:将 LoRA 权重合并到基础模型的权重中,并移除 PeftModel 包装
# 合并后的模型将是全精度 (float16)
merged_model = model.merge_and_unload()

print(f"--- 4. 正在保存合并后的模型到: {MERGED_OUTPUT_DIR} ---")
# 保存全量的合并模型(包含所有微调效果)
merged_model.save_pretrained(MERGED_OUTPUT_DIR)
tokenizer.save_pretrained(MERGED_OUTPUT_DIR)

print("\n🎉 模型合并完成!现在您可以使用这个目录部署您的专家模型了。")
print(f"合并后的模型已保存到:{MERGED_OUTPUT_DIR}")

2. 运行合并脚本

1
PS C:\Users\zhaoz\Desktop\大模型训练> & C:/Users/zhaoz/anaconda3/python.exe c:/Users/zhaoz/Desktop/大模型训练/4_merge_model.py

🚀 步骤二:部署合并后的模型 (实现 Ollama-Like 调用)

合并完成后,您有两种主流的部署方式来提供 API 服务:

选项 A:使用 Hugging Face transformers 搭建 API (推荐)

这是最直接、最灵活的方式,可以完美模拟 Ollama 的 API 接口。

  1. 安装依赖: 需要安装 FastAPIuvicorn 来搭建 Web 服务。

    1
    pip install fastapi uvicorn
  2. 创建 API 脚本 (5_api_server.py):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    import uvicorn
    import torch
    from transformers import AutoModelForCausalLM, AutoTokenizer

    # --- 配置参数 ---
    MERGED_OUTPUT_DIR = "./qwen1.5_1.8b_merged"
    # ------------------

    app = FastAPI(title="Fine-tuned LLM API")

    # 预留给模型的全局变量
    model = None
    tokenizer = None

    # 定义请求体的数据结构
    class GenerationRequest(BaseModel):
    instruction: str
    input_text: str = ""
    max_new_tokens: int = 256
    temperature: float = 0.7

    @app.on_event("startup")
    def load_model():
    """FastAPI 启动时加载模型"""
    global model, tokenizer
    try:
    print("--- API 启动中:正在加载合并后的模型 ---")

    # 使用 float16 节省显存,并加载到 GPU
    model = AutoModelForCausalLM.from_pretrained(
    MERGED_OUTPUT_DIR,
    torch_dtype=torch.float16,
    device_map="auto",
    )
    tokenizer = AutoTokenizer.from_pretrained(MERGED_OUTPUT_DIR)
    tokenizer.pad_token = tokenizer.eos_token
    model.eval()

    print("--- 模型加载成功!API 服务已准备就绪 ---")
    except Exception as e:
    print(f"模型加载失败: {e}")
    raise HTTPException(status_code=500, detail="模型加载失败,请检查路径和显存。")

    def format_prompt(instruction, input_text):
    # 必须与训练时的格式完全一致
    return f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"

    @app.post("/generate")
    def generate_endpoint(request: GenerationRequest):
    """API 核心推理端点"""
    prompt = format_prompt(request.instruction, request.input_text)

    inputs = tokenizer(prompt, return_tensors="pt", truncation=True).to("cuda")

    with torch.no_grad():
    outputs = model.generate(
    **inputs,
    max_new_tokens=request.max_new_tokens,
    temperature=request.temperature,
    do_sample=True,
    pad_token_id=tokenizer.eos_token_id,
    )

    response_text = tokenizer.decode(outputs[0], skip_special_tokens=True)

    # 提取 Response 部分 (与 3_inference.py 逻辑相同)
    try:
    start_index = response_text.index("### Response:\n") + len("### Response:\n")
    response = response_text[start_index:].strip()
    except ValueError:
    response = response_text.strip() # 回退到完整输出

    return {"response": response, "model_id": MERGED_OUTPUT_DIR}

    # 启动服务器 (在脚本末尾添加,注意:uvicorn 运行时端口是 8000)
    if __name__ == "__main__":
    # 在 PowerShell 中,运行此命令
    # uvicorn 5_api_server:app --host 0.0.0.0 --port 8000
    # 如果你想使用 11434 端口,改成 --port 11434
    print("\n请在终端运行:uvicorn 5_api_server:app --host 0.0.0.0 --port 11434")

  3. 运行 API 服务:

    1
    PS C:\Users\zhaoz\Desktop\大模型训练> uvicorn 5_api_server:app --host 0.0.0.0 --port 11434

    现在,您的模型就在 http://127.0.0.1:11434/generate 上提供服务了!您可以使用任何编程语言或工具向该地址发送 POST 请求进行调用。

选项 B:将模型导入到 Ollama (如果坚持使用 Ollama)

如果您希望在 Ollama 客户端中管理和运行此模型,可以采用以下流程:

  1. 转换到 GGUF 格式: Ollama 最好使用 GGUF 格式的模型。您需要使用 llama.cpp 工具链将合并后的 Safetensors 模型转换成 GGUF。这个过程相对复杂,需要安装额外的编译工具和依赖。
  2. 创建 Modelfile: 在 Ollama 中,您需要创建一个 Modelfile 来指向您转换的 GGUF 文件,并定义您的 Chat Prompt 模板,确保 Prompt 模板与训练时的格式一致(### Instruction:\n...\n\n### Response:\n)。
  3. 导入 Ollama: 使用 ollama create your_expert_model -f ./Modelfile 命令将模型导入 Ollama。

推荐: 对于初学者和需要灵活控制 API 的场景,选项 A(FastAPI/Uvicorn)是更快速、更直接的解决方案,它能让您即刻获得一个类似于 Ollama 的 API 调用体验。