문서를 잘게 쪼갤수록 답이 멀어지는 이유
청크 크기와 분할 기준 하나가 임베딩 벡터의 품질을 좌우한다. 잘못 자르면 유사도 검색이 문맥이 끊긴 반쪽짜리 조각만 반환해 정답이 산산조각 나는데, 크게 자른다고 해결되지도 않는다. 자주 저지르는 분할 실수의 원리를 짚고, 검색 재현율과 응답 완결성을 함께 보는 실용적인 평가 지표까지 정리했다. 후보 조각 검증, 컨텍스트 회수 전략, 기준별 실험 방법을 바로 파이프라인에 적용할 수 있게 구성했다.
DevInsight에서 발행해요
본문만 반환합니다.
청크를 잘게 쪼갤수록 검색이 정확해질 거라는 기대는, 실제 파이프라인에서는 반대 방향으로 미끄러지기 십상이다. 청크 크기를 128토큰으로 줄이면 임베딩 벡터는 한두 문장의 국소적 의미만 담게 되고, 그 벡터로 유사도 검색을 돌리면 쿼리가 '진짜 답이 들어 있는 문단'을 통째로 놓친다. 반환되는 건 정답과 무관한 반쪽짜리 조각이다.
원인은 임베딩의 특성에 있다. 벡터는 조각 안의 토큰 전체를 하나로 압축한다. 조각이 짧을수록 그 압축 결과가 단어 하나에 휘둘리기 쉽고, 문맥이라 부를 만한 정보가 담길 자리가 없다. 쿼리와 어휘만 겹치는 조각은 유사도 점수가 높게 잡히는데, 실제 답이 든 문단은 전혀 다른 조각으로 분리돼 있어 점수를 얻지 못한다. 이 실패는 분할 경계가 의미 단위와 어긋난 순간 확정적으로 발생한다.
구체적인 예를 하나 들면 이렇다. 원문이 "위반 시 계약금 전액을 위약금으로 지급해야 한다"는 문장 하나라 하자. 고정 토큰 분할기가 이 문장을 "위반 시 계약금 전액을"과 "위약금으로 지급해야 한다"로 갈라 버리면, "계약 위반 시 부과되는 금액"이라는 질의가 두 번째 조각을 검색해낸다. 그러나 문장의 핵심인 "계약금 전액"은 첫 번째 조각에 남아 컨텍스트에 실리지 않는다. 검색은 성공했고 답은 틀렸다.
여기서 짚어야 할 건 크기 자체가 아니라 분할 기준이다. 고정 토큰 수로 자르는 분할기는 문단의 중간, 때로는 문장의 중간에서 끊는다. 문장이 반으로 갈린 두 조각을 각각 임베딩하면, 원문의 문장 하나가 지니던 의미는 어느 벡터에도 온전히 남지 않는다. 토큰 기반 분할기는 언어의 특성에도 민감하다. 한국어처럼 조사와 어미가 붙는 언어에서는 형태소 경계가 아닌 지점에서 잘리기 쉽고, 토크나이저가 새 줄과 공백을 처리하는 방식에 따라 한글 문서의 분할 위치가 크게 달라진다.
문맥이 벡터에 실리는 조건
내용 기반 분할기가 고정 크기 분할기보다 나은 이유는, 문단과 소제목, 목록 항목 같은 자연스러운 경계가 대개 하나의 완결된 의미 단위와 겹치기 때문이다. 문단 하나가 대체로 하나의 주장을 담는다는 경험칙을 활용하는 셈이다. 다만 이 경험칙이 통하는 건 서술형 문서일 때다. 표, 코드 블록, 절차 목록이 본문인 문서에서는 문단 단위 경계가 오히려 쿼리와 의미 단위를 어긋나게 만든다. 표 한 칸과 그 설명 문장을 서로 다른 조각으로 갈라놓으면, 표만 검색된 조각은 컨텍스트 회수 단계에서 맥락을 재구성할 재료가 없다.
소제목이 본문 조각에 함께 실리지 않는 문제도 같은 계열이다. 문서를 소제목에서 끊으면 소제목만 남고 본문이 다음 조각으로 넘어간다. "해지 절차"라는 소제목으로 검색하는 질의는 소제목 조각은 찾아내지만, 정작 절차 본문이 든 조각은 점수를 얻지 못한다. 대안은 분할 경계를 소제목 직후로 옮기고, 본문 조각에 소제목 경로를 메타데이터로 붙이는 것이다. "3. 계약 / 3.2 해지 절차" 같은 경로가 컨텍스트 회수 단계에서 검색된 조각의 위치를 잡아 준다.
겹침(overlap)을 더하는 습관도 점검 대상이다. 이웃 조각 경계의 문맥을 놓치지 않으려고 10~20% 겹치는 윈도우를 쓰면, 같은 문장이 여러 조각에 중복으로 실린다. 결과는 두 갈래로 나타난다. 하나는 검색 결과의 자기복제다. top-k를 5로 두면 같은 문단이 두 번 등장해 실제 후보는 네 개뿐이다. 다른 하나는 임베딩의 희석이다. 문맥의 절반이 이미 앞 조각에 들어간 문장이 새 벡터의 평균에도 다시 섞이면, 조각 고유의 의미가 묻힌다.
겹침 자체를 버리라는 뜻은 아니다. 섹션 단위로 경계가 뚜렷한 문서라면 겹침 없이도 경계 양쪽의 문맥이 유지된다. 위키식으로 짧은 문단이 끊임없이 이어지는 문서에서는 겹침이 경계 문맥을 지키는 유일한 장치가 되기도 한다. 이건 상황에 따라 다르다. 판단 기준은 검색 결과에서 경계 문맥 누락이 실제로 관찰되느냐다.
검색이 맞아도 답이 틀리는 순간
분할이 완벽해도 검색 단계와 응답 단계 사이에서 정답이 유실된다. top-k로 뽑은 조각의 전후 이웃 조각을 컨텍스트에 포함하지 않는 파이프라인은, 답이 조각 경계를 가로질러 놓였을 때 그 답을 끝내 회수하지 못한다. "계약 해지 조건은 아래 표와 같다"라는 문장과 실제 표가 서로 다른 조각에 있는 경우가 그렇다.
후보 조각 검증은 이 지점에서 필요해진다. 뽑힌 조각들 사이에 겹침이 없는지, 같은 문단에서 이웃한 조각이 빠지지 않았는지를 확인하고, 빠졌으면 함께 회수하는 방식이다. 가장 단순한 구현은 검색 결과 조각의 인접 조각을 덧붙이는 윈도우 확장이다. 비용도 명확하다. 앞뒤를 무작정 붙이면 컨텍스트가 불어나고, 토큰 상한을 넘어 정작 답이 잘려 나가는 상황이 온다. 인접 조각을 몇 개까지 붙일지는 응답에 쓸 토큰 예산을 먼저 정한 뒤 거꾸로 산정하는 편이 낫다.
top-k 자체도 재봐야 한다. 기본값 3~5개는 편하지만, 후보가 너무 적으면 정답 조각이 처음부터 후보에 안 들어오고, 너무 많으면 토큰 예산이 검색에 소진된다. 후보를 넉넉히 뽑고 순위만 다시 매기는 reranking은 이 사이의 타협으로 쓰인다. 초기 후보를 수십 개 뽑아 순위를 다시 매기고 상위 몇 개만 컨텍스트에 넣는 식이다. 검색 단계의 유사도 순서가 순위가 곧 답의 정확도 순서라는 보장은 없다.
재현율과 완결성, 두 개를 분리해서 본다
검색 재현율만 보면 개선 방향이 어긋난다. 재현율은 정답 조각이 top-k에 들어왔는지만 본다. 재현율이 높아도 조각 내용이 반쪽이면 응답은 완결되지 못한다. 반대로 응답 품질만 보면 검색 실패와 응답 실패가 섞여 어느 쪽을 고쳐야 할지 분간이 안 된다.
재현율(recall@k)과 응답 완결성을 분리해서 보는 게 현실적이다. 재현율은 쿼리당 정답 조각 포함 여부로, 응답 완결성은 정답 조각을 컨텍스트로 준 뒤 답이 필수 사실을 몇 개나 포함했는지로 측정한다. 두 지표를 한 표에 놓으면 검색이 망가졌는지, 컨텍스트 회수가 망가졌는지, 응답이 망가졌는지 구분이 된다. 재현율 0.9인데 완결성이 0.4라면 분할이 아니라 컨텍스트 회수나 조각 검증 쪽이 원인이다. 재현율 자체가 0.5라면 분할 기준부터 되돌아봐야 한다.
평가용 질의 세트는 운영 데이터에서 뽑는다. 문서 열 조각씩 뽑아 정답 조각을 하나씩 붙여 놓고, 한 달 치 질의에서 재현율이 낮았던 것들을 골라 섞는다. 수량은 백 건 안팎이면 분할 기준 변경을 비교하기에 충분하다. 분할 기준을 바꿀 때마다 같은 세트로 재현율을 재고, 떨어진 쿼리의 원본 조각을 직접 열어 보라. 거기서 패턴이 보인다. "그래프 제목 질의"에서 재현율이 반복적으로 떨어진다면, 그림 캡션을 본문 조각에 포함하는 규칙을 추가하는 식으로 대응한다.
청크 크기의 적정치는 결국 자기 데이터에서 나온다. 256토큰 안팎이 표준처럼 언급되지만, 이건 상황에 따라 다르다. 표가 많은 문서는 조각 하나가 수백 토큰을 넘어도 경계를 표 단위로 두는 편이 낫고, 짧은 문단이 이어진 문서는 200토큰 미만이 잘 맞는다. 판단 기준은 하나다. 분할 경계가 의미 단위와 어긋난 조각이 전체의 몇 퍼센트인가. 그 비율이 줄어드는 쪽으로 크기와 기준을 함께 옮겨라. 크기만 조정해선 의미 단위와의 어긋남이 사라지지 않고, 잘게 쪼갠다고 답이 가까워지는 일도 없다.
댓글
댓글을 읽어오는 중입니다.
같이 읽으면 좋은 글
방금 읽은 주제와 이어지는 글을 골랐습니다.
검색이 자꾸 엉뚱한 문서를 물어올 때 임베딩보다 먼저 의심해야 할 것
RAG 검색 품질이 오르지 않을 때 임베딩 모델이나 벡터 DB를 먼저 의심하기 쉽지만, 정작 원인은 문서를 기계적으로 잘라 문맥을 조각내는 청크 분할에 있다. 고정 길이 분할, 문장 중간 절단, 표와 제목 분리 같은 흔한 실수가 재현율을 어떻게 망치는지 사례와 함께 짚고, 청크 크기와 오버랩을 문서 구조에 맞게 조정하는 방법, 검색 품질을 측정하는 실용 지표까지 정리한다.
토큰이 흐르는 동안 화면은 왜 산산조각나는가
SSE로 LLM 응답을 토큰 단위로 흘려보내며 화면에 그리는 순간, 마크다운 문법은 중간에 깨지고 중단 요청은 제대로 전달되지 않으며 토큰마다 렌더링이 버벅인다. 이 글은 스트리밍 UI에서 실제로 부딪히는 마크다운 파싱 붕괴, abort 처리와 에러 복구, 렌더링 성능 문제를 실패 사례 순서로 훑고, 부분 렌더링과 버퍼 축적을 최소화하는 백프레셔 방식 등 검증된 해결책을 정리한다.
LLM이 JSON만 뱉기로 약속했을 때 실제로 일어나는 일
OpenAI의 json_object 모드는 구문 유효성만 보장할 뿐 스키마를 강제하지 않는다. Zod/Pydantic 검증, json_repair로 복구하고 validation error를 피드백해 재시도하는 단계별 방어선과 서킷 브레이커, 멀티 프로바이더 폴백까지 실무 패턴을 파헤친다.
이전 글
node_modules를 두 번 지운 뒤에야 패키지 매니저를 다시 봤다
다음 글
200 OK는 무중단 배포를 지켜주지 않는다
DevInsight Digest
새 글이 쌓이면, 피드에서 바로 이어 읽으세요.
과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.