포스트

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. 자주 나오는 실수 / 주의사항
  15. 로딩과 검색 사이에 남겨야 할 정보

1. 문제와 접근

파일을 읽어 텍스트로 만들 때는 내용만큼 출처와 문서 경계를 보존하는 일이 중요하다. TXT·CSV·JSON을 공통 Document 형태로 옮긴 뒤 검색과 생성을 연결해 본다. 클래스 예제에서는 기능을 모으는 것과 서로 다른 책임을 분리하는 것의 균형을 살펴본다.

오늘의 핵심 흐름

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. 파일 형식별 비교 정리

같은 내용이라도 CSV의 한 행, JSON의 한 객체, 텍스트의 한 문단은 서로 다른 문서 경계가 된다. 로더를 비교할 때는 읽힌 글자 수보다 검색 결과에서 원래 항목과 출처를 복원할 수 있는지를 먼저 확인한다.

파일 형식로딩 도구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 비교

모델의 동작 조정과 외부 근거 연결

방식설명특징
Fine-tuning학습 예제로 모델의 동작 조정출력 패턴·작업 수행 개선을 검증, 데이터 준비와 학습 필요
RAG외부 문서를 검색해 답변의 근거로 전달문서 갱신과 출처 확인에 활용, 검색·인덱스 관리 필요

자주 바뀌는 문서와 출처를 답변에 연결하려는 목적이라면 RAG를 먼저 검토한다. 반복되는 출력 형식이나 작업 수행 방식을 개선하려는 목적이라면 학습 예제 기반의 조정을 검토할 수 있다. 선택 기준은 해결하려는 실패 유형이며, 두 방식을 함께 사용할 수도 있다.

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로 구현

DocumentStore는 전체 흐름을 이해하기 위해 저장·검색·생성을 한곳에 모은 예제다. 저장소나 모델을 독립적으로 교체해야 하는 단계에서는 각 의존성을 외부에서 전달하고, 검색 결과만 확인할 수 있는 인터페이스를 남기는 편이 변경을 추적하기 쉽다.

설계

  • 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)  # 이름 기준

15. 로딩과 검색 사이에 남겨야 할 정보

문서 내용이 같아도 원본 파일과 행 정보를 잃으면 잘못된 답변을 되짚기 어렵다. Document로 변환할 때 출처 식별자를 함께 저장하고, 답변에서 사용한 근거를 해당 식별자로 다시 찾을 수 있게 한다.

검색 결과가 기대와 다르면 로더 출력부터 확인한다. 중복 행, 비어 있는 본문, 메타데이터 누락을 먼저 배제한 다음 검색 방식이나 프롬프트를 조정해야 변경의 효과를 분리해서 볼 수 있다.

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

인기 태그