一次使用 RAFT 和 Qwen3 实现端到端领域RAG自适应

最近,我参与了一个项目,其目标是使用特定于领域的数据为客户微调一个小语言模型。在探索了一些方法之后,我决定使用 RAFT(检索增强微调),以充分利用检索和监督学习。

为此,我使用 unsloth 对 qwen3-4b 模型进行了微调。工作流程非常简单 — 首先,我从客户的文件中生成训练数据,然后对其进行清理和结构化,应用聊天风格的模板,最后开始微调过程。

我们最终得到了很好的结果——尤其是在微调了该领域的上下文嵌入模型以及与之相关的 Re-ranker 之后。我在之前的文章中已经介绍了这些部分,我将在这篇文章的末尾链接到它们。在这里,重点是事物的生成方面 — 我们如何构建和塑造数据,以实际教会模型需要了解的内容。

RAFT:使语言模型适应特定领域的 RAG

None

RAFT — 检索增强微调的缩写 — 是一种将检索增强生成与监督微调相结合的方法。Raft 不仅在静态数据上训练模型,还在训练过程中引入相关文档(有时是不相关的文档),帮助模型学习如何在实际上下文中构建答案。这不仅提高了准确性,还减少了幻觉,尤其是在特定领域的任务中。这就像教模型用手里的笔记进行推理 - 并知道何时信任它们。

Qwen3

None

Qwen3 是阿里巴巴推出的最新一代开源语言模型,从 0.6b 和 4b 等紧凑版本一直到具有 235b 参数的大规模 moes。它引入了混合推理设计,让模型在“思考”模式(用于数学、编码或逻辑等复杂任务)和“非思考”模式之间动态切换,以实现更快、更一般的响应。除了推理之外,Qwen3 还支持长上下文窗口(最多 ~128K 个令牌)、跨 119 种语言的多语言理解,以及密集和混合专家架构——这是使用 peft、TRL、Unsloth 等系统进行微调的强大基础…

Unsloth

None

Unsloth 是一个开源微调库,可以更快、更高效地适应 LLM。通过将关键的 PyTorch作替换为自定义的 Triton GPU 内核,它可以将训练速度提高大约 2×,并将内存使用量减少多达 70%,所有这些都不会降低准确性。

它与 Hugging Face、peft 和 trl 工作流程完全兼容,在从入门级 T4 到高端 H100 的 Nvidia GPU 上支持 LoRA/QLoRA,即使在免费的 Colab/Kaggle 环境中也能流畅运行。速度、效率和开源灵活性的结合使 Unsloth 非常适合 RAFT 和 qwen3 微调等更多实验性管道。

代码时间 :

安装依赖项

我们首先使用 llama-indexunsloth 和支持包安装所有必需的库以进行微调和检索。

%%capture
!pip install llama-index
!pip install llama-index-packs-raft-dataset
!pip install llama-index-llms-google-genai llama-index-embeddings-google-genai
%%capture
import os
if "COLAB_" not in "".join(os.environ.keys()):
    !pip install unsloth
else:
    # Do this only in Colab notebooks! Otherwise use pip install unsloth
    !pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft trl triton cut_cross_entropy unsloth_zoo
    !pip install sentencepiece protobuf "datasets>=3.4.1" huggingface_hub hf_transfer
    !pip install --no-deps unsloth

正如我们在上面看到的,设置会根据我们是在 colab 中运行还是在本地环境中运行而自动调整 — 仅在需要时安装额外的低级依赖项。

生成 RAFT 训练数据集

在此步骤中,我们使用 llama-index 的 RAFTDatasetPack 准备数据集以进行检索增强微调。

import os

from llama_index.packs.raft_dataset import RAFTDatasetPack
from llama_index.llms.google_genai import GoogleGenAI
from llama_index.embeddings.google_genai import GoogleGenAIEmbedding



GOOGLE_API_KEY = "xxxxxx-xxxxxxxxxx"
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY

llm = GoogleGenAI(model="gemini-2.5-flash-preview-05-20")

embed_model = GoogleGenAIEmbedding(model_name="text-embedding-004")

file_path = "/path/to.txt" # or md or pdf ..

raft_dataset = RAFTDatasetPack(file_path ,
                               llm=llm ,
                               embed_model=embed_model ,
                               num_questions_per_chunk=3 ,
                               num_distract_docs=3 ,
                               chunk_size=2048)

dataset = raft_dataset.run()

正如我们在上面看到的,Gemini 2.5 模型处理问题的生成,而 Google 的嵌入 API 创建向量表示。数据集是从源文件(TXT、MD 或 PDF)构建的,其中每个块都与生成的问题和干扰项文档配对,以模拟检索场景。最终输出是一个结构化数据集,可用于监督式微调。

格式化和保存数据集

生成初始 RAFT 数据集后,我们将其转换为 Hugging Face 兼容的格式。

import datasets

df = dataset.to_pandas()

# Combine 'user' and 'assistant' columns into a new 'messages' column as list of dictionaries
df['messages'] = df.apply(lambda row: [{'content': row['instruction'], 'role': 'user'},
                                       {'content': row['cot_answer'], 'role': 'assistant'}], axis=1)
dataset = datasets.Dataset.from_pandas(df)


# Optional (save the dataset)

output_path = "data"
dataset.save_to_disk(output_path)
dataset.to_json(output_path + ".jsonl")

Eeach 数据点被重塑为带有userassistant消息的聊天式结构,以匹配指令调整标准。最后,数据集既可以保存到磁盘,也可以保存为 .jsonl 文件,以便灵活地进行下游训练或检查。

使用 unsloth 加载基本模型

在这里,我们使用 unsloth 优化的 FastLanguageModel 包装器加载 qwen3-4b-128k 模型。

from unsloth import FastLanguageModel
import torch

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-4B-128K",
    max_seq_length =4096 ,   # Context length - can be longer, but uses more memory
    load_in_4bit = True,     # 4bit uses much less memory
    load_in_8bit = False,    # A bit more accurate, uses 2x memory
    full_finetuning = False, # We have full finetuning now!
    # token = "hf_...",      # use one if using gated models
)
import re
import random
from multiprocessing import cpu_count

# Set chat template
DEFAULT_CHAT_TEMPLATE = "{% for message in messages %}\n{% if message['role'] == 'user' %}\n{{ '<|user|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'system' %}\n{{ '<|system|>\n' + message['content'] + eos_token }}\n{% elif message['role'] == 'assistant' %}\n{{ '<|assistant|>\n'  + message['content'] + eos_token }}\n{% endif %}\n{% if loop.last and add_generation_prompt %}\n{{ '<|assistant|>' }}\n{% endif %}\n{% endfor %}"
tokenizer.chat_template = DEFAULT_CHAT_TEMPLATE


SYSTEM_PROMPT = """
You are a helpful question answerer who can provide an answer given a question and relevant context.
"""

def apply_chat_template(example, tokenizer):
    messages = example["messages"]
    # We add an empty system message if there is none
    if messages[0]["role"] != "system":
        messages.insert(0, {"role": "system", "content": SYSTEM_PROMPT})
    example["text"] = tokenizer.apply_chat_template(messages, tokenize=False)

    return example

column_names = list(dataset.features)
raw_datasets = dataset.map(apply_chat_template,
                                num_proc=cpu_count(),
                                fn_kwargs={"tokenizer": tokenizer},
                                remove_columns=column_names,
                                desc="Applying chat template",)

正如我们之前看到的,每次交互都包括一条user消息和一个assistant响应——在这里,我们还插入了一个system提示来指导助手的行为。最终输出是一个使用 tokenizer.apply_chat_template 构建的干净、模型就绪的文本字段,使用所有可用的 CPU 内核高效并行应用。

使用 SFTTrainer 配置 LoRA 微调

在本节中,我们使用 unsloth 和 trlSFTTrainer 设置基于 LoRA 的微调。

from trl import SFTTrainer, SFTConfig

raw_datasets = raw_datasets.train_test_split(test_size=0.1)
# create the splits
train_dataset = raw_datasets["train"]
eval_dataset = raw_datasets["test"]

model = FastLanguageModel.get_peft_model(
    model,
    r = 32,           # Choose any number > 0! Suggested 8, 16, 32, 64, 128
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 32,  # Best to choose alpha = rank or rank*2
    lora_dropout = 0, # Supports any, but = 0 is optimized
    bias = "none",    # Supports any, but = "none" is optimized
    # [NEW] "unsloth" uses 30% less VRAM, fits 2x larger batch sizes!
    use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context
    random_state = 3407,
    use_rslora = False,   # We support rank stabilized LoRA
    loftq_config = None,  # And LoftQ
)

trainer = SFTTrainer(
    model = model,
    train_dataset = train_dataset,
    eval_dataset = eval_dataset,
    args = SFTConfig(
        dataset_text_field = "text",
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4, # Use GA to mimic batch size!
        warmup_steps = 5,
        # num_train_epochs = 1, # Set this for 1 full training run.
        max_steps = 30,
        learning_rate = 2e-4, # Reduce to 2e-5 for long training runs
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        report_to = "none", # Use this for WandB etc
    ),
)

tokenizer.pad_token = tokenizer.eos_token

该模型使用 get_peft_model 包装,以关键投影层为目标,并使用低秩自适应策略来减少训练开销。我们将数据集分为 Train 和 Test,然后为 Trainer 配置适度的训练步骤、梯度累积和 8 位优化以提高效率。使用 Unsloth 的优化后端启用梯度检查点,以最少的内存使用量支持更长的序列。这种设置平衡了速度、稳定性和对特定领域数据的适应性。

启动微调过程
train_result = trainer.train()

最后,我们使用 trainer.train() 开始监督微调。正如我们之前设置的所有内容(从数据准备到模型优化)一样,此步骤将运行实际的训练循环,使用 LoRA 配置和我们的格式化数据集将基本 Qwen3 模型适应目标域。结果是一个更能感知上下文、更高效的模型,针对特定于域的任务进行了优化。

测试微调模型

作为最后一步,我们使用结构化提示(包括系统指南和用户查询)测试微调后的模型。

messages = [
     {"role" : "system", "content" : SYSTEM_PROMPT},
    {"role" : "user", "content" : "<Context> + Question"}
]
text = tokenizer.apply_chat_template(
    messages,
    tokenize = False,
    add_generation_prompt = True, # Must add for generation
    enable_thinking = False, # Disable thinking
)

from transformers import TextStreamer
_ = model.generate(
    **tokenizer(text, return_tensors = "pt").to("cuda"),
    max_new_tokens = 256, # Increase for longer outputs!
    temperature = 0.7, top_p = 0.8, top_k = 20, # For non thinking
    streamer = TextStreamer(tokenizer, skip_prompt = True),
)

我们重复使用聊天模板并设置 add_generation_prompt=True 以触发生成。禁用思考模式以获得更快、更简单的响应。使用 TextStreamer 实时流式传输输出,使我们能够快速验证模型在特定于域的输入上的行为。

保存和重新加载微调的模型

训练后,我们将模型和 Tokenizer 都保存在本地的 "lora_model" 下以备将来使用。

model.save_pretrained("lora_model")  # Local saving
tokenizer.save_pretrained("lora_model")
# model.push_to_hub("your_name/lora_model", token = "...") # Online saving
# tokenizer.push_to_hub("your_name/lora_model", token = "...") # Online saving

# Now if you want to load the LoRA adapters we just saved for inference, set False to True:

if False:
    from unsloth import FastLanguageModel
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name = "lora_model", # YOUR MODEL YOU USED FOR TRAINING
        max_seq_length = 2048,
        load_in_4bit = True,
    )

或者,可以将相同的对象推送到 Hugging Face Hub 进行共享或部署。如上所示,重新加载保存的 LoRA 适配器非常简单 — 只需将 FastLanguageModel.from_pretrained 指向保存的目录并重新启用 4 位加载即可。这可确保模型可移植,并准备好进行推理或进一步调整。

导出模型以进行部署
# Merge to 16bit
if False:
    model.save_pretrained_merged("model", tokenizer, save_method = "merged_16bit",)
if False: # Pushing to HF Hub
    model.push_to_hub_merged("hf/model", tokenizer, save_method = "merged_16bit", token = "")

# Merge to 4bit
if False:
    model.save_pretrained_merged("model", tokenizer, save_method = "merged_4bit",)
if False: # Pushing to HF Hub
    model.push_to_hub_merged("hf/model", tokenizer, save_method = "merged_4bit", token = "")

# Just LoRA adapters
if False:
    model.save_pretrained_merged("model", tokenizer, save_method = "lora",)
if False: # Pushing to HF Hub
    model.push_to_hub_merged("hf/model", tokenizer, save_method = "lora", token = "")

Unsloth 支持以多种格式保存模型以进行部署:

  • 使用 merged_16bitmerged_4bit 进行 float16/int4 导出
  • 仅使用 save_method="lora" 保存 LoRA 适配器
# Save to 8bit Q8_0
if False:
    model.save_pretrained_gguf("model", tokenizer,)
# Remember to go to https://huggingface.co/settings/tokens for a token!
# And change hf to your username!
if False:
    model.push_to_hub_gguf("hf/model", tokenizer, token = "")

# Save to 16bit GGUF
if False:
    model.save_pretrained_gguf("model", tokenizer, quantization_method = "f16")
if False: # Pushing to HF Hub
    model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "f16", token = "")

# Save to q4_k_m GGUF
if False:
    model.save_pretrained_gguf("model", tokenizer, quantization_method = "q4_k_m")
if False: # Pushing to HF Hub
    model.push_to_hub_gguf("hf/model", tokenizer, quantization_method = "q4_k_m", token = "")

# Save to multiple GGUF options - much faster if you want multiple!
if False:
    model.push_to_hub_gguf(
        "hf/model", # Change hf to your username!
        tokenizer,
        quantization_method = ["q4_k_m", "q8_0", "q5_k_m",],
        token = "", # Get a token at https://huggingface.co/settings/tokens
    )
  • 对于llama.cpp,请使用 q4_k_mq5_k_mq8_0 等定量方法导出到 GGUF

本文介绍了使用 RAFT 方法微调小型语言模型的端到端过程。我们首先使用 llama-index 和 AI生成结构化训练数据,应用聊天风格的格式,并使用 Unsloth 微调 Qwen3 模型。整个目标是保持高效并针对域进行定制。我们还研究了如何保存和导出模型以进行部署。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI仙人掌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值