티스토리 뷰
첫 번째 RAG 만들기 — Retriever로 찾고, 문서를 넣고, 근거 기반으로 답하게 하기
여기까지 왔으면 이제 드디어 많은 사람이 말하는 그 단어, RAG를 직접 만질 차례입니다.
사실 저는 처음에 RAG를 너무 거창하게 생각했어요.
뭔가 엄청 복잡한 아키텍처 같고, 벡터DB도 붙어야 하고, 검색 최적화도 해야 하고, 평가도 해야 하고…
맞아요. 나중엔 그렇게 커질 수 있습니다.
근데 첫 번째 RAG는 그렇게 시작할 필요가 없어요.
진짜 출발은 생각보다 소박합니다.
질문이 들어오면 관련 문서를 찾고, 그 문서를 프롬프트에 넣고, 그 문서를 근거로 답하게 만든다.
LangChain 문서도 retriever를 비정형 쿼리를 받아 관련 문서를 반환하는 인터페이스로 설명하고, RAG를 특정 소스 정보에 대해 질문에 답하는 애플리케이션을 만드는 대표 기법으로 소개합니다. 또한 “simple Q&A application over an unstructured text data source” 예제를 통해 기본 RAG 흐름을 보여줍니다. (LangChain Docs)
왜 이 단계가 중요할까
지난 글에서 우리는 이미 절반은 해봤습니다.
- 문서 하나를 읽고
- chunk로 나누고
- 의미 기반 검색을 위해 임베딩과 벡터스토어를 만들고
- 비슷한 문서를 찾는 것까지 했죠
그런데 아직은 “검색”까지만 했습니다.
오늘은 여기에 “생성”을 붙입니다.
즉, 이제 구조가 이렇게 됩니다.
- 문서를 chunk로 쪼갠다
- 임베딩해서 벡터스토어에 저장한다
- 질문이 들어오면 retriever가 관련 chunk를 찾는다
- 찾은 chunk를 질문과 함께 모델에 넣는다
- 모델이 문서 근거 기반 답변을 만든다
이게 바로 가장 기본적인 RAG입니다. LangChain의 retrieval 문서와 RAG 튜토리얼도 이 흐름을 중심으로 설명합니다. (LangChain Docs)
오늘 글에서 얻어갈 것
이번 글에서는 딱 이것만 확실히 잡으면 됩니다.
- retriever가 찾은 문서를 어떻게 prompt에 넣는지
- 왜 “문서에 근거해서만 답하라”는 지시가 중요한지
- LangChain에서 가장 작은 RAG 체인을 어떻게 만드는지
- 이 기본형이 나중에 어떻게 고급 RAG로 확장되는지
너무 멋진 예제를 만들려 하기보다,
진짜 동작 원리가 보이는 가장 작은 구조를 만드는 데 집중할게요.
1. RAG를 제일 쉽게 말하면
저는 보통 이렇게 설명합니다.
기본 LLM은 똑똑하지만,
내 문서를 자동으로 알고 있는 건 아닙니다.
그래서 RAG는 이렇게 합니다.
“질문이 들어오면, 관련 문서를 먼저 찾아서, 그걸 같이 보여주고 답하게 하자.”
이게 끝이에요.
Retrieval은 찾는 단계,
Generation은 답하는 단계입니다.
그래서 Retrieval-Augmented Generation,
말 그대로 검색으로 보강된 생성입니다. LangChain 문서도 RAG를 질문 응답 챗봇의 대표 구조로 소개하며, retrieval과 generation을 결합하는 흐름을 보여줍니다. (LangChain Docs)
2. 오늘 만들 구조를 먼저 보자
이번 예제는 아주 작게 갑니다.
준비물
- 짧은 문서 몇 개
- OpenAIEmbeddings
- InMemoryVectorStore
- retriever
- ChatPromptTemplate
- ChatModel
흐름
- 문서를 벡터스토어에 넣는다
- retriever로 관련 문서를 가져온다
- 가져온 문서를 context 문자열로 합친다
- 질문과 함께 모델에 넣는다
- 답을 받는다
LangChain의 OpenAI embeddings 예시와 vector store 가이드는 InMemoryVectorStore와 OpenAIEmbeddings, 그리고 as_retriever()를 이용한 흐름을 보여줍니다. (LangChain Docs)
3. 먼저 실행 코드부터 보기
설치
pip install -U langchain langchain-openai langchain-community
LangChain은 OpenAI 통합을 langchain-openai 패키지로 제공하고, retriever와 vector store는 LangChain의 통합 생태계에서 공통 인터페이스로 다뤄집니다. (LangChain Docs)
코드
import os
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
def format_docs(docs) -> str:
return "\n\n".join(
f"[출처: {doc.metadata.get('source', 'unknown')}]\n{doc.page_content}"
for doc in docs
)
def main() -> None:
if not os.environ.get("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")
documents = [
Document(
page_content="LangChain은 프롬프트, 모델, 툴, 리트리벌을 연결해 LLM 애플리케이션을 구조적으로 개발하게 돕는다.",
metadata={"source": "langchain-note-1"},
),
Document(
page_content="PromptTemplate은 프롬프트를 문자열이 아니라 재사용 가능한 입력 구조로 관리하게 도와준다.",
metadata={"source": "langchain-note-2"},
),
Document(
page_content="RAG는 외부 문서를 검색해서 질문과 관련 있는 문맥을 모델에 함께 넣어 답변 품질을 높이는 방식이다.",
metadata={"source": "langchain-note-3"},
),
Document(
page_content="Structured Output은 모델 응답을 JSON이나 Pydantic 스키마처럼 예측 가능한 형식으로 다루는 데 유용하다.",
metadata={"source": "langchain-note-4"},
),
]
# 1) 문서를 임베딩해서 벡터스토어에 저장
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = InMemoryVectorStore.from_documents(
documents=documents,
embedding=embeddings,
)
# 2) retriever 만들기
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# 3) 모델 준비
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 4) 프롬프트 정의
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"너는 문서 기반 Q&A 도우미다. "
"반드시 제공된 문서 내용에 근거해서만 답하고, "
"문서에 없는 내용은 추측하지 말고 모른다고 말해라. "
"가능하면 답변 끝에 참고한 출처를 함께 정리해라."
),
(
"human",
"문서 내용:\n{context}\n\n"
"질문: {question}"
),
]
)
parser = StrOutputParser()
question = "RAG가 뭐고, 왜 문서 검색이 필요한지 설명해줘."
# 5) 관련 문서 검색
retrieved_docs = retriever.invoke(question)
context = format_docs(retrieved_docs)
# 6) 검색된 문서를 넣어서 답변 생성
chain = prompt | model | parser
answer = chain.invoke(
{
"context": context,
"question": question,
}
)
print("=== 검색된 문서 ===")
for idx, doc in enumerate(retrieved_docs, start=1):
print(f"[{idx}] {doc.metadata.get('source')}")
print(doc.page_content)
print()
print("=== 답변 ===")
print(answer)
if __name__ == "__main__":
main()
4. 이 코드가 실제로 하는 일
이제 이걸 천천히 뜯어보면,
갑자기 RAG가 덜 무섭게 느껴집니다.
1) 문서를 Document로 준비한다
문서 내용과 metadata를 함께 넣습니다.
LangChain의 document 관련 인터페이스는 문서 텍스트와 출처 같은 메타데이터를 함께 다루는 흐름을 중심으로 설계돼 있습니다. (LangChain Docs)
2) OpenAIEmbeddings로 임베딩한다
텍스트를 의미 기반 숫자 벡터로 바꿉니다. OpenAI embeddings 통합 문서는 OpenAIEmbeddings를 사용해 텍스트를 벡터화하는 흐름을 안내합니다. (LangChain Docs)
3) InMemoryVectorStore에 저장한다
지금은 학습용이라 인메모리로 충분합니다.
작고 이해하기 쉬워서 좋아요. OpenAI embeddings 문서 예시도 in-memory vector store를 함께 사용합니다. (LangChain Docs)
4) as_retriever()로 retriever를 만든다
벡터스토어를 “문서를 찾아오는 도구”처럼 사용하게 만듭니다.
retriever는 비정형 쿼리를 받아 관련 문서를 반환하는 인터페이스입니다. (LangChain Docs)
5) 질문으로 관련 문서를 찾는다
retriever.invoke(question)
여기서 질문과 의미가 가까운 문서 2개를 가져옵니다.
6) 가져온 문서를 prompt에 넣는다
이제 모델은 질문만 보지 않고,
관련 문서까지 같이 봅니다.
7) 문서 근거 기반 답을 만든다
이제야 비로소 generation이 붙습니다.
5. retriever와 model은 왜 분리하는가
이건 되게 중요합니다.
처음엔 이런 생각이 들 수 있어요.
“그냥 모델한테 물어보면 안 되나?”
물론 물어볼 수는 있어요.
근데 그건 내 문서를 기준으로 답하는 게 아니라,
모델이 원래 알고 있던 일반 지식으로 답하는 거예요.
RAG에서는 역할이 분리됩니다.
- retriever: 어떤 문서를 보여줄지 결정
- model: 보여준 문서를 바탕으로 답변 생성
이 분리가 중요한 이유는, 문제를 따로 볼 수 있기 때문입니다.
예를 들어 답이 이상할 때:
- 문서를 못 찾은 건지
- 문서는 잘 찾았는데 생성이 이상한 건지
이걸 나눠서 볼 수 있어요.
LangChain의 retrieval 문서도 retriever를 문서 반환 인터페이스로 따로 설명하고, RAG 구조에서 retrieval과 generation을 구분합니다. (LangChain Docs)
6. “문서에 없는 내용은 모른다고 하라”가 왜 중요할까
이건 진짜 RAG에서 자주 놓치는 부분입니다.
많은 분이 retriever만 붙이면 자동으로 근거 기반 답변이 될 거라고 생각해요.
그런데 모델은 여전히 자기 원래 지식도 갖고 있습니다.
그래서 검색 문서를 넣었다고 해도,
프롬프트가 느슨하면 문서 바깥의 내용을 섞어서 말할 수 있어요.
그래서 시스템 메시지에서 이런 제약을 주는 게 좋습니다.
- 제공된 문서에 근거해서만 답해라
- 문서에 없는 내용은 추측하지 마라
- 확실하지 않으면 모른다고 말해라
LangChain의 기본 RAG 가이드도 문서 기반 Q&A에서 검색된 문맥을 바탕으로 답하게 만드는 흐름을 보여주고, 검색 문맥이 핵심 역할을 한다고 설명합니다. (LangChain Docs)
이건 생각보다 차이가 큽니다.
같은 retriever를 써도 프롬프트를 어떻게 쓰느냐에 따라 답변 태도가 꽤 달라져요.
7. 왜 k=2 같은 값이 중요할까
코드에서 이 부분이 있었죠.
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
여기서 k는 몇 개의 문서를 가져올지입니다.
너무 적으면:
- 중요한 근거를 놓칠 수 있어요
너무 많으면:
- 관련 없는 문서까지 섞여서 노이즈가 됩니다
- 프롬프트가 길어져 비용과 지연이 커집니다
그래서 k는 꽤 중요한 튜닝 포인트예요.
LangChain 벡터스토어/리트리버 가이드는 retriever를 검색 파라미터와 함께 설정할 수 있다고 설명합니다. (LangChain Docs)
처음엔 보통 2~4개 정도로 시작해보는 게 무난합니다.
문서 길이와 질문 난이도에 따라 달라져요.
8. 이 구조가 이미 “첫 번째 RAG”인 이유
간혹 이렇게 묻는 분이 있습니다.
“벡터DB도 따로 없고, 체인도 단순한데 이게 정말 RAG인가요?”
네, 맞습니다.
RAG의 본질은 특정 제품 이름이 아니라 구조예요.
- 검색한다
- 검색 결과를 생성에 넣는다
- 근거 기반 답을 만든다
이 세 가지가 있으면 기본형 RAG라고 봐도 됩니다.
LangChain의 RAG 및 retrieval 튜토리얼도 복잡한 에이전트형 RAG 이전에, 문서를 검색해서 질문응답에 활용하는 가장 기본적인 형태부터 설명합니다. (LangChain Docs)
물론 나중에는 더 복잡해질 수 있습니다.
- reranker
- query rewriting
- self-query retriever
- hybrid search
- agentic RAG
- citation formatting
- evaluation
하지만 지금은 출발점이 더 중요합니다.
9. 실무에서 바로 보이는 한계
이 기본형은 정말 좋지만, 한계도 빠르게 드러납니다.
1) 문서 전처리가 약하다
지금은 그냥 짧은 문서를 넣었어요.
실제론 문서 로딩, chunking, metadata 설계가 더 중요합니다. knowledge-base 가이드는 documents, splitters, embeddings, vector stores, retrievers를 모두 핵심 개념으로 다룹니다. (LangChain Docs)
2) 검색 품질이 완벽하지 않다
embedding model, chunk size, overlap, k값에 따라 결과가 달라집니다. (LangChain Docs)
3) 출처 인용이 아직 단순하다
지금은 metadata를 문자열로 붙였을 뿐입니다.
나중엔 더 깔끔한 citation 포맷이 필요할 수 있어요.
4) 답변 평가가 없다
“좋아 보이는 답”과 “정말 정확한 답”은 다를 수 있습니다.
이건 뒤에서 관측과 평가 편에서 다루면 좋습니다.
그래도 괜찮습니다.
첫 번째 RAG는 원래 조금 투박해도 돼요.
지금 중요한 건 구조를 눈으로 보는 겁니다.
10. retriever를 체인 안으로 더 자연스럽게 넣고 싶다면
지금 코드는 retrieval과 generation을 약간 나눠서 썼습니다.
retrieved_docs = retriever.invoke(question)
context = format_docs(retrieved_docs)
answer = chain.invoke({"context": context, "question": question})
이 방식이 오히려 처음엔 더 좋아요.
중간 결과를 눈으로 확인할 수 있으니까요.
- 어떤 문서를 찾았는지
- 왜 답이 그렇게 나왔는지
- 검색이 잘못됐는지 생성이 잘못됐는지
이게 훨씬 잘 보입니다.
나중에는 LCEL 스타일로 더 자연스럽게 연결할 수도 있어요.
하지만 처음엔 지금처럼 검색 결과를 직접 출력해보는 방식이 학습에 훨씬 좋습니다.
11. 직접 실행하면서 꼭 해봐야 할 질문들
이건 진짜 추천합니다.
문서를 그대로 두고 질문만 바꿔보세요.
예를 들면:
- “RAG가 뭐야?”
- “문서 검색으로 답변 보강하는 방식은?”
- “LangChain에서 외부 문서 기반 Q&A는 어떻게 해?”
- “프롬프트만으로 안 되는 이유는 뭐야?”
이렇게 비슷한 질문을 여러 방식으로 던져보면,
retriever가 semantic search를 어느 정도 잘하는지 감이 옵니다.
그리고 문서도 조금 바꿔보세요.
- 같은 뜻을 다른 표현으로 적어보기
- metadata 늘려보기
- 일부 문서를 노이즈처럼 넣어보기
이런 테스트가 꽤 중요합니다.
RAG는 체인 조립보다 결국 검색 품질에서 많이 갈리거든요.
12. 이 글에서 꼭 가져가야 할 한 문장
오늘 내용을 한 줄로 줄이면 이겁니다.
RAG의 첫걸음은 “모델을 더 똑똑하게 만드는 것”이 아니라, 질문할 때 필요한 문서를 먼저 찾아서 같이 보여주는 것”이다.
이 감각이 잡히면,
다음부터는 훨씬 자연스럽습니다.
- 문서를 더 잘 나누기
- 더 좋은 벡터스토어 쓰기
- retriever 고도화
- 답변 품질 튜닝
- 평가와 관측 붙이기
전부 결국 이 기본형 위에 올라갑니다.
마무리
처음 RAG를 만들면 좀 묘해요.
되게 엄청난 걸 한 것 같기도 하고, 한편으로는 “어? 생각보다 단순한데?” 싶기도 하거든요.
저는 그 두 감정이 다 맞다고 생각해요.
구조 자체는 생각보다 단순합니다.
검색하고, 넣고, 답하게 하면 되니까요.
근데 그 단순한 구조 위에서
문서 전처리, 검색 품질, 프롬프트 제약, 출처 표시, 비용 관리, 평가가 다 갈립니다.
그래서 쉬워 보이지만 깊어지는 거예요.
그리고 그게 또 재밌습니다.
적어도 저는 그랬어요.
다음 글 예고
다음 글에서는
RAG 품질이 안 나오는 이유 — chunk 크기, overlap, k값, 프롬프트 문제를 어떻게 봐야 하나
로 이어가겠습니다.
여기서부터는 “만들었다”에서 끝나지 않고,
“왜 어떤 질문엔 잘하고 어떤 질문엔 못하는지”를 진짜 개발자 관점에서 보기 시작할 거예요.
출처
- LangChain Retrieval 문서: retriever는 비정형 쿼리를 받아 관련 문서를 반환하는 인터페이스이며, RAG 아키텍처의 핵심 구성요소로 설명됩니다. (LangChain Docs)
- LangChain RAG 튜토리얼: 비정형 텍스트 소스 위에 simple Q&A 애플리케이션을 만드는 기본 RAG 흐름을 설명합니다. (LangChain Docs)
- LangChain OpenAIEmbeddings 통합 문서: OpenAIEmbeddings, InMemoryVectorStore, as_retriever()를 사용하는 예시를 제공합니다. (LangChain Docs)
- LangChain Retriever integrations 문서: retriever는 벡터스토어보다 더 일반적인 개념이며, 문자열 쿼리를 받아 Document 리스트를 반환한다고 설명합니다. (LangChain Docs)
- LangChain Elasticsearch vector store 문서: as_retriever(search_kwargs=...) 형태로 검색 파라미터를 설정할 수 있는 예시를 보여줍니다. (LangChain Docs)
- LangChain Knowledge Base 튜토리얼: documents, splitters, embeddings, vector stores, retrievers가 retrieval 기반 지식 베이스의 핵심 개념이라고 설명합니다. (LangChain Docs)
LangChain, RAG, Retriever, OpenAIEmbeddings, Vector Store, 문서검색, 생성형AI, LLM개발, AI챗봇, 주니어개발자
'study > langchain' 카테고리의 다른 글
| Tool Calling이 진짜 중요한 이유 — LLM이 답변만 하는 걸 넘어서 외부 기능을 쓰게 만드는 방법 (0) | 2026.04.10 |
|---|---|
| RAG 품질이 안 나오는 이유 — chunk 크기, overlap, k값, 프롬프트 문제를 어떻게 봐야 하나 (0) | 2026.04.08 |
| 임베딩과 벡터스토어를 처음 이해하는 글 — RAG를 배우기 전에 꼭 알아야 할 검색의 언어 (0) | 2026.04.03 |
| LangChain으로 문서 하나 읽고 답하는 AI 만들기 — RAG 전에 꼭 알아야 할 가장 작은 문서 Q&A (0) | 2026.03.31 |
| LangChain 대화 히스토리 다루기 — 긴 대화를 자르고, 요약하고, 비용을 관리하는 방법 (0) | 2026.03.30 |
- Total
- Today
- Yesterday
- Next.js
- DevOps
- llm
- Prisma
- SEO최적화
- 딥러닝
- ai철학
- NestJS
- Express
- nextJS
- 개발블로그
- Redis
- LangChain
- node.js
- seo 최적화 10개
- fastapi
- 웹개발
- 쿠버네티스
- rag
- JWT
- 생성형AI
- CI/CD
- Python
- 백엔드개발
- PostgreSQL
- Docker
- kotlin
- REACT
- JAX
- flax
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |

