DevInsight

개발하며 겪은 판단을 적어요

AI
조회 2약 4분 읽기

검색이 자꾸 엉뚱한 문서를 물어올 때 임베딩보다 먼저 의심해야 할 것

RAG 검색 품질이 오르지 않을 때 임베딩 모델이나 벡터 DB를 먼저 의심하기 쉽지만, 정작 원인은 문서를 기계적으로 잘라 문맥을 조각내는 청크 분할에 있다. 고정 길이 분할, 문장 중간 절단, 표와 제목 분리 같은 흔한 실수가 재현율을 어떻게 망치는지 사례와 함께 짚고, 청크 크기와 오버랩을 문서 구조에 맞게 조정하는 방법, 검색 품질을 측정하는 실용 지표까지 정리한다.

DevInsight에서 발행해요

#RAG#청크 분할#검색 품질#임베딩#벡터 DB#문서 전처리#재현율#LLM

검색이 이상하다는 신호는 엉뚱한 문서가 상위에 올라올 때 처음 드러난다. "환불 규정"을 물었는데 배송 정책 문서가 딸려 오는 식이다. 여기서 대개는 임베딩을 교체하거나 벡터 DB의 인덱스 설정을 만진다. 로그를 뜯어보면 원인은 훨씬 앞단에 있다. 답이 담긴 문장이 이미 두 조각으로 잘려 있거나, 전제 조건이 다른 조각으로 흩어져 있다.

청크 분할은 파이프라인에서 가장 조용한 단계다. 임베딩과 검색은 점수로 평가되지만 분할은 전처리로 취급된다. 뭔가 잘못돼도 에러가 나지 않는다. 검색은 그럭저럭 결과를 돌려주고 품질만 조금씩 깎인다. 문제는 이 저하가 누적된다는 점이다. 상위 문서가 조금씩 어긋나면 답변 전체가 어긋난다.

고정 길이로 자르면 문장이 먼저 죽는다

가장 흔한 실수는 500자나 1000토큰 같은 고정 길이로 기계적으로 자르는 것이다. 문장 중간에서 끊기면 한 조각은 주어를 잃고 다른 조각은 결론을 잃는다. 임베딩은 잘린 조각을 온전한 의미 단위로 취급한다. "반품은 수령 후 7일 이내"라는 문장이 "반품은 수령 후"와 "7일 이내"로 갈라지면, 어느 쪽도 질문과 정확히 맞지 않는다. 두 조각의 벡터가 원래 문장과 다른 위치에 놓이는 셈이다.

오버랩은 절단 피해를 일부 메운다. 대신 저장 비용과 검색 중복이 함께 는다. 오버랩을 청크의 절반까지 주면 같은 문장이 두 번 검색되고, 상위 순위가 중복 결과로 채워진다. 경험적으로 문단이나 문장 경계를 우선하고, 경계가 애매할 때만 10~20% 수준의 오버랩을 둔다. 표로 치면 오버랩은 붕대에 가깝다. 상처의 원인을 덮지만 뼈를 맞추지는 않는다.

임베딩이 조용히 꼬리를 버리는 순간

고정 길이 분할에는 더 음험한 실패 모드가 있다. 임베딩 입력의 최대 토큰을 넘긴 청크는 앞부분만 벡터로 바뀌고 나머지는 조용히 잘린다. 1000토큰짜리 청크를 512토큰 한도에 넣으면 뒤 절반은 검색 대상에서 사라진다. 에러도 경고도 없다. 문서를 확인하려면 토큰 수 분포를 히스토그램으로 찍어 한도를 넘는 청크 비율을 세면 된다. 5%를 넘는다면 그만큼의 문맥이 이미 버려지고 있다.

여기에 언어별 함정이 겹친다. 한글은 같은 글자 수라도 토크나이저에 따라 영어보다 토큰이 많이 나온다. "1000자로 자르기" 규칙은 토큰 기준으로는 1200~1500까지 벌어질 수 있다. 토큰 한도를 지키려면 문자 수가 아니라 실제 토크나이저로 세야 한다. 문자 수 기반 규칙은 한도 초과를 감지하지 못한 채 계속 통과시킨다.

표와 제목을 떼어놓는 순간

문서 구조를 무시하는 실수는 더 고약하다. "3.2 반품 수수료"라는 제목이 본문과 다른 청크로 가면, 본문 조각은 자신이 무엇에 대한 내용인지 잃는다. 표도 마찬가지다. 헤더 행이 본문과 분리되면 "왕복 배송비 3,000원" 같은 셀만 남고, 그 3,000원이 무엇을 뜻하는지 사라진다. 검색은 숫자는 찾아도 맥락은 못 찾는다.

구조를 살리는 분할은 어렵지 않다. 마크다운이나 HTML이면 제목 수준을 따라 나누고, 각 청크 앞에 상위 제목 경로를 붙여 준다.

# 청크를 만들 때 상위 제목을 breadcrumb로 앞에 붙인다 chunk_text = f"{section_path}\n\n{body}" # 예: "3장 반품 > 3.2 수수료"

이 한 줄이 재현율을 눈에 띄게 바꾸는 경우가 많다. 질문과 청크가 같은 어휘를 공유하기 때문이다. 표는 헤더를 첫 행으로 복제해 모든 행 묶음에 붙이면 단절이 줄어든다. 헤더 없는 숫자 덩어리는 임베딩 입장에서 거의 노이즈다.

크기는 문서가 정한다

청크 크기를 하나로 통일하는 순간 문서 구조와 싸우게 된다. 계약서처럼 조항 단위가 분명한 문서는 조항 하나가 자연스러운 청크다. 단계별 안내문은 한 단계가 여러 문단에 걸치므로 단계 전체를 담아야 한다. API 레퍼런스처럼 계층이 깊은 문서는 함수 하나와 그 설명을 함께 묶는 편이 낫다.

크기 자체보다 중요한 것은 "이 청크만 읽고 질문에 답할 수 있는가"다. 답에 필요한 조건이 옆 청크에 있다면 그 분할은 실패다. 200토큰이든 800토큰이든, 청크 하나가 자기완결적인 의미를 이루는지가 기준이 된다.

작은 청크로 검색하고, 검색된 조각의 부모 문단을 문맥으로 넘기는 방식도 있다. 이른바 parent-child 구조다. 검색 정밀도는 올라가지만 인덱스를 두 벌 관리해야 하고 지연이 늘어난다. 문서가 짧고 구조가 단순하면 과한 선택이다.

의미 기반 분할도 만능이 아니다

문장 임베딩을 계산해 유사도가 급격히 떨어지는 지점에서 자르는 semantic chunking은 고정 길이보다 낫다는 평을 듣는다. 다만 임계값 하나로 동작이 결정된다. 임계값이 낮으면 문서가 문장 단위로 흩어지고, 높으면 성격이 다른 절이 한 청크에 뭉친다. 게다가 문서를 넣을 때마다 추가 임베딩 호출이 발생해 색인 비용이 오른다.

판단 기준은 간단하다. 문서 양이 많고 문단 길이 편차가 크면 semantic 방식이 유리하다. 정형 문서가 대부분이고 조항이나 단계 같은 명확한 경계가 있으면 구조 기반 분할이 더 싸고 예측 가능하다. 두 방식을 섞을 때는 경계 규칙이 충돌하지 않는지 먼저 확인해야 한다.

재현율은 눈으로 보이지 않는다

검색 품질을 이야기할 때 "느낌상 좋아졌다"는 판단은 위험하다. 최소한의 측정 장치가 필요하다. 질문 50~100개에 정답 청크를 라벨링하고 recall@k와 MRR을 계산한다. recall@5가 0.6이면 상위 5개 안에 정답이 60%만 들어온다는 뜻이다. 정답 청크가 검색된 순위 분포를 함께 보면 분할 문제인지 임베딩 문제인지 구분하는 실마리가 잡힌다.

표본이 작으면 수치가 흔들린다. 질문 20개로 측정한 재현율 차이 5%p는 노이즈에 묻힌다. 최소 50개, 가능하면 100개를 권한다. 정답 청크가 아예 검색되지 않으면 분할이나 임베딩 문제다. 검색은 되는데 순위가 낮으면 랭킹이나 재정렬 문제다. 이 구분 없이 임베딩부터 바꾸면 비용만 쓰고 원인은 남는다.

정답 청크 라벨이 없으면 질문과 정답 문서를 문서 단위로만 표시해도 절반은 진단된다. 문서 단위 재현율이 낮으면 조각 문제일 가능성이 크다.

바꾸기 전에 확인할 것

분할 방식을 손대기 전에 질문 세트 50개와 정답 청크 라벨을 먼저 만든다. 그다음 recall@5가 10%p 이상 움직이는지 본다. 이 정도 변화가 없으면 임베딩 교체를 보류하고, 제목 경로 부착과 표 헤더 복제처럼 비용이 낮은 교정부터 적용한다. 청크 크기 조정은 마지막 순서다. 대부분의 검색 실패는 크기가 아니라 경계에서 시작된다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

AI 전체 보기
AI

문서를 잘게 쪼갤수록 답이 멀어지는 이유

청크 크기와 분할 기준 하나가 임베딩 벡터의 품질을 좌우한다. 잘못 자르면 유사도 검색이 문맥이 끊긴 반쪽짜리 조각만 반환해 정답이 산산조각 나는데, 크게 자른다고 해결되지도 않는다. 자주 저지르는 분할 실수의 원리를 짚고, 검색 재현율과 응답 완결성을 함께 보는 실용적인 평가 지표까지 정리했다. 후보 조각 검증, 컨텍스트 회수 전략, 기준별 실험 방법을 바로 파이프라인에 적용할 수 있게 구성했다.

#RAG#청크 분할#임베딩#벡터 검색
AI

토큰이 흐르는 동안 화면은 왜 산산조각나는가

SSE로 LLM 응답을 토큰 단위로 흘려보내며 화면에 그리는 순간, 마크다운 문법은 중간에 깨지고 중단 요청은 제대로 전달되지 않으며 토큰마다 렌더링이 버벅인다. 이 글은 스트리밍 UI에서 실제로 부딪히는 마크다운 파싱 붕괴, abort 처리와 에러 복구, 렌더링 성능 문제를 실패 사례 순서로 훑고, 부분 렌더링과 버퍼 축적을 최소화하는 백프레셔 방식 등 검증된 해결책을 정리한다.

#SSE#스트리밍#LLM#마크다운 파싱

이전 글

use client 한 줄로 갈리는 서버와 클라이언트, 번들 크기부터 상태 관리까지 흔들린다

다음 글

eslintrc를 버리고 flat config로 갈아탈 때 조용히 무너지는 지점들

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독