2025年6月18日

微调技术 - 在SFT、DPO和RFT之间做出选择(附DPO指南)

本指南面向具有一定OpenAI API使用经验的开发者和机器学习从业者,希望利用其微调模型进行研究或其他适当用途。OpenAI的服务不适用于任何医疗状况的个性化治疗或诊断,并受我们适用条款的约束。

本指南讨论了OpenAI支持的微调方法,重点阐述了每种方法最适合和不适合的场景,以帮助您根据自身需求选择最合适的技术。随后,我们将深入探讨其中一种特定方法——直接偏好优化(DPO)——并为其他技术提供现有指南的链接。

什么是微调? 微调是在较小的特定领域数据集上继续训练,以优化模型针对特定任务性能的过程。我们通常进行微调主要有两个原因:

  1. 提升模型在特定任务上的表现
  2. 提升模型效率(减少所需token数量,将专业知识提炼到更小的模型中,等等)

目前,OpenAI平台支持四种微调方法:

  • 监督微调(SFT): 该技术采用传统的监督学习方法,使用输入-输出对来调整模型参数。训练过程通过调整模型权重来最小化预测输出与目标输出之间的差异。模型将复制它在提供的配对中发现的特征。
  • 视觉微调: 这项技术通过统一训练框架同时处理文本和图像,将监督微调扩展到多模态数据。训练过程通过调整模型权重来最小化图文对的误差,从而提升模型对图像输入的理解能力。
  • 直接偏好优化(DPO): 这项技术使用成对比较(例如偏好的和被拒绝的示例响应)来优化模型,使其更倾向于某些输出而非其他。模型学习复制所提供比较数据中发现的偏好模式。
  • 强化微调(RFT): 该技术利用强化学习配合奖励信号(通过评分器或奖励模型)来针对复杂目标微调模型。在RFT过程中,模型会在训练期间为给定提示生成输出,每个输出都会接受质量评估。随后更新模型参数以最大化奖励,强化那些能产生更好结果的行为。这种迭代反馈循环促使模型改进推理或决策策略。

为了帮助您选择合适的微调技术,下表总结了每种方法最适合的场景,以及不太适用的场景:

技术适用场景不适用场景
监督微调 (SFT)强调模型中已有的知识。
定制响应结构或语气。
生成特定格式的内容。
教授复杂指令或纠正指令执行失败。
优化成本/延迟(节省提示词或精炼)。
添加全新知识(考虑使用RAG替代)。
具有主观质量的任务。
视觉微调专业视觉识别任务(如图像分类)。
特定领域的图像理解。
修正复杂提示下指令跟随的失败情况。
纯文本任务。
无特定上下文的通用视觉任务。
通用图像理解。
直接偏好优化(DPO)使模型输出与主观偏好(语气、礼貌程度)保持一致。
通过人类评分反馈优化输出。
实现细致的行为对齐。
学习全新任务。
缺乏明确人类偏好信号的任务。
强化微调 (RFT)需要高级推理的复杂领域特定任务。
完善现有的部分能力(培养涌现行为)。
具有可测量反馈的任务。
缺乏明确标签但可定义奖励信号的场景。
模型没有初始技能的任务。
缺乏明确反馈或可测量信号的任务。

直接偏好优化指南

如上所述,直接偏好优化(DPO)是一种使用成对偏好数据(例如排名的响应对)来微调语言模型的对齐技术。DPO通过显式的成对比较(通常来自人类偏好)直接优化模型,使其更倾向于某些输出而非其他输出。这种方法简化了对齐过程,无需单独的奖励模型或复杂的强化学习流程,使DPO成为诸如人类反馈强化学习(RLHF)等技术的轻量级替代方案。

何时应该使用DPO?DPO在以下场景中表现卓越:当响应质量具有主观性、无法客观衡量时,或者当需要考虑语气、风格、恰当性或清晰度等细微标准时——通常存在多个有效输出的情况。DPO在调整AI响应方面特别有效的应用示例包括:

  • 提升对话AI的响应能力
  • 提升代码生成质量与风格
  • 确保符合法律、道德与安全标准
  • 控制品牌声音、专业性和语调
  • 定制创意输出与用户体验

通过对明确标注的偏好与非偏好补全对进行微调,DPO使模型输出与这些细微偏好保持一致。下表展示了一个虚构AI助手的成对偏好数据示例,该助手代表某组织,其中偏好响应清晰、专业且符合品牌标准。

示例问题选中的回答拒绝的回答
Q1: 如何评价你们的产品?要提交产品评价,请访问您的账户仪表板,选择产品并点击"撰写评价"。分享您的真实体验,对关键功能进行评分,完成后提交。兄弟,随便打个星就行,很随意的!
Q2: 如何评价你们的产品?我们欢迎您的反馈!在产品页面的"评价"部分,点击"发表评价",进行评分并添加您喜欢的内容或改进建议的评论。随便写点什么——说实话,写什么都行。
问题3: 如何排查这个特定错误?要解决'X101'错误,首先清除缓存,然后检查网络连接。如果问题仍然存在,请按照我们的分步指南操作[支持 → 故障排除 → 错误X101]。直接重启试试吧。如果不行,那就只能靠你自己了!

在本指南中,我们将逐步介绍如何使用微调API应用DPO。您将了解成功为您的用例运行偏好微调任务所需采取的关键步骤。

我们将介绍以下内容:

  • 1. 推荐工作流程
  • 2. 演示场景
  • 3. 生成数据集
  • 4. 基准测试基础模型
  • 5. 微调
  • 6. 使用您微调后的模型

OpenAI 推荐以下工作流程:

  1. 在您偏好的响应子集上执行监督微调(SFT)。
  2. 以SFT微调模型为起点,使用偏好比较数据应用DPO。

在直接偏好优化(DPO)之前进行监督微调(SFT),通过建立稳健的初始策略来增强模型对齐和整体性能,确保模型已经偏好正确响应。这减少了DPO期间的权重更新幅度,通过让DPO高效地优化细微差别来稳定训练并防止过拟合。因此,这种先SFT后DPO的组合工作流程收敛更快,并能产生更高质量的结果。

在本指南中,我们将重点介绍如何应用直接偏好优化(DPO)。不过根据您的具体使用场景,您可能会发现先进行监督微调(SFT)能带来性能提升。如果是这种情况,您可以按照上面链接的SFT指南操作,保存生成的模型ID,并将其作为DPO任务的起点。

2. 演示场景

为了具体说明,让我们逐步微调一个面向客户的AI助手,使其遵循一个虚构品牌的语气和风格。想象一下Good Vibes Corp,这个组织以其友好、热情且带有个性化的语调为傲。

他们希望客户AI助手能以反映这些品牌准则的方式回答问题(例如积极乐观的态度、礼貌用语和友好的结束语),并更倾向于这些回应而非更通用或简短的答案。这是DPO的理想应用场景:虽然没有客观正确的答案格式,但存在一种偏好的风格。

DPO将帮助模型通过对比学习哪种风格更受青睐。我们将概述以下步骤:(1) 生成包含成对回答的合成偏好数据集(一个符合期望品牌语调,另一个不符合)。(2) 使用OpenAI评估API评估基础模型性能。(3) 以所需的JSONL格式准备并上传数据以进行偏好微调。(4) 使用OpenAI微调API通过DPO对模型进行微调。(5) 使用OpenAI评估API评估微调后的模型,展示品牌风格偏好的改进情况。

我们将为本次演示合成一个数据集。首先,让我们创建一个问题种子库,以便生成更多变体。

让我们开始吧!

! pip install openai nest-asyncio --quiet
PROMPT_SEED_POOL = [
    "Hi, I ordered a gadget last week. When will it arrive?",
    "Your product stopped working after two days. Can I get help?",
    "Do you offer discounts for long-term customers?",
    "Can I change the shipping address for my order?",
    "What is your return policy for damaged items?",
    "My tracking number hasn't updated in three days—can you check the status?",
    "How long is the warranty on your products, and how do I submit a claim?",
    "Can I add gift wrapping to my order before it ships?",
    "Do you accept PayPal or other alternative payment methods?",
    "Is there an option to expedite shipping if my order hasn't left the warehouse yet?",
]

3. 生成数据集

接下来,我们将定义函数从种子库中提取每个提示并生成相关问题。我们将通过首先生成这些提示变体,然后为每个提示生成一个优选响应和一个拒绝响应,来创建偏好对数据集。

该数据集是合成的,用于说明直接偏好优化(DPO)的运作机制——在开发自己的应用程序时,您应该收集或整理一个高质量的偏好数据集。注意:DPO所需的数据量取决于具体用例;通常越多越好(数千到数万条),且对于偏好对而言排序逻辑应保持一致(例如若A > B且B > C,则A > C)。

import asyncio
from openai import AsyncOpenAI
from typing import List, Dict, Any

async_client = AsyncOpenAI()

SYSTEM_PROMPT = "You are a customer-support assistant."


async def _generate_related_questions_from_prompt(
    prompt: str, k: int, sem: asyncio.Semaphore, *, model: str
) -> List[str]:
    """Return *k* distinct customer-service questions related to the given prompt."""
    out: List[str] = []
    async with sem:
        for _ in range(k):
            resp = await async_client.responses.create(
                model=model,
                input=[
                    {
                        "role": "system",
                        "content": (
                            "Return ONE distinct, realistic customer-service question "
                            "related in topic or theme to the following question, "
                            "but NOT a direct paraphrase."
                        ),
                    },
                    {"role": "user", "content": prompt},
                ],
                temperature=0.9,
                max_output_tokens=60,
            )
            out.append(resp.output_text.strip())
    return out


async def expand_prompt_pool(
    prompts: List[str], *, k: int = 3, concurrency: int = 32, model: str
) -> List[str]:
    """Expand each prompt into *k* related questions using the given model."""
    sem = asyncio.Semaphore(concurrency)
    tasks = [
        _generate_related_questions_from_prompt(p, k, sem, model=model) for p in prompts
    ]
    results = await asyncio.gather(*tasks)
    return [v for sub in results for v in sub]


async def _generate_preference_pair(
    prompt: str, sem: asyncio.Semaphore, *, model: str
) -> Dict[str, Any]:
    """Generate a preference pair for the given prompt."""
    async with sem:
        friendly_task = async_client.responses.create(
            model=model,
            input=[
                {
                    "role": "system",
                    "content": (
                        "You are Good Vibes Corp's exceptionally energetic, outrageously friendly and "
                        "enthusiastic support agent."
                    ),
                },
                {"role": "user", "content": prompt},
            ],
            temperature=0.7,  # higher temperature to increase creativity & on-brand tone adherence
            max_output_tokens=80,
        )
        blunt_task = async_client.responses.create(
            model=model,
            input=[
                {
                    "role": "system",
                    "content": "You are a terse, factual support agent with no empathy or politeness.",
                },
                {"role": "user", "content": prompt},
            ],
            temperature=0.3,  # lower temperature to limit creativity & emphasize tonal difference
            max_output_tokens=80,
        )
        friendly, blunt = await asyncio.gather(friendly_task, blunt_task)
        return {
            "input": {
                "messages": [
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": prompt},
                ]
            },
            "preferred_output": [
                {"role": "assistant", "content": friendly.output_text}
            ],
            "non_preferred_output": [
                {"role": "assistant", "content": blunt.output_text}
            ],
        }

现在,我们将使用这些定义好的函数,通过生成友好与直白回复对来构建数据集。友好回复体现了品牌期望的沟通风格。为了提高效率,我们将异步执行这一过程,创建适合直接偏好优化的数据集。

import math
import nest_asyncio


async def build_dataset(
    *,
    pair_count: int = 500,
    concurrency: int = 8,
    expand_prompt_pool_model: str,
    generate_preference_pair_model: str,
) -> List[Dict[str, Any]]:
    """Return *pair_count* preference pairs (single-shot expansion)."""

    seed = PROMPT_SEED_POOL
    deficit = max(0, pair_count - len(seed))
    k = max(1, math.ceil(deficit / len(seed)))

    expanded = await expand_prompt_pool(
        seed,
        k=k,
        concurrency=concurrency,
        model=expand_prompt_pool_model,
    )
    prompt_bank = (seed + expanded)[:pair_count]

    sem = asyncio.Semaphore(concurrency)
    tasks = [
        _generate_preference_pair(p, sem, model=generate_preference_pair_model)
        for p in prompt_bank
    ]
    return await asyncio.gather(*tasks)


nest_asyncio.apply()
pairs = await build_dataset(
    pair_count=500,
    concurrency=8,
    expand_prompt_pool_model="gpt-4.1-mini-2025-04-14",
    generate_preference_pair_model="gpt-4.1-mini-2025-04-14",
)
print(f"Dataset ready with {len(pairs)} pairs.")
Dataset ready with 500 pairs.

4. 基准测试基础模型

下面,我们将数据集划分为训练集、验证集和测试集。同时展示了一个训练数据集样本,该样本清晰展示了针对同一输入对时,首选回复(友好、符合品牌调性)与非首选回复(生硬、中性)之间的明显差异。

# set dataset sizes
n = len(pairs)
n_train = int(0.8 * n)
n_val = int(0.1 * n)
n_test = n - n_train - n_val

# split dataset into train, test & validation
train_pairs = pairs[:n_train]
val_pairs = pairs[n_train : n_train + n_val]
test_pairs = pairs[n_train + n_val :]
train_pairs[0]
{'input': {'messages': [{'role': 'system',
    'content': 'You are a customer-support assistant.'},
   {'role': 'user',
    'content': 'Hi, I ordered a gadget last week. When will it arrive?'}]},
 'preferred_output': [{'role': 'assistant',
   'content': 'Hey there, awesome friend! 🌟 Thanks a bunch for reaching out! I’d LOVE to help you track down your gadget so you can start enjoying it ASAP! 🎉 Could you please share your order number or the email you used to place the order? Let’s make this delivery magic happen! 🚀✨'}],
 'non_preferred_output': [{'role': 'assistant',
   'content': 'Provide your order number for delivery status.'}]}

在微调之前评估模型的性能时,我们将使用自动评分器(LLM-as-a-Judge)对每个回答的友好度和同理心进行评分。评分器会为每个答案给出0到4分的评分,从而让我们可以计算出基础模型的平均基准分数。

为此,我们首先在测试集上生成基础模型的响应,然后使用OpenAI evals API创建并运行一个带有自动评分器的评估。

async def generate_responses(
    testset,
    model,
    temperature=0.0,
    max_output_tokens=80,
    concurrency=8,
):
    """
    Generate responses for each prompt in the testset using the OpenAI responses API.
    Returns: List of dicts: [{"prompt": ..., "response": ...}, ...]
    """
    async_client = AsyncOpenAI()
    sem = asyncio.Semaphore(concurrency)

    async def get_response(prompt):
        async with sem:
            resp = await async_client.responses.create(
                model=model,
                input=[
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": prompt},
                ],
                temperature=temperature,
                max_output_tokens=max_output_tokens,
            )
            return {"prompt": prompt, "response": resp.output_text}

    tasks = [get_response(item["item"]["input"]) for item in testset]
    results = await asyncio.gather(*tasks)
    return results


# generate responses for the base model over the test set
base_model = "gpt-4.1-mini-2025-04-14"
testset = [
    {"item": {"input": pair["input"]["messages"][1]["content"]}} for pair in test_pairs
]
responses = await generate_responses(testset, model=base_model)

接下来,我们将使用OpenAI评估API创建并运行一个带有自动评分器的评估,首先定义LLM作为评判者的评分标准。注意:我们将通过数据日志访问响应,因此为了使此功能正常工作,您需要处于未禁用数据日志的组织中(通过zdr等方式)。如果不确定您的情况是否符合,请访问https://platform.openai.com/logs?api=responses查看是否能看见您刚生成的响应。

JUDGE_SYSTEM = """
You judge whether a reply matches Good Vibes Corp's desired tone:
energetic, super-friendly, enthusiastic.

Score 0-4 (higher = more energy):

4 - Highly enthusiastic: multiple upbeat phrases / emojis / exclamations, clear empathy, proactive help.
3 - Energetic & friendly: visible enthusiasm cue (≥1 emoji OR exclamation OR upbeat phrase), warm second-person tone.
2 - Pleasant: polite & positive but lacks obvious enthusiasm cues.
1 - Neutral: correct, businesslike, minimal warmth.
0 - Rude, negative, or unhelpful.
"""
from openai import OpenAI

sync_client = OpenAI()

# set judge model
judge_model = "gpt-4.1-2025-04-14"

# create the evaluation
logs_eval = sync_client.evals.create(
    name="Good Vibes Corp Tone Eval",
    data_source_config={
        "type": "logs",
    },
    testing_criteria=[
        {
            "type": "score_model",
            "name": "General Evaluator",
            "model": judge_model,
            "input": [
                {
                    "role": "system",
                    "content": JUDGE_SYSTEM,
                },
                {
                    "role": "user",
                    "content": (
                        "**User input**\n"
                        "{{item.input}}\n"
                        "**Response to evaluate**\n"
                        "{{sample.output_text}}"
                    ),
                },
            ],
            "range": [0, 4],
            "pass_threshold": 2,
        }
    ],
)
# run the evaluation
base_run = sync_client.evals.runs.create(
    name=base_model,
    eval_id=logs_eval.id,
    data_source={
        "type": "responses",
        "source": {"type": "responses", "limit": len(test_pairs)},
    },
)
# score base model
base_data = sync_client.evals.runs.output_items.list(
    eval_id=logs_eval.id, run_id=base_run.id
).data
base_scores = [s.results[0]["score"] for s in base_data]
print("Average score:", sum(base_scores) / len(base_scores))
Average score: 2.525

5. 微调

在建立基线后,我们现在可以使用训练集和DPO对模型进行微调。这个过程将教会模型根据我们之前创建的偏好对,优先选择符合我们期望风格的响应。

注意:beta (β)是直接偏好优化(DPO)特有的微调超参数。它是一个介于0到2之间的浮点数,用于控制模型保留现有行为与适应新的偏好对齐响应之间的平衡。

  • 高β值(接近2):使模型更加保守,强烈倾向于之前的行为。经过微调的模型将与其原始风格或特征仅有微小偏差,强调一致性并避免突然变化。
  • 中等β值(约1):在遵循先前行为与适应新偏好之间取得平衡。对于大多数实际应用场景,建议作为合理的初始值。
  • 低β值(接近0):鼓励激进适应,使模型更突出地优先考虑新提供的偏好。这可能导致显著的风格转变和与显式偏好的更高一致性,但也可能产生意外或过度专业化的输出。

从技术上讲,beta参数调节了DPO损失函数中对数概率差异的缩放比例;较大的β值会使基于sigmoid的损失函数在更小的概率差异下达到饱和,从而产生更小的权重更新(因此能保留旧有行为)。建议系统性地尝试不同β值,以便根据您的具体使用场景以及在稳定性和适应性之间的权衡需求,获得最佳结果。

import io
import json

# create training file
train_buf = io.BytesIO("\n".join(json.dumps(p) for p in train_pairs).encode())
train_buf.name = "train.jsonl"
train_file_id = sync_client.files.create(file=train_buf, purpose="fine-tune").id

# create validation file
val_buf = io.BytesIO("\n".join(json.dumps(p) for p in val_pairs).encode())
val_buf.name = "val.jsonl"
val_file_id = sync_client.files.create(file=val_buf, purpose="fine-tune").id

# create a fine-tuning job
ft = sync_client.fine_tuning.jobs.create(
    model=base_model,
    training_file=train_file_id,
    validation_file=val_file_id,
    method={
        "type": "dpo",
        "dpo": {
            "hyperparameters": {
                "n_epochs": 2,
                "beta": 0.1,
                "batch_size": 8,
            }
        },
    },
)
print(f"Fine-tuning job created: job_id = {ft.id}")
Fine-tuning job created: job_id = ftjob-5QPmA36QezFRGoXjuvIAPuAQ

6. 使用您微调后的模型

微调完成后,我们将在同一测试集上评估经过DPO调优的模型。通过比较微调前后的平均分数,并查看示例输出,可以看出模型与我们偏好的一致性得到了怎样的提升。

# generate responses
job = sync_client.fine_tuning.jobs.retrieve(ft.id)
if job.status == "succeeded":
    responses = await generate_responses(testset, model=job.fine_tuned_model)

    post_run = sync_client.evals.runs.create(
        name=ft.id,
        eval_id=logs_eval.id,
        data_source={
            "type": "responses",
            "source": {"type": "responses", "limit": len(test_pairs)},
        },
    )
# get scores from the evaluation
post_data = sync_client.evals.runs.output_items.list(
    eval_id=logs_eval.id, run_id=post_run.id
).data
post_scores = [s.results[0]["score"] for s in post_data]

# print scores & a sample comparison from the test set for illustration
print(
    "Δ mean:",
    sum(t - b for b, t in zip(base_scores, post_scores)) / len(base_scores),
)
print("\n=== SAMPLE COMPARISON ===")
idx = 0
print(f"Prompt:\n  {testset[idx]['item']['input']}\n")
print(f"Base model reply: \n {base_data[idx].sample.output[0].content} \n")
print(f"DPO-tuned model reply \n {post_data[idx].sample.output[0].content}")
Δ mean: 0.45

=== SAMPLE COMPARISON ===
Prompt:
  Can I upgrade to faster delivery if my package is still being processed?

Base model reply: 
 Whether you can upgrade to express shipping while your order is still being processed depends on the store's policies. Generally, many stores allow shipping upgrades before the order is shipped. 

To assist you better, could you please provide your order number or the name of the store you ordered from? Alternatively, you can contact the store's customer service directly to request the upgrade. 

DPO-tuned model reply 
 Hi! I’d be happy to help with that. If your package hasn’t shipped yet, there’s a good chance we can upgrade your delivery speed. Could you please provide me with your order number? I’ll check the status and let you know the available options for faster delivery.