Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 5일 19:42중국어 → 한국어

RAG 쿼리 확장, HyDE와 다중 쿼리 선택·조합법

검색에 적합하지 않은 사용자 질의를 다루기 위해 HyDE와 다중 쿼리 재작성의 선택과 조합 방법을 실전 관점에서 정리했다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

앞선 글에서는 멀티턴 대화에서의 지칭과 생략 문제를 해결했다. 먼저 「그거 얼마야」를 독립적인 query로 복원한 뒤 검색에 넘겼다. 하지만 독립적인 query라고 해서 검색에 자연스럽게 적합한 것은 아니다.

사용자는 「장비가 자꾸 멈춰요」라고 말하는데, 문서에는 「장비 응답 타임아웃」이라고 적혀 있다. 사용자는 「인쇄물에 검은 줄이 있어요」라고 말하는데, 문서에는 「출력 페이지에 세로 줄무늬가 나타남」이라고 적혀 있다. 질문은 매우 완전한데도, 벡터 검색은 표현 차이 때문에 목표 문서를 건져내지 못할 수 있다.

이런 문제에는 쿼리 확장을 해야 한다. HyDE나 다중 query 재작성으로 검색 진입점을 늘리는 것이다. 이번 글은 앞서 사용한 pgvector + bge-m3 환경을 그대로 이어서, 최소 구현과 RRF 융합 코드, 그리고 유형별 평가 세트를 제시하고, 마지막으로 두 가지 방안을 어떻게 선택할지 명확히 설명한다.

1. 먼저 구분하자: 쿼리 재작성과 쿼리 확장은 같은 것이 아니다

이 두 기술은 자주 뒤섞여 언급되지만, 실제로 해결하는 문제는 다르다:

| 기술 | 입력 질문 | 목표 | 출력 | | 쿼리 재작성(앞선 글) | 「그거 얼마야」, 문맥을 떠나면 이해 불가 | 지칭과 생략 보완 | 하나의 독립 query | | 쿼리 확장(이번 글) | 「장비가 자꾸 멈춰요」, 의미는 완전하지만 표현이 검색에 불리 | 사용자 표현과 다른 검색 진입점 추가 | 가정 답변 또는 여러 query 변형 |

멀티턴 상황에서는 두 가지를 직렬로 연결할 수 있다: 먼저 재작성으로 의도를 복원하고, 그다음 확장이 필요한지 판단한다. 순서를 반대로 해서, 먼저 「그거 얼마야」를 주어 없는 문제 세 개로 확장하면 이후는 더 엉망이 될 뿐이다.

이번 글 평가의 query는 모두 이미 독립적인 질문이므로, 쿼리 확장이라는 변수 하나만 다룬다.

2. HyDE: 먼저 가정 답변을 한 단락 쓰고, 그 답변으로 검색한다

HyDE(Hypothetical Document Embeddings)의 발상은 아주 직접적이다. 사용자 질문은 보통 매우 짧은 반면 지식베이스 문서는 긴 경우가 많아, 두 가지가 의미 공간에서 지니는 형태 차이가 크다. 먼저 LLM이 「문서에 등장할 법한 답변」을 한 단락 생성하게 하고, 이 텍스트로 벡터 검색을 수행해 질문과 문서 사이의 표현 차이를 좁힌다.

최소 구현에는 LLM 호출 한 번만 추가하면 된다:

HYDE_PROMPT = """请根据用户问题,写一段可能出现在企业知识库中的答案。 只用于检索,不要求事实完全正确,但不要新增具体型号、数字、日期或政策条件。 使用知识库文档常用的客观说明风格,80~120字,只输出答案正文。 用户问题:{query} """ def hyde_search ( query: str ) -> list [ dict ]: hypothetical_doc = llm_call( HYDE_PROMPT. format (query=query), model= "cheap-small-model" , temperature= 0 , ) return vector_search(hypothetical_doc, top_k= 20 )

예를 들어 사용자가 「장비가 자꾸 멈춰요」라고 물으면, 가정 답변은 「장비 응답 타임아웃은 네트워크 연결, 리소스 점유 또는 작업 큐 블로킹으로 인해 발생할 수 있다」처럼 작성될 수 있다. 이 텍스트는 원래 query보다 지식베이스의 고장 진단 문서에 더 가까이 접근하기 쉽다.

여기에 하나의 강한 제약이 있다: 가정 답변은 오직 검색에만 사용하며, 사실 컨텍스트로서 최종 답변에 넘겨서는 안 되고, 사용자에게 직접 보여줘서도 안 된다. HyDE는 「추측」을 허용하지만, 최종 답변은 반드시 검색된 실제 문서를 기준으로 해야 한다.

3. 다중 query: 한 번에 여러 검색 관점을 생성하고 RRF로 결과를 병합한다

다중 query 재작성은 답변을 생성하지 않고, 모델이 원래 의도를 유지한 몇 가지 질문 방식을 제시하게 한다:

원 query: 장비가 자꾸 멈춰요 변형 1: 장비 응답이 느리거나 타임아웃되는 흔한 원인 변형 2: 장비 실행 중 버벅임이 발생할 때 어떻게 진단하는가 변형 3: 장비 작업 처리 지연의 고장 해결 방법

구현상 모델이 JSON을 직접 반환하게 하고, 파싱에 실패하면 원 query로 되돌아가는 것을 권장한다. 한 번의 형식 이상이 전체 검색 체인을 무너뜨리게 해서는 안 된다:

import json EXPAND_PROMPT = """将用户问题改写成3个适合知识库检索的不同问法。 要求: - 保持原始意图,不新增条件、实体、型号或约束 - 分别覆盖术语表达、故障现象、处理动作三个角度 - 每个问法不超过60字 - 只输出JSON:{"queries": ["...", "...", "..."]} 用户问题:{query} """ def expand_queries ( query: str ) -> list [ str ]: raw = llm_call( EXPAND_PROMPT. format (query=query), model= "cheap-small-model" , temperature= 0 , response_format={ "type" : "json_object" }, ) try : candidates = json.loads(raw).get( "queries" , []) except (json.JSONDecodeError, AttributeError): candidates = [] variants = [] for item in candidates: q = item.strip() if q and q != query and len (q) <= 120 : variants.append(q) # 原 query 永远保留,变体最多取3个 return [query] + variants[: 3 ]

마지막 줄에 주목하자: 원 query는 반드시 검색에 참여해야 한다. 모델이 생성한 변형만 검색하는 것은 사용자 의도를 LLM 호출 한 번에 완전히 맡기는 것과 같다. 변형이 한 번 어긋나면 원래의 검색 진입점도 사라진다.

다중 경로 결과는 직접 이어 붙이지 말고 RRF로 융합하라

각 경로에서 top20씩 가져와 chunk_id 기준으로 중복을 제거하고 RRF 점수를 누적한다:

```python def rrf_fuse(result_lists: list[list[dict]], k: int = 60, top_n: int = 5) -> list[dict]: scores: dict[str, float] = {} docs: dict[str, dict] = {} for results in result_lists: for rank, doc in enumerate(results, start=1): chunk_id = doc["chunk_id"] docs[chunk_id] = doc scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank) top_ids = sorted(scores, key=scores.get, reverse=True)[:top_n] return [docs[chunk_id] for chunk_id in top_ids]

def multi_query_search(query: str) -> list[dict]: queries = expand_queries(query) # 운영 환경에서는 비동기 작업으로 병렬 처리해, 각 경로의 지연 시간이 단순히 더해지지 않도록 한다 result_lists = [vector_search(q, top_k=20) for q in queries] return rrf_fuse(result_lists, top_n=5) ```

여기서도 26번 글의 RRF를 그대로 사용하며, 벡터 유사도를 직접 더하지 않는다. 서로 다른 query의 점수 분포가 일정하지 않기 때문에 순위가 원시 점수보다 더 안정적이다. k=60은 앞서 사용한 기본값을 그대로 따르며, 이 파라미터에 먼저 공을 들일 필요는 없다.

4. 실측: 확장이 유용한지는 query 유형에 달려 있다

테스트 환경은 앞선 글들과 동일하다:

- PostgreSQL 16 + pgvector 0.8 - embedding: bge-m3, 1024차원, cosine 거리 - 각 경로 recall top20, RRF k=60, 최종 top5 - 확장 모델: 동일한 저가 소형 모델, temperature=0 - 지연 시간 측정에는 query 확장과 검색이 포함되며, 최종 답변 생성은 포함하지 않음

평가셋은 27번 글의 독립 query 100개와 ground truth를 재사용했다. 원본 query의 recall@5는 86%였다. 이를 일선 사용자에 더 가까운 구어체와 모호한 표현으로改写하고, 의도와 관련 문서가 변하지 않았음을 사람이 확인한 뒤 네 가지 방식을 비교했다:

| 방식 | 전체 recall@5 | P95 지연 | 확장당 Token | |---|---|---|---| | 원본 query를改写, 확장 없음 | 72% | 210ms | 0 | | 순수 HyDE | 80% | 950ms | 약 160 | | 멀티 query + RRF | 84% | 1410ms | 약 230 | | 원본 query + 멀티 query + RRF | 86% | 1480ms | 약 230 |

유형별 결과가 더 참고할 만하다:

| query 유형 | 표본 수 | 확장 없음 | 순수 HyDE | 멀티 query + RRF | 원본 query 보존 조합 | |---|---|---|---|---|---| | 용어 불일치형 | 40 | 65% | 78% | 80% | 83% | | 모호한 설명형 | 35 | 69% | 77% | 83% | 86% | | 기타 독립 질문 | 25 | 88% | 88% | 92% | 92% |

이 데이터는 현재 장비 지식베이스와 평가셋만 대표하며, 범용 기준으로 삼을 수는 없다. 다만 세 가지 결론은 비교적 안정적이다:

- query 확장이 보완하는 것은 표현 차이이며, 정확한 모델명과 에러 코드에는 도움이 제한적이다. 후자는 여전히 26번 글의 BM25 하이브리드 검색으로 처리해야 한다.

- 멀티 query의 전체 성능은 순수 HyDE보다 낫지만, 대가는 더 많은 검색 요청과 더 높은 지연 시간이다. 의미 공간에서의 '가설 답변'도 때로 방향을 잘못 잡을 수 있다.

- 원본 query를 보존하는 것은 저렴하고 효과적인 보험이다. 벡터 검색 한 경로만 추가할 뿐인데, 이번 테스트에서는 전체 recall@5를 84%에서 86%로 끌어올렸고, 변형 드리프트로 인한 퇴행도 줄였다.

5. 쉽게 빠지는 네 가지 함정

1. HyDE가 존재하지 않는 사실을 가설 답변에 써 넣는 경우

사용자가 'A 제품이 왜 연결되지 않나요'라고 물으면, 모델이 'A 제품 연결 타임아웃은 인증 만료와 관련이 있다'고 가정할 수 있다. 만약 지식베이스에 인증 관련 내용이 없다면, 검색이 잘못된 주제로 끌려가게 된다.

방어 방법: prompt에서 모델명, 숫자, 조건의 신규 추가를 금지하고, 온라인에서는 가설 답변과 원본 query를 기록하며, badcase 복기 시 확장 텍스트를 먼저 보고 그다음에 embedding을 의심한다.

2. 멀티 query가 한 문제를 세 문제로 늘리는 경우

'어떤 배포 방식을 지원하나요'가 '프라이빗 배포를 지원하나요, 국산 OS를 지원하나요, 배포 비용은 얼마인가요'로 확장되었는데, 뒤의 두 질문은 이미 원래 의미를 바꾼 것이다. recall은 올라갈 수 있지만, 최종 답변에 사용자가 묻지 않은 내용이 섞여 들어갈 수 있다.

방어 방법: 변형의 개수와 길이를 제한하고, 원본 query를 보존하며, '제약을 신규 추가하지 말 것'을 prompt에 적는다. 가격, 날짜, 모델명이 관련된 query에서 엔티티 변화가 나타나면 해당 변형은 바로 버린다.

3. recall만 좇고 중복 chunk와 최종 컨텍스트는 보지 않는 경우

다중 경로 검색은 같은 문서의 인접 chunk를 모두 top5에 밀어 넣기 쉽다. recall@5는 좋아 보이지만, 컨텍스트 윈도가 중복 내용으로 가득 차게 된다.

엔지니어링 측면에서는 RRF 이후에 문서 단위 제한을 한 단계 추가할 수 있다: 같은 doc_id는 최대 두 개의 chunk만 유지하고, 나머지 자리는 다른 문서에 넘긴다. 만약 뒤에 reranker가 있다면, 먼저 reranker에 더 넓은 후보 집합을 주고 최종 절단을 수행한다.

4. 모든 query를 일률적으로 확장하는 경우

애초에 검색이 잘 되는 질문에도 LLM 호출 한 번과 여러 경로의 검색 지연을 괜히 치르게 된다. 더 합리적인 방식은 먼저 원본 query로 검색을 실행하고, 다음 조건을 만족할 때 확장하는 것이다:

- 원본 query가 짧고 명확한 엔티티가 부족함;

- 첫 경로 검색 점수가 과거 동일 유형 query의 정상 구간보다 낮음;

- 로그상 고빈도 용어 불일치 또는 모호한 설명 유형에 속함;

- 첫 경로 결과가 저신뢰 거절 응답 흐름으로 들어가기 전에, 확장으로 한 번 만회할 기회를 줌.

임계값은 감으로 정하지 말고, 27번 글의 유형별 평가셋에 따라 각각 보정해야 한다.

6. 선택과 조합: 지금 내 기본안

| 시나리오 | 권장 사항 | | --- | --- | | 다중 턴 후속 질문, 지시어와 생략이 심한 경우 | 먼저 쿼리 재작성을 하고, 바로 확장하지 말 것 | | 사용자 표현이 구어체이고 용어 불일치가 뚜렷한 경우 | 원본 query + 2~3개 변형 + RRF | | 질문이 매우 짧고 문서가 완전한 설명문 위주인 경우 | HyDE를 시도해 볼 수 있으나, 가상 답변은 검색에만 사용 | | 모델명, 오류 코드, 조항 번호 정확 매칭 | 벡터 + BM25, 쿼리 확장이 주된 해법은 아님 | | 원본 query가 이미 안정적인 높은 재현율을 보이는 경우 | 직접 검색, 지연을 증가시키지 않음 | | 실시간성 요구가 높고 호출량이 매우 많은 경우 | 신뢰도가 낮은 query에만 확장을 트리거 |

현재 프로젝트의 기본 파이프라인은 다음과 같습니다.

다중 턴 query → 지시어 해소 / 쿼리 재작성 → 원본 query 먼저 검색 → 신뢰도가 낮을 때 2~3개 변형 생성 → 원본 query와 변형 병렬 리콜 → RRF 융합 → rerank → 생성 및 답변 평가

한 문장으로 정리하면: HyDE와 다중 query는 모두 검색 진입점을 늘리는 것이며, 실제로 제어해야 할 것은 의도 드리프트와 추가 지연입니다. 원본 query를 유지하고, 유형별로 평가하며, 신뢰도가 낮은 요청에만 확장을 트리거하는 것이 단순히 더 많은 변형을 쌓는 것보다 더 중요합니다.

원문은 위챗 공식 계정 「程语AI」에 최초 게재되었습니다. 저는 成风이며, 10여 년간 개발을 해왔고 현재 1인 회사로 AI 애플리케이션 엔지니어링을 하고 있습니다. 댓글로 교류하시는 것을 환영합니다.

程语AI

32

글

3.2k

조회

0

팬