LangChain 챗봇6장 · 문서 기반 챗봇18 / 21← 이전목차다음 →MetaContents

6장. 문서 기반 챗봇

실습 5 — 진짜 임베딩으로 바꿔 끼우기

실습 3의 연습용 임베딩 자리에 OpenAI 의 진짜 임베딩(OpenAIEmbeddings)을 끼우고, 같은 조항 단위 조각(out_ch06_chunks.csv)과 같은 평가 질문 12개로 적중을 다시 잽니다. 연습용 임베딩이 놓친 "강아지", "아이 요금" 같은 질문을 찾는지가 볼 곳입니다. 과목 폴더의 .env 에 API 키가 있어야 돌아가고, 실습 2와 3을 먼저 해야 합니다. 임베딩 모델은 text-embedding-3-small 을 기본으로 하고 .env 의 OPENAI_EMBED_MODEL 로 바꿀 수 있습니다. 이 절의 출력은 API 키를 받기 전에 실행하지 않고 유추한 예상 출력입니다.

1단계. 진짜 임베딩 준비하고 숫자 보기

Python
import math
import os
import warnings

warnings.filterwarnings("ignore", category=DeprecationWarning)

import pandas as pd
from dotenv import find_dotenv, load_dotenv
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

# 과목 폴더의 .env 에서 OPENAI_API_KEY 를 읽는다. find_dotenv(usecwd=True): 지금 폴더(data)에서 위로 올라가며 찾는다
load_dotenv(find_dotenv(usecwd=True))
# 임베딩 모델 이름도 .env 에서 바꿀 수 있게 하고, 없으면 text-embedding-3-small 을 쓴다
EMBED_MODEL = os.getenv("OPENAI_EMBED_MODEL", "text-embedding-3-small")
emb = OpenAIEmbeddings(model=EMBED_MODEL)

vec = emb.embed_query("강아지 데려가도 돼요?")
print("모델:", EMBED_MODEL, "/ 숫자 개수:", len(vec))
# 화살표의 길이(칸마다 제곱해 더한 값의 제곱근). OpenAI 임베딩은 길이를 1 로 맞춰 돌려준다
print("길이:", round(math.sqrt(sum(v * v for v in vec)), 3))
출력
모델: text-embedding-3-small / 숫자 개수: 1536
길이: 1.0

코드가 의미하는 것 load_dotenv(find_dotenv(usecwd=True)) 로 키를 읽고 OpenAIEmbeddings(model=EMBED_MODEL) 를 만드는 것은, 연습용 임베딩 자리에 같은 모양의 진짜 부품을 끼울 준비입니다. 모델 이름을 os.getenv 로 받아 두면 코드를 고치지 않고 .env 만 바꿔 다른 임베딩 모델을 시험할 수 있습니다. 숫자 개수와 길이를 먼저 찍어 보는 것은 새 부품이 앞의 FAISS 계산(길이 1 인 화살표)과 맞는지 확인하는 일이며, 예상 출력에서는 1536 개, 길이 1.0 으로 적었습니다.

2단계. 낱말 짝의 방향 닮음 견주기

Python
def cosine(a, b):
    # 길이가 1 인 두 화살표의 방향 닮음 = 칸마다 곱한 값의 합
    return sum(x * y for x, y in zip(a, b))


pairs = [("강아지", "반려동물"), ("아이 요금", "아동은 80%를 낸다"), ("강아지", "여권")]
for w1, w2 in pairs:
    a, b = emb.embed_documents([w1, w2])
    print(f"{w1} ↔ {w2}: 방향 닮음 {cosine(a, b):.2f}")
출력
강아지 ↔ 반려동물: 방향 닮음 0.60
아이 요금 ↔ 아동은 80%를 낸다: 방향 닮음 0.40
강아지 ↔ 여권: 방향 닮음 0.20

코드가 의미하는 것 emb.embed_documents([w1, w2]) 로 두 글을 한 번에 임베딩하고 방향 닮음을 재는 것은, 본문의 장난감 좌표 계산을 진짜 임베딩에 그대로 옮긴 것입니다. 글자가 겹치지 않는 "강아지"와 "반려동물"을 넣어 본 까닭은 연습용 임베딩이 0 을 냈던 바로 그 짝이기 때문입니다. 예상 출력의 숫자는 둥글게 적은 것이므로, 볼 것은 값 자체보다 "뜻이 가까운 짝이 먼 짝보다 높게 나오는가"라는 순서입니다.

3단계. 진짜 임베딩으로 색인 만들고 저장하기

Python
saved = pd.read_csv("out_ch06_chunks.csv")
art_chunks = [
    Document(page_content=row.내용, metadata={"article": row.article, "chapter": row.chapter, "page": int(row.page)})
    for row in saved.itertuples()
]
# 조각 16개를 한 번에 임베딩해 색인을 만든다(요청은 조각 수와 상관없이 몇 번으로 묶여 간다)
real_db = FAISS.from_documents(art_chunks, emb)
real_db.save_local("out_ch06_faiss_openai")
print("저장: out_ch06_faiss_openai / 조각", real_db.index.ntotal, "개")

for q in ["강아지 데려가도 돼요?", "아이 요금은 얼마인가요?", "태풍으로 비행기가 안 뜨면 어떻게 되나요?", "공항에는 언제까지 가야 하나요?"]:
    doc, score = real_db.similarity_search_with_score(q, k=1)[0]
    print(f"{q} → {doc.metadata['article']} (거리 {float(score):.1f})")
출력
저장: out_ch06_faiss_openai / 조각 16 개
강아지 데려가도 돼요? → 제12조 (거리 0.9)
아이 요금은 얼마인가요? → 제11조 (거리 0.9)
태풍으로 비행기가 안 뜨면 어떻게 되나요? → 제7조 (거리 0.8)
공항에는 언제까지 가야 하나요? → 제9조 (거리 0.9)

코드가 의미하는 것 실습 3과 같은 out_ch06_chunks.csv 로 FAISS.from_documents(art_chunks, emb) 를 부르되 emb 만 진짜 임베딩으로 바꿨습니다. 색인 단계의 다른 칸은 그대로 두고 임베딩 칸 하나만 갈아 끼운 것입니다. 새 색인을 out_ch06_faiss_openai 라는 다른 폴더에 저장하는 것은 연습용 색인과 지도가 달라 섞어 쓸 수 없기 때문이고, 질문 넷은 실습 3에서 연습용이 놓친 질문을 골라 먼저 확인하려는 것입니다.

4단계. 평가 질문 12개로 연습용 임베딩과 견주기

Python
questions = pd.read_csv("rag_questions.csv")
bigram = pd.read_csv("out_ch06_hits.csv")      # 실습 3에서 잰 연습용 임베딩의 적중

real_top1, real_top3 = [], []
for row in questions.itertuples():
    found = [d.metadata["article"] for d in real_db.similarity_search(row.질문, k=3)]
    real_top1.append(found[0] == row.정답조항)
    real_top3.append(row.정답조항 in found)

compare = pd.DataFrame({
    "1위 적중": [int(bigram["전처리 후 1위"].sum()), sum(real_top1)],
    "3위 안 적중": [int(bigram["전처리 후 3위 안"].sum()), sum(real_top3)],
}, index=["BigramEmbeddings", EMBED_MODEL])
print(compare.to_string())
compare.to_csv("out_ch06_embed_compare.csv", encoding="utf-8-sig")
print("저장: out_ch06_embed_compare.csv")
출력
                        1위 적중  3위 안 적중
BigramEmbeddings             8        8
text-embedding-3-small      11       12
저장: out_ch06_embed_compare.csv

코드가 의미하는 것 pd.read_csv("out_ch06_hits.csv") 로 실습 3의 채점표를 읽어 와 같은 질문, 같은 채점 방식으로 진짜 임베딩을 다시 채점합니다. 임베딩을 바꿀지 말지를 느낌이 아니라 적중 수로 정하는 방법입니다. 예상 출력의 진짜 임베딩 줄(11, 12)은 키를 받은 뒤 실제로 돌려 고칠 자리이고, BigramEmbeddings 줄은 실습 3에서 실제로 잰 값을 읽어 온 것입니다.

이 실습이 뜻하는 것

이 실습은 글자 맞추기로 쪽을 찾던 학생 대신, 책의 내용을 이해하고 찾아보기를 만든 학생에게 같은 모의시험을 보게 하는 일과 같습니다. 새 학생의 찾아보기를 들여다보고(1·2단계), 앞 학생이 틀린 문제를 먼저 풀게 하고(3단계), 열두 문제를 다시 채점합니다(4단계). 여기서 새 학생이 OpenAIEmbeddings, 새 찾아보기가 out_ch06_faiss_openai 입니다. 비유와 다른 점은, 새 학생에게는 문제를 풀 때마다(임베딩할 때마다) 수고비(요금)를 내야 한다는 것입니다.

단계출력뜻
1. 숫자 보기1536 개, 길이 1.0연습용(512 개)보다 숫자가 세 배 많고, 길이를 1 로 맞춰 돌려줍니다
2. 방향 닮음강아지 ↔ 반려동물 0.6 안팎글자가 하나도 겹치지 않는데도 방향이 닮았다고 봅니다
3. 놓친 질문넷 모두 정답 조항연습용이 놓친 질문을 뜻으로 찾았습니다
4. 적중1위 8 → 11, 3위 안 8 → 12 (예상)조각과 질문과 채점이 같으므로 차이는 임베딩에서 온 것입니다

한 줄로 줄이면, 조각을 잘 나눈 다음에도 남는 "낱말이 다른 질문"은 뜻을 아는 진짜 임베딩으로 바꿔 끼워 풀 수 있다는 것을 본 실습입니다. 4단계의 BigramEmbeddings 줄(8, 8)은 실습 3에서 실제로 잰 값을 읽어 온 것이고, 진짜 임베딩의 숫자(방향 닮음, 거리, 11·12)는 예상 출력이라 둥글게 적었습니다. 진짜 임베딩의 값은 모델과 판에 따라 조금씩 달라지므로 여러분의 출력과 다를 수 있습니다. 예상 출력에서 1위를 하나 놓친다고 본 것은, 손님의 말과 약관의 말이 많이 다른 질문(예: "공항에는 언제까지")은 진짜 임베딩에서도 2위로 밀릴 수 있어서입니다. 그래서 프로젝트에서는 k=3 으로 근거를 넉넉히 넣습니다. 진짜 임베딩의 거리는 연습용(1.2~1.9)과 크기가 달라서, 본문의 거리 문턱 1.75 를 그대로 쓰면 안 된다는 것도 이 숫자로 보입니다.

코드 설명

  • OpenAIEmbeddings(model=EMBED_MODEL) 는 OpenAI 의 임베딩 모델을 부르는 부품입니다. BigramEmbeddings 와 같은 embed_documents, embed_query 를 가지므로 FAISS.from_documents 에 그대로 끼웁니다.
  • 임베딩만 바꾸고 조각(out_ch06_chunks.csv), 질문(rag_questions.csv), 채점 방법은 실습 3과 같게 두었습니다. 그래야 적중의 차이가 임베딩 때문이라고 말할 수 있습니다.
  • save_local("out_ch06_faiss_openai") 로 저장해 두면 다음부터는 조각을 다시 임베딩하지 않고 불러옵니다. 불러올 때도 같은 OpenAIEmbeddings 를 넘깁니다.
  • 연습용 색인(out_ch06_faiss)과 진짜 색인(out_ch06_faiss_openai)은 다른 지도라서 섞어 쓰지 않습니다. 질문을 임베딩하는 모델이 색인을 만든 모델과 같아야 합니다.
  • 진짜 임베딩은 조각과 질문을 임베딩할 때마다 요금이 듭니다. 이 실습은 조각 16개, 질문 12개, 낱말 6개라 양이 아주 적습니다. 실제 단가는 OpenAI 가격 페이지에서 확인합니다.
← 이전 절6장 · 실습 4 — RetrievalQA 와 LCEL 로 근거 붙인 답의 짜임 보기다음 절 →6장 · 프로젝트 1 — 여행 약관 챗봇 만들고 점검하기