들어가기 전에
지금까지 RAG 파이프라인의 여러 구성 요소를 다뤘습니다. 어떻게 청킹할지, 어떻게 검색할지, 답변의 신뢰성을 어떻게 높일지 말이죠. 그런데 이 모든 과정을 거쳐 시스템을 배포했다고 끝이 아닙니다.
실제 사용자가 시스템을 쓰기 시작하면 그때부터 가장 귀한 데이터가 쌓이기 시작합니다. 어떤 질문에 좋은 답변이 나왔고, 어떤 검색 결과가 클릭되었고, 어떤 답변이 도움이 되지 않았는지. 이 데이터를 그냥 버리면 아깝습니다.
배포 전에 아무리 좋은 벤치마크 데이터로 평가해도 실제 사용자의 질문 패턴을 완전히 예측하기는 어렵습니다.
사용자들이 예상치 못한 방식으로 질문하고, 도메인 특화 용어를 쓰고, 범용 임베딩 모델이 제대로 이해하지 못하는 표현을 사용하기도 합니다. 이런 문제들은 실제로 시스템을 운영해봐야만 발견됩니다.
반대로 생각하면 사용자 피드백은 시스템이 어디서 실패하는지 정확히 짚어주는 지도입니다. "이 질문에 대한 답변이 도움이 됐나요?"라는 물음에 사용자가 '아니오'를 눌렀다면 그 질문과 검색 결과는 시스템의 약점을 드러내는 귀한 데이터 포인트입니다.
이것을 모아서 시스템을 개선하는 과정이 피드백 루프입니다.
이번 글에서는 사용자 피드백을 RAG 시스템 개선에 활용하는 방법을 세 가지 관점에서 정리해보았습니다. 피드백이 왜 모델 품질을 높이는지를 설명하는 RLHF 개념, 어떤 피드백을 어떻게 수집할지에 대한 Implicit vs Explicit Feedback, 그리고 피드백으로 임베딩 모델을 직접 개선하는 Bi-encoder 파인튜닝 순서로 살펴보겠습니다
피드백이 왜 모델 품질을 올릴까?
모델은 왜 처음부터 완벽하지 않은가?
LLM은 인터넷의 방대한 텍스트를 학습해서 언어 패턴을 익힙니다. 그런데 이 사전 학습만으로는 '좋은 답변'을 만들기 어렵습니다. 왜냐하면 학습 데이터에는 좋은 글도 있지만 나쁜 글도 섞여 있고 무엇보다 '이 답변이 사용자에게 유용한가'를 텍스트 예측만으로는 판단할 수 없기 때문입니다.
요리사 비유로 생각해보면 이렇습니다. 사전 학습은 세상의 모든 요리책을 읽는 것과 같습니다.
레시피를 많이 안다고 해서 맛있는 음식을 만들 수 있는 건 아닙니다. 맛을 아는 사람이 먹어보고 '이건 너무 짜다', '이게 더 맛있다'고 피드백을 줘야 요리 실력이 늘어납니다. RLHF(Reinforcement Learning from Human Feedback)는 바로 이 피드백을 모델 학습에 통합하는 방법입니다.
RLHF의 세 단계
RLHF는 크게 세 단계로 이루어집니다.
첫 번째는 지도 학습 파인튜닝입니다. 사람이 직접 '이런 질문에는 이런 답변이 좋다'는 예시를 만들어 모델을 학습시킵니다.
이 단계에서 모델은 어느 정도 좋은 답변의 형태를 배웁니다. 하지만 사람이 직접 답변 예시를 만드는 것은 비용이 많이 드는 작업이라 이것만으로는 한계가 있습니다.
두 번째는 보상 모델 학습입니다. 같은 질문에 대해 모델이 여러 답변을 생성하면 사람이 어느 답변이 더 좋은지 비교해서 순위를 매깁니다. 답변을 직접 작성하는 것보다 비교해서 순위를 매기는 것이 훨씬 쉽기 때문에 데이터를 더 많이 수집할 수 있습니다.
이 선호도 데이터로 '보상 모델(Reward Model)'을 따로 학습시킵니다. 보상 모델은 어떤 답변이 사람이 선호할 만한지를 수치로 예측할 수 있게 됩니다.
세 번째는 강화 학습입니다. 보상 모델이 높은 점수를 줄 것 같은 답변을 생성하도록 원래 LLM을 강화 학습으로 최적화합니다. 모델이 답변을 생성하면 보상 모델이 점수를 매기고 점수가 높은 방향으로 모델의 파라미터가 업데이트됩니다. 인간의 판단을 수치로 바꿔서 자동화된 피드백 신호로 활용하는 것입니다.
RLHF가 실제로 효과적이라는 증거가 있습니다. OpenAI의 InstructGPT는 RLHF를 적용한 13억 파라미터 모델이 RLHF 없이 학습한 1750억 파라미터 GPT-3보다 사람 평가에서 더 높은 점수를 받았습니다. 크기가 100배 넘게 작아도 피드백으로 훨씬 더 유용한 모델을 만들 수 있다는 뜻입니다. Anthropic의 Claude, OpenAI의 GPT-4, Google의 Gemini 모두 RLHF 또는 이와 유사한 방식으로 훈련되었습니다.
RAG에서 RLHF를 직접 구현하는 건 아닙니다
여기서 중요한 맥락이 있습니다. RLHF는 대형 LLM 자체를 훈련할 때 사용하는 기법일 뿐이지 일반적으로 RAG 시스템을 만드는 팀이 직접 구현하는 건 아닙니다. GPT-4나 Claude 같은 모델은 이미 RLHF를 거쳐서 배포됩니다.
그렇다면 왜 이 개념을 알아야 할까요? 피드백이 왜 중요한지의 원리를 이해하기 위해서입니다. RLHF의 핵심 통찰은 '사람이 좋다고 판단하는 것을 모델이 직접 예측하게 하면 성능이 올라간다'는 것입니다. 이 원리는 RAG 시스템에서도 동일하게 적용됩니다. 사용자가 어떤 검색 결과를 클릭했는지, 어떤 답변에 좋아요를 눌렀는지, 어떤 질문에서 계속 재검색을 했는지, 등등 이런 피드백을 수집해서 시스템을 개선하는 것이 바로 RAG 레벨의 피드백 루프입니다.
LLM 전체를 재학습하는 것은 아니지만 검색 파이프라인과 임베딩 모델은 이 피드백으로 충분히 개선할 수 있습니다.
어떤 피드백을 어떻게 모을까 (Implicit vs Explicit Feedback)
피드백의 종류는 크게 두 가지로 나뉩니다. 사용자가 의식적으로 제공하는 Explicit Feedback과 사용자 행동에서 자동으로 수집되는 Implicit Feedback입니다.
어느 쪽이 더 좋다고 단정 짓기 어렵고 실무에서는 두 가지의 특성을 이해하고 적절히 조합해서 쓰는 것이 중요합니다.
직접 물어보는 방식인 Explicit Feedback
사용자가 명시적으로 의사를 표현하는 피드백입니다. 엄지 위로/아래로 버튼, 별점, '도움이 됐나요?' 설문이 대표적이죠.
이 방식의 장점은 신호가 명확하다는 것입니다. '좋아요'를 눌렀다면 그 답변이 유용했다는 것이고, '나빠요'를 눌렀다면 유용하지 않았다는 것입니다. 모호함이 없어서 학습 데이터로 바로 사용할 수 있습니다. 특히 어떤 부분이 나빴는지 텍스트로 남기게 하면 시스템의 어느 단계(검색인지, 생성인지)를 개선해야 하는지 구체적으로 파악할 수 있습니다.
단점은 수집이 어렵다는 것입니다. 대부분의 사용자는 굳이 버튼을 누르지 않습니다. 일반적으로 전체 사용자의 5~10% 정도만 명시적 피드백을 남깁니다.
그렇게 되면 의견을 남기는 사용자는 매우 만족했거나 매우 불만족한 극단적인 케이스가 많아 편향이 생길 수 밖에 없습니다.
평범하게 만족한 사용자는 대부분 아무 버튼도 누르지 않고 지나갑니다.
# Explicit Feedback 수집 패턴 예시
def collect_explicit_feedback(query: str, answer: str, rating: int, comment: str = ""):
"""
rating: 1(매우 불만족) ~ 5(매우 만족)
comment: 선택적 텍스트 피드백 (어떤 점이 나빴는지 등)
"""
feedback_data.append({
"query": query,
"answer": answer,
"rating": rating,
"comment": comment,
"timestamp": datetime.now().isoformat(),
"type": "explicit"
})
행동에서 읽어내는 방식인 Implicit Feedback
사용자가 의식하지 않은 채로 남기는 행동 데이터입니다. 클릭, 체류 시간, 추가 질문 여부, 복사 여부 등이 여기에 해당합니다.
장점은 수집량이 많다는 것입니다. 모든 사용자 인터랙션에서 자동으로 쌓이게 되고 사용자에게 별도의 행동을 요구하지 않아도 됩니다. Explicit Feedback이 5~10%의 사용자에게서만 나온다면 Implicit Feedback은 100%의 사용자 행동에서 수집됩니다.
단점은 해석이 어렵다는 것입니다. 답변 텍스트를 복사했다면 유용했다는 신호일 수 있지만 오류 메시지를 복사해서 검색하러 간 것일 수도 있습니다. 짧은 체류 시간이 빠르게 답을 찾았다는 의미인지 아니면 도움이 안 돼서 바로 떠났다는 의미인지 알기 어렵습니다. 그래서 하나의 시그널만으로 판단하지 말고 여러 시그널을 조합해서 해석하는 것이 중요합니다.
# Implicit Feedback 수집 패턴 예시
def collect_implicit_feedback(
query: str,
retrieved_docs: list,
answer: str,
clicked_doc_ids: list, # 사용자가 클릭한 출처 문서
time_spent_seconds: int, # 답변 화면에 머문 시간
copied_text: bool, # 텍스트 복사 여부
followup_query: str = None # 이어서 한 질문
):
# 여러 시그널을 조합해 점수 계산
implicit_score = 0
if clicked_doc_ids:
implicit_score += 2 # 출처 클릭은 강한 긍정 신호
if time_spent_seconds > 30:
implicit_score += 1 # 충분히 읽었을 가능성
if copied_text:
implicit_score += 2 # 복사는 유용했다는 강한 신호
if followup_query:
implicit_score -= 1 # 바로 다른 질문 → 불만족 가능성
feedback_data.append({
"query": query,
"answer": answer,
"implicit_score": implicit_score,
"signals": {
"clicked_docs": clicked_doc_ids,
"time_spent": time_spent_seconds,
"copied": copied_text,
"had_followup": bool(followup_query)
},
"type": "implicit"
})
위 코드에서 각 시그널에 가중치를 다르게 준 것을 볼 수 있습니다. 출처 클릭(+2)과 텍스트 복사(+2)는 강한 긍정 신호, 30초 이상 체류(+1)는 약한 긍정 신호, 이어서 다른 질문을 한 것(-1)은 약한 부정 신호로 처리했습니다.
이 가중치는 도메인과 제품에 따라 달라질 수 있어서 직접 데이터를 분석하면서 조정해야 합니다.
두 피드백을 어떻게 활용할까?
실무에서는 두 가지를 함께 쓰는 것이 좋습니다. Implicit Feedback으로 데이터를 많이 쌓고, Explicit Feedback으로 신호의 질을 검증하는 식입니다. 예를 들어 Implicit Score가 높은 사례와 낮은 사례가 실제로 Explicit 평점과 얼마나 일치하는지를 주기적으로 확인하면서 가중치를 조정해나갈 수 있습니다.
수집된 피드백 데이터가 쌓이면 두 가지로 활용할 수 있습니다. 하나는 RAG 파이프라인의 어떤 부분이 문제인지 진단하는 것입니다. 특정 유형의 질문에서 계속 낮은 평점이 나온다면 검색 단계를 개선하거나, 프롬프트를 수정하거나, 관련 문서를 추가해야 한다는 신호입니다. 다른 하나는 임베딩 모델 파인튜닝의 학습 데이터로 사용하는 것입니다. 이것이 다음 섹션에서 다룰 내용입니다.
피드백으로 임베딩 모델을 직접 개선하는 Bi-encoder 파인튜닝
Bi-encoder란 무엇인가
RAG에서 벡터 검색의 핵심은 임베딩 모델입니다. 질문과 문서를 각각 벡터로 변환한 뒤 유사도를 계산해서 관련 문서를 찾습니다. 이 때 사용하는 아키텍처가 Bi-encoder입니다.
Bi-encoder는 이름 그대로 두 개의 인코더를 가집니다. 질문을 처리하는 인코더와 문서를 처리하는 인코더가 독립적으로 동작하죠.
문서는 미리 임베딩으로 변환해서 저장해두 검색 시점에는 질문만 인코딩하면 됩니다. 그래서 수백만 개의 문서가 있어도 빠른 검색이 가능합니다.
Cross-encoder처럼 질문과 문서를 함께 입력받아 처리하는 방식은 더 정확하지만 모든 문서 쌍을 실시간으로 계산해야 해서 대규모 검색에는 적합하지 않습니다. Bi-encoder가 RAG 검색에 주로 쓰이는 이유가 여기에 있지요.
범용 임베딩 모델(예: all-MiniLM-L6-v2)은 다양한 도메인에서 잘 동작하도록 학습되어 있습니다.
하지만 특정 도메인에서는 그 도메인의 용어와 개념을 반영해서 파인튜닝한 모델이 훨씬 더 잘 동작합니다.
예를 들어 의료 문서 RAG에서는 '심근경색'과 'heart attack'이 같은 의미라는 것을 '백혈구 수치'와 'WBC count'가 같은 개념이라는 것을 임베딩 모델이 알아야 합니다. 범용 모델은 이런 도메인 특화 관계를 충분히 학습하지 못했을 수 있고 그 결과 관련 문서가 검색 결과에서 밀려나는 일이 생깁니다. 사용자 피드백에서 쌓인 '이 질문에 이 문서가 관련 있었다'는 데이터가 바로 이 문제를 해결하는 열쇠입니다.
피드백 데이터로 학습 데이터 만들기
사용자 피드백에서 학습 데이터를 만드는 핵심 아이디어는 간단합니다. 사용자가 좋다고 판단한 (질문, 문서) 쌍을 Positive 샘플로
나쁘다고 판단한 쌍을 Negative 샘플로 만드는 것입니다.
def create_training_pairs_from_feedback(feedback_data: list) -> list:
"""
피드백 데이터에서 (질문, 긍정 문서) 쌍 생성
"""
training_pairs = []
for feedback in feedback_data:
query = feedback["query"]
retrieved_docs = feedback["retrieved_docs"]
if feedback["type"] == "explicit":
if feedback["rating"] >= 4:
# 높은 평점 → 검색된 상위 문서가 관련 있었다고 판단
positive_doc = retrieved_docs[0]
training_pairs.append({
"query": query,
"positive": positive_doc,
})
elif feedback["rating"] <= 2:
# 낮은 평점 → 검색이 실패했다고 판단
# 관리자가 올바른 문서를 수동으로 레이블링하면 더 좋음
pass
elif feedback["type"] == "implicit":
clicked = feedback["signals"]["clicked_docs"]
if clicked:
# 클릭된 문서 → 관련 있다고 판단
for doc_id in clicked:
training_pairs.append({
"query": query,
"positive": doc_id,
})
return training_pairs
낮은 평점의 경우 pass로 처리한 부분이 보일 겁니다. 답변 생성 단계에서 실패했을 수도 있기 때문에 평점이 낮다고 해서 검색된 문서가 무조건 틀린 건 아닙니다. 이런 케이스는 관리자가 직접 올바른 문서를 레이블링하거나 RAGAS 같은 평가 지표로 검색 단계와 생성 단계의 실패를 구분한 뒤에 학습 데이터로 사용하는 것이 더 안전합니다.
sentence-transformers로 파인튜닝하기
학습 데이터가 준비되었다면 sentence-transformers 라이브러리로 파인튜닝할 수 있습니다.
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
# 1. 사전 학습된 모델 로드
model = SentenceTransformer("all-MiniLM-L6-v2")
# 2. 학습 데이터 준비 (질문-긍정문서 쌍)
train_examples = [
InputExample(texts=["심근경색 치료법은?", "급성 심근경색의 치료는 빠른 혈관 재개통이 핵심입니다..."]),
InputExample(texts=["WBC 수치가 높으면?", "백혈구 수치가 정상 범위를 초과하면 감염이나 염증을 의심합니다..."]),
# ... 더 많은 (질문, 관련 문서) 쌍
]
# 3. DataLoader 설정
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# 4. 손실 함수: MultipleNegativesRankingLoss
# 같은 배치의 다른 쌍들이 자동으로 Negative 샘플이 됨
train_loss = losses.MultipleNegativesRankingLoss(model)
# 5. 파인튜닝
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=3,
warmup_steps=100,
output_path="./my-finetuned-model",
show_progress_bar=True,
)
# 6. 파인튜닝된 모델로 교체
finetuned_model = SentenceTransformer("./my-finetuned-model")
MultipleNegativesRankingLoss가 핵심입니다. 이 손실 함수는 같은 배치 안의 다른 (질문, 문서) 쌍들을 자동으로 Negative 샘플로 사용합니다.
만약 배치 크기가 16이라면 하나의 (질문, 문서) 쌍에 대해 나머지 15개의 문서가 자동으로 Negative가 됩니다.
이 덕분에 '이 질문에는 이 문서가 관련 없다'는 Negative 데이터를 따로 만들지 않아도 됩니다. 단, 배치 크기가 클수록 더 많은 Negative 샘플이 생겨 학습이 효과적이므로 GPU 메모리가 허용하는 한 배치 크기를 키우는 것이 좋습니다.
얼마나 효과가 있을까?
실제 실험 결과를 보면 놀라운데요. Sentence Transformers 파인튜닝 가이드에 따르면 의료 QA 데이터셋에서 4,719개의 질문-문서 쌍만으로 파인튜닝한 결과 더 크고 강력한 bge-base-en-v1.5 모델에 버금가는 검색 성능을 달성했습니다.
학습 시간은 GPU에서 약 1분, 비용은 0.1달러 미만이었습니다.
물론 사용자 피드백에서 나온 학습 데이터는 이보다 노이즈가 많을 수 있습니다. 하지만 도메인에 특화된 (질문, 관련 문서) 쌍이 수천 개만 있어도 의미 있는 개선이 가능하다는 것을 보여줍니다. 처음부터 완벽한 데이터를 기다리기보다는 피드백이 쌓일 때마다 주기적으로 파인튜닝을 반복하는 것이 현실적인 접근입니다.
파이프라인에 통합하기
class FeedbackDrivenRAG:
def __init__(self, embedding_model_path: str):
self.model = SentenceTransformer(embedding_model_path)
self.feedback_buffer = []
self.retrain_threshold = 1000 # 피드백 1000개 쌓이면 재학습
def add_feedback(self, query: str, doc: str, is_relevant: bool):
self.feedback_buffer.append({
"query": query,
"doc": doc,
"relevant": is_relevant
})
if len(self.feedback_buffer) >= self.retrain_threshold:
self.retrain()
def retrain(self):
"""피드백 데이터로 임베딩 모델 재학습"""
positive_pairs = [
InputExample(texts=[f["query"], f["doc"]])
for f in self.feedback_buffer
if f["relevant"]
]
if len(positive_pairs) < 100:
return # 데이터 부족 시 재학습 스킵
dataloader = DataLoader(positive_pairs, shuffle=True, batch_size=16)
loss = losses.MultipleNegativesRankingLoss(self.model)
self.model.fit(
train_objectives=[(dataloader, loss)],
epochs=1,
output_path="./updated-model",
)
self.model = SentenceTransformer("./updated-model")
self.feedback_buffer = []
print(f"모델 재학습 완료. 사용된 피드백: {len(positive_pairs)}개")
retrain_threshold를 1000으로 설정한 것은 하나의 예시입니다. 너무 자주 재학습하면 노이즈에 과적합될 수 있고, 너무 드물게 하면 개선이 늦어집니다. 실제 서비스에서는 트래픽 규모에 따라 주 1회, 월 1회처럼 시간 기반 주기로 설정하거나, 파인튜닝 전후 검색 성능을 검증하는 단계를 추가하는 것이 좋습니다.
중간 정리
| 개념 | 역할 | 핵심 |
| RLHF | 피드백이 왜 효과적인지 원리 이해 | 사람의 선호를 수치화해서 모델 최적화 신호로 활용 |
| Implicit vs Explicit | 어떤 피드백을 어떻게 수집할지 | Explicit은 정확하지만 희소, Implicit은 많지만 해석 필요 |
| Bi-encoder 파인튜닝 | 피드백으로 검색 모델 자체를 개선 | (질문, 관련 문서) 쌍으로 임베딩 공간 재학습 |
이 세 가지는 하나의 사이클을 이룹니다. Implicit/Explicit Feedback으로 데이터를 수집하고 그 중 사람이 좋다고 판단한 것을 신호로 삼아 Bi-encoder 파인튜닝으로 임베딩 모델을 개선합니다.
피드백 수집 → 신호 추출 → 모델 개선, 이 흐름을 반복하는 것이 피드백 루프입니다
마치며
RAG 시스템은 배포 후가 진짜 시작입니다. 사용자가 쌓아주는 피드백은 어떤 벤치마크보다 실제 사용 환경에 맞는 신호입니다. 그 신호를 그냥 버리지 말고 시스템을 더 좋게 만드는 데 활용해보세요.
사이드 프로젝트 규모처럼 작게 시작해도 됩니다. 엄지 위로/아래로 버튼 하나만 추가해도 Explicit Feedback이 쌓이기 시작하거든요. 그리고 클릭 로그를 남기기만 해도 Implicit Feedback이 되죠. 몇 백 개의 (질문, 좋은 문서) 쌍이 모이면 파인튜닝을 시도해볼 수 있습니다.
데이터 플라이휠(data flywheel)이라는 말이 있습니다. 좋은 시스템이 더 많은 사용자를 불러오고, 더 많은 사용자가 더 많은 피드백을 남기고, 더 많은 피드백이 시스템을 더 좋게 만드는 선순환입니다. 피드백 루프를 만드는 것은 그 선순환의 시작입니다.
참고 자료
'AI' 카테고리의 다른 글
| [바미] 텍스트가 아닌 정보를 RAG는 어떻게 읽을까? (2) | 2026.03.31 |
|---|---|
| [바미] AI가 거짓말을 하는지 어떻게 알 수 있을까? - LLM이 그 답변 봐봐! 혹시 사쿠라야? (2) | 2026.03.30 |
| [바미] 구글도 키워드 검색을 버리지 않은 이유 (하이브리드 검색과 RAG-Fusion) (0) | 2026.03.29 |
| [바미] 내가 크롤링한 결과물이 쓰레기가 되는 이유 (2) | 2026.03.28 |
| [바미] 어떻게 자르느냐가 검색 품질을 결정한다. (0) | 2026.03.27 |