📚 Agentic AI - 기업용 자율 에이전트 개발 6장 · 에이전트 ④ RAG 문서검색 Agentic AI란

의미가 비슷하면 숫자도 비슷하다 — 임베딩

한 줄 요약

임베딩(embedding) 은 문장을 숫자 목록으로 바꾸는 기술입니다. 뜻이 비슷한 문장은 비슷한 숫자가 됩니다. 그래서 단어가 안 겹쳐도 관련 있는 조각을 찾을 수 있습니다.


1. 단어로 찾으면 놓칩니다

사용자:  "반품 며칠 이내에 해야 하나요?"
문서:    "청약철회 기간은 상품 수령일로부터 ..."

if "반품" in 문장 으로 찾으면 이 조각을 놓칩니다. "반품"이라는 단어가 없으니까요.

사람은 "청약철회 = 반품"이라는 걸 압니다. 컴퓨터도 알게 하려면 "뜻이 비슷하다"를 계산할 수 있어야 합니다.


2. 임베딩 — 문장을 숫자로

아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.

from common import get_embeddings

emb = get_embeddings()
v = emb.embed_query("반품 기간")
print(len(v))       # 768
print(v[:5])        # [-0.027, 0.0212, 0.006, -0.0524, 0.0005]

문장 하나가 숫자 768개가 됩니다.

이 숫자 목록을 벡터(vector) 라고 부릅니다. 그리고 이 변환을 하는 모델을 임베딩 모델이라고 합니다. 우리는 models/gemini-embedding-001 을 씁니다.

숫자 하나하나에 의미가 있는 건 아닙니다. 768개가 함께 그 문장의 뜻을 나타냅니다. 사람이 읽어서 이해할 수는 없습니다.


3. 그런데 이게 왜 쓸모가 있나

뜻이 비슷한 문장은 비슷한 숫자가 되도록 훈련됐기 때문입니다.

숫자 목록 둘이 얼마나 비슷한지는 계산할 수 있습니다. 그 계산을 코사인 유사도(cosine similarity) 라고 하고, -1에서 1 사이의 값이 나옵니다. 1에 가까울수록 비슷합니다.

왜 하필 -1에서 1인가

숫자 768개를 "방향"으로 보기 때문입니다. 768차원은 못 그리니 2차원으로 줄여 그린 그림으로 보겠습니다.

질문 (기준)"불량 상품 반품은 며칠 이내?""상품 불량·하자 … 30일 이내 반품"유사도 0.815 · 가장 가깝다"단순 변심 청약철회 7일 이내"유사도 0.726"오늘 점심은 김치찌개"유사도 0.518 · 멀다각도가 작다 = 유사도가 1에 가깝다임베딩은 문장을 숫자 묶음(벡터)으로 바꿉니다. 의미가 비슷하면 방향이 비슷해지고, 두 벡터 사이의 각도가 작아집니다.
의미가 비슷하면 벡터의 방향이 비슷해지고 사잇각이 좁아진다

두 화살표 사이의 각도가 좁을수록 1에 가깝습니다. 그 각도를 재는 계산이 코사인이고요. 같은 방향이면 1, 90도로 벌어지면 0, 정반대면 -1입니다. 숫자 768개를 비교했는데 점수 하나가 나오는 이유가 이것입니다.

실제로 계산해 본 결과입니다.

질문: "불량 상품 반품은 며칠 이내에 신청해야 하나요?"

  0.815  상품 불량·하자의 경우 수령일로부터 30일 이내에 반품을 신청할 수 있습니다.
  0.726  단순 변심에 의한 청약철회는 수령일로부터 7일 이내에 가능합니다.
  0.588  VIP 등급은 6개월 누적 구매 금액 150만원 이상 고객에게 부여됩니다.
  0.518  오늘 점심은 김치찌개를 먹었습니다.
  0.518  배터리는 일반 사용 시 약 7일, GPS 사용 시 약 18시간 지속됩니다.

순서를 보세요.

  • 0.815 — 정확히 그 답입니다. 1등
  • 0.726 — 반품 얘기지만 조건이 다릅니다(단순 변심). "반품"이라는 단어가 없는데도 2등으로 올라왔습니다
  • 0.588 아래 — 관련 없는 것들

단어가 안 겹쳐도 뜻으로 찾아냈습니다. 이게 임베딩의 힘입니다.

그런데 "김치찌개"와 "배터리"가 똑같이 0.518입니다. 우연이 아닙니다.

관련 없는 문장끼리는 0.5 언저리에 몰립니다. 한국어 문장이라는 것만으로도 어느 정도는 같은 방향을 보기 때문이죠. 그래서 0.5는 "관련 있음"이 아니라 "바닥값" 에 가깝습니다. 김치찌개와 배터리가 나란한 것은 둘 다 질문과 무관하다는 뜻입니다.

점수의 절대값에 의미를 두지 마세요. 0.518이 "관련 없음"이고 0.815가 "관련 있음"이라는 절대 기준은 없습니다. 모델마다, 언어마다 바닥값이 다릅니다. 중요한 것은 순위입니다 — 관련 있는 것이 위로 올라오는가.

"훈련됐다"는 것은 무슨 뜻인가같은 문맥에 자주 함께 나오는 말끼리 가까워지도록 대량의 글로 학습시킨 결과입니다. "반품"과 "청약철회"가 실제 문서에서 늘 비슷한 자리에 등장하니, 모델이 둘을 가까운 방향에 놓게 된 것이죠. 사람이 규칙을 적어 준 것이 아닙니다.


4. 검색은 이렇게 됩니다

[미리 해 두는 일 — 한 번만]
  조각 15개 ──임베딩──▶ 벡터 15개를 저장

[질문할 때마다]
  질문 ──임베딩──▶ 벡터 1개
                     │
                     ▼
        저장된 15개와 각각 유사도 계산
                     │
                     ▼
              가장 가까운 4개를 꺼냄

질문도 조각도 같은 방식으로 숫자가 되니까 비교할 수 있습니다.

이 방식을 의미 검색(semantic search) 또는 벡터 검색(vector search) 이라고 부릅니다.


5. 인덱스 — 벡터를 담아 두는 곳

벡터 15개면 전부 비교해도 금방입니다. 그런데 10만 개라면? 질문할 때마다 10만 번 계산하는 건 느립니다.

벡터 여러 개를 담아 두고 "이 벡터와 가장 가까운 것 몇 개"를 빨리 찾아 주는 자료구조를 인덱스(index)라고 합니다.

같은 대상을 문맥에 따라 다른 이름으로 부르니 여기서 한 번 정리해 둡니다.

어디서 보게 되나 이름 무엇
이 교재 본문 인덱스 이 이름으로 통일합니다
라이브러리 문서 벡터 저장소(vector store) 같은 것
코드 변수 vectorstore 같은 것
저장된 파일 index.faiss / index.pkl 인덱스를 디스크에 쓴 결과

FAISS 는 인덱스 자체가 아니라 인덱스를 만들어 주는 라이브러리입니다. 그리고 검색기(retriever)는 인덱스에 붙인 창구입니다 — "질문을 주면 조각을 돌려주는 것".

생성 도구저장 구조검색 인터페이스FAISS라이브러리만든다인덱스 = 벡터 15개코드에선 vectorstore문서에선 vector storeas_retriever검색기retriever질문 넣으면 조각 k개나온다save_localindex.faiss — 벡터index.pkl — 원문·출처디스크에 저장한 결과 —인덱스 자체가 아님셋은 역할이 다릅니다. FAISS가 만들고, 인덱스가 담고, 검색기가 꺼내 줍니다. 파일 두 개는 인덱스를 디스크에 저장한 결과입니다.
FAISS 는 만드는 도구, 인덱스는 물건, 검색기는 창구다

인덱스 하나에서 검색기를 여러 개 만들 수 있습니다. 인덱스는 창고, 검색기는 그 창고에 붙은 주문 창구라고 보면 됩니다. 창구마다 "몇 개씩 꺼내 줄지"(k)를 다르게 정할 수 있죠. 4절에서 두 개를 한꺼번에 만들고, 8절에서 왜 둘인지를 봅니다.

그래서 인덱스를 빠르게 만들고 뒤져 주는 도구를 씁니다. 이 과정에서는 FAISS 를 쓰겠습니다.

아래는 4절에서 만들 code/ch06_rag_agent.py 의 일부를 미리 보는 것입니다 — 지금 붙여 넣지 않아도 됩니다.

from langchain_community.vectorstores import FAISS

vs = FAISS.from_documents(chunks, emb)      # 조각들을 임베딩해서 인덱스로

FAISS(Facebook AI Similarity Search) 는 메타(구 페이스북)가 만든 벡터 검색 라이브러리입니다. 설치가 간단하고(pip install faiss-cpu), 서버가 필요 없고, 파일로 저장됩니다. 실습에 적합합니다.

이름 특징
FAISS 파일 기반, 서버 불필요 ← 이 과정
Chroma 파일 기반, 쓰기 쉬움
Pinecone, Weaviate 등 클라우드 서비스, 대규모용

바꾸는 건 한 줄입니다. LangChain이 같은 방식으로 감싸 주기 때문입니다.


6. 768차원인 이유

아래는 제공된 code/common.py 의 일부입니다 — 고치지 않아도 됩니다.

def get_embeddings():
    return GoogleGenerativeAIEmbeddings(model=EMBED_MODEL, output_dimensionality=768)

숫자 하나가 한 차원(dimension)입니다. 숫자 768개 = 768차원. 지금까지 "숫자 768개"라고 부르던 것과 같은 말입니다.

gemini-embedding-001 의 기본 출력은 3072개입니다. 우리는 768개로 줄였습니다.

왜 줄이나 — 숫자가 적을수록 저장 공간이 작고 계산이 빠릅니다. 대신 표현력이 조금 떨어집니다.

우리 실습에는 768로 충분합니다. 문서가 15조각뿐이니까요.

이렇게 차원을 잘라 써도 되도록 훈련된 모델들이 있습니다. 앞쪽 숫자에 중요한 정보가 몰려 있게 만드는 방식인데, 이 기법을 마트료시카 표현학습(Matryoshka Representation Learning) 이라고 부릅니다. 러시아 인형처럼 잘라도 형태가 유지된다는 뜻입니다. 이름만 알아 두세요.


7. 임베딩 모델은 채팅 모델과 다릅니다

헷갈리기 쉬운 지점입니다.

채팅 모델 임베딩 모델
이름 gemini-2.5-flash models/gemini-embedding-001
넣는 것 문장 문장
나오는 것 문장 숫자 768개
하는 일 답을 만든다 뜻을 숫자로 바꾼다
만드는 법 get_chat() get_embeddings()

RAG에는 둘 다 필요합니다.

임베딩 모델 → 조각을 찾는다
채팅 모델   → 찾은 조각을 근거로 답을 만든다

8. 비용과 시간

임베딩도 API 호출입니다. 공짜가 아닙니다.

[인덱스 만들 때]  조각 15개 × 임베딩 호출 = 15번   ← 한 번만
[질문할 때마다]   질문 1개 × 임베딩 호출 = 1번

문서가 10,000조각이면 처음에 10,000번을 호출해야 합니다. 시간도 오래 걸리고 비용도 듭니다.

그래서 만든 인덱스를 저장해 둡니다. 다음 절의 주제입니다.

채팅보다는 훨씬 쌉니다. 임베딩은 단가가 낮은 편입니다. 다만 양이 많으면 무시할 수 없습니다.


스스로 해 보기

직접 유사도를 계산해 보세요. 이번 장의 실습 파일은 4절에서 만듭니다. 여기서는 짧은 파일 하나를 따로 만들어 실행합니다.

새 파일을 만드세요agentic_ai/code/try_embedding.py

import sys, pathlib
sys.path.append(str(pathlib.Path(__file__).resolve().parent))
from common import get_embeddings
import numpy as np

emb = get_embeddings()
question = "불량 상품 반품은 며칠 이내에 신청해야 하나요?"
candidates = [
    "상품 불량·하자의 경우 수령일로부터 30일 이내에 반품을 신청할 수 있습니다.",
    "단순 변심에 의한 청약철회는 수령일로부터 7일 이내에 가능합니다.",
    "VIP 등급은 6개월 누적 구매 금액 150만원 이상 고객에게 부여됩니다.",
    "오늘 점심은 김치찌개를 먹었습니다.",
]
vq = np.array(emb.embed_query(question))
vs = np.array(emb.embed_documents(candidates))
cos = vs @ vq / (np.linalg.norm(vs, axis=1) * np.linalg.norm(vq))
for s, c in sorted(zip(candidates, cos), key=lambda x: -x[1]):
    print(f"  {c:.3f}  {s}")

터미널에서 실행python code/try_embedding.py

  1. 질문을 "환불받으려면 어떻게 하나요?" 로 바꿔 보세요. 순위가 어떻게 달라지나요?
  2. 후보에 여러분이 만든 문장을 추가해 보세요.
  3. 영어 문장을 후보에 넣어 보세요. 한국어 질문과 영어 문장의 유사도는 어떻게 나오나요?

핵심 정리

  • 임베딩은 문장을 숫자 목록(벡터) 으로 바꿉니다. 우리는 768개 = 768차원을 씁니다.
  • 뜻이 비슷하면 숫자도 비슷하도록 훈련됐습니다. 그래서 단어가 안 겹쳐도 찾습니다.
  • 비슷한 정도는 코사인 유사도로 계산합니다 — 두 벡터 사이의 각도입니다. 점수의 절대값이 아니라 순위를 봅니다.
  • 관련 없는 문장끼리는 0.5 언저리에 몰립니다. 0.5는 "관련 있음"이 아니라 바닥값입니다.
  • 검색 = 질문을 벡터로 만들어, 저장된 벡터들과 비교해, 가장 가까운 것을 꺼내는 것입니다.
  • 벡터를 담아 두고 가까운 것을 빨리 찾아 주는 자료구조인덱스입니다. 이 교재는 이 이름으로 통일합니다.
  • FAISS 는 그 인덱스를 만들어 주는 파일 기반 라이브러리입니다. 서버가 필요 없어 실습에 적합합니다.
  • 인덱스 하나에서 검색기(retriever)를 여러 개 만들 수 있습니다. 인덱스는 창고, 검색기는 창구입니다.
  • 임베딩 모델과 채팅 모델은 다릅니다. RAG에는 둘 다 필요합니다.
  • 임베딩도 API 호출이라 비용과 시간이 듭니다. 그래서 만든 인덱스를 저장합니다.
← 이전 절02. 왜 문서를 잘게 잘라야 하나 청킹다음 절 →04. 따라하기 정책 PDF를 잘라 벡터로 만들기