vector database
vector db는 텍스트, 이미지, 음성등을 벡터 embedding 형태로 저장하고 유사도 검색을 수행하는 데이터베이스 많은 문서를 빠르게 검색하기 위한 전용 데이터 베이스
FAISS, Chroma, Milvus, Qdrant, Pinecone, Weaviate, Elasticsearch, OpenSearch, LanceDB가 있다.
AI 및 RAG 학습에는 FAISS, pdf 기반 챗봇은 CHroma, 기업용 RAG서비스에는 Qdran, 초대규모 데이터에는 Milvus, 서버관리없이 빠른구축에는 Pinecone, 키워드 및 의미검색 통합에는 Elasticsearch와 Opensearch를 사용한다
https://standout.tistory.com/1876
RAG(Retrieval-Augmented Generation)와 LLM의 Hallucination(환각): 외부 문서를 검색한 후 검색 결과를 바탕으
RAG(Retrieval-Augmented Generation)기업은 범용 언어모델을 그대로 사용하는 것이 아니라 사내 문서, 정보, 고객데이터 등을 추가해 기업전용 AI시스템을 구축하며 LLM은 이때 존재하지않는 정보를 생성
standout.tistory.com
https://standout.tistory.com/1899
Vector Database: [LangChain] FAISS, Chroma, Milvus, Qdrant, Pinecone, Weaviate, Elasticsearch, OpenSearch, LanceDB
vector database회사에 제품설명서, 업무 메뉴얼, 사내규정, 환불정책, 고객 faq, 기술문서, 계약서 등 수백개에서 수천개까지 존재해 이 문서를 잘게 나눠 chunking 임베딩 embedding하면 엄청난 양의 벡터
standout.tistory.com
FAISS
Facebook AI Similarity Search
엄밀히 말하면 DBMS라기 보다 고성능 벡터 검색 라이브러리에 가깝다 .
FAISS meta 즉 facebook 에서 개발한 벡터 검색 라이브러리로 매우 빠르고 가볍고 파일저장이 쉬운 대규모 검색 성능이 뛰어나다. 대부분의 RAG 시스템은 FAISS로만으로도 충분한다.
다양한 ANN 알고리즘을 지원하나 사용자 관리기능이 거의 없고 메타데이터 기능이 제한적이며 서버 기능이 없다는 단점이 있다. .
https://standout.tistory.com/1897
ANN 알고리즘이란? HNSW, IVF, PQ, DiskANN
ANN 알고리즘 Approximate Nearest Neighbor 근사 최근접 이웃일반적인 검색이 벡터 100, 000개를 모두 비교해 정확하지만 시간이 오래걸린다면, ANN검색은 가까울 가능성이 높은 후보만 선택해 후보끼리만
standout.tistory.com
| 개인 RAG 학습 | FAISS |
| PDF 기반 챗봇 | Chroma |
| 메타데이터 필터링이 많은 기업 문서 검색 | Qdrant |
| 수억 개 이상의 벡터를 다루는 초대규모 서비스 | Milvus |
| 서버를 직접 운영하기 싫은 SaaS·MVP | Pinecone |
| 벡터 검색과 키워드 검색을 함께 사용 | Elasticsearch / OpenSearch |
| GraphQL·지식 그래프 기반 AI 서비스 | Weaviate |
| 로컬 파일 기반 개발, 실험, 데이터 분석 | LanceDB |
특정 단어하나때문에 전체가 하나의 벡터라면 해당 내용만 찾기가 어려우니 문서를 통째로 embedding하지않고 chunking을 진행해 작은 단위로 나누어 chunk마다 embedding을 만든다.
embedding은 무료계산이 아니고 openai api사용하는 경우 문서 1000개를 위해 embedding 1000번 호출하기에는 비용이 증가하고 응답시간이 증가하는 문제가 동시에 발생한다 .
https://standout.tistory.com/1898
Chunking과 Embedding: Fixed Size / Recursive / Overlap Chunking
Chunk1제품의 배터리는 후면 커버를 제거한 후 교체할 수 있습니다.배터리 교체 후에는Chunk2배터리 교체 후에는 반드시 전원을 다시 켜야 합니다.A/S 기간은 구매일 기준 1년입니다.Chunking과 Embedding
standout.tistory.com
사전 인덱싱(Pre-indexing)과 Persistence(영속화)
vector db는 문서를 인덱스를 미리 만들어 한번만 임베딩하여 저장한다. 사전 인덱싱(Pre-indexing)
또 Embedding을 메모리에만 저장하면 프로그램 종료시 모두 사라진다. 따라서 디스크에 저장한다. 영속화. Persistence
모든 벡터를 비교하지않고 필요한 후보만 빠르게 검색하는 ANN 알고리즘 검색을 사용한다.
비싼 계산은 미리 수행해 저장하고 실제 요청시에는 저장된 결과만 빠르게 조회한다.
문서를 미리 처리하여 저장하는 인덱싱 작업과 사용자의 질문에 답하는 서비스를 분리하고 수행시점도 분리하여 빠르고 효율적으로 동작한다. 인덱싱과 질의를 분리.
- 사전인덱싱
문서를 미리 준비하는 과정
pdf - chunking - embedding - 벡터 db 인덱스 생성 - saVe - 디스크에 인덱스 파일 저장
- 서비스
사용자가 질문을 할때 수행되는 과정
디스크 인덱스 파일 - load - 벡터 db 인덱스 - 질문임베딩 - top k 검색
요청핸들러안에서 FAISS.from_documents()를 호출하는 것은 흔한 초보실수이다. 사용자가 질문할때마다 문서를 다시 읽고 청킹, 임베딩, 인덱스 생성해 속도가 매우 느려지고 API 비용이 증가한다.
from fastapi import FastAPI
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
app = FastAPI()
embedding = OpenAIEmbeddings()
@app.post("/chat")
def chat(question: str):
# ① PDF를 매번 읽음
loader = PyPDFLoader("manual.pdf")
documents = loader.load()
# ② 매번 Chunking
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
# ③ 매번 Embedding API 호출
vectorstore = FAISS.from_documents(
chunks,
embedding
)
# ④ 검색
docs = vectorstore.similarity_search(question)
return docs
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# PDF 읽기
loader = PyPDFLoader("manual.pdf")
documents = loader.load()
# Chunking
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
chunks = splitter.split_documents(documents)
# Embedding
embedding = OpenAIEmbeddings()
# FAISS 생성
vectorstore = FAISS.from_documents(
chunks,
embedding
)
# 저장
vectorstore.save_local("faiss_index")
print("FAISS 인덱스 생성 완료")
수령한 실습코드 vector_db_pycharm_project를 분석해보자.
main.py
import
importlib import_module 파일명을 문자열로 선택해 모듈을 동적으로 불러오기 위해 사용한다.
typing 함수 자체를 변수처럼 전달할 때 사용하는 타입 Callable, 딕셔너리의 타입을 명시하는 Dict
menu 문자열정의.
print_menu()콘솔에 메뉴출력 list를 순회하며 출력.
run_module()
모듈이름을 가지고 main함수를 가져다가 main_func에 Callable로 할당하고 실행한다.
main()
사용자가 종료를 선택할때까지 choice번호에 따라 choice에 맞는 모듈명을 가져와 실행한다.
"""PyCharm에서 하나의 메뉴로 15강 벡터DB 실습 파일들을 실행합니다.""" # 이 파일은 프로젝트 전체 실행용 메뉴입니다.
from importlib import import_module # 파일명을 문자열로 선택해 모듈을 동적으로 불러오기 위해 사용합니다.
from typing import Callable, Dict # 메뉴 함수 타입을 명확하게 표시하기 위해 사용합니다.
MENU: Dict[str, tuple[str, str]] = { # 사용자가 선택할 메뉴 번호와 모듈 정보를 저장합니다.
"1": ("01. 대량 검색과 인덱스 저장 개념", "01_numpy_limit_demo"), # 1번 메뉴는 NumPy 한계 설명입니다.
"2": ("02. 벡터DB의 역할", "02_vector_db_role"), # 2번 메뉴는 벡터DB 역할 설명입니다.
"3": ("03. FAISS vs Chroma", "03_faiss_vs_chroma"), # 3번 메뉴는 FAISS/Chroma 비교입니다.
"4": ("04. FAISS 인덱스 생성/저장", "04_build_index"), # 4번 메뉴는 FAISS 인덱스 빌드입니다.
"5": ("05. 저장된 FAISS 인덱스 서비스 검색", "05_service_search"), # 5번 메뉴는 FAISS 검색 서비스입니다.
"6": ("06. 저장/재로드/점수 검색", "06_save_reload_score"), # 6번 메뉴는 저장과 점수 검색입니다.
"7": ("07. Chroma 메타데이터 필터 검색", "07_chroma_filter_search"), # 7번 메뉴는 Chroma 필터 검색입니다.
"8": ("08. FAISS 증분 업데이트", "08_incremental_update"), # 8번 메뉴는 증분 업데이트입니다.
"9": ("09. 정리 체크리스트", "09_checklist_summary"), # 9번 메뉴는 체크리스트입니다.
} # 메뉴 딕셔너리 정의를 끝냅니다.
def print_menu() -> None: # 콘솔에 메뉴를 출력하는 함수입니다.
print("\n=== Vector DB PyCharm 실습 프로젝트 ===") # 프로젝트 제목을 출력합니다.
for key, (title, _) in MENU.items(): # 메뉴 번호와 제목을 순서대로 순회합니다.
print(f"{key}. {title}") # 번호와 제목을 한 줄로 출력합니다.
print("0. 종료") # 종료 메뉴를 출력합니다.
def run_module(module_name: str) -> None: # 선택된 실습 모듈을 실행하는 함수입니다.
module = import_module(module_name) # src 폴더가 실행 경로일 때 모듈명을 이용해 파일을 불러옵니다.
main_func: Callable[[], None] = getattr(module, "main") # 모듈에서 main 함수를 가져옵니다.
main_func() # 가져온 main 함수를 실행합니다.
def main() -> None: # 전체 메뉴 프로그램의 시작 함수입니다.
while True: # 사용자가 종료를 선택할 때까지 메뉴를 반복합니다.
print_menu() # 현재 실행 가능한 메뉴를 출력합니다.
choice = input("실행할 번호를 입력하세요: ").strip() # 사용자 입력을 받아 앞뒤 공백을 제거합니다.
if choice == "0": # 사용자가 종료를 선택했는지 확인합니다.
print("프로그램을 종료합니다.") # 종료 메시지를 출력합니다.
break # 반복문을 종료합니다.
if choice not in MENU: # 존재하지 않는 메뉴 번호인지 확인합니다.
print("잘못된 번호입니다. 다시 입력하세요.") # 잘못된 입력 안내를 출력합니다.
continue # 메뉴를 다시 보여 주기 위해 반복문 처음으로 돌아갑니다.
title, module_name = MENU[choice] # 선택된 메뉴의 제목과 모듈명을 가져옵니다.
print(f"\n--- 실행: {title} ---") # 실행할 예제 제목을 출력합니다.
try: # 예제 실행 중 오류가 발생해도 메뉴 프로그램이 바로 종료되지 않게 합니다.
run_module(module_name) # 선택된 모듈의 main 함수를 실행합니다.
except Exception as exc: # 모든 예외를 잡아 사용자에게 원인을 보여 줍니다.
print(f"실행 중 오류가 발생했습니다: {exc}") # 오류 메시지를 출력합니다.
if __name__ == "__main__": # 이 파일을 직접 실행했는지 확인합니다.
main() # 메뉴 프로그램을 시작합니다.
common.py
import
__future__ annotation 자기자신, 나중에 정의될 타입을 안전하게 사용할 수 있게한다.
hashlib 문자열을 안정적인 숫자벡터로 바꾸기위해 사용하는 해시함수
math, os, path,
typing Iterable, List, Sequence 함수인자와 반환값을 명확하게표시한다.
dorenv
langchan_core.document, embeddingd, splitters 표줌ㄴ문서객체, 기본 인터페이스임베딩, 작은 청크로 나누는 lanchain분할기 resursivecharactertextsplitter를 import한다.
env를 읽어 각 변수에 할당한다.
LocalHashEmbeddings()
__init__() dim 임베딩 차원을 입력받아 객체를 초기화한다.
_tokenize() text를 lower하고 줄바꿈을 공백으로 바꿔 빈 토큰을 제거해 token이 있으면 그대로 사용, 없으면 empty를 return한다.
_hash_token() 토큰을 int로 바꾼다. hashlib.sha256().hexdigest() 놈-256 해시문자열로 변환해 return.
_embed_one() 입력된 문장 하나를 벡터로 변환하기위해 빈벡터를 생성하고 문장을 토큰으로 분리한뒤 숫자로 변환하고 hash_token() 토큰의 등장횟수를 저장한다. 이 벡터 가 없으면 0 를 리턴, 이후 각 값을 L2정규화. 문장의 길이가 길더라고 백터 크기 영향을 줄여 코사인 유사도를 안정적으로 계산할 수 있도록 한다 .
embed_documents() texts를 for문으로 순회하며 리스트로 반환한다.
embed_query() embed_one() 리턴. 질문 하나를 고정 길이 벡터로 변환해 반환한다.
ensure_directories() 필요항 폴더 생성
get_embeddings() 임베딩 객체를 반환하는 함수. google gemini 임베딩을 사용하도록 설정했다. GoogleGenerativeAIEmbeddings() else의 경우에는 기본 LocalHashEmbeddings() api값없이 동작하는 로컬 해시 임베딩. 임베딩가격은 비싸다.
list_document_files() 파일에서 불러와 맞는 파일명을 정렬해 반환한다. path.name
read_text_file()문서읽고 return
load_txt_documents() filesnames를 for문으로 돌려 path 전체경로를 만들고 파일이 없으면 filenotfounderror 발생시키기. 있으면 내용을과 metadata를 읽어 return.
load_pdf_documents() pypdfloader pdf로더느 ㄴ필요할때만 impot하며 마찬가지고 path를 만들어없으면 에러, pypdfloader() 로 읽고 page를 순회해 metadata를 파일명만 설정해 return.
load_documents() 파일읽어 이것이 txt인지 pdf인지 확인해 각 함 수를 호출한다.
chunk_documents() RecursiveCharacterTextSplitter()객체만들어 수행, return.
load_and_chunk() load와 chunk를 한번에 수행해 chnuk return.
print_documents 검색결과를 보기좋게해 return. 출력
print_scored_documents() chunk를 보기좋게해 return. 출력
"""프로젝트 공통 설정과 유틸리티 함수 모음입니다.""" # 이 파일의 목적을 설명하는 모듈 문서 문자열입니다.
from __future__ import annotations # 타입 힌트에서 자기 자신 또는 나중에 정의될 타입을 안전하게 사용할 수 있게 합니다.
import hashlib # 문자열을 안정적인 숫자 벡터로 바꾸기 위해 해시 함수를 사용합니다.
import math # 벡터 정규화 계산에서 제곱근을 사용하기 위해 math 모듈을 불러옵니다.
import os # 환경 변수와 파일 경로 처리를 위해 os 모듈을 불러옵니다.
from pathlib import Path # 운영체제별 경로 차이를 줄이기 위해 Path 객체를 사용합니다.
from typing import Iterable, List, Sequence # 함수 인자와 반환값을 명확히 표시하기 위해 타입 힌트를 불러옵니다.
from dotenv import load_dotenv # .env 파일의 설정값을 환경 변수로 읽기 위해 python-dotenv를 사용합니다.
from langchain_core.documents import Document # LangChain 벡터스토어에 넣을 표준 문서 객체입니다.
from langchain_core.embeddings import Embeddings # 사용자 정의 임베딩 클래스를 만들기 위한 기본 인터페이스입니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter # 긴 문서를 작은 청크로 나누는 LangChain 분할기입니다.
load_dotenv() # 프로젝트 루트의 .env 파일이 있으면 값을 읽어 환경 변수로 등록합니다.
PROJECT_ROOT = Path(__file__).resolve().parents[1] # 현재 파일 기준으로 프로젝트 루트 폴더를 계산합니다.
DATA_DIR = PROJECT_ROOT / "data" # 데이터 파일을 저장하는 data 폴더 경로입니다.
DOCS_DIR = DATA_DIR / "docs" # 원본 문서 TXT/PDF 파일을 저장하는 docs 폴더 경로입니다.
INDEX_DIR = DATA_DIR / "indexes" # FAISS/Chroma 인덱스를 저장하는 indexes 폴더 경로입니다.
FAISS_DIR = INDEX_DIR / "faiss_index" # FAISS 인덱스를 저장할 폴더 경로입니다.
CHROMA_DIR = INDEX_DIR / "chroma_db" # Chroma 인덱스를 저장할 폴더 경로입니다.
LOCAL_EMBEDDING_DIM = int(os.getenv("LOCAL_EMBEDDING_DIM", "256")) # 로컬 해시 임베딩의 벡터 차원 수를 환경 변수에서 읽습니다.
CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "500")) # 문서를 자를 청크 크기를 환경 변수에서 읽습니다.
CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "50")) # 청크 사이에 겹칠 글자 수를 환경 변수에서 읽습니다.
EMBEDDING_BACKEND = os.getenv("EMBEDDING_BACKEND", "local").strip().lower() # 사용할 임베딩 방식을 local 또는 google로 선택합니다.
class LocalHashEmbeddings(Embeddings): # 외부 API 없이 실습할 수 있도록 만든 간단한 로컬 임베딩 클래스입니다.
"""문자열을 고정 길이 벡터로 바꾸는 실습용 임베딩입니다.""" # 클래스의 학습 목적을 설명합니다.
def __init__(self, dim: int = LOCAL_EMBEDDING_DIM) -> None: # 임베딩 차원을 입력받아 객체를 초기화합니다.
self.dim = dim # 이후 모든 문장을 같은 길이의 벡터로 만들기 위해 차원을 저장합니다.
def _tokenize(self, text: str) -> List[str]: # 문장을 간단한 토큰 리스트로 나누는 내부 함수입니다.
normalized = text.lower().replace("\n", " ") # 대소문자 차이를 줄이고 줄바꿈을 공백으로 바꿉니다.
tokens = [token.strip() for token in normalized.split(" ") if token.strip()] # 공백 기준으로 나누고 빈 토큰을 제거합니다.
if tokens: # 토큰이 하나라도 있으면 그대로 사용합니다.
return tokens # 정리된 토큰 리스트를 반환합니다.
return [normalized] if normalized else ["empty"] # 공백 문장도 에러가 나지 않도록 기본 토큰을 반환합니다.
def _hash_token(self, token: str) -> int: # 토큰을 안정적인 정수 인덱스로 바꾸는 내부 함수입니다.
digest = hashlib.sha256(token.encode("utf-8")).hexdigest() # 토큰 문자열을 SHA-256 해시 문자열로 변환합니다.
return int(digest, 16) % self.dim # 해시 값을 벡터 차원 범위 안의 인덱스로 변환합니다.
def _embed_one(self, text: str) -> List[float]: # 문장 하나를 벡터 하나로 변환하는 내부 함수입니다.
vector = [0.0] * self.dim # 지정한 차원 수만큼 0으로 채운 벡터를 만듭니다.
for token in self._tokenize(text): # 문장에서 추출한 각 토큰을 순회합니다.
index = self._hash_token(token) # 토큰이 들어갈 벡터 위치를 해시로 계산합니다.
vector[index] += 1.0 # 해당 위치의 값을 1 증가시켜 토큰 빈도를 반영합니다.
norm = math.sqrt(sum(value * value for value in vector)) # 벡터 길이를 계산해 정규화에 사용합니다.
if norm == 0.0: # 모든 값이 0이면 나눗셈 오류를 방지합니다.
return vector # 0 벡터를 그대로 반환합니다.
return [value / norm for value in vector] # 벡터 길이를 1로 맞춰 유사도 계산이 안정되게 합니다.
def embed_documents(self, texts: List[str]) -> List[List[float]]: # 여러 문서를 한 번에 임베딩하는 LangChain 표준 메서드입니다.
return [self._embed_one(text) for text in texts] # 각 문장을 벡터로 변환해 리스트로 반환합니다.
def embed_query(self, text: str) -> List[float]: # 질문 문장 하나를 임베딩하는 LangChain 표준 메서드입니다.
return self._embed_one(text) # 질문 하나를 고정 길이 벡터로 변환해 반환합니다.
def ensure_directories() -> None: # 프로젝트 실행에 필요한 폴더들을 자동으로 만드는 함수입니다.
DOCS_DIR.mkdir(parents=True, exist_ok=True) # 문서 폴더가 없으면 생성합니다.
FAISS_DIR.mkdir(parents=True, exist_ok=True) # FAISS 인덱스 폴더가 없으면 생성합니다.
CHROMA_DIR.mkdir(parents=True, exist_ok=True) # Chroma 인덱스 폴더가 없으면 생성합니다.
def get_embeddings() -> Embeddings: # 설정값에 따라 임베딩 객체를 반환하는 함수입니다.
if EMBEDDING_BACKEND == "google": # 실제 Google Gemini 임베딩을 사용하도록 설정한 경우입니다.
from langchain_google_genai import GoogleGenerativeAIEmbeddings # Google 임베딩 클래스를 필요할 때만 불러옵니다.
return GoogleGenerativeAIEmbeddings(model=os.getenv("GOOGLE_EMBEDDING_MODEL", "models/text-embedding-004")) # Google 임베딩 객체를 반환합니다.
return LocalHashEmbeddings(dim=LOCAL_EMBEDDING_DIM) # 기본값은 API 키 없이 동작하는 로컬 해시 임베딩입니다.
def list_document_files(extension: str = ".txt") -> List[str]: # docs 폴더에서 특정 확장자의 문서 파일명을 가져오는 함수입니다.
ensure_directories() # 폴더가 없는 상태에서도 안전하게 실행되도록 먼저 폴더를 생성합니다.
return sorted(path.name for path in DOCS_DIR.glob(f"*{extension}")) # 확장자에 맞는 파일명을 정렬해서 반환합니다.
def read_text_file(path: Path) -> str: # UTF-8 텍스트 파일을 읽는 함수입니다.
return path.read_text(encoding="utf-8") # 파일 내용을 문자열로 반환합니다.
def load_txt_documents(filenames: Sequence[str]) -> List[Document]: # 여러 TXT 파일을 LangChain Document 리스트로 로드합니다.
docs: List[Document] = [] # 읽어 온 문서 객체를 담을 빈 리스트를 만듭니다.
for filename in filenames: # 전달받은 파일명을 하나씩 처리합니다.
path = DOCS_DIR / filename # docs 폴더 기준의 전체 파일 경로를 만듭니다.
if not path.exists(): # 파일이 없으면 사용자에게 명확한 오류를 보여 줍니다.
raise FileNotFoundError(f"문서 파일이 없습니다: {path}") # 없는 파일 경로를 포함해 예외를 발생시킵니다.
text = read_text_file(path) # 텍스트 파일 내용을 읽습니다.
docs.append(Document(page_content=text, metadata={"source": filename, "page": 0})) # 내용과 출처 메타데이터를 가진 문서 객체를 추가합니다.
return docs # 전체 문서 객체 리스트를 반환합니다.
def load_pdf_documents(filenames: Sequence[str]) -> List[Document]: # 여러 PDF 파일을 LangChain Document 리스트로 로드합니다.
from langchain_community.document_loaders import PyPDFLoader # PDF 로더는 필요할 때만 불러와 의존성을 명확히 합니다.
docs: List[Document] = [] # PDF 페이지 문서들을 담을 빈 리스트를 만듭니다.
for filename in filenames: # 전달받은 PDF 파일명을 하나씩 처리합니다.
path = DOCS_DIR / filename # docs 폴더 기준의 PDF 전체 경로를 만듭니다.
if not path.exists(): # PDF 파일이 없으면 명확한 오류를 발생시킵니다.
raise FileNotFoundError(f"PDF 파일이 없습니다: {path}") # 없는 파일 경로를 포함해 예외를 발생시킵니다.
pages = PyPDFLoader(str(path)).load() # PDF의 각 페이지를 Document 객체로 로드합니다.
for page in pages: # 로드된 각 페이지 문서를 순회합니다.
page.metadata["source"] = filename # 필터링이 쉽도록 전체 경로 대신 파일명만 source로 저장합니다.
docs.extend(pages) # 현재 PDF의 페이지 문서들을 전체 리스트에 추가합니다.
return docs # 전체 PDF 페이지 문서 리스트를 반환합니다.
def load_documents(filenames: Sequence[str]) -> List[Document]: # TXT와 PDF를 확장자에 따라 자동 로드하는 함수입니다.
if not filenames: # 파일 목록이 비어 있으면 오류를 명확하게 알려 줍니다.
raise ValueError("로드할 파일명이 비어 있습니다.") # 잘못된 입력을 설명하는 예외를 발생시킵니다.
if all(name.lower().endswith(".pdf") for name in filenames): # 모든 파일이 PDF인지 확인합니다.
return load_pdf_documents(filenames) # PDF 전용 로더를 사용해 문서를 반환합니다.
if all(name.lower().endswith(".txt") for name in filenames): # 모든 파일이 TXT인지 확인합니다.
return load_txt_documents(filenames) # TXT 전용 로더를 사용해 문서를 반환합니다.
raise ValueError("TXT와 PDF를 한 번에 섞지 말고 같은 확장자끼리 실행하세요.") # 혼합 확장자 입력을 방지합니다.
def chunk_documents(documents: Sequence[Document]) -> List[Document]: # 긴 문서를 검색용 청크로 나누는 함수입니다.
splitter = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP) # 청크 크기와 겹침 크기를 적용해 분할기를 만듭니다.
return splitter.split_documents(list(documents)) # Document 리스트를 작은 청크 Document 리스트로 변환합니다.
def load_and_chunk(filenames: Sequence[str]) -> List[Document]: # 파일 로드와 청킹을 한 번에 수행하는 편의 함수입니다.
documents = load_documents(filenames) # 파일 목록을 Document 리스트로 로드합니다.
chunks = chunk_documents(documents) # 로드한 문서를 청크 단위로 분할합니다.
return chunks # 검색 인덱스에 넣을 청크 리스트를 반환합니다.
def print_documents(title: str, docs: Sequence[Document], max_chars: int = 160) -> None: # 검색 결과를 보기 좋게 출력하는 함수입니다.
print(f"\n[{title}]") # 결과 묶음의 제목을 출력합니다.
if not docs: # 검색 결과가 없을 때 안내 메시지를 출력합니다.
print("검색 결과가 없습니다.") # 빈 결과임을 사용자에게 알려 줍니다.
return # 더 이상 출력할 문서가 없으므로 함수를 종료합니다.
for index, doc in enumerate(docs, start=1): # 결과 문서를 1번부터 번호를 붙여 순회합니다.
source = doc.metadata.get("source", "?") # 문서 출처 파일명을 가져옵니다.
page = doc.metadata.get("page", "?") # 문서 페이지 정보를 가져옵니다.
content = doc.page_content.replace("\n", " ")[:max_chars] # 줄바꿈을 공백으로 바꾸고 앞부분만 잘라 표시합니다.
print(f"{index}. 출처={source}, page={page}") # 결과 번호와 출처 정보를 출력합니다.
print(f" {content}...") # 청크 내용 일부를 출력합니다.
def print_scored_documents(title: str, scored_docs: Iterable[tuple[Document, float]], max_chars: int = 160) -> None: # 점수 포함 검색 결과를 출력합니다.
print(f"\n[{title}]") # 결과 묶음의 제목을 출력합니다.
for index, (doc, score) in enumerate(scored_docs, start=1): # 문서와 점수를 1번부터 번호를 붙여 순회합니다.
source = doc.metadata.get("source", "?") # 문서 출처 파일명을 가져옵니다.
content = doc.page_content.replace("\n", " ")[:max_chars] # 줄바꿈을 제거하고 내용 앞부분만 표시합니다.
print(f"{index}. 거리={score:.4f}, 출처={source}") # FAISS 거리 점수와 출처를 출력합니다.
print(f" {content}...") # 검색된 문서 청크 일부를 출력합니다.
메뉴 1
numpy_limit_demo.py
common에 list_document_files 을 불러온다
main()
txt파일목록을 가져와 목록과 설명을 출력한다.
"""01. 대량 검색과 인덱스 저장 개념을 단순 출력으로 확인합니다.""" # 이 파일은 NumPy 방식의 한계를 설명하는 실행 예제입니다.
from common import list_document_files # 샘플 문서 목록을 확인하기 위해 공통 함수를 불러옵니다.
def main() -> None: # PyCharm에서 이 파일을 직접 실행할 때 호출되는 메인 함수입니다.
files = list_document_files() # data/docs 폴더에 있는 TXT 문서 파일 목록을 가져옵니다.
print("01. 대량 검색과 인덱스 저장") # 현재 예제의 제목을 출력합니다.
print("문서 파일:", files) # 실습에 사용할 문서 파일 목록을 출력합니다.
print("NumPy 전체 비교 방식은 문서 수가 적을 때는 쉽고 직관적입니다.") # 작은 데이터에서 NumPy 방식이 괜찮다는 점을 설명합니다.
print("하지만 청크가 수천~수만 개가 되면 매 질문마다 전체 벡터를 비교해야 하므로 느려집니다.") # 대량 청크에서 전체 비교가 느려지는 이유를 설명합니다.
print("또 요청마다 문서를 다시 임베딩하면 API 비용과 응답 지연이 크게 증가합니다.") # 재임베딩의 비용 문제를 설명합니다.
print("따라서 비싼 임베딩은 미리 수행하고, 저장된 인덱스를 검색에 재사용해야 합니다.") # 벡터DB 도입 이유를 정리합니다.
if __name__ == "__main__": # 이 파일이 직접 실행될 때만 main 함수를 실행합니다.
main() # 메인 함수를 호출합니다.
메뉴 2
vector_db_role
안내문을 출력한다 .
"""02. 벡터DB의 역할인 인덱싱과 서비스 분리를 설명합니다.""" # 이 파일은 벡터DB의 핵심 역할을 출력으로 보여 줍니다.
def main() -> None: # PyCharm 실행 진입점으로 사용할 메인 함수입니다.
print("02. 벡터DB의 역할") # 현재 예제 제목을 출력합니다.
print("[인덱싱 단계] PDF/TXT 로드 → 청킹 → 임베딩 → 벡터DB 인덱스 → 디스크 저장") # 사전 1회 작업 흐름을 출력합니다.
print("[서비스 단계] 디스크에서 인덱스 로드 → 질문 임베딩 → Top-k 유사도 검색") # 매 요청 작업 흐름을 출력합니다.
print("인덱싱은 느리고 비용이 들지만 문서가 바뀌기 전까지 한 번만 수행합니다.") # 인덱싱의 특성을 설명합니다.
print("서비스는 이미 저장된 벡터를 사용하므로 빠르고 반복 실행에 적합합니다.") # 서비스 검색의 특성을 설명합니다.
print("이 분리가 RAG 시스템을 실제 서비스로 사용할 수 있게 만드는 핵심입니다.") # 인덱싱/서비스 분리의 중요성을 강조합니다.
if __name__ == "__main__": # 직접 실행 여부를 확인합니다.
main() # 메인 함수를 실행합니다.
메뉴 3
falss_vs_chroma.py
main() 안내문을 출력한다 .
"""03. FAISS와 Chroma의 차이를 설명합니다.""" # 이 파일은 두 벡터스토어 선택 기준을 출력합니다.
def main() -> None: # PyCharm에서 직접 실행 가능한 메인 함수입니다.
print("03. FAISS vs Chroma") # 현재 예제 제목을 출력합니다.
print("FAISS: 가볍고 빠른 벡터 검색 라이브러리입니다.") # FAISS의 기본 특징을 출력합니다.
print("FAISS: save_local/load_local로 파일 저장과 재로드가 명확합니다.") # FAISS 영속화 방식을 설명합니다.
print("FAISS: 복잡한 메타데이터 필터가 필요 없는 대부분의 RAG 실습에 적합합니다.") # FAISS 추천 상황을 설명합니다.
print("Chroma: 메타데이터 필터와 컬렉션 관리가 편한 임베디드 벡터DB입니다.") # Chroma의 기본 특징을 출력합니다.
print("Chroma: filter={\"source\": \"멤버십정책.txt\"}처럼 특정 문서만 검색할 때 적합합니다.") # Chroma 필터 예시를 설명합니다.
print("FAISS 점수는 거리(distance)이므로 일반적으로 작을수록 더 가까운 결과입니다.") # FAISS 점수 해석 주의점을 설명합니다.
if __name__ == "__main__": # 직접 실행될 때만 아래 코드를 실행합니다.
main() # 메인 함수를 호출합니다.
메뉴 4
bulid_index()
langchain에서 FAISS를 iport
common에서 필요한 함수들을 imprt한다 .
build_index 임베딩객체를 만들고, chunk를 분석한뒤 from_documents() 임베딩을 수행해 저장한다.
main() 수행
"""04. build_index.py 역할: 문서를 임베딩하여 FAISS 인덱스를 생성하고 저장합니다.""" # 이 파일은 배치 인덱싱 단계입니다.
from langchain_community.vectorstores import FAISS # FAISS 벡터스토어 클래스를 불러옵니다.
from common import FAISS_DIR, get_embeddings, load_and_chunk # 공통 경로, 임베딩 생성, 문서 청킹 함수를 불러옵니다.
BASE_FILES = ["제품매뉴얼_로봇청소기.txt", "제품매뉴얼_스마트워치.txt"] # 최초 FAISS 인덱스에 넣을 샘플 문서 목록입니다.
def build_index() -> None: # FAISS 인덱스를 만드는 배치 함수입니다.
emb = get_embeddings() # 설정된 임베딩 객체를 생성합니다.
chunks = load_and_chunk(BASE_FILES) # 샘플 문서를 읽고 검색 가능한 청크로 분할합니다.
print(f"청크 개수: {len(chunks)}") # 생성된 청크 수를 출력해 인덱싱 규모를 확인합니다.
vs = FAISS.from_documents(chunks, emb) # 모든 청크를 임베딩하고 FAISS 인덱스를 생성합니다.
vs.save_local(str(FAISS_DIR)) # 생성된 FAISS 인덱스를 디스크에 저장합니다.
print(f"FAISS 인덱스 저장 완료: {FAISS_DIR}") # 저장 위치를 사용자에게 안내합니다.
def main() -> None: # main.py 메뉴에서 공통으로 호출할 표준 실행 진입점입니다.
build_index() # 실제 인덱스 생성 함수인 build_index를 호출합니다.
if __name__ == "__main__": # 파일을 직접 실행했는지 확인합니다.
main() # 직접 실행과 메뉴 실행이 같은 흐름을 사용하도록 main 함수를 실행합니다.
메뉴 5
path import하고 FAISS IMport
common에서 필요한 함수를 가져온다 .
load_faiss_index() 인덱스 파일경로를 만들되 파일이 없다면 에러, 임베딩 객체 생성, 그대로 로드하기
search() 저장된 FAISS인덱스를 로드해 similarity_search() 가까운 청크 k개를 반환한다.
main() 질문정의해 search() chunk를 찾아 print_document() 출력
"""05. service.py 역할: 저장된 FAISS 인덱스를 로드하고 검색만 수행합니다.""" # 이 파일은 서비스 검색 단계입니다.
from pathlib import Path # 인덱스 파일 존재 여부를 확인하기 위해 Path를 사용합니다.
from langchain_community.vectorstores import FAISS # 저장된 FAISS 인덱스를 로드하기 위해 FAISS 클래스를 불러옵니다.
from common import FAISS_DIR, get_embeddings, print_documents # 공통 경로, 임베딩, 출력 함수를 불러옵니다.
def load_faiss_index() -> FAISS: # 디스크에 저장된 FAISS 인덱스를 메모리로 로드하는 함수입니다.
index_file = Path(FAISS_DIR) / "index.faiss" # FAISS 핵심 인덱스 파일 경로를 만듭니다.
if not index_file.exists(): # 인덱스 파일이 없으면 먼저 build_index를 실행해야 합니다.
raise FileNotFoundError("FAISS 인덱스가 없습니다. 먼저 src/04_build_index.py를 실행하세요.") # 실행 순서를 안내하는 오류를 발생시킵니다.
emb = get_embeddings() # 검색 질문 임베딩에 사용할 같은 임베딩 객체를 생성합니다.
return FAISS.load_local(str(FAISS_DIR), emb, allow_dangerous_deserialization=True) # 저장된 벡터를 재임베딩 없이 로드합니다.
def search(question: str, k: int = 3): # 질문을 받아 Top-k 문서를 검색하는 함수입니다.
vs = load_faiss_index() # 저장된 FAISS 인덱스를 로드합니다.
return vs.similarity_search(question, k=k) # 질문과 의미적으로 가까운 청크 k개를 검색합니다.
def main() -> None: # PyCharm 실행 진입점입니다.
question = "로봇청소기 물걸레 되나요?" # 실습용 검색 질문입니다.
results = search(question, k=3) # 저장된 인덱스에서 질문과 가까운 청크 3개를 찾습니다.
print_documents(f"검색 질문: {question}", results) # 검색 결과를 출처와 함께 출력합니다.
if __name__ == "__main__": # 이 파일을 직접 실행한 경우에만 메인 함수를 실행합니다.
main() # 서비스 검색 예제를 실행합니다.
메뉴 6
lanchain에서 FAISS 가져오기
common에서 필요한 함수가져오기
main() 임베딩, chunk수행하기, FAISS.from_documents해 저장하기 laod() 해 질문을 정의하고 수행하기 similarity_search_with_score(), print_scored_documents() 출력
"""06. FAISS 인덱스 저장, 재로드, 점수 포함 검색을 확인합니다.""" # 이 파일은 save_local/load_local과 score 검색을 실습합니다.
from langchain_community.vectorstores import FAISS # FAISS 인덱스를 생성하고 로드하기 위해 불러옵니다.
from common import FAISS_DIR, get_embeddings, load_and_chunk, print_scored_documents # 공통 함수와 경로를 불러옵니다.
FILES = ["제품매뉴얼_로봇청소기.txt", "제품매뉴얼_스마트워치.txt"] # 저장/재로드 실습에 사용할 문서 목록입니다.
def main() -> None: # PyCharm에서 실행할 메인 함수입니다.
emb = get_embeddings() # 임베딩 객체를 생성합니다.
chunks = load_and_chunk(FILES) # 문서를 로드하고 청크로 분할합니다.
vs = FAISS.from_documents(chunks, emb) # 청크 전체를 임베딩하고 FAISS 인덱스를 생성합니다.
vs.save_local(str(FAISS_DIR)) # 생성된 인덱스를 index.faiss와 index.pkl로 저장합니다.
print(f"인덱스 저장 완료: {FAISS_DIR}") # 저장 완료 메시지를 출력합니다.
reloaded = FAISS.load_local(str(FAISS_DIR), emb, allow_dangerous_deserialization=True) # 저장된 인덱스를 재임베딩 없이 다시 로드합니다.
query = "배터리 충전 시간은?" # 점수 포함 검색에 사용할 질문입니다.
scored = reloaded.similarity_search_with_score(query, k=2) # 검색 결과와 FAISS 거리 점수를 함께 가져옵니다.
print_scored_documents(f"점수 포함 검색: {query}", scored) # 거리 점수와 검색 결과를 출력합니다.
print("FAISS 점수는 거리이므로 일반적으로 작을수록 질문과 더 가깝습니다.") # 점수 해석 방법을 안내합니다.
if __name__ == "__main__": # 직접 실행된 경우를 확인합니다.
main() # 저장/재로드/점수 검색 실습을 실행합니다.
메뉴7
chroma_filter_search
기본 폴더를 초기화하기 위한 shutil
langchain에서 chroma불러오기,
common에서 필요한 함수가져오기
build_chroma() 기존 저장폴더를 삭제하고 문서를 load해 chunk와 embbeding 객체를 생성해 chroma수행해 return.
search_in_doc() metadata를 뽑아내고, similarity_search() 서치한 결과물 return.
main() build_chroma 인덱스를 생성하고 저장한뒤에 메타데이터가 있는, 없는 질문 정의, search_in_doc(), 풀력하기
필터를 사용하면 관련없는 문서가 섞이는 노이즈를 줄일 수 있음을 이해한다.
"""07. Chroma 메타데이터 필터링 검색을 실습합니다.""" # 이 파일은 특정 문서 안에서만 검색하는 방법을 보여 줍니다.
import shutil # Chroma 기존 저장 폴더를 초기화하기 위해 shutil을 사용합니다.
from langchain_community.vectorstores import Chroma # Chroma 벡터스토어를 사용하기 위해 불러옵니다.
from common import CHROMA_DIR, get_embeddings, load_and_chunk, print_documents # 공통 경로와 유틸리티 함수를 불러옵니다.
FILES = ["환불교환정책.txt", "멤버십정책.txt", "직원핸드북.txt"] # Chroma 필터 실습에 사용할 여러 문서 목록입니다.
def build_chroma() -> Chroma: # Chroma 인덱스를 새로 생성하는 함수입니다.
if CHROMA_DIR.exists(): # 이전 실습 결과가 남아 있으면 결과 혼동을 줄이기 위해 삭제합니다.
shutil.rmtree(CHROMA_DIR) # 기존 Chroma 저장 폴더를 삭제합니다.
chunks = load_and_chunk(FILES) # 여러 문서를 로드하고 청크로 분할합니다.
emb = get_embeddings() # Chroma에 사용할 임베딩 객체를 생성합니다.
return Chroma.from_documents(chunks, emb, persist_directory=str(CHROMA_DIR)) # 청크를 임베딩하고 Chroma DB에 저장합니다.
def search_in_doc(vs: Chroma, query: str, source: str | None = None, k: int = 3): # 특정 문서 필터를 적용할 수 있는 검색 함수입니다.
metadata_filter = {"source": source} if source else None # source가 있으면 해당 파일명만 검색하도록 필터를 만듭니다.
return vs.similarity_search(query, k=k, filter=metadata_filter) # 필터 조건에 맞는 청크 안에서 유사도 검색을 수행합니다.
def main() -> None: # PyCharm 실행 진입점입니다.
vs = build_chroma() # Chroma 인덱스를 생성하고 저장합니다.
query = "VIP 등급 혜택은?" # 멤버십 문서에서 답해야 하는 질문을 준비합니다.
all_results = search_in_doc(vs, query, source=None, k=3) # 필터 없이 전체 문서에서 검색합니다.
filtered_results = search_in_doc(vs, query, source="멤버십정책.txt", k=3) # 멤버십정책 문서 안에서만 검색합니다.
print_documents("필터 없음 - 전체 문서 검색", all_results) # 전체 검색 결과를 출력합니다.
print_documents("필터 적용 - 멤버십정책.txt 안에서만 검색", filtered_results) # 필터 검색 결과를 출력합니다.
print("필터를 사용하면 관련 없는 문서가 섞이는 노이즈를 줄일 수 있습니다.") # 필터링의 효과를 설명합니다.
if __name__ == "__main__": # 직접 실행 여부를 확인합니다.
main() # Chroma 필터 검색 실습을 실행합니다.
메뉴 8
incremental_update.py
기존 폴더를 초기화하기위한 shutil
FAISS langchain_community.vectores import 인덱스 생성, 로드, 증분 ㅊ ㅜ가에 사용
common에서 필요한 함수들 가져오기
reset_faiss_dir() FAISS 폴더를 초기화해 실습결과를 동일하게 만들도록한다. 인덱스 폴더를 삭제하고 다시 생성하도록.
main() 위 reset_faiss_dir() 수행후 임베딩 객체 생성 get_embeddings, 청크수행 load_and_chunk
청크를 활용해 FAISS 인덱스를 만들어 로컬에 저장한다.
질문을 정의하고 similarity_search() 검색을 수행해 출력한다. 새 문서를 저장하고 다시 질문한다. add_documents는 새 청크만 임베딩함으로 전체 재빌드보다 비용과 시간이 줄어듬을 이해한다.
"""08. FAISS 인덱스 증분 업데이트를 실습합니다.""" # 이 파일은 add_documents로 새 문서만 추가하는 방법을 보여 줍니다.
import shutil # 기존 인덱스 폴더를 깨끗하게 초기화하기 위해 사용합니다.
from langchain_community.vectorstores import FAISS # FAISS 인덱스 생성, 로드, 증분 추가에 사용합니다.
from common import FAISS_DIR, get_embeddings, load_and_chunk, print_documents # 공통 경로와 유틸리티 함수를 불러옵니다.
BASE_FILE = ["환불교환정책.txt"] # 최초 인덱스에 넣을 기본 문서입니다.
NEW_FILE = ["멤버십정책.txt"] # 나중에 증분으로 추가할 새 문서입니다.
def reset_faiss_dir() -> None: # 실습 결과를 매번 동일하게 만들기 위해 FAISS 폴더를 초기화합니다.
if FAISS_DIR.exists(): # 기존 FAISS 인덱스 폴더가 존재하는지 확인합니다.
shutil.rmtree(FAISS_DIR) # 기존 인덱스 폴더를 삭제합니다.
FAISS_DIR.mkdir(parents=True, exist_ok=True) # 새 인덱스를 저장할 폴더를 다시 생성합니다.
def main() -> None: # PyCharm 실행 진입점입니다.
reset_faiss_dir() # 이전 실행 결과를 삭제하고 새 실습 상태를 만듭니다.
emb = get_embeddings() # 임베딩 객체를 생성합니다.
base_chunks = load_and_chunk(BASE_FILE) # 환불정책 문서만 청킹합니다.
vs = FAISS.from_documents(base_chunks, emb) # 환불정책 청크만 임베딩하여 최초 FAISS 인덱스를 만듭니다.
vs.save_local(str(FAISS_DIR)) # 최초 인덱스를 디스크에 저장합니다.
query = "VIP 등급 조건은?" # 증분 전후 비교에 사용할 질문입니다.
before = vs.similarity_search(query, k=2) # 멤버십 문서 추가 전 검색을 수행합니다.
print_documents("증분 전 검색 - 환불정책만 있는 상태", before) # 추가 전 검색 결과를 출력합니다.
vs2 = FAISS.load_local(str(FAISS_DIR), emb, allow_dangerous_deserialization=True) # 저장된 기존 인덱스를 로드합니다.
new_chunks = load_and_chunk(NEW_FILE) # 새로 추가할 멤버십정책 문서를 청킹합니다.
vs2.add_documents(new_chunks) # 새 청크만 임베딩하여 기존 인덱스에 덧붙입니다.
vs2.save_local(str(FAISS_DIR)) # 증분 추가된 인덱스를 다시 저장합니다.
after = vs2.similarity_search(query, k=2) # 멤버십 문서 추가 후 같은 질문을 검색합니다.
keep = vs2.similarity_search("환불은 며칠 걸리나요?", k=2) # 기존 환불 지식이 유지되는지 확인합니다.
print_documents("증분 후 검색 - 멤버십정책 추가 상태", after) # 추가 후 검색 결과를 출력합니다.
print_documents("기존 지식 유지 확인 - 환불 질문", keep) # 기존 지식 검색 결과를 출력합니다.
print("add_documents는 새 청크만 임베딩하므로 전체 재빌드보다 비용과 시간이 줄어듭니다.") # 증분 업데이트의 장점을 설명합니다.
if __name__ == "__main__": # 직접 실행된 경우를 확인합니다.
main() # 증분 업데이트 실습을 실행합니다.
메뉴 9
checklist_summarty.pu
단순 체크리스트 확인 문자열 return
"""09. 정리 체크리스트 내용을 콘솔에서 확인합니다.""" # 이 파일은 15강 핵심 내용을 실행 결과로 요약합니다.
def main() -> None: # PyCharm에서 실행할 메인 함수입니다.
print("09. 정리 & 체크리스트") # 제목을 출력합니다.
print("핵심: 벡터DB는 인덱스를 미리 만들어 저장하고 ANN으로 빠르게 검색합니다.") # 한 줄 핵심을 출력합니다.
print("1. 인덱싱과 서비스 분리: from_documents는 배치에서, load_local은 서비스에서 사용합니다.") # 인덱싱/서비스 분리를 정리합니다.
print("2. FAISS: 빠르고 가벼워 대부분의 기본 RAG 실습에 적합합니다.") # FAISS 선택 기준을 정리합니다.
print("3. Chroma: source 같은 metadata 필터로 특정 문서 안에서만 검색할 때 적합합니다.") # Chroma 선택 기준을 정리합니다.
print("4. save_local/load_local: 인덱스를 저장하고 재임베딩 없이 다시 사용합니다.") # 영속화 핵심을 정리합니다.
print("5. add_documents: 새 문서만 임베딩하여 기존 인덱스에 증분 추가합니다.") # 증분 업데이트 핵심을 정리합니다.
print("RAG 진행: 로드 ✓, 청킹 ✓, 임베딩 ✓, 저장/검색 ✓, 생성은 다음 단계입니다.") # RAG 5단계 진행 상황을 출력합니다.
if __name__ == "__main__": # 직접 실행 여부를 확인합니다.
main() # 체크리스트 요약을 실행합니다.
면접준비로 오전만 하고 조퇴, 내일은 결석한다.
'Personal > SK 네트웍스 AI 캠프' 카테고리의 다른 글
| SK 네트웍스 AI 캠프 - 3_초거대언어모델(LLM) - Day43_Tools를 활용한 ReAct 에이전트 구현 (0) | 2026.07.10 |
|---|---|
| [SK네트웍스 Family AI 캠프] 32기 11주차 회고: Day40 ~ Day43 (0) | 2026.07.08 |
| SK 네트웍스 AI 캠프 - 3_초거대언어모델(LLM) - Day41_LLM의 주요 기능과 활용 사례 (0) | 2026.07.08 |
| SK 네트웍스 AI 캠프 - 3_초거대언어모델(LLM) - Day40_LLM 텍스트 생성 제어 파라미터 (0) | 2026.07.07 |
| [SK네트웍스 Family AI 캠프] 32기 10주차 회고: Day35 ~ Day39 + 6월 종합 (0) | 2026.07.05 |