🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Query Transformation 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 25. · 3 Views

Query Transformation 완벽 가이드

RAG 시스템의 성능을 좌우하는 쿼리 변형 기법을 다룹니다. Query Rewriting, Expansion, Multi-Query, HyDE 등 실무에서 바로 적용할 수 있는 핵심 기술을 이북처럼 술술 읽히는 스타일로 설명합니다.


목차

  1. Query Rewriting
  2. Query Expansion
  3. Multi-Query 생성
  4. HyDE (Hypothetical Document Embeddings)
  5. 실습: 쿼리 변형 시스템
  6. 실습: HyDE 구현

1. Query Rewriting

어느 날 김개발 씨가 회사에서 챗봇 시스템을 운영하고 있었습니다. 그런데 사용자들의 질문이 "그거 어떻게 하는 거야?"처럼 모호하게 들어오면 검색 결과가 엉망이었습니다.

선배 박시니어 씨가 다가와 말했습니다. "사용자 질문을 그대로 쓰면 안 돼요.

쿼리를 다시 써야 합니다."

Query Rewriting은 사용자의 원본 질문을 검색에 적합한 형태로 다시 작성하는 기법입니다. 마치 도서관 사서가 "그 책 있나요?"라는 질문을 "파이썬 입문서 2024년판"으로 바꿔주는 것과 같습니다.

모호한 질문을 명확하게 바꿔주면 벡터 검색의 정확도가 크게 향상됩니다.

다음 코드를 살펴봅시다.

from openai import OpenAI

def rewrite_query(original_query):
    # LLM을 활용하여 쿼리를 명확하게 재작성
    client = OpenAI()
    prompt = f"""다음 질문을 검색에 적합하도록 명확하게 다시 작성해주세요.
    원본: {original_query}
    재작성:"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    # 재작성된 쿼리 반환
    return response.choices[0].message.content

# 실제 사용 예시
user_input = "그거 어떻게 설치해?"
better_query = rewrite_query(user_input)
print(f"개선된 쿼리: {better_query}")

김개발 씨는 입사 6개월 차 개발자입니다. 회사에서 내부 문서 검색 챗봇을 운영하고 있는데, 사용자들의 질문이 너무 애매해서 고민이 많았습니다.

"저번에 말한 그거", "아까 그 방법" 같은 질문이 계속 들어왔고, 당연히 검색 결과는 엉망이었습니다. 선배 개발자 박시니어 씨가 김개발 씨의 모니터를 보더니 한숨을 쉬었습니다.

"사용자 질문을 그대로 벡터 DB에 넣으면 안 돼요. 쿼리를 다시 써야 합니다." 쿼리 재작성이란 무엇일까요? 쉽게 비유하자면, 쿼리 재작성은 마치 통역사와 같습니다.

외국인 친구가 "그거 주세요"라고 하면 통역사는 문맥을 파악해서 "테이블 위의 물 한 잔 주세요"라고 정확하게 전달합니다. 마찬가지로 Query Rewriting도 모호한 사용자 질문을 명확하고 구체적인 검색 쿼리로 변환해 줍니다.

왜 필요한가요? RAG 시스템이 없던 시절에는 어땠을까요? 아니, RAG는 있었지만 쿼리 변형 없이 사용자 입력을 그대로 벡터 검색에 넣었습니다.

결과는 참담했습니다. 사용자가 "Python 설치 방법"이라고 물어보면 괜찮았습니다.

하지만 "그거 어떻게 깔아?"라고 물어보면 벡터 검색은 "그거"가 무엇인지 알 수 없었습니다. 대화 맥락도 없고, 구체적인 키워드도 없으니 검색 결과는 랜덤에 가까웠습니다.

더 큰 문제는 대명사와 은어였습니다. "저거", "아까 그거", "그놈" 같은 표현은 사람끼리는 통하지만, 벡터 임베딩 공간에서는 의미 있는 좌표를 만들어내지 못했습니다.

사용자 만족도는 급격히 떨어졌습니다. 해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 Query Rewriting이 등장했습니다.

LLM의 언어 이해 능력을 활용하면 모호한 질문을 명확하게 바꿀 수 있습니다. LLM은 대화 맥락을 이해하고, 대명사가 가리키는 대상을 추론하고, 은어를 표준 용어로 바꿀 수 있습니다.

무엇보다 검색에 최적화된 형태로 쿼리를 재구성할 수 있다는 큰 이점이 있습니다. 코드를 단계별로 살펴보겠습니다 먼저 4번째 줄을 보면 OpenAI 클라이언트를 생성합니다.

이것이 쿼리 재작성의 핵심 도구입니다. 다음으로 6번째 줄에서는 재작성을 요청하는 프롬프트를 만듭니다.

"검색에 적합하도록"이라는 지시어가 중요합니다. 9번째 줄에서 GPT-4 모델을 호출합니다.

여기서 LLM이 원본 쿼리를 분석하고, 더 명확하고 구체적인 형태로 재작성합니다. 마지막으로 15번째 줄에서 재작성된 결과를 반환합니다.

실무에서는 어떻게 활용할까요? 예를 들어 기술 문서 검색 서비스를 운영한다고 가정해봅시다. 사용자가 "Docker 그거 어떻게 써?"라고 물어보면, Query Rewriting을 거쳐 "Docker 컨테이너 기본 사용법 및 명령어"로 변환됩니다.

이렇게 구체화된 쿼리로 검색하면 관련 문서를 훨씬 정확하게 찾아낼 수 있습니다. 글로벌 IT 기업들은 이미 이런 패턴을 적극적으로 사용하고 있습니다.

구글 검색도 사용자 쿼리를 내부적으로 재작성하여 더 나은 결과를 제공합니다. 주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 재작성 과정에서 원래 의도를 왜곡하는 것입니다.

LLM이 너무 창의적으로 해석하면 사용자가 원하지 않는 방향으로 쿼리가 바뀔 수 있습니다. 따라서 프롬프트에 "원래 의도를 유지하면서"라는 제약을 명시해야 합니다.

또한 재작성에 시간이 걸리므로 레이턴시를 고려해야 합니다. 캐싱 전략을 함께 사용하면 좋습니다.

정리하자면 다시 김개발 씨의 이야기로 돌아가 봅시다. Query Rewriting을 적용한 후, 챗봇의 답변 정확도가 눈에 띄게 향상되었습니다.

사용자들도 "이제 제대로 찾아주네요!"라는 피드백을 보내기 시작했습니다. 쿼리 재작성을 제대로 활용하면 RAG 시스템의 성능을 크게 개선할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - LLM 호출 비용을 줄이려면 자주 나오는 쿼리는 캐싱하세요

  • 재작성 결과를 사용자에게 보여주고 확인받는 것도 좋은 방법입니다
  • 프롬프트에 도메인 특화 지식을 넣으면 더 정확한 재작성이 가능합니다

2. Query Expansion

김개발 씨가 쿼리 재작성을 성공적으로 적용한 다음 날, 새로운 문제가 생겼습니다. 사용자가 "Python"이라고만 검색하면 파이썬 관련 문서 중 일부만 찾아졌습니다.

박시니어 씨가 말했습니다. "쿼리를 확장해야 해요.

동의어, 관련어까지 함께 검색하면 놓치는 문서가 없어집니다."

Query Expansion은 원본 쿼리에 동의어, 관련어, 하위 개념을 추가하여 검색 범위를 넓히는 기법입니다. 마치 낚싯줄 하나로 낚시하던 것을 그물로 바꾸는 것과 같습니다.

단일 키워드로는 놓칠 수 있는 관련 문서들을 모두 찾아낼 수 있습니다.

다음 코드를 살펴봅시다.

def expand_query(original_query):
    # LLM을 활용하여 동의어와 관련어 생성
    client = OpenAI()
    prompt = f"""다음 검색어와 관련된 동의어, 관련어, 하위 개념을 5개 생성해주세요.
    검색어: {original_query}

    형식: 쉼표로 구분된 리스트"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    # 원본 쿼리 + 확장 키워드 결합
    expanded_terms = response.choices[0].message.content
    all_queries = [original_query] + expanded_terms.split(", ")

    return all_queries

# 사용 예시
queries = expand_query("Python")
print(f"확장된 쿼리: {queries}")

김개발 씨는 Query Rewriting을 성공적으로 적용한 후 뿌듯해했습니다. 하지만 다음 날 아침, 사용 로그를 분석하다가 이상한 점을 발견했습니다.

사용자가 "머신러닝"으로 검색하면 관련 문서 10개 중 3개만 검색되었습니다. 왜 그럴까요?

문서 안에는 "ML", "기계학습", "Machine Learning" 같은 다양한 표현이 섞여 있었기 때문입니다. 벡터 검색도 완벽하지 않아서, 표현이 다르면 유사도가 낮아졌습니다.

쿼리 확장이란 무엇일까요? 쉽게 비유하자면, 쿼리 확장은 마치 여러 개의 그물을 동시에 던지는 것과 같습니다. 낚싯줄 하나로 고기를 잡으면 운이 좋아야 잡히지만, 그물 여러 개를 던지면 확률이 높아집니다.

Query Expansion도 원본 키워드 하나만 쓰는 게 아니라, 동의어와 관련어를 함께 검색해서 누락을 방지합니다. 왜 필요한가요? 초창기 RAG 시스템은 사용자 쿼리 하나로만 검색했습니다.

문제는 어휘 불일치(Vocabulary Mismatch) 였습니다. 사용자는 "자동차"라고 검색하는데, 문서에는 "차량", "vehicle", "car" 같은 표현이 쓰여 있었습니다.

벡터 임베딩이 어느 정도 의미적 유사성을 포착하지만, 완벽하지 않았습니다. 특히 약어나 전문 용어는 더욱 그랬습니다.

더 큰 문제는 검색 재현율(Recall) 이었습니다. 정확도는 괜찮은데, 관련 문서를 너무 많이 놓쳤습니다.

사용자는 "왜 이 문서는 안 나와?"라고 불만을 제기했습니다. 해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 Query Expansion이 등장했습니다.

원본 쿼리에 동의어, 관련어, 하위 개념을 추가하면 검색 범위가 넓어집니다. LLM은 방대한 언어 지식을 가지고 있어서, "Python"을 입력하면 "파이썬", "Python3", "Py", "파이썬 프로그래밍" 같은 관련어를 자동으로 생성할 수 있습니다.

무엇보다 도메인 특화 용어까지 확장할 수 있다는 큰 이점이 있습니다. 코드를 단계별로 살펴보겠습니다 먼저 4번째 줄을 보면 LLM에게 동의어와 관련어 생성을 요청합니다.

이때 "5개 생성"이라고 명확히 지시하는 것이 중요합니다. 너무 많으면 노이즈가 생기고, 너무 적으면 효과가 없습니다.

9번째 줄에서 GPT-4가 관련어를 생성합니다. 여기서 LLM의 언어 지식이 빛을 발합니다.

16번째 줄에서는 원본 쿼리와 확장된 키워드를 리스트로 결합합니다. 이 리스트의 각 항목으로 별도로 검색하거나, 모두 임베딩해서 평균을 내는 방식으로 활용할 수 있습니다.

실무에서는 어떻게 활용할까요? 예를 들어 의료 문서 검색 시스템을 운영한다고 가정해봅시다. 의사가 "당뇨"로 검색하면, Query Expansion을 통해 "당뇨병", "diabetes", "DM", "혈당 조절 장애" 같은 관련어가 자동으로 추가됩니다.

이렇게 확장된 쿼리로 검색하면 다양한 표현으로 작성된 의료 문서를 모두 찾아낼 수 있습니다. 스택오버플로우 같은 Q&A 플랫폼도 이런 기법을 사용합니다.

"React Hook"으로 검색하면 "useState", "useEffect", "React Hooks" 같은 관련어까지 함께 검색합니다. 주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 키워드를 추가하는 것입니다.

관련어가 10개, 20개로 늘어나면 오히려 정확도가 떨어집니다. 관련 없는 문서까지 검색되기 때문입니다.

따라서 5~7개 정도로 제한하는 것이 좋습니다. 또한 확장된 키워드의 품질 검증도 필요합니다.

LLM이 가끔 엉뚱한 관련어를 생성할 수 있으므로, 도메인 전문가가 샘플을 검토하는 과정이 중요합니다. 정리하자면 김개발 씨는 Query Expansion을 적용한 후, 검색 재현율이 30%에서 85%로 급상승했습니다.

사용자들이 "이제 원하는 문서가 다 나와요!"라고 만족해했습니다. 쿼리 확장을 제대로 활용하면 RAG 시스템이 훨씬 더 포괄적인 검색 결과를 제공할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 확장 키워드는 5~7개가 적당합니다. 너무 많으면 노이즈가 생깁니다

  • 도메인별로 전문 용어 사전을 만들어두면 확장 품질이 향상됩니다
  • 각 확장 키워드에 가중치를 다르게 주는 방법도 효과적입니다

3. Multi-Query 생성

어느 날 김개발 씨는 복잡한 질문을 받았습니다. "Python으로 웹 크롤링하면서 동시에 데이터베이스에 저장하는 방법 알려줘." 한 번의 검색으로는 관련 문서를 모두 찾기 어려웠습니다.

박시니어 씨가 조언했습니다. "복잡한 질문은 여러 개의 하위 질문으로 쪼개서 각각 검색하세요."

Multi-Query 생성은 하나의 복잡한 질문을 여러 개의 간단한 하위 질문으로 분해하는 기법입니다. 마치 큰 문제를 작은 문제들로 나누어 해결하는 분할 정복 알고리즘과 같습니다.

각 하위 질문으로 독립적으로 검색한 후 결과를 종합하면 더 완전한 답변을 얻을 수 있습니다.

다음 코드를 살펴봅시다.

def generate_multi_queries(complex_query):
    # 복잡한 질문을 하위 질문들로 분해
    client = OpenAI()
    prompt = f"""다음 복잡한 질문을 3개의 간단한 하위 질문으로 분해해주세요.
    각 질문은 독립적으로 검색 가능해야 합니다.

    원본 질문: {complex_query}

    하위 질문 (번호로 구분):"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    # 하위 질문들을 리스트로 변환
    sub_queries = response.choices[0].message.content.split("\n")
    sub_queries = [q.strip() for q in sub_queries if q.strip()]

    return sub_queries

# 사용 예시
complex_q = "Python 웹 크롤링과 DB 저장 통합 방법"
sub_qs = generate_multi_queries(complex_q)
print(f"하위 질문들: {sub_qs}")

김개발 씨는 점점 RAG 시스템 운영에 익숙해졌습니다. 그런데 어느 날, 한 사용자가 매우 복잡한 질문을 던졌습니다.

"FastAPI로 RESTful API 서버 만들면서 JWT 인증 구현하고 PostgreSQL 연동하는 전체 구조 알려줘." 김개발 씨는 이 질문을 그대로 벡터 검색에 넣었습니다. 결과는 실망스러웠습니다.

관련 문서가 일부만 나왔고, 특정 주제에 치우쳐 있었습니다. Multi-Query 생성이란 무엇일까요? 쉽게 비유하자면, Multi-Query는 마치 큰 피자를 여러 조각으로 나누는 것과 같습니다.

피자 전체를 한입에 먹을 수 없으니, 조각으로 나눠서 하나씩 먹습니다. 복잡한 질문도 마찬가지입니다.

"FastAPI + JWT + PostgreSQL"이라는 큰 질문을 "FastAPI 서버 구축", "JWT 인증 구현", "PostgreSQL 연동" 세 개로 나누면 각각 검색하기 훨씬 쉬워집니다. 왜 필요한가요? 전통적인 RAG는 단일 쿼리로 검색했습니다.

문제는 복합 질문이었습니다. 벡터 임베딩은 질문 전체를 하나의 좌표로 변환합니다.

그런데 질문에 여러 주제가 섞여 있으면, 임베딩 공간에서 명확한 위치를 찾기 어렵습니다. "FastAPI"와 "JWT"와 "PostgreSQL"이 모두 담긴 문서는 드물기 때문에, 검색 결과가 불완전했습니다.

더 큰 문제는 정보 손실이었습니다. 복잡한 질문을 하나의 벡터로 압축하면서, 각 세부 주제의 중요도가 흐려졌습니다.

어떤 부분은 과도하게 강조되고, 어떤 부분은 무시되었습니다. 해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 Multi-Query 생성이 등장했습니다.

복잡한 질문을 여러 개의 간단한 하위 질문으로 분해하면, 각 주제별로 독립적인 검색이 가능해집니다. LLM은 질문의 구조를 분석하고, 논리적으로 연결된 하위 질문들을 생성할 수 있습니다.

무엇보다 각 하위 질문의 결과를 종합하면 원래 복잡한 질문에 대한 완전한 답변을 만들 수 있다는 큰 이점이 있습니다. 코드를 단계별로 살펴보겠습니다 먼저 4번째 줄을 보면 LLM에게 질문 분해를 요청합니다.

"3개의 간단한 하위 질문"이라고 명확히 지시하는 것이 핵심입니다. 5번째 줄의 "독립적으로 검색 가능"이라는 조건도 매우 중요합니다.

11번째 줄에서 GPT-4가 하위 질문들을 생성합니다. 여기서 LLM의 논리적 분해 능력이 빛을 발합니다.

17번째 줄에서는 생성된 결과를 리스트로 파싱합니다. 각 하위 질문이 독립적인 항목이 됩니다.

이후에는 각 하위 질문으로 별도의 벡터 검색을 수행하고, 결과를 병합합니다. 실무에서는 어떻게 활용할까요? 예를 들어 기술 지원 챗봇을 운영한다고 가정해봅시다.

사용자가 "로그인이 안 되고 비밀번호 재설정도 작동 안 해요"라고 질문하면, Multi-Query를 통해 "로그인 실패 원인", "비밀번호 재설정 문제 해결" 두 개로 분해됩니다. 각각 검색해서 두 가지 문제에 대한 해결책을 모두 제시할 수 있습니다.

아마존이나 구글 같은 기업의 고객 지원 시스템도 이런 패턴을 사용합니다. 복잡한 문의를 세부 주제로 나눠서 처리합니다.

주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 하위 질문을 생성하는 것입니다. 하위 질문이 10개, 20개로 늘어나면 검색 비용이 폭증하고, 결과 병합도 복잡해집니다.

3~5개가 적당합니다. 또한 하위 질문들이 서로 독립적이어야 합니다.

만약 하위 질문 간에 의존성이 있으면, 검색 결과를 종합할 때 모순이 생길 수 있습니다. 따라서 프롬프트에 "독립적으로"라는 조건을 명시해야 합니다.

정리하자면 김개발 씨는 Multi-Query 생성을 적용한 후, 복잡한 질문에 대한 답변 완성도가 크게 향상되었습니다. 사용자들이 "이제 필요한 정보가 한 번에 다 나와요!"라고 좋아했습니다.

Multi-Query 기법을 제대로 활용하면 복합 질문도 효과적으로 처리할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 하위 질문은 3~5개가 적당합니다. 너무 많으면 비용과 복잡도가 증가합니다

  • 각 하위 질문의 검색 결과에 가중치를 다르게 줄 수도 있습니다
  • 병합 시 중복 제거와 랭킹 재조정이 필요합니다

4. HyDE (Hypothetical Document Embeddings)

김개발 씨가 RAG 시스템을 잘 운영하고 있던 어느 날, 박시니어 씨가 흥미로운 아이디어를 제안했습니다. "질문으로 검색하지 말고, 가상의 답변을 만들어서 그걸로 검색해보면 어때요?" 김개발 씨는 고개를 갸우뚱했습니다.

"그게 무슨 말씀이신가요?"

HyDE는 사용자 질문에 대한 가상의 답변 문서를 생성하고, 그 답변으로 검색하는 혁신적인 기법입니다. 마치 답을 이미 알고 있는 척하며 그 답과 비슷한 실제 문서를 찾는 것과 같습니다.

질문과 문서 사이의 임베딩 간극을 줄여 검색 정확도를 크게 향상시킵니다.

다음 코드를 살펴봅시다.

def hyde_search(user_question):
    # 1단계: 가상의 답변 문서 생성
    client = OpenAI()
    prompt = f"""다음 질문에 대한 상세한 답변을 작성해주세요.
    실제 정보가 없어도 그럴듯한 답변을 만들어주세요.

    질문: {user_question}

    답변:"""

    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}]
    )

    # 가상 답변 문서
    hypothetical_doc = response.choices[0].message.content

    # 2단계: 가상 답변으로 벡터 검색
    # embedding = get_embedding(hypothetical_doc)
    # results = vector_db.search(embedding, top_k=5)

    return hypothetical_doc

# 사용 예시
question = "FastAPI에서 비동기 처리는 어떻게 하나요?"
hypo_doc = hyde_search(question)
print(f"가상 답변: {hypo_doc}")

김개발 씨는 박시니어 씨의 말을 듣고 처음에는 이해가 되지 않았습니다. "답을 모르는데 어떻게 답으로 검색해요?" 하지만 박시니어 씨는 미소를 지으며 설명하기 시작했습니다.

"벡터 임베딩 공간에서 질문과 답변은 다른 영역에 있어요. 질문은 '어떻게', '왜' 같은 의문형 표현이 많고, 답변은 '~합니다', '~입니다' 같은 서술형 표현이 많죠.

이 차이 때문에 질문으로 검색하면 답변 문서를 잘 못 찾을 수 있어요." HyDE란 무엇일까요? 쉽게 비유하자면, HyDE는 마치 외국어 시험에서 답안지를 먼저 쓰고 그와 비슷한 모범 답안을 찾는 것과 같습니다. 물론 처음 쓴 답안은 틀릴 수 있지만, 그 답안과 비슷한 형식과 어휘를 가진 진짜 모범 답안을 찾는 데는 도움이 됩니다.

HyDE는 Hypothetical Document Embeddings의 약자입니다. 사용자 질문을 받으면, LLM이 그 질문에 대한 가상의 답변을 먼저 생성합니다.

이 가상 답변은 사실 여부와 무관하게, 답변처럼 보이는 문서입니다. 그리고 이 가상 답변을 임베딩해서 벡터 검색을 수행합니다.

왜 필요한가요? 전통적인 RAG는 사용자 질문을 직접 임베딩해서 검색했습니다. 문제는 쿼리-문서 불일치였습니다.

질문은 "FastAPI 비동기 처리 방법은?"처럼 짧고 의문형입니다. 반면 문서는 "FastAPI에서 비동기 처리는 async와 await 키워드를 사용합니다.

먼저 함수 정의 앞에 async를 붙이고..."처럼 길고 서술형입니다. 임베딩 공간에서 이 둘의 거리가 생각보다 멀었습니다.

더 큰 문제는 어휘 스타일 차이였습니다. 질문에는 "어떻게", "방법", "알려줘" 같은 요청 표현이 많고, 문서에는 "구현", "설정", "사용" 같은 설명 표현이 많습니다.

벡터 모델이 의미를 어느 정도 포착하지만, 여전히 간극이 있었습니다. 해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 HyDE가 등장했습니다.

가상의 답변을 생성하면, 그 답변은 실제 문서와 비슷한 어휘와 스타일을 가집니다. LLM은 방대한 학습 데이터를 통해 "답변은 어떻게 생겼는지" 잘 알고 있습니다.

따라서 가상 답변의 임베딩은 실제 답변 문서의 임베딩과 가까워집니다. 무엇보다 질문-문서 간극을 문서-문서 비교로 바꿀 수 있다는 큰 이점이 있습니다.

코드를 단계별로 살펴보겠습니다 먼저 4번째 줄을 보면 LLM에게 가상 답변 생성을 요청합니다. 중요한 것은 "실제 정보가 없어도 그럴듯한 답변"이라는 지시입니다.

사실 여부는 중요하지 않습니다. 답변처럼 보이기만 하면 됩니다.

11번째 줄에서 GPT-4가 가상 문서를 생성합니다. 여기서 LLM의 문서 생성 능력이 핵심입니다.

17번째 줄에서 생성된 가상 답변을 반환합니다. 실제로는 이것을 임베딩해서 벡터 검색에 사용합니다.

주석 처리된 20~21번째 줄이 실제 검색 과정입니다. 가상 답변을 임베딩하고, 벡터 DB에서 유사한 실제 문서를 찾습니다.

실무에서는 어떻게 활용할까요? 예를 들어 법률 문서 검색 시스템을 운영한다고 가정해봅시다. 변호사가 "계약 해지 조건은?"이라고 질문하면, HyDE는 "계약 해지 조건은 다음과 같습니다.

첫째, 일방 당사자의 중대한 계약 위반이 있을 경우..."처럼 가상 답변을 생성합니다. 이 가상 답변으로 검색하면, 실제 법률 문서에서 비슷한 서술 구조를 가진 조항을 정확히 찾아낼 수 있습니다.

최근 구글 딥마인드와 오픈AI 연구팀도 HyDE를 활용한 검색 시스템을 발표했습니다. 주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 가상 답변을 그대로 사용자에게 보여주는 것입니다.

가상 답변은 사실이 아닐 수 있으므로, 절대 최종 답변으로 쓰면 안 됩니다. 오직 검색 용도로만 사용해야 합니다.

또한 HyDE는 추가 LLM 호출이 필요하므로 비용과 레이턴시가 증가합니다. 중요한 쿼리에만 선택적으로 적용하는 것이 좋습니다.

정리하자면 김개발 씨는 HyDE를 적용한 후, 특히 전문 용어가 많은 기술 문서 검색에서 정확도가 크게 향상되었음을 확인했습니다. "이제 정말 원하는 문서가 상위에 나와요!" HyDE를 제대로 활용하면 질문-문서 간 임베딩 간극을 효과적으로 극복할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 가상 답변은 사실 검증 없이 검색 용도로만 사용하세요

  • 도메인 특화 프롬프트를 사용하면 더 나은 가상 문서를 생성할 수 있습니다
  • 여러 개의 가상 답변을 생성하고 앙상블하는 방법도 효과적입니다

5. 실습: 쿼리 변형 시스템

김개발 씨는 이제 배운 내용을 모두 통합할 준비가 되었습니다. 박시니어 씨가 과제를 주었습니다.

"지금까지 배운 Query Rewriting, Expansion, Multi-Query를 하나의 시스템으로 만들어보세요. 사용자 질문에 따라 적절한 기법을 선택하는 지능형 시스템이면 좋겠네요."

쿼리 변형 시스템은 사용자 질문의 특성을 분석하여 적절한 쿼리 변형 기법을 자동으로 선택하고 적용하는 통합 솔루션입니다. 마치 숙련된 사서가 질문의 종류에 따라 최적의 검색 전략을 구사하는 것과 같습니다.

단순 질문, 모호한 질문, 복합 질문 각각에 맞는 처리 방식을 제공합니다.

다음 코드를 살펴봅시다.

class QueryTransformer:
    def __init__(self):
        self.client = OpenAI()

    def analyze_query_type(self, query):
        # 질문 유형 분석: simple, ambiguous, complex
        if len(query.split()) <= 3:
            return "simple"
        elif "?" not in query and len(query) < 20:
            return "ambiguous"
        elif "그리고" in query or "," in query:
            return "complex"
        return "simple"

    def transform(self, query):
        query_type = self.analyze_query_type(query)

        if query_type == "ambiguous":
            # 모호한 질문 -> Rewriting
            return self.rewrite(query)
        elif query_type == "complex":
            # 복합 질문 -> Multi-Query
            return self.multi_query(query)
        else:
            # 단순 질문 -> Expansion
            return self.expand(query)

    def rewrite(self, query):
        # Query Rewriting 로직
        return f"[재작성] {query}"

    def expand(self, query):
        # Query Expansion 로직
        return [query, f"{query} 관련어1", f"{query} 관련어2"]

    def multi_query(self, query):
        # Multi-Query 로직
        return [f"하위질문1: {query}", f"하위질문2: {query}"]

# 사용 예시
transformer = QueryTransformer()
result = transformer.transform("Python 웹 크롤링과 데이터 저장")
print(f"변형 결과: {result}")

김개발 씨는 드디어 실전 프로젝트를 시작했습니다. 지금까지 배운 Query Rewriting, Expansion, Multi-Query를 각각 따로 사용했는데, 이제는 이것들을 하나로 통합해야 했습니다.

박시니어 씨는 힌트를 주었습니다. "모든 질문에 같은 기법을 쓰면 안 돼요.

질문의 특성을 먼저 분석해야 합니다." 쿼리 변형 시스템이란 무엇일까요? 쉽게 비유하자면, 쿼리 변형 시스템은 마치 응급실의 트리아지(Triage) 와 같습니다. 응급실에 환자가 오면 먼저 증상을 보고 중증도를 판단합니다.

생명이 위급한 환자는 즉시 수술실로, 경미한 환자는 일반 진료실로 보냅니다. 쿼리 변형 시스템도 질문을 받으면 먼저 유형을 분석하고, 그에 맞는 최적의 변형 기법을 적용합니다.

왜 통합 시스템이 필요한가요? 초기에는 각 기법을 수동으로 선택했습니다. 개발자가 로그를 보고 "이 질문은 모호하니 Rewriting을 쓰자", "저 질문은 복잡하니 Multi-Query를 쓰자"라고 판단했습니다.

문제는 확장성이었습니다. 사용자가 하루에 수천 개의 질문을 던지면, 일일이 수동으로 처리할 수 없습니다.

또한 개발자의 주관적 판단에 따라 결과가 달라졌습니다. 일관성이 없었고, 실수도 잦았습니다.

더 큰 문제는 최적화 기회 손실이었습니다. 어떤 질문에는 여러 기법을 조합하는 것이 좋은데, 수동으로는 그런 세밀한 조정이 어려웠습니다.

해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 지능형 쿼리 변형 시스템이 등장했습니다. 시스템이 질문의 길이, 구조, 키워드를 자동으로 분석합니다.

그리고 사전에 정의된 규칙이나 머신러닝 모델에 따라 최적의 변형 기법을 선택합니다. 무엇보다 일관성 있고 자동화된 처리가 가능하다는 큰 이점이 있습니다.

코드를 단계별로 살펴보겠습니다 먼저 5번째 줄의 analyze_query_type 메서드가 핵심입니다. 이 메서드는 질문을 분석하여 "simple", "ambiguous", "complex" 세 가지 유형으로 분류합니다.

78번째 줄을 보면 단어 수가 3개 이하면 단순 질문으로 판단합니다. 910번째 줄은 물음표가 없고 짧으면 모호한 질문으로 분류합니다.

11~12번째 줄은 "그리고"나 쉼표가 있으면 복합 질문으로 봅니다. 15번째 줄의 transform 메서드가 전체 흐름을 제어합니다.

먼저 질문 유형을 파악하고, 18~25번째 줄에서 유형에 따라 적절한 변형 메서드를 호출합니다. 모호한 질문은 rewrite, 복합 질문은 multi_query, 단순 질문은 expand를 사용합니다.

29~38번째 줄은 각 변형 기법의 실제 구현 부분입니다. 실무에서는 여기에 앞서 배운 LLM 기반 로직을 넣으면 됩니다.

실무에서는 어떻게 활용할까요? 예를 들어 고객 지원 챗봇을 운영한다고 가정해봅시다. 사용자가 "환불"이라고만 입력하면 시스템은 이를 모호한 질문으로 판단하고 Query Rewriting을 적용합니다.

"환불 정책 및 절차"로 재작성됩니다. 반대로 "결제 실패 오류와 환불 처리 방법"처럼 복합 질문이 오면, Multi-Query로 "결제 실패 오류 해결", "환불 처리 절차" 두 개로 분해합니다.

각 질문에 최적화된 처리가 자동으로 이루어집니다. 실리콘밸리의 여러 스타트업들이 이런 지능형 시스템을 구축하여 고객 만족도를 높이고 있습니다.

주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 분류 규칙을 너무 단순하게 만드는 것입니다. 위 코드의 analyze_query_type은 예시일 뿐, 실제로는 더 정교한 분류 로직이 필요합니다.

머신러닝 분류기를 사용하는 것도 좋은 방법입니다. 또한 각 기법의 성능 모니터링이 필수입니다.

어떤 유형에서 정확도가 떨어지는지 지속적으로 추적하고 개선해야 합니다. 정리하자면 김개발 씨는 통합 쿼리 변형 시스템을 완성한 후, 전체 RAG 파이프라인의 성능이 획기적으로 개선되었음을 확인했습니다.

사용자 만족도도 눈에 띄게 상승했습니다. 지능형 시스템을 제대로 구축하면 다양한 질문 유형을 효과적으로 처리할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 분류 규칙은 도메인에 맞게 커스터마이징하세요

  • A/B 테스트로 각 기법의 효과를 정량적으로 검증하세요
  • 사용자 피드백을 수집하여 분류 정확도를 지속적으로 개선하세요

6. 실습: HyDE 구현

김개발 씨의 마지막 과제는 HyDE를 실제로 구현하는 것이었습니다. 박시니어 씨가 말했습니다.

"HyDE는 개념은 간단하지만, 실제로 구현하려면 여러 단계를 거쳐야 해요. 가상 문서 생성, 임베딩, 검색, 결과 후처리까지 전체 파이프라인을 만들어보세요."

HyDE 구현은 가상 답변 생성부터 벡터 검색, 결과 재랭킹까지 포함하는 완전한 파이프라인입니다. 마치 전체 오케스트라가 조화롭게 연주하는 것처럼, 각 단계가 유기적으로 연결되어야 합니다.

실무에 바로 적용 가능한 엔드투엔드 솔루션을 구축합니다.

다음 코드를 살펴봅시다.

import numpy as np
from openai import OpenAI

class HyDERetriever:
    def __init__(self, vector_db):
        self.client = OpenAI()
        self.vector_db = vector_db

    def generate_hypothetical_doc(self, question):
        # 가상 답변 문서 생성
        prompt = f"""질문에 대한 상세하고 전문적인 답변을 작성하세요.

        질문: {question}
        답변:"""

        response = self.client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7  # 다양성을 위해 약간 높임
        )

        return response.choices[0].message.content

    def retrieve(self, question, top_k=5):
        # 1단계: 가상 문서 생성
        hypo_doc = self.generate_hypothetical_doc(question)

        # 2단계: 가상 문서 임베딩
        embedding = self.client.embeddings.create(
            model="text-embedding-3-small",
            input=hypo_doc
        ).data[0].embedding

        # 3단계: 벡터 검색
        results = self.vector_db.search(
            query_vector=embedding,
            top_k=top_k
        )

        # 4단계: 결과 반환
        return {
            "hypothetical_doc": hypo_doc,
            "retrieved_docs": results
        }

# 사용 예시
# retriever = HyDERetriever(vector_db)
# result = retriever.retrieve("FastAPI 비동기 처리 방법은?")
# print(result["retrieved_docs"])

김개발 씨는 드디어 HyDE의 실제 구현 단계에 도달했습니다. 개념은 이해했지만, 실제 코드로 만들려니 여러 고민이 생겼습니다.

"가상 문서를 어떻게 생성하지? 임베딩은?

벡터 DB와 어떻게 연동하지?" 박시니어 씨가 옆에 앉아 차근차근 설명해주었습니다. "HyDE는 여러 단계로 이루어져 있어요.

하나씩 구현해 나가면 됩니다." HyDE 파이프라인이란 무엇일까요? 쉽게 비유하자면, HyDE 파이프라인은 마치 자동차 공장의 조립 라인과 같습니다. 원자재가 들어가서 여러 공정을 거쳐 완성된 자동차가 나오듯, 사용자 질문이 들어가서 가상 문서 생성, 임베딩, 검색, 후처리를 거쳐 최종 검색 결과가 나옵니다.

각 단계가 정확히 작동해야 전체 시스템이 제대로 동작합니다. 왜 완전한 파이프라인이 필요한가요? 초기 실험에서는 각 단계를 따로 실행했습니다.

Jupyter 노트북에서 한 셀씩 수동으로 돌렸습니다. 문제는 재현성과 유지보수였습니다.

코드가 여기저기 흩어져 있으니, 나중에 수정하거나 디버깅하기 어려웠습니다. 또한 프로덕션 환경에 배포하려면 모든 단계를 하나의 모듈로 통합해야 했습니다.

수동 실행으로는 실시간 서비스가 불가능했습니다. 더 큰 문제는 에러 처리였습니다.

중간 단계에서 실패하면 어떻게 할까요? 가상 문서 생성이 실패하면?

임베딩 API가 타임아웃되면? 체계적인 에러 처리 없이는 안정적인 서비스를 만들 수 없었습니다.

해결책이 등장했습니다 바로 이런 문제를 해결하기 위해 통합 HyDE 파이프라인이 등장했습니다. 모든 단계를 하나의 클래스로 캡슐화하면 코드가 깔끔해지고 재사용이 쉬워집니다.

또한 각 단계의 입력과 출력이 명확해져서 디버깅도 편해집니다. 무엇보다 프로덕션 배포가 간단해진다는 큰 이점이 있습니다.

코드를 단계별로 살펴보겠습니다 먼저 4번째 줄의 HyDERetriever 클래스가 전체 시스템을 담고 있습니다. 5~7번째 줄에서 OpenAI 클라이언트와 벡터 DB를 초기화합니다.

9번째 줄의 generate_hypothetical_doc 메서드가 첫 번째 단계입니다. 11~13번째 줄의 프롬프트가 중요합니다.

"상세하고 전문적인"이라는 지시어가 고품질 가상 문서를 만드는 핵심입니다. 16~19번째 줄에서 GPT-4를 호출합니다.

여기서 temperature를 0.7로 설정한 것에 주목하세요. 너무 낮으면 획일적인 답변이 나오고, 너무 높으면 일관성이 떨어집니다.

0.7이 적당한 균형점입니다. 23번째 줄의 retrieve 메서드가 전체 파이프라인을 실행합니다.

26번째 줄에서 가상 문서를 생성하고, 29~32번째 줄에서 임베딩을 생성합니다. OpenAI의 text-embedding-3-small 모델을 사용합니다.

35~38번째 줄에서 벡터 DB 검색을 수행합니다. 가상 문서의 임베딩으로 검색하는 것이 HyDE의 핵심입니다.

마지막으로 41~44번째 줄에서 가상 문서와 검색 결과를 함께 반환합니다. 실무에서는 어떻게 활용할까요? 예를 들어 연구 논문 검색 서비스를 운영한다고 가정해봅시다.

연구자가 "Transformer 아키텍처의 Self-Attention 메커니즘 최적화 방법"이라고 검색하면, HyDE가 가상의 논문 초록을 생성합니다. "본 연구에서는 Transformer의 Self-Attention 메커니즘을 최적화하기 위해 다음과 같은 방법을 제안한다.

첫째, Sparse Attention을 도입하여 계산 복잡도를 O(n²)에서 O(n log n)으로 감소시켰다..." 이런 가상 초록으로 검색하면, 실제 논문들의 초록과 임베딩이 매우 유사해서 관련 논문을 정확히 찾아낼 수 있습니다. 구글 스칼라나 Semantic Scholar 같은 학술 검색 엔진도 유사한 기법을 연구하고 있습니다.

주의할 점도 있습니다 초보 개발자들이 흔히 하는 실수 중 하나는 가상 문서의 길이를 조절하지 않는 것입니다. 너무 짧으면 정보가 부족하고, 너무 길면 임베딩 모델의 토큰 제한을 초과할 수 있습니다.

프롬프트에 "200~300 단어로"라는 제약을 추가하는 것이 좋습니다. 또한 캐싱 전략이 필수입니다.

같은 질문이 반복되면 가상 문서를 매번 생성할 필요가 없습니다. 질문을 키로 하여 가상 문서를 캐싱하면 비용과 레이턴시를 크게 줄일 수 있습니다.

마지막으로 폴백 메커니즘도 중요합니다. HyDE가 실패하면 일반 쿼리 검색으로 자동 전환되도록 구현해야 서비스 안정성이 보장됩니다.

정리하자면 김개발 씨는 완전한 HyDE 파이프라인을 구현한 후, 실제 프로덕션 환경에 배포했습니다. 특히 전문 용어가 많은 기술 문서 검색에서 기존 방식 대비 정확도가 40% 이상 향상되었습니다.

"드디어 제대로 된 RAG 시스템을 만들었어요!" 김개발 씨는 뿌듯해했습니다. 박시니어 씨도 고개를 끄덕이며 박수를 쳐주었습니다.

HyDE 파이프라인을 제대로 구현하면 검색 품질을 획기적으로 개선할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 가상 문서 길이를 200~300 단어로 제한하세요

  • 자주 나오는 질문은 가상 문서를 캐싱하여 비용을 절감하세요
  • 에러 발생 시 일반 검색으로 폴백하는 메커니즘을 구현하세요
  • A/B 테스트로 HyDE와 일반 검색의 성능을 비교 검증하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#RAG#QueryTransformation#HyDE#VectorSearch#RAG,쿼리변환,HyDE

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.