대부분의 RAG 품질 문제는 임베딩 모델(embedding model) 이전에 발생합니다. 만약 원시 HTML을 입력하거나, 탐색 메뉴, 쿠키 배너 및 푸터가 여전히 붙어 있는 텍스트를 추출하여 사용한다면, 모든 청크(chunk)에 동일한 상투적인 내용물(boilerplate)이 포함됩니다. 그 결과 검색은 답변과 마찬가지로 "개인정보 보호정책 | 문의하기 | © 2026" 같은 내용을 자주 일치시킵니다.
본 게시글에서는 이 문제를 해결하는 두 가지 단계를 다룹니다. 즉, 각 페이지를 주요 콘텐츠의 깨끗한 마크다운(Markdown)으로 변환하고, 그 마크다운을 제목별로 청킹하여 각 청크가 인용 출처와 함께 일관성 있는 섹션이 되도록 하는 것입니다.
공개 고지: 저는 Aurelix이며, 크롤링 단계에 사용되는 Website to Markdown Actor를 제작했습니다. 이는 유료 도구입니다 (가격은 Cost 섹션 참고). 청킹 코드는 모든 출처의 마크다운에서 작동합니다.
일반적인 접근 방식
- 범위 내에서 크롤링(Crawl within a scope)을 수행합니다. 일반적으로 전체 도메인(domain)이 아닌 하나의 문서 섹션(
/docs/...)만 대상으로 합니다. 볼륨은 늘리지만 의미는 적은 변경 로그, 태그 페이지 및 페이지네이션 아카이브는 제외해야 합니다.
- 주요 콘텐츠만 유지합니다. 페이지에
<article> 또는 <main> 요소가 있는 경우 이를 사용하고, 그렇지 않다면 가장 큰 콘텐츠 블록을 사용합니다. 탐색 메뉴(nav), 헤더, 푸터, 사이드바, 쿠키 배너 및 스크립트는 제거해야 합니다.
- 일반 텍스트가 아닌 마크다운으로 변환합니다. 제목, 목록, 표, 코드 블록은 의미를 담고 있습니다. 마크다운은 이를 유지하지만, 일반 텍스트는 이를 평탄화(flatten)시킵니다.
- 구조 우선 청킹 후 크기별로 분할합니다. 제목을 기준으로 나누어 청크가 섹션이 되게 한 다음, 너무 큰 섹션은 단락 경계에서 나눕니다. 코드 블록 내부에서는 절대 분할하지 마십시오.
- 모든 청크에 메타데이터를 포함합니다: 출처 URL, 페이지 제목, 그리고 제목 경로(
페이지 > 섹션 > 하위 섹션)입니다. 이는 검색을 개선하고 모델이 출처를 인용할 수 있게 합니다.
- 매너를 지킵니다. robots.txt 파일을 존중하고 천천히 크롤링해야 합니다. 그것은 다른 사람의 서버이기 때문입니다.
1단계: 마크다운으로 크롤링하기
Website to Markdown Actor는 다음을 가져옵니다:
startUrls (필수): 시작할 페이지들; 이 링크 내에서 범위가 추적됩니다.
maxPages (기본값 50, 최대 10,000): 이 많은 페이지를 변환한 후 중단합니다.
scope: "path" (시작 URL 폴더 아래의 URL만) 또는 "domain" (각 시작 URL의 전체 사이트).
includeGlobs / excludeGlobs: URL 패턴. 여기서 **는 모든 것을 일치시키고, *는 /를 제외한 모든 것을 의미합니다.
각 페이지는 하나의 레코드가 됩니다:
{
"url": "https://docs.apify.com/academy",
"title": "Apify Academy | Academy | Apify Documentation",
...
}
마크다운 내부의 링크는 절대 경로로 만들어지며, 코드 블록은 언어 태그를 유지하여 아래 청커(chunker)에 도움이 됩니다.
2단계: 헤딩별로 청크 분할 (Chunk by headings)
import json
import os
import re
...
이러한 선택을 한 이유 (Why these choices)
- 임베딩 텍스트 내 헤딩 경로. "
timeout 옵션을 60으로 설정하세요"라는 청크만으로는 의미가 적습니다. 이를 Crawler configuration > Timeouts와 같은 접두사로 붙이면, 임베딩 모델은 인간이 페이지 레이아웃에서 얻는 맥락을 얻게 됩니다. 표시를 위해 순수한 text는 별도로 저장합니다.
- 토큰이 아닌 문자(Characters). 2,000 문자는 영어 산문 기준으로 약 400~500 토큰에 해당하며, 검색 청크의 일반적인 범위입니다. 만약 임베딩 모델에 하드 토큰 제한이 있다면,
len()을 토크나이저 카운트(tokenizer count)로 교체하세요. 로직은 변하지 않습니다.
- 코드 블록은 절대 분할되지 않음. 코드 예시가 절반만 있는 것보다 아예 없는 것이 더 나쁩니다.
MAX_CHARS보다 긴 단일 코드 블록은 크기가 큰 청크 하나로 유지되며, 이는 보통 올바른 처리 방식입니다.
- 작은 후미 청크는 병합됨. 섹션의 이전 청크에 합쳐지므로 한 줄짜리 고아가 임베딩되지 않습니다.
- 안정적인 ID (Stable IDs) (
url#index)를 사용하면 재크롤링(re-crawl) 및 upsert(업데이트)가 가능합니다. 이를 last_modified와 짝지으면 변경된 페이지만 다시 임베드할 수 있습니다.
chunks.jsonl에서 벡터 스토어에 로딩하는 것은 하나의 반복(loop)으로 처리됩니다: embed_text를 임베드하고, text, url, title와 메타데이터로 heading_path를 저장한 다음, 각 답변의 인용구(citation)와 함께 url을 반환합니다.
크롤링 범위를 정확하게 설정하기 (Getting the crawl scope right)
- 필요한 섹션에서 시작하기:
https://example.com/docs/를 사용하고 scope: "path"로 설정하면 /docs/...만 크롤링합니다.
- 버전 관리 문서:
excludeGlobs: ["**/v1/**"]를 사용하여 오래된 버전을 제외하거나, 동일한 답변이 세 가지 다른 버전 번호로 세 번 나타나는 경우를 처리할 수 있습니다.
- 페이지네이션 목록:
excludeGlobs: ["**?page=*"]는 링크 목록인 아카이브 페이지를 건너뜁니다.
- 20페이지 테스트 실행 후
word_count 확인. 단어가 매우 적은 페이지는 종종 인덱스 페이지이므로, 아무것도 추가하지 않는다면 청킹(chunking) 전에 필터링합니다.
제한 사항 (Limits)
- 자바스크립트 렌더링 불가. 서버가 전송하는 HTML을 읽기 때문에 대부분의 문서 사이트, 블로그, 지식 기반 및 서버 렌더링된 React 또는 Next.js를 지원합니다. 클라이언트 측 전용 페이지(빈 앱 셸)는 건너뛰고 로그에 기록되며 요금이 부과되지 않습니다.
- HTML만 가능. PDF, 이미지 및 기타 파일은 건너뜁니다.
- 시작 URL당 하나의 사이트.
www.와 베어 도메인은 하나의 사이트로 간주하며, 다른 서브도메인은 추적하지 않습니다.
- 속도. 사이트당 초당 최대 2개의 새 요청을 보낼 수 있으며,
robots.txt가 크롤링 지연 시간(Crawl-delay)을 설정하면 더 느려집니다. 변환은 CPU 바운드입니다: README에 따르면 256MB로 시간당 약 1,000개의 대형 문서 페이지를 측정할 수 있으므로, 기본 한 시간 실행으로 대략 1,000개에서 3,000개 페이지를 처리할 수 있습니다. 이 작업은 타임아웃 전에 스스로 중지하며 지금까지 완료된 페이지는 유지됩니다.
- HTML이 2MB를 초과하는 페이지는 건너뛰고 요금이 부과되지 않습니다.
- 메인 콘텐츠 감지(Main-content detection)는 자동이므로, 비정상적인 레이아웃에서는 약간의 보일러플레이트가 남아 있거나 사이드 블록이 누락될 수 있습니다. 몇 페이지를 샘플 확인하세요.
robots.txt는 항상 준수되며 끌 수 없습니다. 로그인, 유료 결제 장벽(paywall) 또는 봇 인증(bot challenge) 뒤에 있는 페이지는 접근할 수 없습니다.
- 이 방식으로 사용할 권리가 있는 사이트만 크롤링하세요. 콘텐츠의 저작권은 소유자에게 있습니다.
Apify 스토어의 주요 대안들과 비교했을 때(전체 표는 각 스토어 페이지에 있습니다):
- 1,000 페이지당 고정 비용 $1.50: Apify의 RAG Web Browser보다 34% 저렴하며, Apify's Website Content Crawler와 같은 8 GB에서 컴퓨트 유닛(compute-unit) 청구가 없습니다.
- 전체 사이트를 256 MB로 크롤링하고 robots.txt를 항상 준수하며, 실패한 페이지, 중복 및 빈 페이지에는 요금이 부과되지 않습니다.
- 제공하지 않는 기능: Apify Actors가 모두 지원하는 JavaScript 렌더링(JavaScript rendering).
비용 (Cost)
작성 시점 기준으로, README에는 Apify의 무료 플랜에서 1,000 페이지당 $1.50이며 유료 플랜(Bronze, Silver, Gold)에서는 1,000 페이지당 $1.05이고, 실행당 $0.001이 추가로 명시되어 있습니다. 건너뛴 페이지(robots.txt, 오류, 비-HTML, 중복 URL 또는 동일한 Markdown, JavaScript 전용 페이지, 주요 콘텐츠가 없는 페이지)는 요금이 부과되지 않습니다.
100페이지 예제는 최대 $0.15의 비용이 발생합니다. Apify의 무료 플랜에는 월 $5 크레딧이 포함되어 있으며, README에 따르면 이는 약 3,300 페이지 분량입니다.
링크 (Links)