CPN 한국어 자습서 · 러닝패스 2 / 4 — Building with the Claude API
RAG
Text embeddings
관련 청크를 찾는 가장 흔한 방법이 시맨틱 검색입니다. 정확한 단어 일치를 보는 키워드 검색과 달리, 시맨틱 검색은 질문과 청크의 의미를 이해합니다. 그 바탕이 텍스트 임베딩 — 텍스트의 의미를 숫자 벡터로 바꾼 것입니다. 이번 장은 VoyageAI의 voyage-3-large로 청크를 임베딩해 봅니다.
Stephen Grider · Anthropic 기술 스태프
지난 장에서 문서를 청크로 쪼갰습니다. 이제 사용자 질문과 가장 관련 있는 청크를 어떻게 찾을지 봅니다. 가장 흔한 방법은 시맨틱 검색(semantic search)입니다. 시맨틱 검색은 키워드 검색과 다릅니다. 키워드 검색은 정확히 같은 단어가 들어 있는지를 보지만, 시맨틱 검색은 사용자 질문과 각 청크의 의미와 맥락을 이해해 관련성을 따집니다. 예를 들어 질문에 "버그"라는 단어가 없어도, 의미가 가까운 "결함"·"오류"가 든 청크를 찾아낼 수 있습니다.
시맨틱 검색을 가능하게 하는 핵심이 텍스트 임베딩(text embedding)입니다. 텍스트 임베딩은 텍스트에 담긴 의미를 숫자로 표현한 것입니다. 단어와 문장을 컴퓨터가 수학적으로 다룰 수 있는 형태 — 숫자들의 리스트(벡터)로 바꿉니다. 예를 들어 "I'm very happy today!"는 [0.275, -0.05, ...] 같은 벡터로, "That movie wasn't great"는 [0.597, 0.095, ...] 같은 벡터로 바뀝니다. 임베딩 모델이 텍스트를 입력으로 받아 이런 숫자 리스트를 출력합니다.
여기서 중요한 직관이 있습니다. 임베딩 모델은 의미가 비슷한 텍스트에 비슷한 벡터를 부여합니다. 그래서 두 텍스트의 벡터가 수학적으로 가까우면, 두 텍스트의 의미도 가깝다고 볼 수 있습니다. 질문을 임베딩하고 각 청크를 임베딩한 뒤, 질문 벡터에 가장 가까운 청크 벡터를 찾으면 — 그게 바로 의미상 가장 관련 있는 청크입니다. 벡터를 어떻게 "비교"하는지는 다음 장의 주제입니다.
구현은 VoyageAI 라이브러리를 씁니다. 먼저 %pip install voyageai로 설치한 뒤, load_dotenv()로 API 키를 불러오고 voyageai.Client()로 클라이언트를 만듭니다. 임베딩 모델은 voyage-3-large를 사용합니다. 텍스트 청크에 함수를 한 번 돌리면, 그 의미를 표현하는 부동소수 숫자들의 리스트가 돌아옵니다.
그래서 작은 헬퍼 generate_embedding을 만듭니다. 텍스트 하나를 받아 client.embed([text], model=..., input_type=...)를 호출하고, 결과의 첫 번째 임베딩을 돌려줍니다. input_type은 이 텍스트가 검색 질의(query)인지 저장할 문서(document)인지를 모델에 알려 주는 힌트입니다. 같은 임베딩 모델이라도 질의용·문서용을 구분하면 검색 품질이 조금 더 좋아집니다.
마지막으로 지난 장의 chunk_by_section으로 report.md를 청크로 쪼개고, 첫 번째 청크에 generate_embedding을 돌려 봅니다. 그러면 화면 가득 긴 부동소수 숫자 리스트가 출력됩니다. 이게 그 청크의 의미를 담은 벡터입니다. 임베딩 자체는 이렇게 빠르고 단순합니다. 진짜 과제는 이 임베딩들을 어떻게 비교해 사용자 질문과 가장 비슷한 청크를 찾는지 — 다음 장에서 전체 RAG 흐름과 함께 다룹니다.
이 장에서 배우는 것What you'll learn
약 7분시맨틱 검색 = 단어 일치가 아니라 의미로 관련 청크 찾기
텍스트 임베딩 = 텍스트의 의미를 담은 숫자 벡터
의미가 비슷하면 벡터도 비슷 — 이게 검색의 핵심 직관
VoyageAI voyageai.Client() + voyage-3-large 모델
input_type = query(질의) · document(문서) 구분 힌트
generate_embedding(chunk) → 긴 부동소수 리스트 = 그 청크의 벡터
청크를 만들었으니, 이제 질문과 가장 관련 있는 청크를 찾을 차례입니다. 가장 흔한 방법이 시맨틱 검색 — 정확한 단어 일치가 아니라 의미로 관련성을 따집니다.
질문에 든 단어가 그대로 들어 있는 청크를 찾습니다.
질의: "소프트웨어 결함을 몇 개 고쳤나?"
질문과 청크를 임베딩해 의미가 가까운 청크를 찾습니다.
질의: "소프트웨어 결함을 몇 개 고쳤나?"
RAG에서 관련 청크를 찾을 때 주로 시맨틱 검색을 쓰는 이유입니다. 단어가 정확히 일치하지 않아도 의미로 연결하기 때문입니다. (뒤 장에서 두 방식을 함께 쓰는 하이브리드도 다룹니다.)
시맨틱 검색을 가능하게 하는 핵심이 텍스트 임베딩입니다. 텍스트의 의미를 숫자 벡터로 바꿉니다 — 의미가 비슷하면 벡터도 비슷합니다.
임베딩 모델은 텍스트를 받아 의미를 담은 숫자 벡터를 출력합니다. 1번·2번 문장은 의미가 비슷해 벡터의 첫 숫자(0.275 · 0.262)가 가깝고, 뜻이 다른 3번(0.597)은 멉니다.
구현은 VoyageAI 라이브러리를 씁니다. 설치 후 클라이언트를 만들고, voyage-3-large 모델로 청크를 임베딩합니다.
# VoyageAI 임베딩 라이브러리 설치 %pip install voyageai
from dotenv import load_dotenv import voyageai load_dotenv() # .env 에서 VOYAGE_API_KEY 로드 client = voyageai.Client() # 임베딩 클라이언트 생성
지난 장의 chunk_by_section도 그대로 가져옵니다. 그리고 임베딩 헬퍼를 정의합니다 — 텍스트 하나를 받아 의미 벡터를 돌려줍니다. input_type은 이 텍스트가 질의인지 문서인지를 모델에 알려 주는 힌트입니다.
import re def chunk_by_section(document_text): pattern = r"\n## " return re.split(pattern, document_text)
def generate_embedding(text, model="voyage-3-large", input_type="query"): result = client.embed([text], model=model, input_type=input_type) return result.embeddings[0]
report.md를 읽어 섹션으로 쪼갠 뒤, 첫 번째 청크에 generate_embedding을 돌립니다.
with open("./report.md", "r") as f: text = f.read() chunks = chunk_by_section(text) generate_embedding(chunks[0])
[-0.0543627, 0.0146131, -0.0170223, 0.0005244, 0.0331902, -0.0218764, 0.0092118, -0.0411530, 0.0157744, 0.0029381, 0.0260417, -0.0083921, 0.0194552, -0.0376610, 0.0011248, ... (총 1024개의 부동소수 — 이 청크의 의미를 담은 벡터)]
임베딩 자체는 이렇게 빠르고 단순합니다. 핵심은 이 벡터들을 어떻게 비교해 질문과 가장 비슷한 청크를 찾는지입니다 — 다음 장에서 코사인 유사도·벡터 데이터베이스와 함께 전체 RAG 흐름을 봅니다.
voyage-3-large로 임베딩. input_type은 query·document 힌트.generate_embedding(chunk) → 긴 부동소수 리스트. 비교 방법은 다음 장.청크를 의미 벡터로 바꿀 수 있게 됐습니다. 이제 이 벡터들을 저장·비교해 질문에 가장 가까운 청크를 찾는 — 청킹부터 검색까지 이어지는 전체 RAG 흐름입니다. → 전체 RAG 흐름
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 76개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.