에이전트·RAG

AGENT / 50번째 글

문서를 벡터 하나로 뭉개지 않는 검색

긴 청크를 임베딩 하나에 밀어 넣으면 세부가 지워집니다. 레이트 인터랙션은 토큰마다 벡터를 남겨 그 손실을 피하는 방식입니다. MaxSim이 무엇을 계산하는지, 저장 용량이 왜 문제가 되는지, 어느 자리에 놓아야 값을 하는지 정리합니다.

PALDYN Team16 MIN READ

지난 글에서 하이브리드 검색의 손잡이를 순서대로 돌리는 이야기를 했다. 그런데 손잡이를 다 돌려도 안 올라가는 자리가 있다. NN을 200까지 키우고, RRF의 kk를 바꾸고, 리랭커를 붙여도 특정 질의에서만 계속 엉뚱한 문서가 올라오는 경우다.

그럴 때는 손잡이가 아니라 구조를 봐야 한다. 벡터 검색은 문서 하나를 벡터 하나로 바꿔 두고 질의 벡터와 견준다. 800자짜리 청크도, 두 문장짜리 청크도 똑같이 실수 1,024개다. 이 압축이 어디까지 견디는지가 오늘 이야기의 출발점이다.

벡터 하나에 청크를 다 담을 때

사내 문서에서 뽑은 청크 하나가 이렇다고 하자.

저희는 환불 규정상 수령일로부터 7일 이내에만 접수를 받습니다. 교환은 14일까지 가능하며, 배송비는 단순 변심인 경우 고객 부담입니다. 사업자 회원은 별도 약관이 적용됩니다.

여기에는 환불 기한, 교환 기한, 배송비 부담, 사업자 예외까지 네 가지 사실이 들어 있다. 임베딩 모델은 이 네 가지를 벡터 하나에 담아야 하므로 「반품·교환 정책에 관한 안내문」 정도의 평균값을 만든다. 개별 사실은 그 평균에 녹아 흐려진다.

그래서 「환불은 며칠까지」 같은 질의가 들어오면 이 청크가 최상위로 올라오지 않는 일이 생긴다. 답이 분명히 안에 있는데도, 벡터끼리 견줄 때는 「환불 7일」이라는 구체적인 대응이 보이지 않기 때문이다. 대신 「환불 정책 안내」라는 제목만 있고 알맹이는 없는 문서가 더 가깝게 나오기도 한다. 제목 문서는 담아야 할 사실이 하나뿐이라 벡터가 흐려지지 않았다.

이것을 정보 병목이라고 부른다 — 담아야 할 내용이 담을 그릇보다 클 때 세부가 먼저 지워지는 현상이다. 청킹 전략에서 청크를 잘게 자르라고 하는 이유가 여기에 있고, 잘게 자르면 문맥이 끊긴다는 반대쪽 문제도 여기서 나온다.

질의와 문서를 언제 만나게 하는가

병목을 피하는 방법은 하나뿐이다. 질의를 알기 전에 문서를 벡터 하나로 요약해 두지 않는 것이다. 그런데 얼마나 늦출 수 있는지에 따라 방식이 셋으로 갈린다.

질의와 문서를 언제 만나게 하는가

바이인코더는 질의와 문서를 각각 따로 인코딩해 벡터 하나씩으로 만든 뒤 내적 한 번으로 견주는 방식이다. 지금 쓰는 벡터 검색이 전부 이것이다. 문서 벡터를 미리 만들어 색인에 넣어 둘 수 있어서 검색이 빠르다.

크로스 인코더는 질의와 문서를 한 덩어리로 붙여 모델에 통째로 넣고 점수 하나를 받는 방식이다. 리랭킹에서 쓰는 것이 이쪽이다. 질의를 봐야 계산을 시작할 수 있으니 미리 해 둘 수 있는 일이 없고, 후보 하나마다 모델을 한 번씩 돌려야 한다.

레이트 인터랙션은 그 사이다. 문서를 인코딩할 때 벡터 하나로 뭉치지 않고 토큰마다 벡터 하나씩 남겨 둔다. 질의가 들어오면 질의 토큰 벡터와 문서 토큰 벡터를 맞대어 점수를 낸다. 「만남」을 토큰 단위까지 늦추되, 모델을 다시 돌리지는 않는다. 이 방식을 처음 정리한 모델 이름을 따 ColBERT 계열이라고도 부른다.

MaxSim이 계산하는 것

레이트 인터랙션의 점수 함수는 MaxSim이다. 질의 토큰마다 문서 토큰 전부와의 유사도를 재고, 그중 최댓값 하나만 골라 더한다.

S(q,d)=∑i∈qmax⁡j∈d  Eqi⋅EdjS(q, d) = \sum_{i \in q} \max_{j \in d} \; E_{q_i} \cdot E_{d_j}

MaxSim은 행마다 최댓값 하나씩만 가져간다

말로 풀면 「질의의 낱말마다, 문서에서 그 낱말에 가장 잘 대응하는 자리 하나를 찾아 점수를 매기고, 그것들을 합한다」이다. 여기서 두 가지가 따라 나온다.

질의의 모든 낱말이 각자 걸릴 자리를 찾아야 점수가 높다. 「환불」만 잘 걸리고 「며칠」이 아무 데도 안 걸리면 그 행의 최댓값이 작아 총점이 깎인다. 벡터 하나끼리 견줄 때는 이런 부분 누락이 평균에 묻혔는데, 여기서는 행마다 드러난다.

문서 쪽 토큰이 남아도 벌점이 없다. 최댓값만 가져가므로 문서가 길어서 안 쓰인 토큰이 아무리 많아도 점수가 내려가지 않는다. 짧은 질의로 긴 청크를 찾는 상황에 잘 맞는 성질이고, 앞의 네 가지 사실이 든 청크가 「환불 며칠」에서 다시 살아나는 이유다.

동시에 이 성질이 약점이기도 하다. 문서에 아무 말이나 잔뜩 넣어 두면 어떤 질의든 걸릴 자리가 생긴다. 표제어만 나열한 색인 페이지 같은 것이 전 질의에서 상위에 뜨는 일이 실제로 생기니, 그런 문서는 색인에서 빼는 편이 낫다.

값의 대가는 저장 용량이다

토큰마다 벡터를 남긴다는 말은 색인 크기가 토큰 수만큼 늘어난다는 말이다. 이 계산을 먼저 해 보지 않고 도입하면 반드시 막힌다.

문서 100만 개, 청크 하나가 평균 300토큰, 원본 임베딩 차원 1,024, 실수 하나에 2바이트(fp16)라고 하자.

방식 벡터 개수 차원 바이트/벡터 총량
바이인코더 100만 1,024 2,048 약 2.0 GB
레이트 인터랙션 (그대로) 3억 1,024 2,048 약 614 GB
차원을 128로 줄여서 3억 128 256 약 77 GB
128차원 + 2비트 양자화 3억 128 32 약 9.6 GB

그대로 두면 300배다. 그래서 실제 구현은 처음부터 줄이는 것을 전제로 만들어져 있다.

차원 축소가 첫 번째다. 토큰 벡터는 문서 전체를 담을 필요가 없고 그 자리의 낱말 하나만 담으면 되므로, 96~128차원으로 줄여도 성능이 잘 버틴다. 모델 마지막에 선형 사영 한 층을 붙여 학습 때부터 줄여 둔다.

양자화가 두 번째다. 실수 하나를 2비트나 1비트로 눌러 담는다. 임베딩 모델에서 다룬 양자화와 같은 이야기인데, 레이트 인터랙션에서는 압축률이 훨씬 공격적이다. 어차피 최댓값 하나를 고르는 계산이라 개별 유사도 값의 정밀도가 덜 중요하기 때문이다.

토큰 가지치기가 세 번째다. 조사·구두점처럼 어느 질의에도 의미 있게 걸리지 않는 토큰의 벡터는 아예 저장하지 않는다. 한국어에서는 이 비중이 특히 커서 30~40%가 그냥 사라진다.

셋을 다 적용해도 바이인코더의 다섯 배쯤이다. 검색 지연도 그만큼 늘어난다 — 후보 문서마다 행렬 곱을 한 번씩 해야 하므로, 벡터 하나끼리 내적하는 것보다 무겁다.

어느 자리에 놓을 것인가

여기가 실무의 갈림길이다. 레이트 인터랙션은 1차 검색으로도, 리랭커 자리로도 쓸 수 있고 대가가 서로 다르다.

1차 검색으로 리랭커 자리로
색인 전체 문서의 토큰 벡터 필요 상위 후보만 계산하면 됨
저장 용량 위 표대로 크게 늘어남 원본 텍스트만 있으면 됨
지연 후보 축소 알고리즘 필요 후보 수 MM에 비례
얻는 것 1차에서 놓치던 문서를 건짐 순위만 고침
크로스 인코더 대비 — 훨씬 빠름, 정확도는 조금 낮음

1차 검색으로 쓰면 앞 절의 「답이 안에 있는데 안 올라오던 문서」가 실제로 올라온다. 이것이 레이트 인터랙션을 쓰는 본래 이유다. 다만 3억 개 벡터에서 상위 후보를 뽑는 것은 그냥은 안 되고, 토큰 벡터를 군집으로 묶어 후보 문서를 먼저 좁힌 뒤 그 문서들만 정확히 채점하는 2단 구조가 필요하다. ColBERT 계열에서 PLAID라고 부르는 것이 이 후보 축소 장치다. ANN 알고리즘에서 다룬 근사 최근접 탐색과 발상은 같고, 대상이 문서가 아니라 토큰이라는 점이 다르다.

리랭커 자리로 쓰면 저장 문제가 통째로 사라진다. 상위 50건의 토큰 벡터를 그때그때 계산하면 되기 때문이다. 크로스 인코더보다 빠르고, 바이인코더 점수보다 세밀하다. 다만 1차 검색이 못 건진 문서는 여전히 못 건진다 — 지난 글의 「앞쪽 손잡이가 뒤쪽 상한을 정한다」가 여기서도 그대로다.

처음 도입한다면 리랭커 자리부터가 안전하다. 색인을 다시 만들지 않아도 되고, 효과가 없으면 그냥 빼면 된다.

쓰지 않는 편이 나은 경우

청크가 이미 짧으면 정보 병목이 크지 않다. 두세 문장짜리 청크는 벡터 하나로도 충분히 담긴다. 이 경우 레이트 인터랙션은 저장 용량만 늘리고 정확도는 별로 안 올린다.

질의가 한두 낱말이면 MaxSim의 행이 한둘뿐이라 이점이 작다. 「환불 규정 며칠까지인지 알려 줘」처럼 낱말이 여럿 든 질의에서 차이가 벌어진다.

정확한 문자열이 관건이면 키워드 검색 쪽이 여전히 낫다. 오류 코드나 법령 조항 번호는 BM25가 확실히 잡는다. 레이트 인터랙션은 그 자리를 대신하는 것이 아니라 벡터 쪽을 개선하는 것이다.

모델 교체가 잦으면 재색인 비용이 300배로 뛴다. 임베딩 모델을 자주 바꾸는 단계라면 그 단계가 지난 뒤에 올리는 편이 낫다.

무엇을 재서 판단할 것인가

도입 여부는 느낌이 아니라 숫자로 정한다. RAG 평가에서 만든 지표를 그대로 쓰되, 두 가지를 따로 본다.

첫째, 1차 검색의 recall@50이 이미 충분한가. 0.95를 넘고 있다면 1차 검색을 바꿔 얻을 것이 없다. 리랭커 자리만 고민하면 된다.

둘째, 낱말이 여럿 든 질의에서만 갈리는가. 평가 집합을 질의 길이로 나눠 재 보면 대개 긴 질의 쪽에서만 차이가 난다. 실제 서비스의 질의가 짧은 쪽에 몰려 있다면 전체 지표가 올라도 체감은 없다.

정리

  • 벡터 하나에 긴 청크를 담으면 세부가 지워진다. 이것이 손잡이로는 안 고쳐지는 자리다
  • 레이트 인터랙션은 토큰마다 벡터를 남겨 그 손실을 피한다
  • MaxSim은 질의 토큰마다 최댓값 하나씩만 가져가 더한다 — 부분 누락에 민감하고, 문서 길이에는 관대하다
  • 대가는 저장 용량이다. 차원 축소·양자화·토큰 가지치기를 전제로 계산한다
  • 리랭커 자리부터 시작하면 색인을 다시 만들지 않아도 된다
  • 짧은 청크, 짧은 질의, 잦은 모델 교체에서는 값을 못 한다

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

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

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

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

11 MIN
에이전트·RAG2026.08.23

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

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

11 MIN
에이전트·RAG2026.08.23

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

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

13 MIN