HyDE (Hypothetical Document Embeddings)

질문을 그대로 임베딩하지 않고, LLM이 먼저 "이 질문에 답하는 그럴듯한 문서"를 지어내게 한 뒤 그 가상 문서의 임베딩으로 실제 문서를 찾는 검색 기법입니다. 짧은 질문과 긴 답변 문서는 문체·길이·용어가 달라 임베딩 공간에서 멀리 떨어지는데, 답변처럼 생긴 텍스트를 다리로 써서 그 간극을 줄입니다. 가상 문서의 내용이 틀려도 괜찮습니다. 쓰는 것은 내용이 아니라 문서가 놓일 위치입니다.

언제 쓰나

  • 질문과 문서의 형태 차이가 클 때: "연차 규정"(2어절) vs "제12조(연차유급휴가) ① 회사는 1년간 80% 이상 출근한…"(긴 조항)
  • 사용자가 전문 용어를 모를 때: 가상 문서가 "써멀 페이스트", "스로틀링" 같은 용어를 대신 꺼내 줌
  • 학습 데이터(질문–문서 쌍)가 없어 임베딩 모델을 도메인에 맞게 튜닝할 수 없을 때 (원 논문의 문제 설정이 zero-shot dense retrieval)
  • 벡터 검색을 쓰고, 질의당 LLM 생성 1회의 지연을 감당할 수 있을 때

맞지 않는 경우: 주문번호·에러코드 같은 식별자 검색, 모델이 도메인을 전혀 몰라 엉뚱한 주제의 가상 문서를 쓰는 경우, 지연에 민감한 자동완성.

작동 방식

  1. 가상 문서 생성: "이 질문에 답하는 문서의 한 단락을 써라"로 N개(보통 1~5개)를 생성합니다. 샘플링 온도를 조금 올려 다양하게 만듭니다.
  2. 임베딩: 가상 문서들을 문서 인덱싱에 쓴 것과 같은 임베딩 모델로 벡터화합니다.
  3. 벡터 결합: 원 논문은 N개 가상 문서 임베딩과 원 질문 임베딩까지 함께 평균해 최종 쿼리 벡터를 만듭니다. 원 질문을 섞어 두면 가상 문서가 빗나갔을 때 완충이 됩니다.
  4. 검색: 이 벡터로 실제 문서를 찾고, 생성 단계에는 실제 문서만 넘깁니다. 가상 문서는 버립니다.

왜 틀린 내용이어도 되는가: 임베딩은 세부 사실(7일인지 14일인지)보다 주제·문체·용어 분포를 주로 반영합니다. 가상 문서의 틀린 세부는 임베딩의 "병목"을 지나며 상당 부분 뭉개지고, 결과적으로 "환불 정책 조항처럼 생긴 문서들" 근처를 가리키게 됩니다.

예시

질문 → 가상 문서

질문가상 문서 (LLM 생성, 사실 확인 안 함)
연차 규정연차는 근로기준법에 따라 근로자에게 부여되는 유급 휴가입니다. 1년간 일정 비율 이상 출근한 근로자에게 부여되며, 사용하지 않은 연차는 회사 규정에 따라 수당으로 정산될 수 있습니다. 사용 시에는 사전 신청과 승인 절차를 거칩니다.
환불 정책환불 정책은 상품 구매 후 일정 기간 내에 구매를 취소하고 대금을 돌려받을 수 있는 제도입니다. 상품의 상태와 사용 여부에 따라 환불 조건이 달라지며, 환불 금액은 원래 결제 수단으로 반환됩니다.
노트북 발열노트북 발열은 CPU와 GPU가 높은 부하를 받을 때 발생합니다. 과도한 발열은 성능 저하(스로틀링)와 시스템 불안정을 일으킬 수 있으며, 쿨링 팬 청소, 써멀 페이스트 교체, 통풍이 잘되는 사용 환경으로 완화할 수 있습니다.
주문 취소주문 취소는 상품이 발송되기 전에 주문을 철회하는 것입니다. 배송 준비가 시작되기 전까지 주문 내역 페이지에서 신청할 수 있으며, 취소가 완료되면 결제 금액이 원래 결제 수단으로 환불됩니다.

"노트북 발열" 2어절 질문이 "스로틀링", "써멀 페이스트", "쿨링 팬" 같은 문서 쪽 용어를 가진 벡터로 바뀌는 것이 요점입니다.

프롬프트

다음 질문에 답하는 {domain} 문서의 한 단락을 작성하세요.
실제 사내 문서에 있을 법한 용어와 문체로 3~5문장을 쓰세요.
정확한 수치를 모르면 "일정 기간", "회사 규정에 따라"처럼 일반적으로 표현하세요.
단락만 출력하세요.

질문: {question}

"정확한 수치를 모르면 일반적으로"라는 지시는 검색 품질을 위한 것입니다. 지어낸 "7일 이내"가 들어가면 "7일"이 들어간 다른 정책 문서 쪽으로 벡터가 끌려갈 수 있습니다.

코드 (TypeScript)

type Complete = (prompt: string, opts?: { temperature?: number }) => Promise<string>;
type Embed = (texts: string[]) => Promise<number[][]>;          // 문서 인덱싱과 같은 모델
type VectorSearch = (vector: number[], k: number) => Promise<{ id: string; text: string }[]>;

function mean(vectors: number[][]): number[] {
  const dim = vectors[0].length;
  const out = new Array<number>(dim).fill(0);
  for (const v of vectors) for (let i = 0; i < dim; i++) out[i] += v[i] / vectors.length;
  return out;
}

export async function hydeSearch(
  question: string,
  deps: { complete: Complete; embed: Embed; search: VectorSearch },
  n = 3,
  k = 10,
) {
  const prompt = HYDE_PROMPT.replace("{domain}", "사내 정책").replace("{question}", question);

  // 1) 가상 문서 N개 (병렬, 약간의 다양성)
  const hypotheticals = await Promise.all(
    Array.from({ length: n }, () => deps.complete(prompt, { temperature: 0.7 })),
  );

  // 2) 가상 문서 + 원 질문을 함께 임베딩해 평균 (원 논문 방식)
  //    코사인 유사도를 쓰는 인덱스라면 평균 후 L2 정규화를 하세요.
  const vectors = await deps.embed([...hypotheticals, question]);
  const queryVector = mean(vectors);

  // 3) 실제 문서 검색 — 가상 문서는 여기서 버리고, 생성에는 실제 문서만 사용
  return deps.search(queryVector, k);
}

const HYDE_PROMPT = `...위 프롬프트 전문...`;

Haystack 등 여러 프레임워크가 HyDE 파이프라인 예제를 제공하지만, 위처럼 LLM 호출 + 임베딩 평균만으로 충분히 구현됩니다.

장단점

장점단점
짧은 질문–긴 문서의 형태 비대칭 완화질의마다 LLM 생성 → 지연이 가장 큰 쿼리 변환 기법 중 하나
사용자가 모르는 도메인 용어를 가상 문서가 보충모델이 도메인을 모르면 엉뚱한 방향의 벡터를 만듦
질문–문서 학습 쌍 없이 적용 가능 (zero-shot)식별자·숫자 정확 매칭에는 도움이 안 됨
여러 가상 문서 평균으로 안정성 확보가상 문서가 일반 상식을 쓰면 사내 특수 규정 대신 일반론 문서가 걸림

함정

⚠️ 함정 — 가상 문서를 컨텍스트에 넣음: 가상 문서는 검색용 벡터를 만드는 중간 산출물입니다. 이를 답변 생성 컨텍스트에 넣거나 사용자에게 보여주면 지어낸 "구매일로부터 7일 이내"가 사실처럼 답변에 나옵니다.

⚠️ 함정 — 일반론 편향: "연차 규정"에 대해 LLM은 근로기준법 일반론을 씁니다. 사내 문서가 "연차는 입사일 기준으로 부여한다"처럼 특수하면, 일반론 벡터가 외부 법령 요약 문서를 더 가깝게 볼 수 있습니다. 프롬프트에 도메인과 문서 문체를 구체적으로 주고, 원 질문 경로를 함께 돌리세요.

⚠️ 함정 — 임베딩 모델 불일치: 가상 문서를 인덱스와 다른 임베딩 모델로 벡터화하면 공간 자체가 달라 결과가 무작위에 가깝습니다. 에러는 나지 않습니다.

⚠️ 함정 — 비대칭 임베딩 모델의 prefix: query: / passage: 처럼 질의와 문서에 다른 접두어를 요구하는 임베딩 모델이 있습니다. HyDE의 가상 문서는 문서 역할이므로 문서용 접두어를 붙여야 의도대로 동작합니다. 사용하는 모델 문서를 확인하세요.

관련 기법

참고 자료