에이전트·RAG

AGENT / 49번째 글

하이브리드 검색은 붙이는 것보다 맞추는 것이 일이다

키워드와 벡터를 합쳤는데 기대만큼 안 좋아지는 흔한 상황을 다룹니다. 손잡이가 어디에 있는지, 만지는 순서가 왜 정해져 있는지, RRF의 k가 실제로 무엇을 바꾸는지, 한국어에서 특히 갈리는 자리를 정리합니다.

PALDYN Team16 MIN READ

지난 글에서 매 요청 맨 앞에 실려 가는 시스템 프롬프트를 어디까지 적을지 정했고, 이번 글부터는 그 뒤에 붙을 문서를 골라 오는 검색 쪽으로 옮긴다. 주제는 흔한 상황 하나다. 키워드 검색과 벡터 검색을 합쳐 하이브리드로 바꿨는데, 기대했던 만큼 좋아지지 않는 경우다.

검색 전략에서 BM25와 벡터 검색이 무엇이고 RRF로 어떻게 합치는지는 이미 다뤘다. 여기서는 그다음 이야기를 한다. 붙이는 것은 라이브러리 몇 줄이고, 실제 일은 붙인 뒤에 있다.

왜 붙였는데 안 좋아지는가

하이브리드 검색이 기본값 그대로 잘 도는 경우는 드물다. 이유는 셋이다.

기본값이 특정 상황에 맞춰진 값이기 때문이다. 널리 쓰이는 k=60k=60 같은 값은 영어 웹 문서와 학술 검색 벤치마크에서 잘 나온 값이지 우리 문서에서 잘 나온 값이 아니다.

두 검색기의 실력이 대칭이 아니기 때문이다. 문서 종류에 따라 한쪽이 확연히 낫다. 제품 코드·오류 메시지·법령 조항이 많으면 키워드 쪽이 강하고, 서술형 설명 문서가 많으면 벡터 쪽이 강하다. 그런데 융합 기본값은 둘을 대등하게 다룬다.

어디가 문제인지 안 보이기 때문이다. 최종 답이 나쁘다는 것만 알고, 그것이 1차 검색이 문서를 못 건져서인지 융합이 순위를 망친 것인지 리랭커가 밀어낸 것인지 구별하지 못한다. 구별을 못 하니 손잡이를 아무거나 돌리게 된다.

세 번째가 가장 크다. 그래서 튜닝 이야기는 값이 아니라 순서에서 시작해야 한다.

손잡이는 네 자리에 있다

손잡이는 네 자리에 있고, 만지는 순서가 있다

순서에 이유가 있다. 앞쪽 손잡이가 뒤쪽의 상한을 정하기 때문이다. 1차 검색이 후보에 못 넣은 문서는 융합도 리랭커도 살려낼 수 없다. 리랭커는 주어진 후보를 다시 줄 세우는 물건이지 없는 문서를 만들어 내지 않는다.

그런데 실무에서 가장 흔한 순서가 정확히 거꾸로다. 답이 나쁘면 리랭커부터 바꾼다. 리랭커 교체는 API 한 줄이라 제일 쉽기 때문인데, 문제가 1차 검색에 있으면 아무 효과가 없다.

만지기 전에 딱 하나만 재면 순서가 정해진다. 후보 재현율이다.

recall@N=상위 N건 안에 정답 문서가 들어온 질의 수전체 질의 수\text{recall@}N = \frac{\text{상위 } N \text{건 안에 정답 문서가 들어온 질의 수}}{\text{전체 질의 수}}

이 값이 낮으면 뒤쪽은 볼 필요가 없다. 예를 들어 recall@100이 0.72라면, 질의 넷 중 하나는 정답 문서가 애초에 후보에 없다. 이 상태에서 리랭커를 아무리 좋은 것으로 바꿔도 최종 정확도의 천장은 0.72다.

측정에는 질의와 정답 문서 쌍이 필요하다. 100~200쌍이면 방향을 정하는 데 충분하다. 실제 사용자 질의에서 뽑고, 정답은 사람이 붙인다. 이 작업을 건너뛰면 이후 모든 튜닝이 감으로 하는 일이 된다.

1. 후보 깊이와 토크나이저

첫 손잡이는 두 검색기가 각각 몇 건을 내놓느냐다.

NN을 키우면 재현율이 오르지만 어느 지점부터 거의 안 오른다. 대신 융합과 리랭킹 비용은 계속 는다. 실무에서는 50에서 시작해 100, 200으로 올려 가며 recall@N 곡선을 보고, 곡선이 평평해지는 지점 조금 뒤에서 멈춘다. 대개 100~200 사이다.

같은 자리에 훨씬 큰 손잡이가 하나 더 있는데 자주 잊힌다. 키워드 검색의 토크나이저다. 한국어에서 이 값이 재현율을 가장 크게 흔든다.

  • 어간 처리 — 「검색하다」·「검색했다」·「검색합니다」가 같은 토큰으로 묶이지 않으면 질의와 문서가 안 만난다. 형태소 분석기를 붙이면 이 문제가 상당 부분 해결된다
  • 복합명사 분해 — 「전자정부표준프레임워크」를 통째로 한 토큰으로 두면 「표준 프레임워크」로 검색했을 때 안 걸린다. 반대로 너무 잘게 쪼개면 엉뚱한 문서가 걸린다
  • 불용어 — 조사와 어미가 토큰에 남아 있으면 점수 계산이 흐려진다
  • 숫자·영문 혼합 — 제품 코드나 버전 문자열이 많은 문서라면 이 형태가 온전히 보존되는지 확인한다

기본 공백 분리 토크나이저로 한국어 BM25를 돌리고 있다면, 형태소 분석기로 바꾸는 것 하나가 뒤쪽 손잡이 전부를 합친 것보다 큰 개선을 낼 수 있다.

2. 융합 — k와 가중치

후보가 충분히 들어오면 다음은 둘을 어떻게 합치느냐다.

RRF(Reciprocal Rank Fusion)는 문서의 점수 대신 순위만 써서 합치는 방법이다. 문서 dd가 검색기 ii에서 ri(d)r_i(d)위였다면

RRF(d)=∑iwik+ri(d)\text{RRF}(d) = \sum_i \frac{w_i}{k + r_i(d)}

로 최종 점수를 매긴다. 여기 손잡이가 둘 있다. 상수 kk와 검색기별 가중치 wiw_i다.

kk가 무엇을 바꾸는지는 그림 하나로 보인다.

RRF의 k 하나가 순위를 얼마나 믿을지를 정한다

kk가 크면 1위와 20위의 기여도 차이가 작아진다. 즉 몇 위였는지보다 두 검색기가 함께 뽑았는지가 중요해진다. kk가 작으면 반대로 상위 몇 건만 강하게 밀린다.

그래서 선택 기준이 이렇게 정리된다.

상황 방향
두 검색기 실력이 비슷하고 상보적이다 kk를 크게 (60 이상)
한쪽이 확연히 정확하다 kk를 작게 (10~30) 하고 그쪽 가중치를 올린다
질의가 짧고 키워드성이 강하다 키워드 쪽 가중치를 올린다
질의가 서술형 문장이다 벡터 쪽 가중치를 올린다

가중치는 질의 종류에 따라 동적으로 바꿀 수도 있다. 질의에 따옴표나 제품 코드, 오류 번호가 들어 있으면 키워드 쪽을 올리고, 문장 형태면 벡터 쪽을 올리는 식이다. 규칙 몇 줄로도 눈에 띄는 차이가 난다.

def fuse(keyword_hits, vector_hits, k=60, w_kw=1.0, w_vec=1.0):
    """순위 기반 융합. 점수를 섞지 않고 순위만 쓴다."""
    scores = {}
    for rank, doc_id in enumerate(keyword_hits, start=1):
        scores[doc_id] = scores.get(doc_id, 0.0) + w_kw / (k + rank)
    for rank, doc_id in enumerate(vector_hits, start=1):
        scores[doc_id] = scores.get(doc_id, 0.0) + w_vec / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)


def weights_for(query: str) -> tuple[float, float]:
    """질의 형태로 가중치를 고른다. 규칙 몇 줄이면 충분하다."""
    has_code = any(c.isdigit() for c in query) or '"' in query
    is_sentence = len(query.split()) >= 6
    if has_code and not is_sentence:
        return 1.5, 0.7      # 키워드 쪽에 기울인다
    if is_sentence:
        return 0.7, 1.5      # 벡터 쪽에 기울인다
    return 1.0, 1.0

점수를 직접 섞지 않는 이유

RRF 대신 두 검색기의 점수를 정규화해서 가중합하는 방법도 있다. 이론적으로는 순위만 쓰는 것보다 정보를 더 쓰는 셈이라 나아 보이는데, 실무에서는 대개 더 나쁘다.

BM25 점수는 상한이 없고 문서 길이와 코퍼스 통계에 따라 범위가 매번 다르다. 벡터 유사도는 보통 −1에서 1 사이다. 두 값을 같은 자리에 놓으려면 정규화가 필요한데, 여기서 문제가 생긴다.

  • 질의마다 정규화 기준이 달라진다. 결과 집합의 최솟값·최댓값으로 정규화하면 어떤 질의에서는 1위와 2위의 차이가 크게 벌어지고 다른 질의에서는 거의 붙는다
  • 한 검색기가 아무것도 못 찾은 질의에서 그 검색기의 최고 점수가 1.0으로 올라간다. 쓰레기가 만점을 받는다
  • 재현이 안 된다. 코퍼스가 조금 바뀌면 점수 분포가 바뀌어 어제 맞춰 둔 가중치가 오늘 안 맞는다

순위는 이런 문제가 없다. 그래서 특별한 이유가 없으면 순위 기반으로 시작하고, 점수 기반은 두 검색기의 점수 분포를 실제로 확인하고 정규화 방식을 고정할 수 있을 때만 쓴다.

3. 리랭커 — 여기서 지연이 는다

융합까지 맞춘 다음이 리랭커다. 리랭킹에서 다뤘듯 교차 인코더는 질의와 문서를 함께 넣어 점수를 내므로 정확하지만 후보 수에 비례해 느리다.

손잡이는 몇 건을 리랭커에 넣느냐(MM)다. 융합 결과 상위 몇 건까지 다시 줄 세울지다.

  • MM이 작으면 빠르지만 융합이 아래로 밀어 둔 정답을 못 살린다
  • MM이 크면 잘 살리지만 지연이 그만큼 는다

10~30 사이에서 시작한다. 여기서 재는 것은 최종 정확도만이 아니라 정확도 이득 대비 늘어난 지연이다. MM을 20에서 50으로 올려 정확도가 1%p 오르고 지연이 300ms 늘었다면, 그 교환이 우리 제품에서 맞는지는 검색 품질이 아니라 제품이 답할 문제다.

무엇이 문제인지 가르는 표

증상만 보고 손잡이를 고르면 대개 틀린다. 증상마다 먼저 확인할 것이 정해져 있다.

증상 먼저 확인할 것 그다음 손잡이
정답 문서가 아예 안 나온다 recall@N NN과 토크나이저
정답이 후보엔 있는데 아래에 있다 융합 전후 순위 비교 kk와 가중치
특정 유형 질의에서만 나쁘다 그 유형만 따로 측정 질의별 동적 가중치
짧은 질의에서 나쁘다 키워드 검색 단독 점수 토크나이저·불용어
최근 문서를 못 찾는다 색인 시점과 갱신 주기 검색이 아니라 색인 문제다
순위는 좋은데 답이 나쁘다 검색이 아니라 생성 쪽 청크 크기·문맥 구성

마지막 두 줄이 중요하다. 검색 문제로 보이는 것 중 상당수가 검색 문제가 아니다. 색인이 밀려 있거나, 청크가 잘려 정답 문장이 두 조각에 걸쳐 있거나, 문맥 구성에서 잘려 나가는 경우다. 튜닝을 시작하기 전에 이 둘을 먼저 배제한다.

측정은 두 층으로 한다

마지막으로 측정 이야기. 최종 답의 품질만 보면 튜닝을 못 한다. 최종 품질은 검색·문맥 구성·생성이 전부 섞인 값이라 어느 것을 바꿔서 좋아졌는지 알 수 없기 때문이다.

두 층을 따로 본다.

  • 검색 층 — recall@N, 정답의 평균 순위, 융합 전후 순위 변화. 정답 문서 쌍만 있으면 되고 모델을 안 불러도 되어 싸고 빠르다. 값 하나 바꿀 때마다 돌린다
  • 응답 층 — 최종 답의 정확성과 근거 일치. 비싸고 느리다. 검색 층에서 개선이 확인된 것만 여기로 올린다

검색 층 세트는 100~200 질의면 충분하고, 실제 사용자 질의에서 뽑되 잘 안 되는 질의를 일부러 포함한다. 잘 되는 것만 모으면 어떤 변경을 해도 점수가 안 움직인다.

정리하면

  • 붙이는 것은 몇 줄이고 실제 일은 순서를 지켜 맞추는 것이다
  • 앞쪽 손잡이가 뒤쪽 상한을 정한다. recall@N부터 잰다
  • 한국어에서는 토크나이저가 가장 큰 손잡이일 때가 많다
  • RRF의 kk는 「함께 뽑혔는지」와 「몇 위였는지」 사이의 저울이다
  • 점수를 직접 섞는 것은 정규화가 고정될 때만
  • 리랭커의 MM은 정확도가 아니라 지연과의 교환으로 정한다
  • 검색 층과 응답 층을 따로 잰다

읽어주셔서 감사합니다. 😊

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

에이전트는 어디서 어긋나기 시작하는가

에이전트가 이상한 답을 낼 때 증상은 마지막에 보이지만 어긋난 자리는 그보다 앞입니다. 한 걸음이 무너지는 여섯 자리를 나누고, 증상에서 원인을 거슬러 찾는 방법과 자리별 처방을 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트가 무엇을 했는지 나중에 알 수 있게 만들기

에이전트는 같은 입력에도 다른 경로로 갑니다. 그래서 로그 몇 줄로는 왜 그렇게 됐는지 복원이 안 됩니다. 트레이스를 어떻게 나누고 구간마다 무엇을 붙이며 어떤 지표를 볼지 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트 비용은 걸음 수보다 빨리 늘어난다

걸음이 두 배면 비용은 두 배가 아니라 서너 배입니다. 왜 그렇게 되는지, 어디서 새는지, 상한을 몇 겹으로 어떻게 거는지와 실제로 효과가 큰 순서대로의 대응을 정리합니다.

13 MIN