실무 Multi-Agent 오케스트레이션 12장 · 문서 로딩과 청킹 5 / 8 ← 이전목차다음 → TechLead Cro

12장. 문서 로딩과 청킹

메타데이터 — 조각에 출처를 적어 둔다

한 줄 요약

청크에는 본문만이 아니라 "이 조각이 어느 문서의 어느 조항에서 왔는가" 를 함께 적어 둡니다. 이것을 메타데이터(metadata) 라고 합니다. 답변에 근거를 붙이려면 반드시 필요하고, 자를 때 붙이지 않으면 나중에는 붙일 수 없습니다.


1. 왜 필요한가

표준 질문 Q3의 검증 포인트는 이렇습니다.

"회사 전액 부담" + (근거: 제N조) 표기

답변에 "(근거: 반품교환환불정책 제2조)"를 붙이려면, 검색으로 찾은 청크가 자기가 어디서 왔는지 알고 있어야 합니다.

청크의 본문만 봅시다.

① 하루포인트는 구매확정 다음 날 등급별 적립률에 따라 자동 적립된다. ② 적립금·쿠폰으로 결제한 금액에는 적립되지 않는
다. ③ 리뷰 작성 시 일반 리뷰 100포인트, 포토 리뷰 500포인트가 추가 적립된다(상품당 1회).

이 조각만 보고 "멤버십 정책 제3조"라는 것을 알아낼 방법은 없습니다. 원문과 이어져 있는 자르는 순간에만 알 수 있습니다.


2. 청크 하나의 모양

이번 장에서 만드는 청크는 딕셔너리입니다.

{
    "id": "policy-002",
    "text": "제3조 (하루포인트 적립)\n① 하루포인트는 구매확정 다음 날 …",
    "metadata": {
        "source": "하루마켓_멤버십정책.pdf",
        "doc_title": "멤버십정책",
        "chunk_no": 2,
        "article": "제3조 (하루포인트 적립), 제4조 (하루포인트 사용), 제5조 (하루포인트 유효기간과 소멸)",
    },
}
키 뜻 쓰이는 곳
id 청크의 고유 번호 저장하고 다시 찾을 때 (13장)
text 청크 본문 검색 대상, 모델에게 건네는 근거
source 원본 파일 이름 어느 파일인지
doc_title 사람이 읽을 문서 제목 답변의 근거 표기 (14장)
chunk_no 몇 번째 청크인지 순서를 따질 때
article 이 청크에 내용이 담긴 조항 제목 전부 답변의 근거 표기 (14장)

article은 문자열 하나입니다. 조항이 여럿이면 쉼표로 이어 적습니다.


3. 청크와 조항은 일대일이 아니다

조항 하나는 150~400자쯤이고 청크는 500자까지 채웁니다. 그래서 한 청크에 조항이 두세 개 들어갑니다. 겹침 때문에 앞 조항의 끝부분으로 시작하는 청크도 있습니다.

(policy-003의 시작)
내한다. ③ 유효기간이 지나 소멸된 포인트는 복구되지 않는다. …      ← 제5조의 끝부분. 제목 줄은 없다
제6조 (등급 조정과 제외)
① 반품·취소로 실결제액이 산정 기준에 미달하게 되면 …

이 청크의 첫 문장을 근거로 답한다면 출처는 제5조여야 합니다. 그런데 청크 안에는 "제5조"라는 제목 줄이 없습니다. 그래서 꼬리표를 붙일 때 세 가지를 합니다.

하는 일 왜
청크 안의 조항 제목 줄을 모두 모은다 한 청크에 조항이 여럿 들어 있다
청크가 제목 줄로 시작하지 않으면 직전 조항을 물려받는다 앞머리는 앞 조항에서 이어진 내용이다
청크가 제목 줄에서 끝나면 그 조항은 뺀다 제목만 걸쳤고 내용은 다음 청크에 있다

4. 메타데이터를 만드는 코드

문서 제목은 파일 이름에서 만듭니다.

# 파일명에서 문서 제목 추출: 하루마켓_멤버십정책.pdf → 멤버십정책
doc_title = source_name.replace("하루마켓_", "").replace(".pdf", "")

조항 제목 줄은 정규식으로 찾습니다. 정규식(regular expression)은 글자의 모양을 적어 두고 그 모양에 맞는 곳을 찾는 방법입니다.

HEADING = re.compile(r"^(?:제\d+조 |자주 묻는 질문|부칙).*$", re.MULTILINE)
부분 뜻
^ … $ 줄의 처음부터 끝까지. re.MULTILINE을 주면 글 전체가 아니라 각 줄의 처음과 끝을 뜻합니다
제\d+조 "제", 숫자 하나 이상, "조", 빈칸. 제3조 (하루포인트 적립) 같은 줄입니다
자주 묻는 질문, 부칙 조항 번호가 없는 두 부분도 제목으로 봅니다

이 정규식으로 꼬리표를 만드는 함수입니다.

def find_articles(text: str, chunk: str) -> str:
    names = HEADING.findall(chunk)             # 청크 안에 있는 조항 제목 전부
    before = HEADING.findall(text[:text.find(chunk)])
    if before and not HEADING.match(chunk):
        names.insert(0, before[-1])
    if names and chunk.endswith(names[-1]):
        names.pop()
    return ", ".join(dict.fromkeys(names))     # 중복 제거 (순서 유지)
줄 하는 일
HEADING.findall(chunk) 청크 안의 제목 줄을 순서대로 모두 모읍니다
text[:text.find(chunk)] 원문에서 이 청크보다 앞에 있는 부분입니다. 거기서 마지막 제목이 직전 조항입니다
not HEADING.match(chunk) 청크가 제목 줄로 시작하지 않을 때만 직전 조항을 맨 앞에 넣습니다
chunk.endswith(names[-1]) 청크가 제목 줄에서 끝나면 그 조항을 뺍니다
", ".join(dict.fromkeys(names)) 같은 이름이 두 번 들어가지 않게 하고 쉼표로 잇습니다

text(원문 전체)를 함께 넘기는 이유가 여기 있습니다. 직전 조항은 청크만 봐서는 알 수 없고 원문에서 청크의 자리를 찾아야 알 수 있습니다. 출처는 자르는 순간에만 붙일 수 있다는 말의 실제 모습입니다.


5. 다른 장이 가져다 쓸 수 있게

청크 목록은 13장과 15장에서 다시 씁니다. 그래서 함수로 내놓습니다.

def load_policy_chunks():
    """정책 PDF → 청크 목록. 13장(인덱싱)에서 import 해서 사용."""
    (중략)
    return documents

8장에서 haru_tools.py를 여러 장이 불러 쓰게 만든 것과 같은 방식입니다. 다른 파일에서는 이렇게 가져옵니다.

from lesson12_rag_chunking import load_policy_chunks

chunks = load_policy_chunks()

파이썬은 다른 파일에서 무엇을 가져올 때 그 파일을 처음부터 끝까지 한 번 실행합니다. 그래서 이 파일은 화면에 찍는 일을 모두 맨 끝의 if __name__ == "__main__": 아래에 두었습니다. 그 아래의 코드는 이 파일을 직접 실행할 때만 돕니다. 다른 파일이 load_policy_chunks를 가져갈 때는 아무것도 찍히지 않습니다.


핵심 정리

  • 메타데이터는 청크가 어디서 왔는지 적어 둔 정보입니다. 답변에 근거를 붙이는 재료입니다.
  • 출처는 자르는 순간에만 알 수 있습니다. 나중에는 붙일 수 없습니다.
  • 청크 하나는 id, text, metadata(source, doc_title, chunk_no, article)로 이루어집니다.
  • article에는 청크에 내용이 담긴 조항 제목을 모두 적습니다. 제목 없이 시작하는 청크는 직전 조항을 물려받습니다.
  • 청크 목록은 load_policy_chunks()로 다른 장에서 가져다 씁니다. 화면 출력은 if __name__ == "__main__": 아래에 둡니다.
← 이전 절청킹 — 어디서, 얼마나 크게 자를 것인가다음 절 →따라하기 — 정책 문서 로딩과 청킹
오명운 · macro@prag-ai.com