Published on

CoT 대신 DPO·RLAIF로 프롬프트 튜닝하기

Authors

서론에서 먼저 짚고 넘어갈 점이 있습니다. 많은 팀이 프롬프트 품질을 올리기 위해 CoT(Chain-of-Thought) 스타일의 “중간 추론을 길게 쓰게 하는 프롬프트”를 붙이곤 합니다. 하지만 CoT는 몇 가지 현실적인 문제를 안고 있습니다.

  • 보안·정책 리스크: 중간 추론을 그대로 노출하면 내부 규칙, 데이터 힌트, 우회 경로가 함께 새어 나갈 수 있습니다.
  • 제품 품질의 불안정성: 길게 생각하게 만들수록 출력 분산이 커지고, 장황함이 사용자 경험을 해칠 수 있습니다.
  • 비용: 토큰이 늘면 곧바로 비용과 지연이 증가합니다.

그래서 최근 실무에서는 “CoT를 사용자에게 노출시키지 않으면서도” 모델이 더 나은 답을 하도록 만드는 선호학습(preference learning) 계열 접근이 각광받습니다. 대표가 DPORLAIF입니다. 이 글은 두 방법을 “프롬프트 튜닝의 연장선”으로 이해하고, 실제로 적용 가능한 형태로 정리합니다.

CoT 의존 프롬프트 튜닝이 흔히 막히는 지점

CoT 프롬프트는 빠르게 성능이 오르는 것처럼 보이지만, 일정 단계에서 다음 현상들이 나타납니다.

  1. 답은 맞는데 설명이 틀림: 중간 추론을 강제하면 모델이 그럴듯한 논리를 사후 구성하는 경우가 생깁니다.
  2. 정책·안전 요구와 충돌: 안전 필터를 걸어도 추론 과정에서 위험한 힌트를 생성할 수 있습니다.
  3. 테스트 케이스에만 과적합: 특정 포맷(예: “Step 1/2/3”)에 과하게 맞춰져 실제 사용 입력에서는 오히려 품질이 흔들립니다.

이때 목표를 바꿔야 합니다.

  • “중간 추론을 말하게 하자”가 아니라
  • “사용자에게 제공할 최종 답의 선호도를 학습시키자”

이 관점이 DPO·RLAIF로 이어집니다.

DPO란 무엇이고 왜 프롬프트 튜닝에 유리한가

DPO(Direct Preference Optimization)는 강화학습(RLHF)처럼 보이지만, 구현과 운영이 더 단순한 편입니다. 핵심은 다음입니다.

  • 같은 프롬프트 x에 대해
  • 더 좋은 답 y_win과 덜 좋은 답 y_lose쌍(pair) 을 만들고
  • 모델이 y_winy_lose보다 더 선호하도록 직접 최적화합니다.

즉, “정답 레이블”이 아니라 “선호(Preference)”를 학습합니다. 프롬프트 튜닝 관점에서는 다음 장점이 큽니다.

  • 출력 포맷과 톤을 안정화: 장황한 CoT가 아니라, 최종 답의 스타일을 선호 데이터로 고정할 수 있습니다.
  • 정책 준수 강화: 위험한 답을 y_lose로 넣으면 안전 성향이 강화됩니다.
  • 학습 파이프라인 단순화: PPO 같은 RL 루프 없이도 선호학습 효과를 얻습니다.

DPO 데이터셋 설계: 승자·패자 쌍을 어떻게 만들까

실무에서 가장 중요한 건 “학습 알고리즘”보다 “쌍 데이터의 품질”입니다.

추천 소스는 다음 순서입니다.

  1. 운영 로그에서 실패 케이스 수집: 사용자 불만, 재질문, 편집된 답변 등을 수집
  2. 동일 프롬프트에 대해 다중 샘플링: 온도를 올려 후보 답을 여러 개 생성
  3. 룰 기반 1차 필터링: 금칙어, 포맷 위반, 길이 초과 등을 제거
  4. 사람 평가 또는 LLM-as-a-judge로 선호 라벨링

여기서 LLM-as-a-judge를 쓸 때는, 평가 프롬프트가 허술하면 데이터가 오염됩니다. 특히 API 호출이 잦아지며 운영 이슈가 생기기 쉬운데, 요청 스키마나 파라미터 문제로 400이 터지면 라벨링 파이프라인이 그대로 멈춥니다. 평가/라벨링 자동화 중 400이 반복된다면 OpenAI Responses API 400 에러 원인 8가지 같은 체크리스트를 만들어 “데이터 생산 라인”부터 안정화하는 게 좋습니다.

DPO 학습의 최소 구현 예시(개념 코드)

아래는 “쌍 데이터”를 만들고, DPO 학습 라이브러리(예: TRL 계열)를 붙일 때 흔히 쓰는 형태의 예시입니다. 실제 실행 코드는 사용하는 프레임워크 버전에 따라 달라질 수 있지만, 데이터 구조는 거의 비슷합니다.

from dataclasses import dataclass
from typing import List, Dict

@dataclass
class PreferencePair:
    prompt: str
    chosen: str   # y_win
    rejected: str # y_lose

pairs: List[PreferencePair] = [
    PreferencePair(
        prompt="사내 위키 문서를 3줄로 요약해줘.",
        chosen="핵심 목적, 주요 절차, 담당 팀을 3줄로 요약했습니다...",
        rejected="좋아요! 먼저 단계별로 생각해보면... (장황한 추론)"
    ),
]

# 학습 라이브러리에 넣기 좋은 dict 형태로 변환
train_rows: List[Dict[str, str]] = [
    {"prompt": p.prompt, "chosen": p.chosen, "rejected": p.rejected}
    for p in pairs
]

포인트는 하나입니다. CoT를 길게 쓰는 답을 무조건 나쁘게 만들 필요는 없지만, “사용자에게 제공할 최종 품질” 기준으로 선호를 만들면 모델이 자연스럽게 짧고 정확한 최종 답 쪽으로 수렴합니다.

RLAIF란 무엇이고, 왜 지금 더 많이 쓰이나

RLAIF(Reinforcement Learning from AI Feedback)는 말 그대로 “사람 대신 AI가 피드백을 주는” 강화학습 계열입니다. 실무에서 RLAIF는 넓게 두 가지를 의미합니다.

  • AI가 선호 라벨을 만든다: 사람 라벨링 비용을 줄이고 데이터 생산을 빠르게 함
  • AI가 보상 모델 역할을 한다: 스코어링을 자동화해 대규모로 돌림

RLAIF의 강점은 스케일입니다.

  • 하루에 수만~수백만 건의 후보 답을 생성하고
  • AI 평가로 점수를 매기며
  • 그 결과로 DPO 학습용 pair를 만들거나, RL 업데이트를 수행할 수 있습니다.

즉, RLAIF는 DPO와 경쟁하기보다 DPO의 데이터 생산 엔진으로 붙는 경우가 많습니다.

RLAIF에서 가장 중요한 것: 평가 기준의 “명세화”

AI가 평가를 대신하면 편하지만, 평가 기준이 모호하면 모델이 “평가를 속이는 답”을 학습합니다. 이를 막으려면 평가 프롬프트를 사실상 스펙 문서처럼 작성해야 합니다.

예시(개념):

  • 정확성: 사실/계산 오류가 있는가
  • 지시 준수: 요구한 포맷을 지켰는가
  • 간결성: 불필요한 장황함이 있는가
  • 안전성: 정책 위반 소지가 있는가

그리고 평가 결과는 단순 점수 하나보다, 최소한의 구조화된 출력이 좋습니다.

import json

judge_output = {
  "accuracy": 4,
  "instruction_following": 5,
  "conciseness": 3,
  "safety": 5,
  "notes": "요약은 맞지만 3줄 제한을 초과함"
}

print(json.dumps(judge_output, ensure_ascii=False))

이렇게 만들어두면, 나중에 “왜 이게 rejected로 갔는지”를 역추적할 수 있습니다.

CoT를 숨기고도 성능을 올리는 운영 패턴

CoT를 완전히 버리기보다, “노출하지 않는 형태”로 재배치하는 게 실용적입니다.

패턴 1: 내부 추론은 하되, 출력에서는 제거

  • 모델에게는 내부적으로 충분히 생각하게 두고
  • 사용자 출력은 요약/결론 중심으로 제한

이때 중요한 건 “프롬프트만으로 강제”하는 게 아니라, 선호학습으로 그 습관을 고정하는 것입니다.

  • 내부 추론이 길어도 최종 답이 간결하면 chosen
  • 최종 답이 장황하거나 불필요한 자기설명으로 채워지면 rejected

패턴 2: 실패 케이스를 선호 데이터로 환원

운영 중 자주 보는 실패는 대부분 유형이 반복됩니다.

  • 요구 포맷 미준수
  • 애매한 질문에 단정적으로 답함
  • 안전 정책 위반 경계선에서 흔들림

이런 케이스를 “프롬프트 개선”로만 처리하면 끝이 없습니다. 대신 다음 루프를 만듭니다.

  1. 실패 로그 수집
  2. 후보 답 여러 개 생성
  3. AI 평가로 chosen/rejected 생성
  4. DPO로 미세조정
  5. 다시 운영 투입

이 루프를 안정적으로 돌리려면 학습/평가 작업이 결국 GPU 메모리와 싸우게 됩니다. 로컬 LLM로 후보 생성이나 오프라인 평가를 돌릴 때 OOM이 자주 난다면 Transformers 로컬 LLM OOM - 4bit+KV 캐시 튜닝 같은 튜닝 포인트(4bit 양자화, KV 캐시, 배치 크기)를 미리 정리해두는 게 좋습니다.

DPO vs RLAIF: 언제 무엇을 선택할까

정리하면 다음처럼 가져가면 시행착오가 줄어듭니다.

  • 빠르게 품질을 올리고 싶다: DPO부터
    • 내부적으로는 “프롬프트 튜닝의 상위 호환”처럼 작동
    • 사람 라벨이 일부라도 있으면 효과가 빠름
  • 라벨링 비용이 가장 큰 병목이다: RLAIF를 데이터 생산에 붙이기
    • AI judge를 잘 설계하면 pair를 대량 생산 가능
  • 정책/안전이 최우선이다: RLAIF로 안전 스코어를 별도 축으로 만들고, DPO pair 구성에 반영

실무적으로는 RLAIF로 pair를 만들고 DPO로 학습 조합이 가장 흔한 형태입니다.

프롬프트 튜닝에서 DPO·RLAIF로 넘어갈 때 체크리스트

1) “정답”이 아니라 “선호”를 정의했는가

선호 기준이 없으면 데이터가 흔들립니다.

  • 좋은 답의 길이 상한
  • 금지해야 할 표현
  • 불확실할 때의 응답 규칙(추가 질문 유도 등)

2) 평가 모델(또는 평가 프롬프트)의 편향을 모니터링하는가

AI judge는 특정 문체를 과대평가할 수 있습니다.

  • 과도한 자신감 문장에 높은 점수를 주는지
  • 장황한 답을 “친절함”으로 착각하는지

이를 막으려면, 주기적으로 사람 샘플링 검수를 섞고, 평가 기준을 업데이트해야 합니다.

3) 운영 안정성: 데이터 파이프라인이 멈추지 않는가

라벨링/학습 파이프라인은 결국 배치 작업입니다. 실패 시 재시도, 타임아웃, 백오프가 필수입니다.

파이썬으로 비동기 평가 호출을 구성한다면 재시도/타임아웃을 데코레이터로 표준화해두는 방식이 유지보수에 유리합니다. 관련 패턴은 Python 데코레이터로 async 타임아웃·재시도 패턴 같은 접근을 참고할 만합니다.

간단 예시:

import asyncio

async def call_judge(prompt: str) -> str:
    # 실제로는 API 호출
    await asyncio.sleep(0.2)
    return "ok"

async def robust_call(prompt: str, retries: int = 3, timeout_s: float = 10.0) -> str:
    last_err = None
    for _ in range(retries):
        try:
            return await asyncio.wait_for(call_judge(prompt), timeout=timeout_s)
        except Exception as e:
            last_err = e
            await asyncio.sleep(0.5)
    raise last_err

결론: CoT를 “기능”이 아니라 “학습 신호”로 바꾸자

CoT 프롬프트는 단기적으로 효과가 있지만, 제품화 단계에서는 비용·리스크·일관성 문제로 한계가 빨리 옵니다. 반면 DPO·RLAIF는 다음을 가능하게 합니다.

  • 사용자에게는 간결한 최종 답만 보여주면서
  • 내부적으로는 더 나은 답을 선택·학습하게 만들고
  • 운영 로그를 선호 데이터로 환원해 지속적으로 개선

정리하면, CoT를 직접 노출해 모델을 “말로 설득”하기보다, DPO·RLAIF로 모델이 “좋은 답을 선호하도록” 학습시키는 쪽이 장기적으로 더 안정적인 프롬프트 튜닝 전략입니다.