문서와 대화하기: AWS Blocks를 활용한 RAG 파이프라인 구축
요약
AWS Blocks를 사용하여 문서 업로드부터 답변 생성까지 이어지는 RAG(검색 증강 생성) 파이프라인을 구축하는 방법을 소개합니다. 인프라, 백엔드, 프론트엔드를 통합된 개발 경험으로 제공하여 복잡한 RAG 애플리케이션 개발을 단순화합니다.
핵심 포인트
- AWS Blocks를 통한 RAG 워크플로우(추출, 청킹, 임베딩, 검색) 구현
- 인프라와 코드를 통합하여 개발자 경험(DX) 개선
- Agent Toolkit for AWS의 AWS Blocks skill을 활용한 최적의 코드 생성
- 문서 라이프사이클 전체를 지원하는 챗봇 애플리케이션 구축
사용자가 AI 애플리케이션에서 기대하는 첫 번째 기능 중 하나는 겉보기에는 매우 단순합니다:
문서를 업로드합니다. 질문을 합니다. 정확한 답변을 얻습니다.
내부 지식 포털(internal knowledge portal), 고객 지원 어시스턴트(customer support assistant), 또는 문서 검색(documentation search)을 구축하든 상관없이, 워크플로우는 놀라울 정도로 유사합니다.
사용자는 파일을 업로드합니다. 애플리케이션은 텍스트를 추출하고, 이를 청크(chunks)로 분할하며, 임베딩(embeddings)을 생성하고, 가장 관련 있는 구절을 검색(retrieves)한 다음, 이를 사용하여 LLM(Large Language Model)의 답변에 근거를 제공합니다.
**검색 증강 생성 (Retrieval-Augmented Generation, RAG)**이라고 알려진 이 패턴은 비공개 데이터를 사용하여 질문에 답하는 AI 애플리케이션을 구축하는 표준 방식이 되었습니다.
개별 AWS 서비스들은 매우 훌륭합니다.
과제는 이 서비스들을 응집력 있는 개발자 경험(developer experience)으로 조립하는 것입니다.
전형적인 구현에는 S3, 문서 추출(document extraction), 벡터 저장소(vector storage), Bedrock, 인증(authentication), API, 백그라운드 작업(background jobs), IAM 권한, 그리고 프론트엔드 클라이언트(frontend clients)를 서로 연결하는 작업이 필요합니다. 그런 다음 몇 분마다 매번 배포하지 않고도 이 모든 것을 로컬에서 개발할 수 있는 방법을 찾아야 합니다.
이것이 바로 AWS Blocks가 해결하도록 설계된 문제입니다.
인프라, 백엔드 코드, 프론트엔드 클라이언트를 별개의 프로젝트로 취급하는 대신, AWS Blocks는 이를 동일한 애플리케이션의 서로 다른 뷰(views)로 취급합니다.
완전한 "문서와 대화하기" 애플리케이션을 구축하여 이것이 개발 경험을 어떻게 변화시키는지 살펴보겠습니다.
우리가 구축할 것
우리의 애플리케이션은 전체 문서 라이프사이클(document lifecycle)을 지원할 것입니다.
문서 업로드
│
▼
...
그 결과, 사용자가 업로드한 문서만을 사용하여 질문에 답변하고, 해당 답변이 정확히 어디에서 왔는지 인용할 수 있는 챗봇이 완성됩니다.
시작하기
새로운 AWS Blocks 애플리케이션으로 시작하겠습니다.
npx @aws-blocks/create-blocks-app chat-with-documents
cd chat-with-documents
aws-blocks/ 디렉토리를 확인하세요. 이곳에서 Building Blocks, 타입이 지정된 API (typed APIs), 그리고 인프라를 정의하게 됩니다. 이 글 전반에 걸쳐, 우리는 이 프로젝트를 확장하여 완전한 검색 증강 생성 (Retrieval-Augmented Generation (RAG)) 애플리케이션을 구축하는 데 집중할 것입니다.
코드를 작성하기 전에, Agent Toolkit for AWS에서 AWS Blocks skill을 설치하세요.
npx skills add https://github.com/aws/agent-toolkit-for-aws --skill aws-blocks
이 skill을 사용하면 AI 코딩 어시스턴트가 최신 AWS Blocks 가이드, 검증된 구현 패턴 (implementation patterns), 일반적인 실수 (common pitfalls), 그리고 모범 사례 (best practices)에 접근할 수 있습니다. AWS Blocks는 빠르게 진화하고 있으므로, 이 skill을 사용하면 코드 생성이 현재의 API 및 권장 아키텍처와 일치하도록 보장할 수 있습니다.
프로젝트 스캐폴딩 (scaffolding)이 완료되고 skill 설치가 끝났으므로, 이제 문서 인제스션 (ingestion) 및 검색 (retrieval) 파이프라인을 구축할 준비가 되었습니다.
프로젝트 구조
AWS Blocks의 목표 중 하나는 인프라, 백엔드 API, 그리고 프론트엔드 코드를 함께 유지하는 것입니다.
CDK 프로젝트, Lambda 함수, 생성된 SDK, 그리고 프론트엔드 애플리케이션을 서로 다른 저장소 (repositories)로 분리하는 대신, AWS Blocks는 각 기능이 수직적 슬라이스 (vertical slice)로 구축되는 단일 프로젝트를 권장합니다.
이 글을 위한 프로젝트는 다음과 같은 모습일 수 있습니다:
chat-with-documents/
├── aws-blocks/
...
각 디렉토리는 단일 책임을 가집니다.
| 디렉토리 | 목적 |
|---|---|
aws-blocks/ | Building Blocks, 타입이 지정된 API, 그리고 이를 실행하는 데 필요한 인프라를 정의합니다. |
| ... |
RAG가 보기보다 복잡한 이유
사용자 관점에서 PDF와 대화하는 것은 거의 마법처럼 보입니다.
하지만 막후에서는 상당히 많은 일이 일어납니다.
- 업로드된 파일 저장.
- PDF, Word 문서 또는 이미지에서 텍스트 추출.
- 대형 문서를 의미 있는 청크 (chunks)로 분할.
- 임베딩 (embeddings) 생성.
- 벡터 (vectors) 저장.
- 관련 청크 검색.
- 해당 청크들만 LLM에 전송.
- 인용 (citations)이 포함된 답변 반환.
이 모든 단계마다 인프라가 도입됩니다.
모든 단계는 권한 (permissions), API, 배포 관련 문제, 그리고 로컬 개발의 어려움을 수반합니다.
대부분의 프레임워크는 배포를 더 쉽게 만드는 데 집중합니다.
AWS Blocks는 **개발 (development)**을 더 쉽게 만드는 데 집중합니다.
코드 기반 인프라 (Infrastructure from Code, IFC)
전통적인 AWS 애플리케이션은 보통 다음과 같은 모습을 띱니다.
CDK
│
...
인프라 (Infrastructure)는 하나의 프로젝트에 존재합니다.
애플리케이션 코드 (Application code)는 다른 곳에 존재합니다.
생성된 클라이언트 (Generated clients)는 동기화 상태를 유지해야 합니다.
로컬 모의 객체 (Local mocks)는 대개 수동으로 작성됩니다.
모든 배포는 구성 요소들이 서로 어긋날 (drift apart) 또 다른 기회를 만듭니다.
AWS Blocks는 완전히 다른 접근 방식을 취합니다.
인프라가 애플리케이션 코드를 생성하는 대신, 애플리케이션이 인프라를 정의합니다.
이 패턴을 **코드 기반 인프라 (Infrastructure from Code, IFC)**라고 부릅니다.
단일 빌딩 블록 (building block)이 모든 환경에 필요한 모든 것을 포함합니다.
KnowledgeBase 블록
├── 인프라 (CDK)
...
프론트엔드는 백엔드가 정의한 것과 정확히 동일한 API를 임포트 (import)합니다.
로컬 개발 중에는 자동으로 로컬 구현체 (local implementations)와 통신합니다.
샌드박스 (sandbox) 환경에서는 실제 AWS 서비스와 통신합니다.
프로덕션 (production) 환경에서는 배포된 인프라와 통신합니다.
애플리케이션 코드는 절대 변하지 않습니다.
생성된 SDK는 없습니다.
별도의 클라이언트 패키지도 없습니다.
환경별 구현체도 없습니다.
오직 하나의 API만 존재합니다.
코드형 인프라 (Infrastructure as Code, IaC)에서는 먼저 인프라를 기술한 다음, 이를 사용하는 애플리케이션을 작성합니다.
코드 기반 인프라 (Infrastructure from Code, IFC)는 그 관계를 뒤집습니다. 애플리케이션 코드를 먼저 작성하면, AWS Blocks가 이를 실행하는 데 필요한 인프라를 도출합니다.
마법: 하나의 임포트, 다중 구현체
AWS Blocks의 가장 흥미로운 아이디어 중 하나는 동일한 임포트 (import)가 실행되는 위치에 따라 다르게 동작할 수 있다는 점입니다.
Knowledge Base를 임포트한다고 가정해 보겠습니다.
import { KnowledgeBase } from "@aws-blocks/bb-knowledge-base"
해당 임포트는 환경에 따라 다르게 해석됩니다.
| 환경 (Environment) | 구현 (Implementation) |
|---|---|
| 로컬 개발 (Local development) | 로컬 모의 구현 (Local mock) |
| ... | |
| 이것은 JavaScript 조건부 내보내기 (conditional exports)를 통해 작동합니다. |
Node는 자동으로 올바른 구현을 선택합니다.
사용자의 애플리케이션에는 if (local) 또는 if (production)과 같은 로직이 전혀 필요하지 않습니다.
구현은 변경되지만,
코드는 변경되지 않습니다.
이것이 개발 경험 (development experience)이 기존의 클라우드 애플리케이션과 극적으로 다르게 느껴지는 가장 큰 이유 중 하나입니다.
지식 베이스 (Knowledge Base) 생성하기
우리 애플리케이션에는 임베딩 (embeddings)을 저장하고 의미론적 검색 (semantic search)을 수행할 공간이 필요합니다.
그것이 바로 KnowledgeBase 블록이 제공하는 기능입니다.
import { Scope, ApiNamespace } from "@aws-blocks/blocks"
import { KnowledgeBase } from "@aws-blocks/bb-knowledge-base"
...
단 몇 줄의 코드 안에 많은 일이 일어나고 있습니다.
KnowledgeBase는 단순히 AWS 리소스를 프로비저닝 (provision)만 하는 것이 아닙니다. 애플리케이션에서 직접 호출할 수 있는 런타임 API (runtime API)도 노출합니다. 인프라와 함께 API를 정의함으로써, AWS Blocks는 Lambda 함수, API Gateway 경로, OpenAPI 사양 또는 생성된 프론트엔드 SDK를 수동으로 만들 필요성을 제거합니다.
별도의 인프라를 작성하지 않습니다.
단순히 애플리케이션에 무엇이 필요한지 선언하기만 하면 됩니다.
이것이 바로 실전에서의 코드 기반 인프라 (Infrastructure from Code)입니다. 애플리케이션이 자신의 동작과 이를 실행하는 데 필요한 인프라를 모두 정의합니다.
프론트엔드는 생성된 클라이언트가 필요하지 않습니다
백엔드 API가 완전히 타입이 지정되어 (fully typed) 있기 때문에, 프론트엔드는 이를 단순히 임포트(import)하여 다른 TypeScript 함수처럼 호출하기만 하면 됩니다.
const { results } = await api.search(
"How do I rotate API keys?"
)
프론트엔드가 HTTP 요청을 보내거나 생성된 SDK를 호출하는 것이 아니라는 점에 주목하세요. 단순히 백엔드에서 노출된 타입이 지정된 API를 호출하는 것입니다.
생성하거나 동기화해야 할 별도의 SDK가 없습니다.
로컬 개발 중에는 이 호출이 로컬 모의 구현 (local mock implementation)을 대상으로 실행됩니다.
샌드박스 (Sandbox) 및 프로덕션 (Production) 환경에서는 정확히 동일한 코드가 배포된 AWS 리소스를 대상으로 실행됩니다.
API는 동일하게 유지됩니다. 오직 구현 방식만 바뀝니다.
문서 업로드 (Uploading documents)
RAG 파이프라인의 전반부는 전혀 AI와 관련이 없습니다.
그것은 바로 문서 처리 (Document processing)입니다.
사용자는 PDF, DOCX 파일, 스프레드시트 또는 이미지를 업로드합니다.
이 파일들은 처리되기 전에 저장될 공간이 필요합니다.
AWS Blocks는 정확히 이 목적을 위해 FileBucket을 제공합니다.
업로드 후, 백그라운드 작업 (Background job)이 텍스트를 추출하여 지식 소스 (Knowledge source)에 기록합니다.
uploads/
├── contract.pdf
├── handbook.docx
...
중요한 점을 하나 주목하십시오.
애플리케이션은 임베딩 (Embeddings)을 직접 조작하지 않습니다.
단순히 깨끗한 텍스트를 생성할 뿐입니다.
지식 베이스 (Knowledge Base)가 청킹 (Chunking), 인덱싱 (Indexing), 그리고 검색 (Retrieval)을 자동으로 처리합니다.
청킹 (Chunking)은 대부분의 사람들이 생각하는 것보다 더 중요합니다
답변 품질에 영향을 미치는 가장 큰 요인 중 하나는 언어 모델 (Language model)이 아닙니다.
바로 청킹 (Chunking)입니다.
대규모 문서는 임베딩 (Embeddings)이 생성되기 전에 더 작은 조각으로 나누어져야 합니다.
청크 (Chunks)가 너무 작으면 중요한 문맥 (Context)이 손실됩니다.
청크가 너무 크면 검색 (Retrieval)의 정확도가 떨어집니다.
AWS Blocks는 워크로드 (Workload)에 따라 다양한 청킹 전략 (Chunking strategies)을 지원합니다.
| 전략 (Strategy) | 최적의 용도 |
|---|---|
| 의미론적 (Semantic) | 문서, 기사, 매뉴얼 |
| ... |
대부분의 애플리케이션에서 의미론적 청킹 (Semantic chunking)은 검색 품질과 단순성 사이에서 최상의 균형을 제공합니다.
전략을 선택하는 것은 단순히 블록을 구성하는 또 다른 단계일 뿐입니다.
const kb = new KnowledgeBase(scope, "docs", {
source: "./knowledge",
chunking: {
...
검색 (Retrieval)은 RAG가 지능적으로 변하는 지점입니다
사용자가 질문을 할 때, 우리는 모든 문서를 LLM에 보내지 않습니다.
대신, 그 질문과 가장 관련이 높은 조각들만 검색합니다.
예를 들어, 사용자가 다음과 같이 질문한다고 가정해 봅시다:
API 키를 어떻게 교체하나요?
검색 엔진은 다음과 같은 결과를 반환할 수 있습니다:
- 보안 가이드 (Security Guide) → API 키 교체 (API Key Rotation)
- 운영 매뉴얼 (Operations Manual) → 비밀 관리 (Secrets Management)
- 배포 가이드 (Deployment Guide) → 환경 변수 (Environment Variables)
이러한 청크 (Chunks)들이 LLM을 위한 문맥 (Context)이 됩니다.
모델은 관련 없는 문서를 절대 보지 않습니다.
이를 통해 응답 속도는 더 빨라지고, 비용은 저렴해지며, 정확도는 현저히 높아집니다.
멀티테넌트 애플리케이션 (Multi-tenant applications)
대부분의 SaaS 제품은 단 하나의 지식 베이스 (Knowledge base)를 가지고 있지 않습니다.
수백 개를 가지고 있습니다.
모든 조직은 오직 자신의 문서만을 검색해야 합니다.
별도의 벡터 데이터베이스 (Vector databases)를 유지 관리하는 대신, AWS Blocks를 사용하면 문서 메타데이터 (Metadata)를 사용하여 검색을 필터링할 수 있습니다.
const results = await kb.retrieve(question, {
filter: {
folder: {
...
workspace-123/ 아래에 저장된 문서는 자동으로 적절한 메타데이터를 부여받으므로, 별도의 인덱스 (Indexes)를 유지 관리하지 않고도 검색 범위를 단일 테넌트 (Tenant)로 제한할 수 있습니다.
애플리케이션 로직은 정확히 동일하게 유지됩니다.
AWS 없는 로컬 개발 (Local development without AWS)
이 지점이 AWS Blocks가 진정으로 빛을 발하는 부분입니다.
전통적인 클라우드 개발은 종종 다음과 같은 모습을 보입니다.
코드 작성
│
...
그러한 피드백 루프 (Feedback loop)는 쉽게 몇 분이 소요될 수 있습니다.
AWS Blocks는 이를 로컬 구현체 (Local implementation)로 대체합니다.
개발 중에는:
- 문서들이 로컬에서 인덱싱 (Indexed)됩니다.
- 로컬 구현체는 임베딩 (Embeddings) 대신 TF-IDF를 사용하여 인덱싱이 거의 즉각적으로 이루어지며 AWS 계정이 필요하지 않으면서도, 프로덕션 (Production)에서 사용할 것과 동일한 검색 API를 유지합니다.
- 업로드된 파일은 사용자의 머신에 그대로 남습니다.
- API는 완전한 타입 (Fully typed)을 유지합니다.
- 프론트엔드 (Frontend) 코드는 프로덕션과 정확히 동일하게 동작합니다.
AWS 계정이 필요하지 않습니다.
자격 증명 (Credentials)이 필요하지 않습니다.
배포 (Deployment)가 필요하지 않습니다.
경험이 만족스러워지면 샌드박스 (Sandbox)로 전환할 수 있습니다.
코드는 변경되지 않습니다.
오직 구현체 (Implementation)만 변경될 뿐입니다.
이것이 다르게 느껴지는 이유: 서비스 통합 대신 수직적 개발 (Vertical development instead of service integration)
대부분의 클라우드 애플리케이션은 **수평적 (Horizontally)**으로 구축됩니다.
인프라 (Infrastructure)를 프로비저닝 (Provisioning)하는 것으로 시작하여, API를 노출하고, SDK를 생성한 다음, 마지막으로 모든 것을 프론트엔드에 연결합니다.
프론트엔드 (Frontend)
│
생성된 클라이언트 (Generated Client)
...
각 계층은 독립적으로 개발됩니다. 작은 기능 하나를 구현하더라도 종종 여러 프로젝트에 걸친 변경, 백엔드 테스트를 위한 배포, 그리고 인프라(Infrastructure)와 애플리케이션 코드 간의 인수인계가 필요합니다.
AWS Blocks는 대신 소프트웨어를 구축하는 수직적 (Vertical) 방식을 권장합니다.
단일 기능인 **문서와 대화하기 (Chat with documents)**를 생각해 보세요.
스택의 다섯 가지 서로 다른 계층을 건드리는 대신, 필요한 모든 것을 소유하는 하나의 수직적 슬라이스 (Vertical slice)를 구축합니다.
문서와 대화하기 (Chat with Documents)
├── 문서 업로드 (Upload documents)
...
해당 기능 전체가 애플리케이션 코드로서 함께 존재합니다.
인프라 (Infrastructure), 런타임 구현 (Runtime implementation), 로컬 개발 경험 (Local development experience), 그리고 프론트엔드 API (Frontend API)는 단순히 동일한 빌딩 블록 (Building blocks)의 서로 다른 표현일 뿐입니다.
이것은 당신의 개발 방식을 변화시킵니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기