포스트

2026-03-19 강의 정리

LangChain의 RAG 콤포넌트 - 문서 로더 + 텍스트 분할

2026-03-19 강의 정리

목차

  1. 오늘 강의 개요
  2. LangChain Document 클래스 복습
  3. 파일 로딩 - 텍스트 파일 (.txt)
  4. 파일 로딩 - CSV 파일
  5. 파일 로딩 - JSON 파일
  6. 파일 형식별 비교 정리
  7. Fine-tuning vs RAG 비교
  8. 코사인 유사도 기반 RAG 직접 구현
  9. rag_with_langchain - LLM 연결
  10. 클래스(OOP) 기초 - Calculator, WordCounter
  11. DocumentStore 클래스 - RAG를 OOP로 구현
  12. collections.Counter 활용
  13. 핵심 개념 총정리
  14. 자주 나오는 실수 / 주의사항

1. 오늘 강의 개요

오늘은 실제 파일에서 문서를 로딩하고, 코사인 유사도 기반의 간단한 RAG를 직접 구현한다. 또한 OOP(클래스)로 문서 검색 시스템을 만든다.

오늘의 핵심 흐름

1
2
3
4
5
6
7
8
9
파일 (txt / csv / json)
        ↓ 로딩
Document 객체들 (page_content + metadata)
        ↓ 임베딩
벡터들 (1536차원)
        ↓ 코사인 유사도 검색
관련 문서 top-k개
        ↓ context 구성
LLM 답변 생성

오늘 만들 것

  1. 각 파일 형식별 로더 함수
  2. retrieve_by_similarity() - 코사인 유사도 기반 검색
  3. rag_with_langchain() - 검색 + LLM 답변
  4. DocumentStore 클래스 - 위 기능을 OOP로 통합

2. LangChain Document 클래스 복습

Document 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_core.documents import Document

doc = Document(
    page_content = "문서의 실제 텍스트 내용",
    metadata     = {
        "source"    : "company_policy.txt",
        "category"  : "사내규정",
        "char_count": 500
    }
)

print(doc.page_content)           # 텍스트 내용
print(doc.metadata['source'])     # 파일명
print(doc.metadata['char_count']) # 문자 수

page_content : 임베딩되고 검색에 사용되는 실제 텍스트

metadata : 검색 결과에서 출처 표시, 필터링 등에 활용

왜 Document 클래스를 쓰나?

  • 텍스트와 메타데이터를 함께 관리
  • LangChain의 모든 도구(벡터스토어, retriever 등)가 Document를 표준으로 사용
  • “어떤 파일의 어떤 부분에서 온 내용인지” 추적 가능

3. 파일 로딩 - 텍스트 파일 (.txt)

샘플 파일 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
from pathlib import Path

SAMPLE_DIR = Path('sample_data')
SAMPLE_DIR.mkdir(exist_ok=True)

sample_texts = {
    'company_policy.txt': "주식회사 모두의연구소 사내 규정\n\n제1조 (목적)...",
    'ai_report.txt'     : "2024년 인공지능 산업 동향 보고서...",
    'product_manual.txt': "스마트 홈 허브 v3.0 사용자 매뉴얼...",
}

for filename, content in sample_texts.items():
    (SAMPLE_DIR / filename).write_text(content, encoding='utf-8')

텍스트 파일 로딩 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def load_text_files(directory):
    documents = []
    for fp in sorted(directory.glob('*.txt')):   # *.txt 파일 모두 찾기
        text = fp.read_text(encoding='utf-8')
        doc  = Document(
            page_content = text,
            metadata     = {
                "source"    : fp.name,
                "char_count": len(text),
            }
        )
        documents.append(doc)
    return documents

all_docs = load_text_files(Path('sample_data'))
print(f"로딩된 파일 수: {len(all_docs)}")

for doc in all_docs:
    print(f"  {doc.metadata['source']} ({doc.metadata['char_count']}자)")

Path.glob(‘*.txt’) : 디렉토리에서 .txt 파일 모두 찾기

sorted() : 파일명 알파벳 순 정렬 (일관성)

fp.read_text(encoding=’utf-8’) : 파일 내용 읽기


4. 파일 로딩 - CSV 파일

CSV 파일 생성

1
2
3
4
5
6
7
8
9
10
11
12
import csv

csv_data = [
    {'이름': '김철수', '부서': '개발팀', '직급': '선임'},
    {'이름': '이영희', '부서': '기획팀', '직급': '매니저'},
    {'이름': '박지민', '부서': '개발팀', '직급': '주임'},
]

with open(SAMPLE_DIR / 'employees.csv', 'w', encoding='utf-8', newline='') as f:
    writer = csv.DictWriter(f, fieldnames=['이름', '부서', '직급'])
    writer.writeheader()
    writer.writerows(csv_data)

CSV 로딩 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def load_csv_file(file_path):
    documents = []
    with open(file_path, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            # 각 컬럼을 "키: 값 | 키: 값" 형식으로 변환
            content = ' | '.join(f'{k}: {v}' for k, v in row.items())
            doc = Document(
                page_content = content,
                metadata     = {"source": file_path.name}
            )
            documents.append(doc)
    return documents

csv_docs = load_csv_file(SAMPLE_DIR / 'employees.csv')
for doc in csv_docs:
    print(doc.page_content)

CSV 로딩 결과

1
2
3
이름: 김철수 | 부서: 개발팀 | 직급: 선임
이름: 이영희 | 부서: 기획팀 | 직급: 매니저
이름: 박지민 | 부서: 개발팀 | 직급: 주임

csv.DictReader : CSV를 딕셔너리 형태로 읽기 (첫 행 = 헤더)

‘.join(…) : 키-값 쌍을구분자로 이어 붙임

각 행(row) → 하나의 Document로 변환


5. 파일 로딩 - JSON 파일

JSON 파일 생성

1
2
3
4
5
6
7
8
9
10
11
import json

test_json = {
    'name'    : 'RAG 프로젝트',
    'version' : '1.0',
    'features': ['검색', '생성', '인덱싱'],
    'author'  : '모두의연구소'
}

with open('sample_data/test.json', 'w', encoding='utf-8') as f:
    json.dump(test_json, f, ensure_ascii=False, indent=2)

JSON 로딩 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def load_json_as_document(file_path):
    path = Path(file_path)
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # JSON 전체를 들여쓰기 형식 문자열로 변환 → page_content
    content = json.dumps(data, ensure_ascii=False, indent=2)
    keys    = list(data.keys())   # 최상위 키 목록

    return Document(
        page_content = content,
        metadata     = {'source': path.name, 'keys': keys}
    )

doc = load_json_as_document('sample_data/test.json')
print(doc.page_content)
# {
#   "name": "RAG 프로젝트",
#   "version": "1.0",
#   "features": ["검색", "생성", "인덱싱"],
#   ...
# }
print(doc.metadata['keys'])   # ['name', 'version', 'features', 'author']

json.loads() : 문자열 → 딕셔너리 (파싱)

json.dumps(indent=2) : 딕셔너리 → 들여쓰기 있는 JSON 문자열 (보기 좋음)

metadata의 keys 필드 : 어떤 키가 있는지 빠르게 확인 가능


6. 파일 형식별 비교 정리

파일 형식로딩 도구page_content 구성특징
.txtfp.read_text()파일 전체 텍스트가장 단순
.csvcsv.DictReader“키: 값 | 키: 값” (행별)각 행 → Document
.jsonjson.load()json.dumps(indent=2)전체 JSON 문자열

실제 RAG 프로젝트에서

  • PDF, Word 파일: PyPDF2, python-docx + LangChain 로더 사용
  • 웹 크롤링: langchain_community.document_loaders.WebBaseLoader
  • 오늘 실습은 기초 파일 형식으로 원리 이해

7. Fine-tuning vs RAG 비교

둘 다 LLM에 새로운 지식을 추가하는 방법

방식설명특징
Fine-tuningGPT 모델 자체를 추가 학습비용 높음, 지식이 모델에 내재화, 업데이트 어려움, 학습 데이터 필요
RAG외부 문서를 검색해서 답변저비용, 지식 업데이트 쉬움, 실시간 최신 정보 반영 가능

실무에서는 대부분 RAG를 선택 이유: 회사 내부 문서는 자주 업데이트됨 → Fine-tuning은 업데이트마다 재학습 필요

Fine-tuning은 특수 도메인 용어/말투 학습에 사용

RAG의 기본 원리 (Retrieval-Augmented Generation)

  1. 문서들을 벡터로 변환하여 저장 (인덱싱)
  2. 사용자 질문도 벡터로 변환
  3. 코사인 유사도로 관련 문서 검색
  4. 검색된 문서를 context로 LLM에 전달
  5. LLM이 context 기반으로 답변 생성

8. 코사인 유사도 기반 RAG 직접 구현

지식 베이스 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from sklearn.metrics.pairwise import cosine_similarity
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model='text-embedding-3-small')

knowledge_base = [
    {
        'id'     : 1,
        'content': '파이썬은 1991년 귀도 반 로섬이 만든 프로그래밍 언어입니다. '
                   '간결한 문법과 풍부한 라이브러리가 특징입니다.',
        'source' : 'programming_guide.txt'
    },
    {
        'id'     : 2,
        'content': 'RAG는 Retrieval-Augmented Generation의 약자로, '
                   'LLM이 외부 데이터를 참조하여 답변을 생성하는 기술입니다.',
        'source' : 'ai_glossary.txt'
    },
    {
        'id'     : 3,
        'content': '벡터 데이터베이스는 텍스트를 숫자 벡터로 변환하여 '
                   '유사도 기반 검색을 제공하는 데이터베이스입니다.',
        'source' : 'database_manual.txt'
    },
]

유사도 기반 검색 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def retrieve_by_similarity(query, documents, top_k=2):
    """코사인 유사도로 가장 관련 있는 문서 top_k개 반환"""
    contents  = [doc['content'] for doc in documents]

    query_vec = embeddings.embed_query(query)
    doc_vecs  = embeddings.embed_documents(contents)

    scores = cosine_similarity([query_vec], doc_vecs)[0]
    ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)

    return [(doc, float(score)) for doc, score in ranked[:top_k]]

# 테스트
results = retrieve_by_similarity("RAG 기술에 대해 알려주세요", knowledge_base)
for doc, score in results:
    print(f"[{score:.4f}] (출처: {doc['source']}) {doc['content'][:50]}")
# [0.5590] (출처: ai_glossary.txt) RAG는 Retrieval-Augmented Generation...

데이터 흐름

1
2
3
4
5
6
7
query "RAG 기술에 대해..."
      ↓ embed_query()
query_vec [0.12, -0.34, ...]
      ↓ cosine_similarity()
doc_vecs와 유사도 계산: [0.56, 0.15, 0.23, ...]
      ↓ sorted(..., reverse=True)
[(RAG 문서, 0.56), (벡터DB 문서, 0.23), ...]  상위 2개 반환

9. rag_with_langchain - LLM 연결

검색 + LLM 답변 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model='gpt-4o-mini')

def rag_with_langchain(query, documents):
    """검색된 문서를 context로 사용하여 LLM 답변 생성"""
    if not documents:
        return '참고할 문서가 없습니다'

    # 문서를 context 텍스트로 변환
    contexts = '\n'.join(f"- {doc}" for doc in documents)

    messages = [
        SystemMessage(content='제공된 문서를 기반으로 정확하게 답변해주세요. '
                              '문서에 없는 내용은 모른다고 하세요.'),
        HumanMessage(content=f'참고 문서:\n{contexts}\n\n질문: {query}')
    ]
    response = llm.invoke(messages)
    return response.content

# 통합 실행
query   = 'RAG가 무엇인가요?'
results = retrieve_by_similarity(query, knowledge_base, top_k=2)
top_docs = [doc['content'] for doc, _ in results]
answer  = rag_with_langchain(query, top_docs)
print(answer)

RAG 전체 흐름

1
2
3
4
5
6
7
8
9
사용자 질문
    ↓ retrieve_by_similarity()
관련 문서 top-k개 (cosine similarity 기반)
    ↓ context 구성
[참고 문서]\n- 문서1\n- 문서2
    ↓ rag_with_langchain()
LLM (문서 기반 답변 생성)
    ↓
최종 답변 (출처 명시)

10. 클래스(OOP) 기초 - Calculator, WordCounter

클래스 개념

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# class : 변수(속성)와 메서드를 묶는 단위 (설계도)
# __init__ : 인스턴스 생성 시 자동 호출되는 초기화 메서드
# self : 인스턴스 자기 자신을 가리키는 참조

class Calculator:
    def __init__(self):
        self.history = []    # 인스턴스 변수

    def add(self, a, b):
        result = a + b
        self.history.append(f'{a} + {b} = {result}')
        return result

    def subtract(self, a, b):
        result = a - b
        self.history.append(f'{a} - {b} = {result}')
        return result

    def get_history(self):
        return self.history

사용 예시

1
2
3
4
5
6
7
calc = Calculator()       # 인스턴스 생성 (설계도 → 실체)
calc.add(1, 2)            # 3
calc.subtract(10, 3)      # 7
calc.get_history()        # ['1 + 2 = 3', '10 - 3 = 7']

calc2 = Calculator()      # 새 인스턴스 (별개의 history)
calc2.get_history()       # []  ← calc와 독립적!

인스턴스를 여러 개 만들면 각각 독립적인 데이터(history) 가짐

WordCounter 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import Counter

class WordCounter:
    def __init__(self):
        self.texts = []

    def add_text(self, text):
        self.texts.append(text)

    def count_words(self):
        """전체 단어 수 합계"""
        return sum(len(t.split()) for t in self.texts)

    def most_common(self, n):
        """가장 많이 등장한 n개 단어"""
        all_words = []
        for t in self.texts:
            all_words.extend(t.split())
        return Counter(all_words).most_common(n)

# 사용 예시
wc = WordCounter()
wc.add_text("AI가 세상을 바꾸고 있습니다 AI는 미래 기술입니다")
wc.add_text("AI와 머신러닝이 산업을 변화시키고 있습니다")
print(wc.count_words())    # 14
print(wc.most_common(3))   # [('AI가', 1), ('AI는', 1), ('AI와', 1)]

11. DocumentStore 클래스 - RAG를 OOP로 구현

설계

  • add() : 문서 추가
  • count() : 문서 수 확인
  • search() : 키워드 문자열 검색 (단순)
  • retrieve() : 임베딩 유사도 기반 검색
  • generate() : 검색 + LLM 답변 생성

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from sklearn.metrics.pairwise import cosine_similarity

class DocumentStore:
    def __init__(self):
        self.documents = []    # 저장된 문서 리스트
        self.next_id   = 1     # 자동 증가 ID
        self.embeddings = OpenAIEmbeddings(model='text-embedding-3-small')
        self.llm        = ChatOpenAI(model='gpt-4o-mini')

    def add(self, content, source='unknown'):
        """문서 추가"""
        self.documents.append({
            'id'     : self.next_id,
            'content': content,
            'source' : source
        })
        self.next_id += 1

    def count(self):
        """저장된 문서 수 반환"""
        return len(self.documents)

    def search(self, keyword):
        """키워드 포함 문서 반환 (단순 문자열 검색)"""
        return [doc for doc in self.documents if keyword in doc['content']]

    def retrieve(self, query, top_k=3):
        """임베딩 유사도 기반 상위 k개 문서 반환"""
        if not self.documents:
            return []

        contents  = [doc['content'] for doc in self.documents]
        query_vec = self.embeddings.embed_query(query)
        doc_vecs  = self.embeddings.embed_documents(contents)

        scores = cosine_similarity([query_vec], doc_vecs)[0]
        ranked = sorted(zip(self.documents, scores), key=lambda x: x[1], reverse=True)
        return ranked[:top_k]

    def generate(self, query):
        """검색된 문서를 context로 사용하여 LLM 답변 생성"""
        retrieved = self.retrieve(query)
        if not retrieved:
            return "관련 문서를 찾을 수 없습니다."

        contexts = '\n'.join(
            f"[문서 {d[0]['id']}, 출처: {d[0]['source']}] {d[0]['content']}"
            for d in retrieved
        )
        messages = [
            SystemMessage(content='제공된 문서를 기반으로 정확하게 답변해주세요. '
                                   '출처를 명시해주세요.'),
            HumanMessage(content=f'참고 문서:\n{contexts}\n\n질문: {query}')
        ]
        return self.llm.invoke(messages).content

사용 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
store = DocumentStore()

# 문서 추가
store.add('파이썬은 데이터 분석에 좋다', 'guide.txt')
store.add('RAG는 검색증강 생성이다',     'glossary.txt')
store.add('LangChain은 LLM 프레임워크', 'langchain.txt')

print(store.count())              # 3

# 키워드 검색
results = store.search('파이썬')
print(results)   # [{'id': 1, 'content': '파이썬은...', 'source': 'guide.txt'}]

# 임베딩 유사도 검색
results = store.retrieve('RAG 기술이 뭔가요?', top_k=2)
for doc, score in results:
    print(f"[{score:.4f}] {doc['content']}")

# RAG 답변 생성
answer = store.generate('RAG에 대해 설명해주세요')
print(answer)
# RAG는 "검색증강 생성"의 약자로... [문서 2, 출처: glossary.txt]

OOP로 묶는 장점

  • 여러 DocumentStore 인스턴스를 독립적으로 운영 가능 (사내 규정용, 제품 매뉴얼용, AI 보고서용 각각 별도 store)
  • add/search/retrieve/generate 인터페이스가 일관됨
  • 내부 구현(임베딩 모델, LLM)을 변경해도 사용 코드 불변

12. collections.Counter 활용

Counter란?

1
2
3
4
5
6
7
8
9
10
11
from collections import Counter

# 리스트의 각 요소 빈도 계산
all_words = [1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5]
c = Counter(all_words)
print(c)
# Counter({1: 5, 2: 4, 3: 3, 4: 2, 5: 1})

# 가장 많이 등장한 n개
c.most_common(2)
# [(1, 5), (2, 4)]   ← (값, 빈도) 튜플

텍스트 단어 빈도 분석

1
2
3
4
5
6
7
texts     = ["AI가 세상을 바꾸고 AI가 발전한다", "AI는 미래 기술이다"]
all_words = []
for t in texts:
    all_words.extend(t.split())   # 단어 리스트로 합침

print(Counter(all_words).most_common(3))
# [('AI가', 2), ('세상을', 1), ('바꾸고', 1)]

Counter(iterable) : 각 요소의 빈도를 딕셔너리 형태로

.most_common(n) : 상위 n개 (값, 빈도) 튜플 리스트

FAG 시스템에서 자주 질문되는 키워드 분석 등에 활용


13. 핵심 개념 총정리

개념핵심 내용
Document 클래스page_content + metadata로 문서 표현
.txt 로딩Path.glob(‘*.txt’) + read_text()
.csv 로딩DictReader로 각 행을 Document로 변환
.json 로딩json.load() + json.dumps(indent=2)
RAG 기본 구조검색(cosine similarity) → context → LLM 답변
Fine-tuning vs RAG학습 vs 검색, 비용/업데이트 용이성 차이
class설계도, 속성(변수) + 메서드(함수)의 묶음
__init__인스턴스 생성 시 자동 호출, 초기화
self인스턴스 자기 자신을 참조
DocumentStoreOOP로 add/count/search/retrieve/generate 통합
Counteriterable 요소 빈도 계산, most_common(n)

오늘 배운 것의 RAG 시스템 위치

1
2
3
4
5
6
7
8
9
10
11
.txt/.csv/.json 로딩  ← 오늘 배움
        ↓
Document 객체들       ← 오늘 배움
        ↓
임베딩 → 벡터         ← 17일
        ↓
cosine_similarity 검색 ← 오늘 배움
        ↓
LLM 답변 생성         ← 오늘 배움
        ↓
FAISS + LCEL 체인     ← 내일 (20일)

14. 자주 나오는 실수 / 주의사항

실수 1: CSV 파일 쓸 때 newline=’’ 빠뜨리기

1
2
3
# 빠뜨리면 Windows에서 각 행마다 빈 줄이 생김
with open('employees.csv', 'w', encoding='utf-8') as f:    # 잘못됨
with open('employees.csv', 'w', encoding='utf-8', newline='') as f:  # 올바름

실수 2: JSON dumps vs loads 혼동

1
2
3
4
5
6
7
8
9
# json.loads() : 문자열 → 딕셔너리 (parsing, 읽기)
data = json.loads('{"name": "RAG"}')

# json.dumps() : 딕셔너리 → 문자열 (serialization, 쓰기)
text = json.dumps({"name": "RAG"}, ensure_ascii=False)

# 파일 작업:
# json.load(file)  : 파일 → 딕셔너리
# json.dump(data, file) : 딕셔너리 → 파일

실수 3: __init__에서 self 빠뜨리기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DocumentStore:
    def __init__(self):
        documents = []    # 틀림! 로컬 변수로 생성됨

    def add(self, content):
        documents.append(content)   # NameError! documents가 없음

# 올바른 코드
class DocumentStore:
    def __init__(self):
        self.documents = []   # self.로 인스턴스 변수 생성

    def add(self, content):
        self.documents.append(content)   # self.로 접근

실수 4: retrieve 결과의 구조 착각

1
2
3
4
5
6
7
8
9
10
# retrieve()는 [(doc_dict, score), ...] 반환
results = store.retrieve("RAG란?")

for item in results:
    print(item)          # (딕셔너리, 점수) 튜플

# 올바른 언패킹
for doc, score in results:
    print(f"score: {score:.4f}")
    print(f"content: {doc['content']}")

실수 5: glob 패턴 주의

1
2
3
4
5
6
7
# .txt 파일만 찾기
directory.glob('*.txt')    # 올바름 (현재 폴더)
directory.glob('**/*.txt') # 하위 폴더까지 재귀 탐색

# 파일 이름에 숫자 있을 때 정렬 주의
sorted(directory.glob('*.txt'))        # 문자열 정렬 (0, 1, 10, 2, ...)
sorted(directory.glob('*.txt'), key=lambda p: p.name)  # 이름 기준

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그