티스토리 뷰
LangChain으로 문서 하나 읽고 답하는 AI 만들기 — RAG 전에 꼭 알아야 할 가장 작은 문서 Q&A
octo54 2026. 3. 31. 12:03LangChain으로 문서 하나 읽고 답하는 AI 만들기 — RAG 전에 꼭 알아야 할 가장 작은 문서 Q&A
여기서부터 좀 재밌어집니다.
이전까지는 LangChain으로
프롬프트를 만들고,
체인을 연결하고,
대화 히스토리를 관리하는 흐름까지 왔죠.
그런데 아직 모델은 기본적으로 자기 안에 있는 지식만 가지고 답합니다.
즉, 내가 가진 문서나 회사 자료나 강의 노트나 README를 “읽고 답하는” 단계는 아니었어요.
이제 그걸 해볼 차례입니다.
다만 이번 글은 일부러 RAG까지 바로 가지 않겠습니다.
왜냐면 많은 분이 여기서 너무 빨리 벡터스토어, 임베딩, 검색기로 뛰어들다가 정작 핵심을 놓치거든요.
핵심은 생각보다 단순해요.
문서를 읽는다 → 적당히 나눈다 → 질문과 함께 넣는다 → 근거 기반으로 답하게 만든다
LangChain 문서도 문서 로더는 Slack, Notion, Google Drive 같은 다양한 소스의 데이터를 공통된 Document 형식으로 읽어들이는 표준 인터페이스라고 설명하고, 텍스트 분할에서는 대부분의 경우 RecursiveCharacterTextSplitter부터 시작하라고 안내합니다. 또 retrieval 튜토리얼은 “자체 데이터로 검색 가능한 지식 베이스를 만들고, 그 위에 최소한의 RAG 워크플로를 얹을 수 있다”고 설명해요. (LangChain Docs)
왜 “문서 하나 읽고 답하기”부터 해야 할까
솔직히 말하면, RAG라는 단어가 너무 빨리 나옵니다.
요즘은 뭘 하나 해도 다 RAG라고 부르잖아요.
근데 주니어 입장에서는 순서가 중요해요.
처음부터 벡터스토어까지 가면 머릿속이 이렇게 됩니다.
- 문서 로더?
- chunk?
- 임베딩?
- similarity search?
- retriever?
- chain?
- context window?
이게 한 번에 다 들어오면 솔직히 좀 질립니다.
그래서 저는 늘 이 단계를 추천합니다.
먼저 해볼 것
- 파일 하나 읽기
- 문서를 조각내기
- 그 조각들을 프롬프트에 넣기
- 질문에 답하게 만들기
나중에 붙일 것
- 임베딩
- 벡터스토어
- 검색기
- reranking
- 고급 RAG
이 순서가 좋은 이유는,
나중에 RAG를 배울 때도 결국 본질이 똑같다는 걸 알게 되기 때문이에요.
**RAG도 결국 “질문과 관련 있는 문서 조각을 찾아서 같이 넣는 것”**입니다.
그런데 지금은 검색기를 만들지 않고, 작은 문서니까 그냥 직접 넣어보는 거예요.
오늘 글에서 얻어갈 것
이번 글에서는 아래 4가지를 확실히 가져가면 됩니다.
- LangChain의 Document가 대충 어떤 느낌인지
- 문서를 왜 분할해야 하는지
- 가장 작은 문서 Q&A 체인을 어떻게 만드는지
- 왜 이 단계가 나중의 RAG와 이어지는지
특히 두 번째가 중요해요.
문서를 그냥 통째로 넣으면 될 것 같지만, 실제로는 문서 길이가 길어질수록 오히려 잘 안 됩니다.
LangChain의 splitters 가이드는 대부분의 경우 RecursiveCharacterTextSplitter를 추천하고, retrieval/knowledge-base 튜토리얼은 예시로 1000자 chunk, 200자 overlap 같은 식으로 문서를 나누어 검색과 생성에 쓰는 흐름을 보여줍니다. overlap은 문맥이 경계에서 잘리는 문제를 줄이는 데 도움을 준다고 설명해요. (LangChain Docs)
1. 문서 로더는 뭘 하는 도구인가
LangChain에서 문서 로더는 아주 중요한 출발점입니다.
문서 로더는 쉽게 말하면
파일이나 외부 데이터를 읽어서 LangChain의 Document 형식으로 바꿔주는 도구예요.
공식 문서는 document loaders가 다양한 소스의 데이터를 Document 포맷으로 읽어들이는 표준 인터페이스라고 설명합니다. (LangChain Docs)
이 Document에는 보통 두 가지가 들어 있습니다.
- page_content: 실제 텍스트 내용
- metadata: 출처, 파일명, 페이지 정보 같은 부가 정보
이 구조가 중요한 이유는 나중에 답변할 때
“어디서 가져온 내용인지”를 같이 다루기 쉬워지기 때문입니다.
이번 글에서는 제일 단순하게 텍스트 파일 하나를 읽어보겠습니다.
복잡한 PDF보다 먼저 이걸 하는 게 훨씬 좋아요.
왜냐면 핵심은 PDF 파싱이 아니라 문서 기반 답변 흐름을 이해하는 데 있으니까요.
2. 왜 문서를 쪼개야 할까
이 질문을 많이 받습니다.
“문서 하나면 그냥 통째로 넣으면 안 되나요?”
짧으면 됩니다.
정말 짧으면요.
그런데 대부분의 문서는 생각보다 금방 길어집니다.
그리고 문서를 통째로 넣으면 이런 문제가 생겨요.
- 토큰이 커진다
- 질문과 상관없는 내용이 너무 많이 들어간다
- 모델이 핵심을 놓치기 쉬워진다
- 나중에 검색 기반 구조로 확장하기 어렵다
그래서 문서를 적당한 크기의 chunk로 나누는 게 중요합니다.
LangChain 문서는 generic text에는 RecursiveCharacterTextSplitter를 추천하고, 이 splitter는 기본적으로 "\n\n", "\n", " ", "" 같은 구분자를 순서대로 시도해 가능한 한 문단과 문장을 보존하려고 한다고 설명합니다. (LangChain Docs)
이 설명이 꽤 좋습니다.
그냥 기계적으로 잘라버리는 게 아니라,
가능한 자연스러운 경계를 유지하면서 쪼개려는 방식이라는 거니까요.
3. 오늘 만들 예제의 전체 그림
이번 글에서 만들 건 아주 작습니다.
입력
- notes.txt 같은 텍스트 문서 하나
처리
- 문서 로드
- chunk 분할
- chunk들을 하나의 context로 묶기
- 질문과 함께 모델에 전달
출력
- 문서 내용에 근거한 답변
아직은 검색도 없습니다.
문서가 작다는 가정하에, 쪼갠 chunk를 모두 넣거나 일부만 넣는 정도로 갑니다.
이게 별거 아닌 것 같아도 되게 중요해요.
나중에 RAG를 배울 때도 구조는 거의 같습니다.
- 지금: 작은 문서라서 직접 다 넣음
- 나중: 큰 문서라서 관련 chunk만 검색해서 넣음
즉, 오늘은 RAG의 검색기 없는 미니 버전이라고 생각하면 딱 좋습니다.
4. 먼저 준비할 텍스트 파일
예를 들어 프로젝트 폴더에 notes.txt를 하나 만듭니다.
LangChain은 LLM 애플리케이션을 더 구조적으로 개발하기 위한 프레임워크다.
핵심 구성요소로는 프롬프트, 모델, 출력 파서, 리트리벌, 툴, 에이전트 등이 있다.
주니어 개발자가 처음에는 PromptTemplate, 체인 연결, structured output 같은 기초부터 익히는 것이 좋다.
RAG는 외부 문서를 검색해서 모델 답변에 근거를 추가하는 방식이다.
이 정도 길이면 사실 통째로 넣어도 됩니다.
그런데 일부러 splitter를 같이 써볼 겁니다.
왜냐면 나중으로 이어지는 감각이 훨씬 좋아지거든요.
5. 실행 코드 — 문서 로드 + 분할 + 질문응답
아래 코드는 바로 실행할 수 있는 가장 작은 예제입니다.
설치
pip install -U "langchain[openai]" langchain-community langchain-text-splitters
LangChain은 provider별 통합 패키지를 분리해서 제공하고 있고, 문서 로더와 텍스트 분할도 별도 패키지/통합 흐름으로 안내합니다. Python integrations overview는 LangChain이 chat model, embeddings, tools, document loaders, vector stores 등 1000개 이상 통합 생태계를 제공한다고 설명합니다. (LangChain Docs)
코드
import os
from langchain.chat_models import init_chat_model
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
def format_docs(docs) -> str:
return "\n\n".join(doc.page_content for doc in docs)
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
# 1) 문서 로드
loader = TextLoader("notes.txt", encoding="utf-8")
documents = loader.load()
# 2) 문서 분할
splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50,
)
split_docs = splitter.split_documents(documents)
# 3) context 문자열 만들기
context_text = format_docs(split_docs)
# 4) 모델 준비
model = init_chat_model("gpt-5-nano", model_provider="openai")
# 5) 프롬프트 정의
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"너는 문서 기반 Q&A 도우미다. "
"반드시 제공된 문서 내용에 근거해서만 답하고, "
"문서에 없는 내용은 모른다고 말해라."
),
(
"human",
"문서 내용:\n{context}\n\n"
"질문: {question}"
),
]
)
chain = prompt | model | StrOutputParser()
# 6) 질문 실행
question = "LangChain을 처음 공부할 때 먼저 익히면 좋은 것은 뭐야?"
answer = chain.invoke(
{
"context": context_text,
"question": question,
}
)
print("=== split docs ===")
for i, doc in enumerate(split_docs, start=1):
print(f"[{i}] {doc.page_content}\n")
print("=== answer ===")
print(answer)
if __name__ == "__main__":
main()
6. 이 코드가 실제로 하는 일
이 코드를 딱 6단계로 보면 이해가 쉽습니다.
1) TextLoader로 파일을 읽는다
문서를 LangChain Document 객체로 바꿉니다. document loader는 이런 표준화된 로딩을 위한 인터페이스입니다. (LangChain Docs)
2) RecursiveCharacterTextSplitter로 나눈다
generic text에는 이 splitter가 보통 추천됩니다. 문단/문장 경계를 최대한 유지하려고 시도하기 때문입니다. (LangChain Docs)
3) chunk를 문자열 context로 합친다
이번 글은 작은 문서용 예제니까, 일단 chunk를 전부 이어붙여도 됩니다.
4) 프롬프트에 context와 question을 같이 넣는다
이제 모델은 “질문만” 보는 게 아니라 “문서 + 질문”을 같이 봅니다.
5) 시스템 메시지로 답변 규칙을 준다
문서에 없는 내용은 모른다고 하라고 명시합니다.
6) 답을 출력한다
이제 모델은 일반 지식이 아니라, 주어진 문서를 근거로 대답하는 방향으로 움직입니다.
7. 여기서 제일 중요한 포인트
이 글에서 진짜 중요한 건 코드보다 이 감각입니다.
문서 기반 Q&A는 결국 “문서를 context로 넣는 프롬프트 설계”에서 시작한다.
이걸 이해하면 RAG가 갑자기 덜 어렵게 보입니다.
많은 분이 RAG를 뭔가 거대한 기술처럼 느끼는데,
사실 구조는 이거예요.
- 관련 문서 조각을 찾는다
- 그걸 질문과 함께 모델에 넣는다
- 문서 기반으로 답하게 한다
지금은 1번에서 검색기를 안 붙였을 뿐입니다.
작은 문서라서 그냥 전부 넣는 거죠.
8. “문서에 없는 건 모른다고 말해라”가 중요한 이유
이거 은근히 중요합니다.
문서 기반 Q&A를 만든다고 해도,
모델은 기본적으로 자기 원래 지식도 갖고 있습니다.
그래서 프롬프트를 느슨하게 쓰면 문서에 없는 내용까지 아는 척할 수 있어요.
그래서 시스템 메시지에서 이런 제약을 주는 게 좋습니다.
- 제공된 문서에 근거해서만 답해라
- 문서에 없는 정보는 추측하지 마라
- 확실하지 않으면 모른다고 말해라
이건 나중에 RAG에서도 그대로 중요합니다.
검색 품질만큼, 생성 단계에서 근거 의존성을 어떻게 강하게 주느냐도 중요하거든요.
9. 작은 문서에선 이 방식이 충분한 이유
예를 들어:
- README
- 짧은 강의 노트
- 회의 메모
- 블로그 초안
- FAQ 텍스트
이 정도는 굳이 벡터스토어까지 안 가도 됩니다.
그냥 문서를 읽고, 적당히 분할해서, 질문과 함께 넣는 것만으로도 꽤 쓸 만해요.
오히려 처음엔 이 방식이 훨씬 좋습니다.
왜냐면 문제를 더 선명하게 볼 수 있거든요.
- 문서가 너무 길어서 문제인가?
- chunk가 너무 커서 문제인가?
- 질문이 애매해서 문제인가?
- 프롬프트가 약해서 문제인가?
RAG까지 한 번에 들어가면 이 원인들이 다 섞여 보입니다.
그래서 지금처럼 작은 구조를 먼저 해보는 게 되게 중요해요.
10. 여기서 바로 만나는 한계
물론 이 방식은 금방 한계를 만납니다.
1) 문서가 길어지면 전부 넣기 어렵다
토큰 한계와 비용 문제가 바로 나옵니다. 긴 문서는 결국 검색이 필요해져요. retrieval 튜토리얼도 자체 데이터로 검색 가능한 지식 베이스를 만드는 방향으로 이어집니다. (LangChain Docs)
2) 질문과 상관없는 chunk도 같이 들어간다
작은 문서에선 괜찮지만, 커질수록 노이즈가 됩니다.
3) 여러 문서를 다루기 어렵다
한 파일, 두 파일까진 괜찮아도 수십 개가 되면 검색 단계가 필수입니다.
즉, 이 글의 방식은 끝이 아니라
RAG 직전의 가장 작은 출발점입니다.
11. chunk size와 overlap은 어떻게 잡아야 하나
이건 정말 많이 궁금해하죠.
정답은 문서 종류마다 다르지만, 초반엔 너무 복잡하게 가지 않는 게 좋아요.
LangChain의 semantic search/knowledge-base 튜토리얼은 예시로 1000 characters + 200 overlap을 사용하고, recursive splitter 가이드는 이 splitter가 generic text에 좋은 기본값이라고 설명합니다. (LangChain Docs)
초반에는 이렇게 생각하면 편합니다.
- 짧은 문서: 200~500자 정도
- 보통 문서: 500~1000자 정도
- overlap: chunk size의 10~20% 정도
왜 overlap이 필요하냐면,
중요한 문장이 chunk 경계에서 잘릴 수 있기 때문입니다.
튜토리얼도 overlap이 중요한 문맥이 경계에서 분리되는 걸 줄이는 데 도움을 준다고 설명합니다. (LangChain Docs)
12. 조금 더 실무적으로 바꿔보기
지금 예제는 질문을 코드 안에 하드코딩했죠.
실제로는 입력을 받아서 여러 질문을 해볼 수 있게 하는 게 좋습니다.
while True:
question = input("질문: ").strip()
if question.lower() in {"exit", "quit"}:
break
answer = chain.invoke(
{
"context": context_text,
"question": question,
}
)
print(f"답변: {answer}\n")
이렇게 바꾸면 작은 문서 챗봇처럼 쓸 수 있습니다.
그리고 여기서 한 단계만 더 가면
이미 “문서 기반 챗봇”의 출발점이 됩니다.
13. 문서 출처까지 보여주고 싶다면
지금은 page_content만 합쳐서 넣었지만,
실제로는 metadata를 같이 활용하는 게 좋습니다.
예를 들어 나중에는 이런 식으로 만들 수 있어요.
- 파일명 표시
- chunk 번호 표시
- 페이지 번호 표시
- 답변 아래에 참고 문서 출처 노출
LangChain의 Document 구조가 metadata를 함께 갖는 이유도 결국 이런 활용 때문입니다. document loaders가 데이터를 공통된 Document 포맷으로 읽고, downstream에서 일관되게 다루도록 한다는 설명과 이어집니다. (LangChain Docs)
14. 오늘 글에서 꼭 가져가야 할 한 문장
오늘 내용을 한 줄로 줄이면 이겁니다.
문서 기반 Q&A의 본질은 “모델이 똑똑해지게 하는 것”이 아니라, 질문할 때 필요한 문서 맥락을 같이 넣어주는 것”이다.
이 감각이 생기면,
이제 RAG가 왜 필요한지도 자연스럽게 보입니다.
- 문서가 많아진다
- 전부 넣을 수 없다
- 그럼 관련 chunk만 찾아야 한다
- 그래서 retrieval이 필요하다
즉, 오늘 글은 RAG의 전 단계이면서 동시에 핵심 원리 그 자체예요.
마무리
저는 처음 문서 기반 Q&A를 만들었을 때,
괜히 더 복잡하게 생각했어요.
벡터DB부터 붙여야 할 것 같고, 임베딩도 당장 해야 할 것 같고요.
근데 막상 해보면 시작은 훨씬 소박합니다.
문서를 하나 읽고,
적당히 나누고,
질문과 같이 넣고,
“이 문서 안에서만 답해”라고 말하는 것.
진짜 출발은 여기더라고요.
그걸 먼저 해봐야, 나중에 RAG가 왜 필요한지도 납득이 됩니다.
아무것도 없이 바로 RAG로 가면 그냥 조립만 하게 돼요.
근데 이 단계를 거치면 구조가 이해됩니다.
그 차이가 꽤 큽니다.
다음 글 예고
다음 글에서는
임베딩과 벡터스토어를 처음 이해하는 글 — RAG를 배우기 전에 꼭 알아야 할 검색의 언어
로 이어가겠습니다.
이제부터는 “문서를 다 넣을 수 없을 때, 어떻게 관련 조각만 찾아올 것인가”를 본격적으로 다루게 됩니다.
출처
- LangChain Document loader integrations: document loaders는 Slack, Notion, Google Drive 같은 다양한 소스에서 데이터를 읽어 Document 형식으로 변환하는 표준 인터페이스라고 설명합니다. (LangChain Docs)
- LangChain Text splitter integrations: 대부분의 경우 RecursiveCharacterTextSplitter부터 시작하라고 권장합니다. (LangChain Docs)
- RecursiveCharacterTextSplitter 가이드: 기본 구분자 목록 ["\n\n", "\n", " ", ""]를 순서대로 시도하며 문단/문장/단어 경계를 최대한 유지하려고 한다고 설명합니다. (LangChain Docs)
- LangChain Retrieval tutorial: 자체 데이터로 검색 가능한 지식 베이스를 만들고, 그 위에 최소한의 RAG 워크플로를 올릴 수 있다고 설명합니다. (LangChain Docs)
- LangChain knowledge-base tutorial: 예시로 1000자 chunk와 200자 overlap을 사용하며, overlap이 문맥 분리 문제를 줄이는 데 도움을 준다고 설명합니다. (LangChain Docs)
- LangChain providers overview: LangChain은 chat model, embeddings, tools, document loaders, vector stores 등 광범위한 provider 통합 생태계를 제공한다고 설명합니다. (LangChain Docs)
LangChain, Document Loader, TextLoader, RecursiveCharacterTextSplitter, 문서기반QnA, RAG입문, 생성형AI, LLM개발, 벡터스토어, 주니어개발자
'study > langchain' 카테고리의 다른 글
| 임베딩과 벡터스토어를 처음 이해하는 글 — RAG를 배우기 전에 꼭 알아야 할 검색의 언어 (0) | 2026.04.03 |
|---|---|
| LangChain 대화 히스토리 다루기 — 긴 대화를 자르고, 요약하고, 비용을 관리하는 방법 (0) | 2026.03.30 |
| LangChain 대화형 챗봇 만들기 — 메시지 히스토리와 문맥 유지를 처음부터 이해하는 방법 (0) | 2026.03.26 |
| LangChain 체인 연결하기 — Prompt | Model | Parser를 부품처럼 조립하는 법 (0) | 2026.03.23 |
| LangChain 출력 제어하기 — Structured Output으로 JSON 응답을 안정적으로 받는 방법 (0) | 2026.03.20 |
- Total
- Today
- Yesterday
- 딥러닝
- LangChain
- fastapi
- Express
- Next.js
- JAX
- CI/CD
- REACT
- 압박면접
- Prisma
- 백엔드개발
- Docker
- Python
- NestJS
- rag
- DevOps
- seo 최적화 10개
- Redis
- 쿠버네티스
- node.js
- PostgreSQL
- nextJS
- flax
- llm
- 개발블로그
- kotlin
- 웹개발
- SEO최적화
- ai철학
- JWT
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
