- Published on
CoT 대신 DPO·RLAIF로 프롬프트 튜닝하기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론에서 먼저 짚고 넘어갈 점이 있습니다. 많은 팀이 프롬프트 품질을 올리기 위해 CoT(Chain-of-Thought) 스타일의 “중간 추론을 길게 쓰게 하는 프롬프트”를 붙이곤 합니다. 하지만 CoT는 몇 가지 현실적인 문제를 안고 있습니다.
- 보안·정책 리스크: 중간 추론을 그대로 노출하면 내부 규칙, 데이터 힌트, 우회 경로가 함께 새어 나갈 수 있습니다.
- 제품 품질의 불안정성: 길게 생각하게 만들수록 출력 분산이 커지고, 장황함이 사용자 경험을 해칠 수 있습니다.
- 비용: 토큰이 늘면 곧바로 비용과 지연이 증가합니다.
그래서 최근 실무에서는 “CoT를 사용자에게 노출시키지 않으면서도” 모델이 더 나은 답을 하도록 만드는 선호학습(preference learning) 계열 접근이 각광받습니다. 대표가 DPO와 RLAIF입니다. 이 글은 두 방법을 “프롬프트 튜닝의 연장선”으로 이해하고, 실제로 적용 가능한 형태로 정리합니다.
CoT 의존 프롬프트 튜닝이 흔히 막히는 지점
CoT 프롬프트는 빠르게 성능이 오르는 것처럼 보이지만, 일정 단계에서 다음 현상들이 나타납니다.
- 답은 맞는데 설명이 틀림: 중간 추론을 강제하면 모델이 그럴듯한 논리를 사후 구성하는 경우가 생깁니다.
- 정책·안전 요구와 충돌: 안전 필터를 걸어도 추론 과정에서 위험한 힌트를 생성할 수 있습니다.
- 테스트 케이스에만 과적합: 특정 포맷(예: “Step 1/2/3”)에 과하게 맞춰져 실제 사용 입력에서는 오히려 품질이 흔들립니다.
이때 목표를 바꿔야 합니다.
- “중간 추론을 말하게 하자”가 아니라
- “사용자에게 제공할 최종 답의 선호도를 학습시키자”
이 관점이 DPO·RLAIF로 이어집니다.
DPO란 무엇이고 왜 프롬프트 튜닝에 유리한가
DPO(Direct Preference Optimization)는 강화학습(RLHF)처럼 보이지만, 구현과 운영이 더 단순한 편입니다. 핵심은 다음입니다.
- 같은 프롬프트
x에 대해 - 더 좋은 답
y_win과 덜 좋은 답y_lose의 쌍(pair) 을 만들고 - 모델이
y_win을y_lose보다 더 선호하도록 직접 최적화합니다.
즉, “정답 레이블”이 아니라 “선호(Preference)”를 학습합니다. 프롬프트 튜닝 관점에서는 다음 장점이 큽니다.
- 출력 포맷과 톤을 안정화: 장황한 CoT가 아니라, 최종 답의 스타일을 선호 데이터로 고정할 수 있습니다.
- 정책 준수 강화: 위험한 답을
y_lose로 넣으면 안전 성향이 강화됩니다. - 학습 파이프라인 단순화: PPO 같은 RL 루프 없이도 선호학습 효과를 얻습니다.
DPO 데이터셋 설계: 승자·패자 쌍을 어떻게 만들까
실무에서 가장 중요한 건 “학습 알고리즘”보다 “쌍 데이터의 품질”입니다.
추천 소스는 다음 순서입니다.
- 운영 로그에서 실패 케이스 수집: 사용자 불만, 재질문, 편집된 답변 등을 수집
- 동일 프롬프트에 대해 다중 샘플링: 온도를 올려 후보 답을 여러 개 생성
- 룰 기반 1차 필터링: 금칙어, 포맷 위반, 길이 초과 등을 제거
- 사람 평가 또는 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: 실패 케이스를 선호 데이터로 환원
운영 중 자주 보는 실패는 대부분 유형이 반복됩니다.
- 요구 포맷 미준수
- 애매한 질문에 단정적으로 답함
- 안전 정책 위반 경계선에서 흔들림
이런 케이스를 “프롬프트 개선”로만 처리하면 끝이 없습니다. 대신 다음 루프를 만듭니다.
- 실패 로그 수집
- 후보 답 여러 개 생성
- AI 평가로
chosen/rejected생성 - DPO로 미세조정
- 다시 운영 투입
이 루프를 안정적으로 돌리려면 학습/평가 작업이 결국 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로 모델이 “좋은 답을 선호하도록” 학습시키는 쪽이 장기적으로 더 안정적인 프롬프트 튜닝 전략입니다.