
AWS에서 RAG 시스템 구축하기 (전편) — 전체 설계와 Ingestion 파이프라인
요약
AWS 환경에서 PDF 문서를 기반으로 동작하는 RAG 시스템의 전체 설계와 Ingestion 파이프라인 구축 방법을 다룹니다. 서버리스 아키텍처를 활용하여 데이터 수집부터 벡터 스토어 저장까지의 과정을 상세히 설명합니다.
핵심 포인트
- AWS Lambda와 Step Functions를 활용한 서버리스 RAG 아키텍처 설계
- 복잡한 레이아웃의 PDF를 Markdown으로 변환하는 Ingestion 파이프라인 구축
- OpenSearch를 벡터 스토어로 활용한 데이터 인덱싱 과정
- Map 기능을 이용한 PDF 병렬 처리로 데이터 처리 효율화
서론
이 기사는 AWS 상에서 RAG (Retrieval-Augmented Generation, 검색 증강 생성) 시스템을 구축하는 과정을 정리한 것입니다. 내용이 많기 때문에 전편과 후편, 2회에 나누어 해설합니다. 이번에는 그 전편입니다.
전편: 프로젝트 전체상과 Ingestion (데이터 투입) 파이프라인 -
후편: 백엔드·프론트엔드와 평가
프로젝트 전체상
무엇을 만드는가
PDF 문서에 대해 질문하면 근거를 바탕으로 답변해 주는 RAG 시스템을 AWS 상에 구축합니다.
사실 이전에, 심플한 RAG에 대해 【생성 AI 입문】 프레임워크를 사용하지 않고 제로에서부터 심플한 RAG를 구현한다는 기사를 쓴 적이 있습니다. 다만, 그때 만든 것은 「수동으로 동작하는」 단계의 것이었습니다. 이번에는 거기서 한 걸음 더 나아가, 클라우드 (AWS) 상에서 동작하는, 일련의 기능이 갖춰진 RAG 시스템으로서 다시 만듭니다. 데이터의 수집부터 검색·답변까지 모두 AWS 상에서 완결하는 것이 목표입니다.
전체 아키텍처

시스템 전체 구성 (Frontend / Backend / Ingestion)
시스템은 크게 3개의 블록으로 나뉩니다.
Frontend (화면): 프레임워크를 사용하지 않고, 순수 HTML·CSS·JavaScript만으로 만듭니다. 정적 파일이므로 그대로 배포할 수 있습니다. -
Backend (검색·답변): 서버를 상시 띄우는 것이 아니라, Lambda를 중심으로 한 서버리스 (Serverless) 구성으로 합니다. 요청이 왔을 때만 동작하므로 평소에는 비용이 거의 들지 않습니다. -
Ingestion (데이터 투입): PDF를 가져오는 일련의 처리를 Step Functions로 구성합니다. 여러 단계의 흐름과 성패를 한눈에 파악할 수 있는 것이 장점입니다. 가져온 데이터는 검색 가능한 형태로 벡터 스토어 (OpenSearch)에 저장합니다.
사용 데이터셋
allganize/RAG-Evaluation-Dataset-JA를 사용합니다 (총 65개의 PDF / 약 2,121페이지 / 5개 도메인 / MIT). 관공서의 보고서나 업계 가이드라인 등 실무적인 문서로, 「상정 질문」과 「정답」이 포함된 평가용 데이터셋이므로, 후편에서 검색 정밀도를 수치로 평가할 수 있습니다. 본 프로젝트에서는 VLM의 비용과 시간을 절약하기 위해, 5개 도메인 (금융 / IT·정보통신 / 제조 / 공공·행정 / 유통·소매)에서 각 5개씩 총 25개로 압축하여 다룹니다.
모두 표·그래프·수식이 많은 「읽기 어려운 PDF」로, 단순한 텍스트 추출로는 의미가 깨져버립니다. 그래프는 이미지이므로 글자를 추출할 수 없고, 표도 레이아웃을 잃으면 수치와 항목의 대응을 알 수 없게 됩니다. 이 읽기 어려움을 어떻게 극복하느냐가 Ingestion 설계의 핵심 테마입니다.
Ingestion 파이프라인
PDF는 그대로는 검색할 수 없습니다. 그래서 다음 3가지 단계를 순서대로 거쳐 검색 가능한 형태로 변환합니다.
Parse (해석): PDF를 읽어 본문·표·그림을 Markdown 텍스트로 변환 -
Chunk & Embed (분할·벡터화): Markdown을 검색하기 쉬운 길이로 분할하고, 각 단편을 벡터화(Vectorization)함 -
Index (투입): 텍스트와 벡터를 OpenSearch에 등록하여 검색할 수 있게 함
Map을 통해 여러 PDF를 병렬 처리할 수 있으므로, 문서가 늘어나도 전체 처리 시간을 짧게 유지할 수 있습니다. 본 프로젝트는 파일이 적기 때문에 동시 실행 수는 5로 설정했지만, 파일 수가 많을 경우 늘릴수록 처리 시간을 단축할 수 있습니다.
단계 ①: Parse (레이아웃 해석과 VLM을 통한 Markdown화)
이 부분이 Ingestion에서 가장 중요한 단계입니다. 「읽기 어려운 PDF」를 의미를 유지한 채 Markdown으로 변환합니다.
VLM에 대한 입력: 페이지 전체인가, 요소별인가
실무에서는 페이지 전체를 고성능 VLM (Claude Opus 4.8 등)에 통째로 전달하여 Markdown화하는 방식도 자주 사용됩니다. 페이지 전체의 문맥을 유지할 수 있는 것이 장점입니다. 다만 본 프로젝트는 개인 개발이므로, 저렴하고 빠른 Claude Haiku 4.5를 사용합니다.
여기서 문제가 되는 것은 Claude에 전달할 수 있는 이미지 해상도의 상한선입니다. Claude Haiku 4.5는 긴 변이 1,568px(약 1.15MP)까지, Claude Opus 4.7 이후의 고성능 모델은 긴 변이 2,576px(약 3.75MP)까지 다룰 수 있습니다. 페이지 전체를 1,568px로 축소하면 내부의 표나 그래프의 세부 사항이 너무 작아져서, VLM(Vision Language Model)이 정확하게 읽어내지 못하게 됩니다. 따라서 본 프로젝트에서는 페이지 전체가 아니라 표, 그림, 수식 등의 요소만 잘라내어 VLM에 전달하고 Markdown으로 변환합니다. 이렇게 하면 요소당 해상도를 확보할 수 있어 저렴한 모델로도 정확도를 유지할 수 있기 때문입니다. 게다가 본문과 같은 텍스트 요소는 VLM을 사용하지 않고 무료로 Markdown화할 수 있으므로, VLM에는 도표와 수식만 전달하면 되어 비용도 절감할 수 있습니다. 페이지 전체의 문맥은 다소 희생되지만, **비용과 정확도의 트레이드오프 (Trade-off)**로서 감수하기로 했습니다.
레이아웃 탐지: RapidLayout
먼저 페이지 내에 무엇이 어디에 있는지(제목, 본문, 표, 그림, 그래프 등)를 탐지합니다. 이를 위해 RapidLayout의 pp_doc_layoutv3 모델을 사용합니다. 이 모델은 25종의 요소 라벨을 반환하며, 가볍기 때문에 Lambda 상에서도 동작합니다.

RapidLayout(pp_doc_layoutv3)가 탐지한 요소를 프레임과 라벨로 시각화한 것
요소 라우팅(Element Routing)이라는 개념
탐지된 요소를 종류에 따라 서로 다른 방법으로 처리합니다. 본 게시물에서는 이를 '요소 라우팅'이라고 부릅니다.
| 요소 종류 | 처리 방법 |
|---|---|
doc_title | 페이지 제목 (# ) |
paragraph_title | 절 제목 (## ) |
table / chart / image / display_formula | VLM에 전달하여 Markdown화 |
header / footer / page_number / seal | 폐기 (검색 노이즈가 되기 때문) |
| 기타 텍스트 | 그대로 본문으로 추출 |
프롬프트 설계
VLM의 출력 품질은 프롬프트(Prompt)에 따라 크게 달라집니다. 본 프로젝트에서 실제로 사용 중인 프롬프트는 다음과 같습니다.
【역할】 당신은 세부 사항까지 정확하게 읽어내는 일본어 기업 문서 판독 어시스턴트입니다. 위 이미지는 문서 페이지에서 잘라낸 그림·그래프·표·수식 영역입니다.
【태스크】 내용을 후속 검색(RAG)에서 사용할 수 있는 일본어 Markdown으로 작성하십시오.
【지시】
...
특히 효과적이었던 지시는 다음과 같습니다.
#사용 제한: VLM이 도표 제목에#(최상위 제목)을 사용하지 않도록 합니다.#를 남용하면 후속 Markdown 청크(Chunk) 분할이 어긋날 수 있습니다. 이를##이하로 통일하여 방지합니다.- 여백의 파편 무시: 도표가 잘리지 않도록 여백을 포함하여 잘라내는 과정에서 가장자리에 인접한 요소가 찍힐 수 있습니다. "주요 도표에 속하는 내용만"이라고 명시하여 무관한 파편을 제외합니다.
- 종류별 전용 규칙: 표, 그래프, 수식 각각에 전용 Markdown화 규칙을 마련했습니다 (표 $\rightarrow$ Markdown 테이블, 그래프 $\rightarrow$ 축 라벨·범례·경향, 수식 $\rightarrow$ LaTeX).
비용 절감: 이미지 해시를 통한 VLM 캐싱
VLM 호출은 비용이 발생하는 작업입니다. 따라서 이미지의 해시값(sha256)을 키(Key)로 하여, Markdown화한 결과를 S3에 캐싱합니다 (vlm-cache/{sha256}.json). 동일한 도표를 두 번 처리하더라도, 두 번째부터는 캐시에서 읽기만 하면 되므로(Amazon Bedrock을 호출하지 않음) 재실행 시 비용이 들지 않고 속도도 빠릅니다.
단계 ②: Chunk & Embed (분할 및 임베딩)
청크 전략
Parse를 통해 얻은 Markdown을 검색하기 쉬운 길이의 청크로 분할합니다.
본 프로젝트에서는 우선 기본에 충실한 전략을 채택했습니다.
- 절(
##) 단위로 분할: 절과 페이지의 경계를 넘지 않도록 합니다. - 최대 512 토큰으로 분할: 구분자를 재귀적으로 따라가는 재귀적 분할(Recursive Splitting) 방식을 사용합니다.
- 오버랩(Overlap) 12%: 문맥이 끊기지 않도록 인접한 청크와 약간 겹치게 합니다.
- 각 청크에 브레드크럼(Breadcrumb: 문서 > 페이지 제목 > 절)을 전치하고,
parent_id를 통해 절로 추적할 수 있도록 합니다.
브레드크럼(Breadcrumb)을 전치하는 이유는, 청크(Chunk) 단체만으로도 "어느 문서의 어느 절인지"를 알 수 있게 하기 위해서입니다. 문맥이 붙으면 단편적인 정보만으로는 모호할 수 있는 표나 수치도 검색 및 답변의 정밀도가 높아집니다. 또한 parent_id가 있다면, 검색에서 적중한 작은 청크로부터 동일한 절의 내용으로 범위를 넓혀 전달하는 것(parent-child 검색)도 가능합니다.
이 512 토큰, 12% 오버랩(Overlap)이라는 값은 RAG의 청크 설계에서 전형적인 기준입니다. 너무 크면 한 청크에 여러 화제가 섞여 검색 결과가 흐릿해지고, 너무 작으면 문맥이 끊기기 때문에, 그 중간 단계로서 400512 토큰에 12할의 오버랩을 두는 구성이 자주 권장됩니다.
참고로, 청크의 길이(토큰 수)는 토크나이저(Tokenizer)를 사용하지 않고 간이로 추정했습니다(일본어는 1글자 $\simeq$ 1토큰, 영숫자는 그보다 조금 적게). 엄밀하게 계산하지 않아도 검색 결과에는 거의 영향을 미치지 않기 때문입니다.
임베딩(Embedding): Amazon Titan Text Embeddings V2
각 청크를 임베딩 모델을 통해 벡터화(Vectorization)합니다.
Amazon Bedrock의 임베딩 모델은 크게 Titan 계열과 Cohere 계열 두 가지가 있습니다. 본 프로젝트는 우선 AWS 표준으로서 RAG에 최적화된 Amazon Titan Text Embeddings V2(차원 1024)를 **기본(Default)**으로 채택했습니다.
먼저 이 Titan V2로 진행합니다. 후편의 평가에서 품질이 부족하다고 느껴지면, Cohere Embed v4로 교체하여 차이가 발생하는지 테스트할 예정입니다.
청크에 포함할 메타데이터(Metadata)
각 청크는 본문과 벡터뿐만 아니라, "어느 문서의 어디에서 왔는지"를 나타내는 메타데이터도 함께 저장합니다. 이것이 후편의 검색에서 효과를 발휘합니다.
| 필드 | 내용 | 주요 용도 |
|---|---|---|
doc_id | 문서 식별자 | 재투입 시 업데이트/삭제하는 단위 |
source_file | 원본 PDF 파일명 | 답변에 출처를 표기 |
domain | 도메인 (finance 등) | 도메인별로 필터링하여 검색 |
page_no | 페이지 번호 | "몇 페이지인지"를 출처에 표시 |
page_title / section | 페이지 제목 / 절 제목 | 문맥 보충 |
parent_id | 동일한 절의 묶음 | 인접한 청크를 묶어서 처리 |
chunk_index | 페이지 내 일련번호 | 순서 복원 |
token_count | 청크의 토큰 수 | 길이 확인 및 디버깅 |
메타데이터가 있으면, 예를 들어 "finance 도메인만 검색하기" 또는 "답변에 출처(파일명, 페이지 번호)를 붙이기"와 같은 작업을 후편에서 쉽게 수행할 수 있습니다.
단계 ③: 인덱스(Index, OpenSearch로 투입)
청크와 벡터를 Amazon OpenSearch Service에 투입합니다. 전편에서는 의미 검색용 벡터(knn_vector)와 키워드 검색용 텍스트를 가진 하이브리드 검색용 인덱스를 준비하는 것까지 진행합니다(실제 검색 로직은 후편에서 다룹니다). 설계에서 중요한 포인트는 두 가지가 있습니다.
엔진과 알고리즘 (engine / method)
OpenSearch의 k-NN은 엔진을 faiss / lucene / nmslib 중에서 선택할 수 있습니다. 본 프로젝트는 대규모 데이터에 강하며 Bedrock Knowledge Bases의 기본값이기도 한 faiss를 채택하였고, 알고리즘은 속도와 정밀도의 균란이 좋은 HNSW로 설정했습니다.
유사도 측정 방식 (space_type)
의미 검색에서는 코사인 유사도(Cosine Similarity)가 정석이지만, faiss가 직접 대응하는 것은 l2(유clidean 거리)와 innerproduct(내적)입니다. 여기서 정규화된(Unit) 벡터 간의 내적은 코사인 유사도와 일치한다는 성질을 이용합니다. Amazon Titan Text Embeddings V2는 기본적으로 정규화된 벡터를 반환하므로, space_type을 innerproduct로 설정하면 코사인 유사도와 동일한 검색이 가능합니다.
키워드 검색 측의 텍스트에는 단어를 분리하는 형태소 분석기 kuromoji를 지정합니다. 이것이 없으면 BM25(키워드 검색)가 제대로 작동하지 않습니다.
요약 및 차기 예고
전편에서는 S3에 PDF를 두는 것만으로 OpenSearch까지 자동으로 투입되는 Ingestion 파이프라인을 만들었습니다. 요점은 다음과 같습니다.
Step Functions를 통해 3가지 단계(parse / chunk_embed / index)를 병렬 실행, 재시도(retry) 및 시각화하며 수행
요소 라우팅(Element Routing)과 VLM을 사용하여 표, 그래프, 수식을 요소별로 추출하여 Markdown으로 변환(비용과 정밀도의 트레이드오프). VLM 캐시를 통해 재실행 비용도 절감
절(section) 단위로 분할하고 breadcrumb과 parent_id를 부여하여, 청크(chunk) 단독으로도 문맥을 유지
임베딩(Embedding)은 Titan V2를 기본값으로 설정 (후편에서 Cohere와 비교)
OpenSearch(faiss)에 BM25(kuromoji)와 kNN의 하이브리드 검색 인덱스 준비
후편에서는 검색(하이브리드 검색)과 평가, 그리고 API 및 프론트엔드를 다룹니다. "하이브리드 검색이 정말 효과가 있는가?"를 평가 데이터셋의 수치로 확인합니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기