RAG 쿼리 변환 (RAG Query Transformation, RAG 쿼리 변환)
2026.10.03 · 16분
1. 쿼리 변환이란?
쿼리 변환(Query Transformation)은 사용자 질문을 검색하기 좋은 형태로 바꾼 뒤 검색에 넘기는 단계입니다. 질문을 다시 쓰거나, 여러 개로 늘리거나, 쪼개거나, 어느 저장소로 보낼지 정하는 일이 모두 여기에 포함됩니다.
사용자 질문
│
▼
┌────────────────────────┐
│ 쿼리 변환 (LLM 호출 등) │ 재작성 / 독립화 / multi-query / HyDE / 분해 / step-back / 라우팅
└───────────┬────────────┘
▼
검색용 쿼리 1~N개
│
▼
검색 → (리랭킹) → LLM 생성
▲
사용자 원래 질문 ───────────┘ ← 답변 생성에는 보통 원래 질문을 그대로 쓴다검색에 쓰는 쿼리와 답변 생성에 쓰는 질문은 분리합니다. 쿼리 변환은 "무엇을 찾을지"만 바꾸고, "무엇을 답할지"는 그대로 둡니다.
2. 사용자 질문을 그대로 검색에 쓰면 왜 나쁜가?
2-1. 질문과 문서는 생김새가 다르다
질문: "회사 노트북 잃어버렸는데 어떡해요 ㅠㅠ"
문서: "IT 자산 분실 시 처리 절차: 분실 인지 즉시 보안팀(내선 1234)에 신고하고..."질문은 짧고 구어체이고, 문서는 길고 공식 용어를 씁니다. "노트북"과 "IT 자산", "잃어버렸는데"와 "분실"은 사람이 보면 같은 뜻이지만 키워드 검색에서는 일치하지 않고, 임베딩에서도 거리가 생깁니다.
2-2. 대화 맥락에 기대는 질문
사용자: 연차는 1년에 며칠이에요?
봇: 입사 1년 이상이면 15일입니다.
사용자: 그럼 반차는요?"그럼 반차는요?"만으로 검색하면 무엇을 묻는지 알 수 없습니다. "반차도 연차 일수에서 차감되나요?"인지 "반차 신청 방법"인지 맥락이 있어야 압니다.
2-3. 한 질문에 여러 질문이 섞여 있다
"작년 대비 올해 A팀과 B팀의 예산 증가율을 비교해 줘"이 질문에 답하려면 A팀 작년, A팀 올해, B팀 작년, B팀 올해 예산 문서가 모두 필요합니다. 질문 하나로 검색하면 이 중 일부만 걸릴 가능성이 높습니다.
2-4. 너무 구체적이거나 너무 모호하다
| 유형 | 예시 | 문제 |
|---|---|---|
| 너무 구체적 | "3월 14일 배포한 v2.3.1에서 로그인 버튼이 회색인 이유" | 정확히 이 내용을 담은 문서는 없고, 원리 문서가 필요하다 |
| 너무 모호 | "보안 관련 규정" | 관련 문서가 수백 개라 상위 결과가 무작위에 가깝다 |
쿼리 변환 기법들은 각각 이 문제들 중 하나를 겨냥합니다.
3. 쿼리 재작성 (Query Rewriting)
가장 기본적인 기법입니다. LLM에게 질문을 검색에 적합한 문장으로 다시 쓰게 합니다.
원래: "회사 노트북 잃어버렸는데 어떡해요 ㅠㅠ"
재작성: "업무용 노트북(IT 자산) 분실 시 신고 및 처리 절차"- 구어체 → 문서 용어
- 감정 표현, 군더더기 제거
- 오타 수정, 약어 풀기 ("ㅇㅈ" → "인증")
from openai import OpenAI
client = OpenAI()
MODEL = "gpt-4o-mini" # 쿼리 변환에는 작고 빠른 모델이면 충분한 경우가 많다
def llm(prompt: str) -> str:
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
return resp.choices[0].message.content.strip()
REWRITE = """다음 질문을 사내 문서 검색에 쓸 검색어 문장으로 다시 쓰세요.
공식 용어를 쓰고, 불필요한 말은 빼고, 한 줄로만 출력하세요.
질문: {q}"""
search_query = llm(REWRITE.format(q="회사 노트북 잃어버렸는데 어떡해요 ㅠㅠ"))주의: 재작성이 원래 의미를 바꿀 수 있습니다. "환불 안 되는 경우"가 "환불 조건"으로 바뀌면 검색 결과가 달라집니다. 원래 질문으로도 함께 검색해 두면 안전합니다.
4. 대화 맥락 반영 (Follow-up 질문 독립화)
멀티턴 챗봇에서는 후속 질문을 대화 기록 없이도 이해되는 독립 질문(standalone question)으로 바꿉니다.
대화 기록:
사용자: 연차는 1년에 며칠이에요?
봇: 입사 1년 이상이면 15일입니다.
현재 질문: "그럼 반차는요?"
독립 질문: "반차를 사용하면 연차 15일에서 얼마나 차감되나요?"CONDENSE = """대화 기록과 마지막 질문이 주어집니다.
마지막 질문을 대화 기록 없이도 이해할 수 있는 독립된 질문 하나로 다시 쓰세요.
대명사와 생략된 대상을 구체적으로 채우세요. 질문만 출력하세요.
대화 기록:
{history}
마지막 질문: {q}"""
def standalone(history: list[tuple[str, str]], q: str) -> str:
if not history:
return q # 첫 질문은 변환할 필요 없음
h = "\n".join(f"{role}: {text}" for role, text in history[-6:]) # 최근 몇 턴만
return llm(CONDENSE.format(history=h, q=q))- 대화형 RAG에서는 거의 필수입니다. 이 단계가 없으면 두 번째 질문부터 검색이 무너집니다.
- 대화 기록 전체를 넣지 말고 최근 몇 턴만 넣어 비용과 혼동을 줄입니다.
- 주제가 완전히 바뀐 질문("근데 주차는 어디에 해요?")은 이전 맥락을 섞지 않도록 프롬프트에 명시합니다.
5. Multi-query와 결과 합치기
5-1. 아이디어
하나의 질문을 여러 관점의 쿼리로 바꿔 각각 검색하고, 결과를 합칩니다. 표현 하나에 운을 거는 대신 여러 번 던져서 recall을 높입니다.
원래 질문: "재택근무 중 장비 지원 받을 수 있나요?"
생성된 쿼리
q1: "재택근무자 장비 지원 정책"
q2: "원격근무 모니터 키보드 구매 비용 지원"
q3: "재택 근무 환경 지원금 신청 방법"
q1 검색 ─┐
q2 검색 ─┼─> 결과 합치기 (RRF) ─> top-k
q3 검색 ─┘5-2. 결과 합치기: RRF
각 쿼리의 검색 결과는 점수 스케일이 다를 수 있으므로, 점수 대신 순위로 합치는 RRF(Reciprocal Rank Fusion)를 많이 씁니다 (Cormack et al., 2009).
RRF 점수(d) = Σ 1 / (k + rank_i(d)) (k는 보통 60)
i: 각 결과 목록예시 (k=60):
| 문서 | q1 순위 | q2 순위 | q3 순위 | RRF 점수 |
|---|---|---|---|---|
| A | 1 | 3 | - | 1/61 + 1/63 ≈ 0.0323 |
| B | 2 | 1 | 2 | 1/62 + 1/61 + 1/62 ≈ 0.0487 |
| C | - | 2 |
6. HyDE (Hypothetical Document Embeddings)
6-1. 아이디어
질문을 그대로 임베딩하지 말고, LLM에게 질문에 대한 가상의 답변 문서를 먼저 쓰게 한 뒤, 그 가상 문서를 임베딩해서 검색합니다. Gao et al.(2022)의 논문 Precise Zero-Shot Dense Retrieval without Relevance Labels에서 제안되었습니다.
질문: "회사 노트북 잃어버렸는데 어떡해요"
│
▼ LLM: "이 질문에 답하는 사내 문서를 써 봐"
가상 문서: "업무용 노트북을 분실한 경우, 즉시 보안팀에 신고해야 합니다.
신고 후 원격 잠금 및 데이터 삭제 조치가 진행되며, 경위서를 제출..."
│
▼ 임베딩
벡터 검색 → 실제 "IT 자산 분실 처리 절차" 문서6-2. 왜 효과가 있나?
질문과 문서는 생김새가 다르지만(2-1), 가상 문서와 실제 문서는 생김새가 비슷합니다. 둘 다 서술형이고, 비슷한 용어를 씁니다. 그래서 "질문 ↔ 문서" 대신 "문서 ↔ 문서" 유사도로 검색하게 됩니다.
가상 문서는 검색에만 쓰고 답변에는 쓰지 않기 때문에 사실관계가 틀려도 괜찮습니다. 실제 문서와 "모양과 어휘"만 비슷하면 됩니다.
import numpy as np
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("BAAI/bge-m3")
HYDE = """다음 질문에 답하는 사내 규정 문서의 한 단락을 작성하세요.
정확하지 않아도 됩니다. 실제 문서처럼 공식적인 문체로 쓰세요.
질문: {q}"""
def hyde_vector(q: str) -> np.ndarray:
fake_doc = llm(HYDE.format(q=q))
return embedder.encode(fake_doc, normalize_embeddings=True)6-3. 한계
- LLM 생성이 한 번 들어가므로 지연이 가장 크게 늘어나는 기법 중 하나입니다.
- LLM이 모르는 사내 고유 정보(제품 코드명, 내부 약어)는 엉뚱한 가상 문서를 만들어 검색을 오히려 망칠 수 있습니다.
- 키워드 검색(BM25)보다는 벡터 검색에 붙일 때 의미가 있습니다.
7. 쿼리 분해 (Sub-question Decomposition)
복합 질문을 독립적으로 검색 가능한 하위 질문들로 쪼갭니다.
질문: "작년 대비 올해 A팀과 B팀의 예산 증가율을 비교해 줘"
하위 질문
1. A팀 2025년 예산
2. A팀 2026년 예산
3. B팀 2025년 예산
4. B팀 2026년 예산
각각 검색 → 문서 모음 → LLM이 계산·비교해서 답변DECOMPOSE = """다음 질문에 답하려면 어떤 정보를 각각 찾아야 하는지
독립적으로 검색 가능한 하위 질문으로 나누세요. 최대 {n}개, 한 줄에 하나씩.
나눌 필요가 없으면 원래 질문 하나만 출력하세요.
질문: {q}"""
def decompose_search(q: str, search, n: int = 4) -> list[str]:
subs = [s for s in llm(DECOMPOSE.format(q=q, n=n)).splitlines() if s.strip()]
docs: list[str] = []
for s in subs:
for d in search(s)[:3]:
if d not in docs:
docs.append(d)
return docs7-1. 병렬 분해 vs 순차 분해
| 방식 | 예시 | 특징 |
|---|---|---|
| 병렬 | "A팀, B팀 예산 비교" → 하위 질문을 동시에 검색 | 빠름. 하위 질문끼리 독립적일 때 |
| 순차 (multi-hop) | "우리 팀장의 직속 상사가 맡은 프로젝트는?" → 팀장 찾기 → 상사 찾기 → 프로젝트 찾기 | 앞 답이 다음 질문에 필요. 느리고, 에이전트 형태에 가까워진다 |
순차 분해가 자주 필요해지면 고정 파이프라인보다 에이전트가 검색을 반복하는 구조를 검토합니다.
8. Step-back Prompting
질문이 너무 구체적일 때, 한 단계 물러선 일반적인 질문을 만들어 원리·배경 문서를 함께 검색합니다. Google DeepMind의 Zheng et al.(2023) Take a Step Back: Evoking Reasoning via Abstraction in Large Language Models에서 제안된 프롬프팅 기법을 검색에 응용한 것입니다.
원래 질문: "v2.3.1에서 비밀번호 5회 틀리면 왜 30분 잠기나요?"
step-back: "계정 잠금 정책은 어떻게 동작하나요?"
원래 질문으로 검색 → 릴리스 노트 v2.3.1 (구체적 변경 사항)
step-back으로 검색 → 보안 정책 문서 (잠금 규칙의 원리와 이유)
↓
두 결과를 함께 LLM에 제공STEP_BACK = """다음 질문의 배경이 되는 더 일반적인 개념이나 원리를 묻는 질문 하나를 만드세요.
질문만 출력하세요.
질문: {q}"""
def step_back_search(q: str, search, k: int = 3) -> list[str]:
general = llm(STEP_BACK.format(q=q))
return list(dict.fromkeys(search(q)[:k] + search(general)[:k])) # 순서 유지 중복 제거- 원래 질문 검색을 대체하지 않고 보완합니다.
- "왜?"를 묻는 질문, 원리 설명이 필요한 질문에 효과적입니다.
- 단순 사실 조회("대표 전화번호는?")에는 쓸 이유가 없습니다.
9. 라우팅 (Routing)
질문을 보고 어느 인덱스, 어느 데이터 소스, 어느 처리 방식으로 보낼지 정합니다.
┌──> 인사 규정 인덱스
│
질문 ──> 라우터 ─────┼──> 기술 문서 인덱스
(LLM 또는 │
분류 모델) ├──> SQL DB (매출, 수치 데이터)
│
└──> 검색 없이 바로 답변 (인사말, 잡담)9-1. 라우팅 방식
| 방식 | 설명 | 특징 |
|---|---|---|
| 규칙 기반 | 키워드, 정규식 ("매출", "%" → SQL) | 빠르고 공짜. 표현이 다양하면 놓친다 |
| 임베딩 유사도 | 각 경로의 대표 질문과 비교 | 빠름. 경로 설명을 잘 만들어야 한다 |
| LLM 분류 | LLM에게 경로 목록을 주고 고르게 함 | 유연함. 호출 비용과 지연 |
| 메타데이터 필터 추출 | 질문에서 "2025년", "인사팀" 같은 조건을 뽑아 필터로 사용 | 검색 범위를 줄여 정확도를 올린다 |
ROUTES = {
"hr": "휴가, 급여, 복리후생, 인사 평가 등 인사 규정",
"tech": "개발 가이드, API, 배포, 장애 대응 등 기술 문서",
"sql": "매출, 주문 수, 사용자 수 등 숫자 데이터 조회",
"none": "인사, 잡담 등 문서 검색이 필요 없는 대화",
}
ROUTE = """질문을 처리할 경로 하나를 고르세요. 경로 이름만 출력하세요.
경로:
{routes}
질문: {q}"""
def route(q: str) -> str:
routes = "\n".join(f"- {k}: {v}" for k, v in ROUTES.items())
choice = llm(ROUTE.format(routes=routes, q=q)).strip().lower()
return choice if choice in ROUTES else "hr" # 알 수 없는 출력은 기본 경로로- 라우터가 틀리면 엉뚱한 인덱스에서 검색하므로 라우팅 정확도 자체를 따로 평가해야 합니다.
10. 기법별 비용과 언제 쓰는가
| 기법 | 추가 LLM 호출 | 추가 검색 횟수 | 주로 개선하는 것 | 언제 쓰나 |
|---|---|---|---|---|
| 쿼리 재작성 | 1회 | 0~1회 | 어휘 불일치 | 구어체·오타가 많은 사용자 질문 |
| 대화 독립화 | 1회 (첫 턴 제외) | 0 | 맥락 의존 질문 | 멀티턴 챗봇이면 거의 필수 |
| Multi-query | 1회 | N회 | recall | 같은 내용이 다양한 표현으로 흩어진 문서 |
| HyDE | 1회 (생성이 길어 느림) | 0 | 질문-문서 모양 차이 | 짧은 질문 vs 긴 서술형 문서, 일반 지식 도메인 |
| 쿼리 분해 | 1회 이상 | 하위 질문 수 | 복합 질문 |
11. 언제 쓰지 않는가
- 검색 품질을 측정하지 않은 상태. 어떤 질문에서 검색이 실패하는지 모르면 어떤 변환이 필요한지 알 수 없습니다. 실패 사례를 먼저 모읍니다.
- 지연 예산이 빠듯한 경우. 변환 하나마다 LLM 호출이 붙습니다. 검색 자체보다 쿼리 변환이 더 오래 걸리는 일이 흔합니다.
- 질문이 이미 잘 정제된 경우. 검색창에 키워드를 넣는 내부 도구, 정형화된 입력은 변환 이득이 작습니다.
- 변환이 의미를 왜곡하는 경우. 부정문, 숫자, 고유명사가 변환 과정에서 바뀌면 검색이 틀어집니다. 원래 질문 검색을 항상 함께 남겨 두면 이 위험을 줄일 수 있습니다.
12. 장점과 단점
| 장점 | 설명 |
|---|---|
| 검색 실패 원인을 직접 겨냥 | 어휘 차이, 맥락 의존, 복합 질문을 각각 해결한다 |
| 기존 인덱스를 그대로 쓴다 | 문서를 다시 임베딩하지 않고 질문 쪽만 바꾼다 |
| 조합 가능 | 필요한 기법만 골라 순서대로 붙인다 |
| 단점 | 설명 |
|---|---|
| 지연 증가 | 검색 전에 LLM 호출이 들어간다 |
| 비용 증가 | 쿼리 수, 검색 수가 늘어난다 |
| 오류 전파 | 변환이 틀리면 이후 단계가 모두 틀린다 |
| 디버깅 어려움 | "왜 이 문서가 나왔지?"를 보려면 변환된 쿼리까지 로그로 남겨야 한다 |
13. 핵심 정리
쿼리 변환은 사용자 질문을 검색하기 좋은 형태로 바꾸는 단계다. 질문은 짧고 구어체이며 대화 맥락에 기대고 여러 질문이 섞여 있어서, 그대로 검색하면 문서와 잘 맞지 않는다. 재작성은 어휘를 맞추고, 대화 독립화는 후속 질문을 혼자 이해되게 만들고, multi-query와 RRF는 여러 표현으로 recall을 올리고, HyDE는 가상 답변 문서로 문서끼리 비교하게 하고, 분해는 복합 질문을 쪼개고, step-back은 원리 문서를 함께 찾고, 라우팅은 올바른 소스로 보낸다. 모든 기법은 LLM 호출과 지연을 추가하므로, 검색 실패 사례를 먼저 측정하고 그 원인에 맞는 기법만 붙이며, 답변 생성에는 원래 질문을 쓴다.