logo

청킹과 문맥 보강

문서를 고정 길이·문단·제목 단위로 나눠 비교하고, 청크에 빠진 앞뒤 맥락을 LLM으로 보강하는 Contextual Retrieval을 실습한다.

  • 청킹(chunking): 문서를 검색 단위가 되는 작은 조각(청크, chunk)으로 나누는 일. RAG 색인 단계의 첫 작업.
  • 문서 전체를 통째로 넣지 않는 이유:
    • 비용과 길이: 문서가 많으면 모델의 컨텍스트 창에 다 들어가지 않고, 들어가더라도 토큰 비용이 큼.
    • 정확도: 질문과 관계없는 내용이 많이 섞이면 모델이 필요한 부분을 놓치기 쉬움.

청크 크기의 균형

청크 장점 단점
크게 앞뒤 맥락이 함께 들어감 관련 없는 내용이 섞이고, 검색 결과 몇 개만으로 컨텍스트가 참
작게 질문과 관련된 부분만 정확히 찾음 맥락이 끊겨 청크만 읽고는 뜻을 알기 어려움
  • 흔히 수백 토큰 정도로 자르고, 이웃한 청크끼리 조금 겹치게 함.
  • 정답은 없음. 문서의 구조와 질문의 유형에 따라 달라지므로 검색 결과를 보며 조정.

실습 준비

  • 실습 자료 다운로드: mindmall_guide.py. 현재 폴더에 업로드. 안내서 원문이 문자열 변수 guide에 들어 있음.
  • API 키를 openrouter-key.txt에 저장하고, 현재 폴더에 업로드. 마지막 절의 문맥 보강에서 사용.
!pip install -q openai
from pathlib import Path
from openai import OpenAI

client = OpenAI(
    base_url="https://openrouter.ai/api/v1",
    api_key=Path("openrouter-key.txt").read_text().strip(),
)
model = "openai/gpt-4.1-mini"

from mindmall_guide import guide

고정 길이로 나누기

  • 정해진 글자 수마다 자르는 가장 단순한 방법.
  • 겹침(overlap): 경계에 걸친 문장이 양쪽 청크 중 하나에는 온전히 남도록 앞 청크의 끝부분을 다음 청크에 다시 넣음.
def chunk_fixed(text, size, overlap):
    step = size - overlap  # 다음 청크의 시작 위치까지 거리
    return [text[start:start + size] for start in range(0, len(text), step)]
fixed = chunk_fixed(guide, size=300, overlap=50)
len(fixed)  # 청크 수
실행 결과
8
fixed[3]
실행 결과
'는다. 골드 회원도 추가 배송비는 내야 한다.\n\n배송 조회는 주문 내역에서 운송장 번호를 눌러 확인한다. 출고 후 3일이 지나도 도착하지 않으면 고객센터로 문의한다.\n\n## 4. 교환과 반품\n\n상품을 받은 날로부터 7일 안에 교환이나 반품을 신청할 수 있다. 신청은 주문 내역의 교환·반품 버튼으로 한다.\n\n단순 변심으로 반품하면 왕복 배송비 6,000원을 고객이 낸다. 상품에 불량이 있거나 주문과 다른 상품이 배송된 경우에는 배송비를 마인드몰이 낸다. 이 경우 수령 후 30일 안에 신청하면 된다.\n\n다음 경우에는 단순 변심에 의한 '
  • 청크가 문장이나 단어 중간에서 시작하고 끝남. 글자 수만 보고 잘랐기 때문.
  • 3번 청크의 마지막 50글자(" 낸다. 이 경우 수령 후 30일 안에…")가 4번 청크(fixed[4])의 앞에 다시 나옴. 이것이 겹침.
  • 장점: 구현이 쉽고 청크 크기가 고름. 단점: 문서의 구조를 무시함.

문서 구조로 나누기

  • 사람이 쓴 문서에는 이미 의미 단위가 있음: 제목, 문단, 목록.
  • 마크다운 문서는 ## 제목으로 절을, 빈 줄로 문단을 구분.

문단 단위: 빈 줄로 구분된 문단 하나가 청크 하나.

def chunk_by_paragraph(text):  # 빈 문단과 제목 줄은 제외
    paragraphs = [p.strip() for p in text.split("\n\n")]
    return [p for p in paragraphs if p and not p.startswith("#")]
by_paragraph = chunk_by_paragraph(guide)
len(by_paragraph)
실행 결과
23
  • 제목 단위(text.split("\n## ")): 절 하나가 청크 하나. 이 안내서에서는 절마다 166-371자로 길이가 크게 다름. 긴 절은 다시 나눠야 할 수 있음.
  • 문단 단위: 청크가 짧고 문장이 잘리지 않음. 대신 문단 하나만 보고는 맥락이 부족할 수 있음.
  • 실무에서는 섞어서 사용. 예: 제목으로 먼저 나누고, 너무 긴 절은 문단으로, 그래도 긴 문단은 고정 길이로 나눔. 이런 방식을 재귀적 분할이라 함.

맥락이 빠진 청크

by_paragraph[19]
실행 결과
'- 떨어뜨리거나 물에 젖는 등 고객의 부주의로 고장 난 경우\n- 공식 수리점이 아닌 곳에서 분해하거나 수리한 경우'
  • 문단 단위로 나눈 청크 중 하나. 이 두 경우가 어떻게 된다는 것인지 이 청크만으로는 알 수 없음. 바로 앞 문단 "무상 수리 기간에도 다음 경우는 유상 수리다."가 다른 청크로 떨어져 나갔기 때문.
  • 검색에서도 불리함. "키보드를 물에 빠뜨렸는데 수리비를 내야 하나요?"를 검색하면 이 청크가 답을 담고 있는데도, 청크 안에 '수리비'나 '유상'이라는 말이 없음.
  • 같은 문제의 예: "매출이 이전 분기보다 3% 늘었다." → 어느 회사의 어느 분기인지 청크에 없음.

Contextual Retrieval

  • 청크마다 문서 전체에서의 위치와 맥락을 설명하는 짧은 문장을 LLM으로 만들어 청크 앞에 붙이는 방법. Anthropic이 2024년에 제안.
  • 맥락을 붙인 청크로 색인하면 검색할 때 그 설명도 함께 비교됨.
def add_context(document, chunk):
    prompt = f"""<document>
{document}
</document>

다음은 위 문서에서 뽑은 청크입니다.
<chunk>
{chunk}
</chunk>

검색이 잘 되도록, 이 청크가 문서의 어느 부분이며 무엇에 관한 내용인지
한두 문장으로 설명하세요. 설명만 답하세요."""
    response = client.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt}])
    return f"{response.choices[0].message.content}\n\n{chunk}"  # 설명을 청크 앞에 붙임
print(add_context(guide, by_paragraph[19]))
실행 결과
이 청크는 문서의 6장 ‘품질 보증’ 항목에 속하며, 무상 수리 기간 중에도 유상 수리가 되는 경우에 관한 내용입니다.

- 떨어뜨리거나 물에 젖는 등 고객의 부주의로 고장 난 경우
- 공식 수리점이 아닌 곳에서 분해하거나 수리한 경우
  • 붙은 설명에 '품질 보증', '유상 수리' 같은 말이 들어감. 청크만 읽어도 뜻이 통하고, 검색할 단어도 늘어남.
  • 비용: 청크마다 문서 전체를 다시 보내므로 청크 수만큼 LLM을 호출. 색인할 때 한 번만 하면 되고, 같은 문서를 반복해 보내는 부분은 프롬프트 캐싱으로 비용을 줄일 수 있음.
  • Anthropic의 실험(검색 결과 상위 20개에 정답 청크가 없는 비율):
    • 맥락을 붙인 청크로 임베딩 검색: 실패율 35% 감소(5.7% → 3.7%).
    • 임베딩 검색과 BM25 키워드 검색에 모두 적용: 49% 감소(5.7% → 2.9%).
    • 여기에 rerank 추가: 67% 감소(5.7% → 1.9%).

출처: Anthropic(2024), Introducing Contextual Retrieval.

퀴즈

문제 1 / 4맞음: 0힌트: 0틀림: 0채점중: 0남음: 4

고정 길이 청킹에서 이웃한 청크를 조금씩 겹치게 하는 이유는?

  • ○경계에서 잘린 문장이 한쪽 청크에는 온전히 남도록 하기 위해
  • ○청크의 수를 줄여 색인 비용을 낮추기 위해
  • ○모든 청크의 길이를 다르게 만들기 위해
  • ○검색 결과에 같은 청크가 두 번 나오도록 하기 위해

퀴즈를 풀려면 대화형 기능을 불러와야 합니다.