CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API
RAG
Implementing the RAG flow
이제 전체 RAG 흐름을 5단계 코드로 구현합니다. 노트북엔 샘플 벡터 데이터베이스 VectorIndex가 이미 들어 있어, 우리는 핵심 메서드 add_documents·search만 쓰면 됩니다. report.md를 청킹 → 임베딩 → 저장 → 질의 임베딩 → 검색해, 질문에 가장 가까운 청크 2개를 찾아냅니다.
Stephen Grider · Anthropic 기술 스태프
전체 RAG 흐름을 이해했으니, 이번엔 003_vectordb 노트북에서 예제로 직접 구현해 봅니다. 이 노트북에는 벡터 데이터베이스의 샘플 구현이 VectorIndex 클래스로 들어 있습니다. 한 번 들여다봐도 좋지만, 필요한 건 다 설명하겠습니다. 핵심 메서드는 add_documents(문서 추가)와 search(검색)이고, 내부에서 코사인 거리로 유사도를 계산합니다. 지난 영상에서 본 다섯 단계를 그대로 코드로 옮깁니다.
1단계. 먼저 report.md 파일을 열어 텍스트를 읽어 둡니다(노트북과 같은 디렉터리에 있습니다). 그리고 지난 장의 chunk_by_section으로 섹션 단위로 청킹합니다 — chunks = chunk_by_section(text). 제대로 됐는지 확인하려고 chunks[2]를 출력해 보면 목차(Table of Contents)가 보이고, 인덱스 3은 그다음 섹션, 그다음은 또 그다음 섹션이 나옵니다.
2단계. 각 청크의 임베딩을 만듭니다. 임베딩 함수는 살짝 고쳐서, 문자열 하나든 문자열 리스트든 받을 수 있게 했습니다. 리스트를 넘기면 각각에 대한 임베딩을 만들어 임베딩 리스트로 돌려줍니다. 그래서 embeddings = generate_embedding(chunks) 한 줄로 모든 청크를 한꺼번에 임베딩합니다.
3단계. 벡터 스토어 인스턴스를 만듭니다. 영상에서는 임베딩과 청크를 zip으로 짝지어 하나씩 store.add(vector=..., metadata={"content": chunk})로 넣는 방식을 설명했는데, 노트북 최종본은 레이트리밋을 피하려고 벌크 방식 store.add_documents([...])를 씁니다. 결과는 같습니다. 그런데 왜 content(원본 청크 텍스트)를 함께 저장할까요? 나중에 벡터 DB를 조회하면 관련 임베딩들이 돌아오는데, 임베딩 숫자만으로는 우리에게 아무 의미가 없습니다. 우리가 정작 원하는 건 그 임베딩에 대응하는 원본 텍스트입니다. 그래서 임베딩을 저장할 때 원본 청크 텍스트(또는 최소한 그 ID)를 같이 넣어 둬야, 나중에 실제 텍스트를 되찾을 수 있습니다.
4단계. 언젠가 사용자가 질문을 합니다. 그 질문도 같은 방식으로 임베딩해야 합니다. generate_embedding으로 user_embedding을 만드는데, 질문은 "what did the software engineering department do last year"(소프트웨어 엔지니어링 부서가 작년에 무엇을 했나)로 둡니다.
5단계. 관련 문서를 찾습니다. 스토어를 사용자 임베딩으로 검색하되, 가장 관련 있는 청크를 하나만이 아니라 둘 찾고 싶습니다. results = store.search(user_embedding, 2). 그리고 for doc, distance in results:로 돌면서 거리와 함께 문서 내용을 출력합니다. 각 청크가 아주 길어서 앞 200자만 잘라 봅니다. 실행하면 결과가 나옵니다.
Section 2가 가장 좋은 결과로 나오고, 코사인 거리 0.71입니다. 그다음으로 가까운 청크는 0.72의 Methodology(방법론) 섹션입니다. 이 두 청크가 우리가 던진 질문에 가장 관련 있다고 판단된 청크입니다. 이게 우리의 전체 RAG 워크플로입니다. 잘 동작하지만, 한두 시나리오에서는 기대대로 되지 않는 경우가 있습니다. 그래서 아직 개선할 점이 한두 가지 더 남아 있고, 다음 장부터 그 이야기를 시작합니다.
이 장에서 배우는 것What you'll learn
약 5분전체 RAG 흐름을 5단계 코드로 구현
VectorIndex 제공 — 핵심 메서드 add_documents·search
임베딩 함수를 리스트도 받게 수정 → 한 번에 전체 청킹 임베딩
임베딩과 함께 content(원본 텍스트)를 저장하는 이유
벌크 add_documents로 레이트리밋 회피
검색 결과: Section 2 (거리 0.71) · Methodology (0.72)
노트북엔 샘플 벡터 데이터베이스가 VectorIndex 클래스로 이미 들어 있습니다. 직접 다 짤 필요 없이 핵심 메서드 add_documents·search만 쓰면 됩니다 — 내부에서 코사인 거리로 유사도를 계산합니다.
from dotenv import load_dotenv import voyageai load_dotenv() client = voyageai.Client()
# VectorIndex 는 노트북에서 제공됨 — 핵심만 발췌 class VectorIndex: def __init__(self, distance_metric="cosine", embedding_fn=None): ... def add_documents(self, documents): ... # 벌크 임베딩 후 저장 def search(self, query, k=1): ... # 코사인 거리로 정렬, 상위 k def _cosine_distance(self, vec1, vec2): # 1.0 - 코사인 유사도 ...
모든 청크를 한 번에 임베딩하려고, 임베딩 함수를 문자열 하나든 리스트든 받게 고칩니다. 리스트를 넘기면 임베딩 리스트를 돌려줍니다.
def generate_embedding(chunks, model="voyage-3-large", input_type="query"): is_list = isinstance(chunks, list) input = chunks if is_list else [chunks] # 문자열 하나도 리스트로 result = client.embed(input, model=model, input_type=input_type) return result.embeddings if is_list else result.embeddings[0]
이제 전체 흐름을 다섯 단계로 짭니다. 아래 스테퍼에서 각 단계를 클릭하면 코드와 그 단계의 이유가 펼쳐집니다.
report.md를 읽어 chunk_by_section으로 쪼갭니다. chunks[2]를 출력하면 목차가, 인덱스 3은 그다음 섹션이 나오며 청킹이 잘 됐는지 확인합니다.
임베딩 함수를 리스트도 받게 고쳤습니다. 리스트를 넘기면 각 청크의 임베딩을 리스트로 돌려줍니다 — 한 줄로 전체 청크를 임베딩합니다.
VectorIndex를 만들고 벌크로 추가합니다(레이트리밋 회피). 왜 content를? 조회하면 임베딩 숫자만 돌아오는데 그건 의미가 없습니다 — 원본 청크 텍스트를 함께 저장해야 실제 내용을 되찾습니다.
사용자 질문도 같은 임베딩 모델로 벡터로 바꿉니다. 질문과 청크가 같은 공간의 벡터여야 비교할 수 있습니다.
search(user_embedding, 2)로 가장 가까운 청크 2개를 받습니다. 결과: Section 2 (거리 0.71)가 최상, 다음이 Methodology (0.72).
다섯 단계 모두 클릭해 보면 청킹 → 임베딩 → 저장 → 질의 → 검색의 흐름이 한눈에 들어옵니다. 핵심은 임베딩과 함께 원본 content를 저장해 둔 덕에, 검색 결과로 실제 청크 텍스트를 되찾는다는 점입니다.
다섯 단계를 한 셀에 모아 실행합니다. 사용자 질문에 가장 가까운 청크 2개와 그 코사인 거리가 출력됩니다.
with open("./report.md", "r") as f: text = f.read() # 1. 섹션으로 청킹 chunks = chunk_by_section(text) # 2. 각 청크 임베딩(리스트를 넘기면 임베딩 리스트 반환) embeddings = generate_embedding(chunks) # 3. 스토어 생성 + 벌크 추가(레이트리밋 회피) store = VectorIndex() store.add_documents([{"content": chunk} for chunk in chunks]) # 4. 사용자 질문 임베딩 user_embedding = generate_embedding( "what did the software engineering department do last year" ) # 5. 가장 관련 청크 2개 검색 results = store.search(user_embedding, 2) for doc, distance in results: print(distance) print(doc["content"][:200]) print()
0.71 Section 2: Software Engineering — Project Phoenix This quarter the software engineering department completed the migration to the new microservices architecture (Project Phoenix). The team resolved a record number of bugs and shipped ... 0.72 Methodology This report aggregates findings across all departments using a unified review framework. Each section was prepared by the respective domain lead ...
Section 2(0.71)와 Methodology(0.72)가 잘 나옵니다. 하지만 한두 시나리오에서는 기대대로 되지 않습니다 — 다음 장의 어휘 검색(BM25)으로 이를 보완합니다.
VectorIndex 제공 — 핵심은 add_documents·search, 내부는 코사인 거리.add_documents로 레이트리밋 회피. 결과: Section 2 0.71.시맨틱 검색만으로 잘 동작하지만, 일부 질의에서는 엉뚱한 청크가 끼어듭니다. 다음 장에서는 고전적 어휘 검색을 더해 이를 잡습니다. → BM25 어휘 검색
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.