Haystack 첫 RAG 파이프라인
Haystack 첫 RAG 파이프라인 (End-to-End RAG)
Haystack로 검색 증강 생성(RAG) 파이프라인을 처음부터 만들어 볼게요. 문서를 색인한 뒤, 질문이 들어오면 관련 문서를 찾아서 LLM이 그 문서를 근거로 답하도록 연결하는 구조예요. 실제 코드를 따라가다 보면 Haystack 파이프라인이 어떻게 흘러가는지 한눈에 보일 거예요.
출처: 공식문서
파이프라인 구성 요소
이 튜토리얼은 네 가지 핵심 컴포넌트를 조합해요.
SentenceTransformersTextEmbedder: 사용자 질문을 벡터(임베딩)로 바꿔요.InMemoryEmbeddingRetriever: 그 벡터를 이용해 문서 스토어에서 관련 문서를 가져와요.ChatPromptBuilder: 검색 결과와 질문을 담을 프롬프트 템플릿을 만들어요.ChatGenerator: 최종 답변을 생성해요. LLM은 클라우드 API(OpenAI, Anthropic, Mistral 등)를 쓸 수도 있고, Ollama나 vLLM으로 로컬에서 실행할 수도 있어요.
RAG용으로 사용한 문서는 인메모리 문서 스토어(InMemoryDocumentStore)에 저장하고, 임베딩은 SentenceTransformersDocumentEmbedder로 만들어 넣어요.
설치
pip install haystack-ai mistral-haystack transformers-haystack "datasets>=2.6.1" sentence-transformers-haystack
문서 저장소 초기화
from haystack.document_stores.in_memory import InMemoryDocumentStore
document_store = InMemoryDocumentStore()
InMemoryDocumentStore는 별도 의존성 없이 바로 시작할 수 있는 가장 단순한 문서 저장소예요. 작은 프로젝트나 디버깅에는 좋지만 큰 문서 컬렉션에는 확장이 어려워서, 운영 시스템에는 외부 데이터베이스를 쓰는 편이 좋아요.
데이터 가져오고 색인하기
예제로는 고대 세계 7대 불가사의 위키피디아 페이지를 Hugging Face 데이터셋(bilgeyucel/seven-wonders)에서 받아와서 Haystack Document로 바꿔요.
from datasets import load_dataset
from haystack import Document
dataset = load_dataset("bilgeyucel/seven-wonders", split="train")
docs = [Document(content=doc["content"], meta=doc["meta"]) for doc in dataset]
문서 임베더를 all-MiniLM-L6-v2 모델로 초기화하고, 문서마다 임베딩을 만들어 문서 스토어에 써요.
from haystack_integrations.components.embedders.sentence_transformers import SentenceTransformersDocumentEmbedder
doc_embedder = SentenceTransformersDocumentEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")
docs_with_embeddings = doc_embedder.run(docs)
document_store.write_documents(docs_with_embeddings["documents"])
RAG 파이프라인 만들기
질문 임베더와 리트리버를 초기화해요. 문서를 만들 때 쓴 것과 같은 임베딩 모델을 질문에도 써야 해요. 그래야 벡터 공간이 일치해서 검색이 잘 돼요.
from haystack_integrations.components.embedders.sentence_transformers import SentenceTransformersTextEmbedder
from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
text_embedder = SentenceTransformersTextEmbedder(model="sentence-transformers/all-MiniLM-L6-v2")
retriever = InMemoryEmbeddingRetriever(document_store)
다음으로 프롬프트 템플릿을 만들어요. Jinja2 루프 문법으로 검색된 문서 여러 개의 내용을 프롬프트에 펼쳐 넣는 게 핵심이에요.
from haystack.components.builders import ChatPromptBuilder
from haystack.dataclasses import ChatMessage
template = [
ChatMessage.from_user(
"""
Given the following information, answer the question.
Context:
{% for document in documents %}
{{ document.content }}
{% endfor %}
Question: {{question}}
Answer:
"""
)
]
prompt_builder = ChatPromptBuilder(template=template, required_variables="*")
required_variables="*"를 주면 모든 프롬프트 변수가 필수로 검증돼요. 기본값으로는 모든 변수가 선택 취급된다는 점만 기억해 두세요.
LLM(챗 생성기)은 환경에 맞는 것으로 하나만 골라 초기화해요. 클라우드 API 방식과 로컬 모델 방식이 있어요.
# OpenAI (API 키 필요)
import os
from getpass import getpass
from haystack.components.generators.chat import OpenAIChatGenerator
if "OPENAI_API_KEY" not in os.environ:
os.environ["OPENAI_API_KEY"] = getpass("Enter OpenAI API key:")
chat_generator = OpenAIChatGenerator(model="gpt-4o-mini")
# Hugging Face 오픈웨이트 모델 (로컬 추론, API 키 불필요)
from haystack_integrations.components.generators.transformers import TransformersChatGenerator
chat_generator = TransformersChatGenerator(model="Qwen/Qwen3-4B-Instruct-2507")
이제 컴포넌트를 파이프라인에 더하고 연결해요. text_embedder.embedding → retriever.query_embedding, retriever → prompt_builder, prompt_builder.prompt → llm.messages로 이어요.
from haystack import Pipeline
basic_rag_pipeline = Pipeline()
basic_rag_pipeline.add_component("text_embedder", text_embedder)
basic_rag_pipeline.add_component("retriever", retriever)
basic_rag_pipeline.add_component("prompt_builder", prompt_builder)
basic_rag_pipeline.add_component("llm", chat_generator)
basic_rag_pipeline.connect("text_embedder.embedding", "retriever.query_embedding")
basic_rag_pipeline.connect("retriever", "prompt_builder")
basic_rag_pipeline.connect("prompt_builder.prompt", "llm.messages")
질문하기
파이프라인을 실행할 때는 run()에 질문을 text_embedder와 prompt_builder 양쪽에 넘겨줘야 해요. 그래야 템플릿의 {{question}} 자리가 실제 질문으로 채워져요.
question = "What does Rhodes Statue look like?"
response = basic_rag_pipeline.run({"text_embedder": {"text": question}, "prompt_builder": {"question": question}})
print(response["llm"]["replies"][0].text)
만약 모델을 Colab 런타임에 로컬로 띄웠다면 첫 실행이 느릴 수 있어요. LLM을 로드하고 추론 준비를 하는 시간이 필요하기 때문이에요.