Γιατί το RAG Είναι η Πιο Σημαντική Τεχνική στο AI Σήμερα
Αν δουλεύεις με μεγάλα γλωσσικά μοντέλα (LLMs), πιθανότατα έχεις ήδη ζήσει αυτό: ρωτάς κάτι, παίρνεις μια σίγουρη απάντηση — και είναι εντελώς λάθος. Τα λεγόμενα hallucinations (ψευδαισθήσεις) δεν είναι bug, είναι χαρακτηριστικό του πώς δουλεύουν τα LLMs. Και ειλικρινά, είναι ένα πρόβλημα που δεν λύνεται εύκολα.
Εκτός αν χρησιμοποιήσεις RAG.
Το Retrieval-Augmented Generation ανακτά σχετικά δεδομένα από εξωτερικές πηγές πριν απαντήσει το LLM. Δηλαδή, αντί να "φαντάζεται" μια απάντηση, βασίζεται σε πραγματικά έγγραφα. Η αγορά RAG εκτιμάται ότι θα φτάσει τα 11 δισ. δολάρια μέχρι το 2030, με ετήσιο ρυθμό ανάπτυξης 49,1% — κάτι που δεν πρέπει να σε εκπλήσσει αν σκεφτείς πόσο ευκολότερο είναι από το fine-tuning.
Σε αυτόν τον οδηγό θα φτιάξουμε ένα πλήρες RAG pipeline με Python βήμα-βήμα. Στη συνέχεια θα εφαρμόσουμε τεχνικές βελτιστοποίησης που (στην πράξη) μειώνουν το latency κατά 70% και αυξάνουν την ακρίβεια κατά 20%+. Λοιπόν, ας μπούμε στο ψητό.
Αρχιτεκτονική ενός RAG Pipeline: Τα 5 Βασικά Στάδια
Κάθε RAG pipeline αποτελείται από πέντε στάδια που δουλεύουν σαν αλυσίδα. Αν ένα κρίκο αστοχήσει, πέφτει η ποιότητα σε όλο το pipeline:
- Εισαγωγή Εγγράφων (Document Ingestion) — Φόρτωση αρχείων PDF, Markdown, HTML ή κειμένου.
- Τεμαχισμός Κειμένου (Chunking) — Σπάσιμο σε μικρότερα κομμάτια που χωράνε στο context window του LLM.
- Δημιουργία Embeddings — Μετατροπή κάθε chunk σε διανυσματική αναπαράσταση.
- Αποθήκευση σε Vector Store — Αποθήκευση σε μια βάση δεδομένων για γρήγορη αναζήτηση ομοιότητας.
- Ανάκτηση και Παραγωγή (Retrieval & Generation) — Εύρεση σχετικών chunks και δημιουργία απάντησης.
Ας τα υλοποιήσουμε ένα-ένα με πραγματικό κώδικα.
Βήμα 1: Εγκατάσταση Περιβάλλοντος
Ξεκινάμε με ένα εικονικό περιβάλλον Python και τις απαραίτητες βιβλιοθήκες. Χρησιμοποιούμε το LangChain 1.2 που φέρνει ενοποιημένο Runnable interface, μαζί με FAISS για τοπικό vector store.
python -m venv rag-env
source rag-env/bin/activate
pip install langchain==1.2.10 langchain-openai langchain-community
pip install faiss-cpu pypdf sentence-transformers
pip install ragas # Για αξιολόγηση
Η δομή του project μοιάζει κάπως έτσι:
rag-project/
├── data/ # Τα έγγραφά μας
├── vectorstore/ # Αποθήκευση FAISS index
├── src/
│ ├── ingest.py # Εισαγωγή εγγράφων
│ ├── retriever.py # Λογική ανάκτησης
│ ├── pipeline.py # Κεντρικό RAG pipeline
│ └── evaluate.py # Αξιολόγηση
└── .env # API keys
Βήμα 2: Εισαγωγή και Τεμαχισμός Εγγράφων
Εδώ κρύβεται μια παγίδα στην οποία πέφτουν σχεδόν όλοι: η ποιότητα του chunking επηρεάζει δραματικά τα αποτελέσματα. Πολύ μικρά chunks χάνουν πλαίσιο, πολύ μεγάλα αραιώνουν τη σχετική πληροφορία.
Η γενική σύσταση; 256 με 512 tokens — αλλά θα μιλήσουμε περισσότερο γι' αυτό παρακάτω στο FAQ.
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Φόρτωση εγγράφων από φάκελο
loader = DirectoryLoader(
"data/",
glob="**/*.pdf",
loader_cls=PyPDFLoader,
show_progress=True
)
documents = loader.load()
# Στρατηγικός τεμαχισμός με overlap
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
length_function=len,
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = text_splitter.split_documents(documents)
print(f"Δημιουργήθηκαν {len(chunks)} chunks από {len(documents)} έγγραφα")
Γιατί RecursiveCharacterTextSplitter;
Ο RecursiveCharacterTextSplitter δοκιμάζει να χωρίσει πρώτα στα μεγαλύτερα σημεία διαχωρισμού (διπλές αλλαγές γραμμής) και σταδιακά πάει σε μικρότερα. Το αποτέλεσμα: μια παράγραφος δεν κόβεται στη μέση, εκτός αν υπερβαίνει τα 512 tokens. Αυτό κάνει τεράστια διαφορά στην ποιότητα ανάκτησης.
Βήμα 3: Επιλογή Embedding Model
Η επιλογή embedding model είναι κρίσιμη — και δυστυχώς δεν υπάρχει "one size fits all". Σύμφωνα με τα benchmarks MTEB του 2026, οι τρεις πιο δημοφιλείς επιλογές είναι:
- OpenAI text-embedding-3-large — Κορυφαία ακρίβεια (MTEB: 64.6), managed API, κόστος $0.13/1M tokens
- BGE-M3 — Εξαιρετικό open-source, πολύγλωσσο (100+ γλώσσες), MTEB: 63.0, δωρεάν self-hosted
- all-MiniLM-L6-v2 — Ταχύτατο και ελαφρύ, ιδανικό για prototyping, latency κάτω από 30ms
Για production με ελληνικό περιεχόμενο, προσωπικά θα πήγαινα με BGE-M3. Η πολυγλωσσική υποστήριξη είναι εντυπωσιακή και δεν πληρώνεις API calls:
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
# Πολυγλωσσικό BGE-M3 - ιδανικό για ελληνικά
embeddings = HuggingFaceBgeEmbeddings(
model_name="BAAI/bge-m3",
model_kwargs={"device": "cpu"},
encode_kwargs={"normalize_embeddings": True}
)
# Εναλλακτικά με OpenAI
# from langchain_openai import OpenAIEmbeddings
# embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
Βήμα 4: Αποθήκευση σε Vector Store με FAISS
Το FAISS (Facebook AI Similarity Search) είναι μια από τις ταχύτερες βιβλιοθήκες Approximate Nearest Neighbor. Για μεγάλη κλίμακα σε production, θα ήθελες κάτι σαν Pinecone, Qdrant ή pgvector. Αλλά για τοπική ανάπτυξη και prototyping, το FAISS κάνει μια χαρά τη δουλειά.
from langchain_community.vectorstores import FAISS
# Δημιουργία FAISS index από τα chunks
vectorstore = FAISS.from_documents(
documents=chunks,
embedding=embeddings
)
# Αποθήκευση στο δίσκο για επαναχρησιμοποίηση
vectorstore.save_local("vectorstore/faiss_index")
# Φόρτωση αποθηκευμένου index
# vectorstore = FAISS.load_local(
# "vectorstore/faiss_index",
# embeddings,
# allow_dangerous_deserialization=True
# )
Βήμα 5: Κατασκευή του RAG Pipeline
Τώρα ήρθε η ώρα να συνδέσουμε τα πάντα. Χρησιμοποιούμε το LCEL (LangChain Expression Language) με τον pipe operator (|) — μια δηλωτική σύνταξη που κάνει τον κώδικα πολύ πιο ευανάγνωστο.
Ομολογώ ότι στην αρχή ήμουν σκεπτικός με το LCEL, αλλά μόλις το δοκιμάσεις σε πραγματικό project, καταλαβαίνεις γιατί αξίζει.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Ρύθμιση LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Prompt template με σαφείς οδηγίες
prompt = ChatPromptTemplate.from_template("""
Είσαι ένας εξειδικευμένος βοηθός. Απάντησε στην ερώτηση βασιζόμενος
ΑΠΟΚΛΕΙΣΤΙΚΑ στο παρακάτω πλαίσιο. Αν δεν βρίσκεις την απάντηση
στο πλαίσιο, πες ότι δεν γνωρίζεις.
Πλαίσιο:
{context}
Ερώτηση: {question}
Απάντηση:
""")
# Δημιουργία retriever
retriever = vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": 5}
)
# Βοηθητική συνάρτηση μορφοποίησης
def format_docs(docs):
return "\n\n---\n\n".join(doc.page_content for doc in docs)
# LCEL RAG Chain
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough()
}
| prompt
| llm
| StrOutputParser()
)
# Χρήση
response = rag_chain.invoke("Ποια είναι τα πλεονεκτήματα του RAG;")
print(response)
Τεχνικές Βελτιστοποίησης: Από Βασικό σε Production
Ωραία, έχουμε ένα RAG pipeline που δουλεύει. Αλλά ας είμαστε ρεαλιστές — σε production θα αντιμετωπίσεις τρία κρίσιμα ζητήματα: ακρίβεια, ταχύτητα και κόστος. Ας δούμε πώς τα λύνουμε.
1. Hybrid Search: BM25 + Vector Search
Η σημασιολογική αναζήτηση πιάνει εννοιολογικά σχετικά κείμενα, αλλά μπορεί να χάσει ακριβείς όρους — κωδικούς προϊόντων, τεχνικά ακρωνύμια, ονόματα. Ο BM25 (κλασική keyword search) πιάνει αυτά, αλλά χάνει τα παραπλήσια.
Ο συνδυασμός τους; Βελτιώνει το recall κατά 1-9% σύμφωνα με πρόσφατες μελέτες. Δεν είναι τρομερό νούμερο, αλλά στην πράξη αυτό το 1-9% μπορεί να σημαίνει τη διαφορά ανάμεσα σε σωστή και λάθος απάντηση.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# BM25 retriever (keyword-based)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# Vector retriever (semantic)
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": 10}
)
# Συνδυασμός με Reciprocal Rank Fusion
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6] # Βάρος σε κάθε retriever
)
2. Reranking: Δεύτερο Πέρασμα για Μέγιστη Ακρίβεια
Εδώ είναι που γίνεται η μαγεία. Το reranking χρησιμοποιεί ένα cross-encoder μοντέλο που βαθμολογεί κάθε ζεύγος (ερώτηση, chunk) ξεχωριστά. Σε αντίθεση με τα bi-encoder embeddings, ο cross-encoder βλέπει και τα δύο κείμενα ταυτόχρονα — κι αυτό του δίνει πολύ καλύτερη κρίση.
Η Anthropic αναφέρει μείωση 67% στις αποτυχημένες ανακτήσεις με reranking. Αυτό δεν είναι μικρό νούμερο.
from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
# Cross-encoder model για reranking
cross_encoder = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3"
)
reranker = CrossEncoderReranker(
model=cross_encoder,
top_n=5 # Κράτα τα 5 καλύτερα
)
# Συνδυασμός: πρώτα hybrid retrieval, μετά reranking
compression_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid_retriever
)
3. Semantic Caching: Μείωση Latency κατά 70%
Κάτι που πολλοί παραβλέπουν: στα production συστήματα, πολλές ερωτήσεις είναι σημασιολογικά παρόμοιες. Γιατί να τρέχεις ολόκληρο το pipeline κάθε φορά;
Με semantic caching, ερωτήσεις όπως «Τι είναι το RAG;» και «Εξήγησέ μου το RAG» επιστρέφουν cached αποτέλεσμα. Και τα νούμερα μιλάνε μόνα τους: εξοικονόμηση έως 68,8% στα κόστη LLM σε τυπικά production workloads.
from langchain_community.cache import InMemoryCache
from langchain.globals import set_llm_cache
import hashlib
# Βασικό in-memory cache
set_llm_cache(InMemoryCache())
# Για production: semantic cache με Redis
# from langchain_community.cache import RedisSemanticCache
# set_llm_cache(RedisSemanticCache(
# redis_url="redis://localhost:6379",
# embedding=embeddings,
# score_threshold=0.95
# ))
4. Query Transformation: Βελτίωση Εισερχόμενης Ερώτησης
Ας είμαστε ειλικρινείς: οι χρήστες σπάνια γράφουν τέλεια διατυπωμένες ερωτήσεις. Κάποιος μπορεί να ρωτήσει "πώς κάνω πιο γρήγορο το AI μου;" ενώ εννοεί κάτι πολύ πιο συγκεκριμένο.
Η μετατροπή του query πριν την ανάκτηση βελτιώνει δραματικά τα αποτελέσματα. Δύο βασικές τεχνικές:
- Query Rewriting — Αναδιατύπωση με domain-specific ορολογία
- Query Decomposition — Σπάσιμο σύνθετων ερωτήσεων σε υπο-ερωτήσεις
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# Query rewriting chain
rewrite_prompt = ChatPromptTemplate.from_template("""
Αναδιατύπωσε την παρακάτω ερώτηση ώστε να είναι πιο ακριβής
και τεχνικά σαφής για αναζήτηση σε τεχνική βάση γνώσεων.
Διατήρησε το αρχικό νόημα.
Αρχική ερώτηση: {question}
Αναδιατυπωμένη ερώτηση:
""")
rewrite_chain = rewrite_prompt | ChatOpenAI(temperature=0) | StrOutputParser()
# Χρήση: αναδιατυπώνεται πριν φτάσει στον retriever
rewritten = rewrite_chain.invoke({
"question": "Πώς κάνω πιο γρήγορο το AI μου;"
})
# Αποτέλεσμα: "Ποιες τεχνικές βελτιστοποίησης latency
# υπάρχουν για RAG pipelines σε production;"
Πλήρες Production Pipeline: Όλα Μαζί
Ήρθε η ώρα να βάλουμε όλα τα κομμάτια στη θέση τους. Ο παρακάτω κώδικας συνδυάζει hybrid search, reranking και LCEL σε μια class που μπορείς να χρησιμοποιήσεις κατευθείαν:
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import EnsembleRetriever
from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
class ProductionRAGPipeline:
"""Production-ready RAG pipeline με hybrid search και reranking."""
def __init__(self, chunks, embeddings):
# Vector store
self.vectorstore = FAISS.from_documents(chunks, embeddings)
# Hybrid retriever
bm25 = BM25Retriever.from_documents(chunks)
bm25.k = 15
vector = self.vectorstore.as_retriever(search_kwargs={"k": 15})
hybrid = EnsembleRetriever(
retrievers=[bm25, vector],
weights=[0.35, 0.65]
)
# Reranker
cross_encoder = HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3"
)
reranker = CrossEncoderReranker(model=cross_encoder, top_n=5)
self.retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=hybrid
)
# LLM και prompt
self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
self.prompt = ChatPromptTemplate.from_template("""
Είσαι εξειδικευμένος βοηθός. Απάντησε ΜΟΝΟ βάσει του πλαισίου.
Αν δεν βρίσκεις απάντηση, πες ότι δεν γνωρίζεις.
Παράθεσε τις πηγές σου.
Πλαίσιο:
{context}
Ερώτηση: {question}
""")
def _format_docs(self, docs):
return "\n\n---\n\n".join(
f"[Πηγή: {d.metadata.get('source', 'Άγνωστη')}]\n{d.page_content}"
for d in docs
)
def query(self, question: str) -> str:
chain = (
{
"context": self.retriever | self._format_docs,
"question": RunnablePassthrough()
}
| self.prompt
| self.llm
| StrOutputParser()
)
return chain.invoke(question)
# Χρήση
pipeline = ProductionRAGPipeline(chunks, embeddings)
answer = pipeline.query("Πώς λειτουργεί το semantic caching;")
print(answer)
Αξιολόγηση RAG Pipeline με RAGAS
Δεν μπορείς να βελτιστοποιήσεις αυτό που δεν μετράς — κλασική αλήθεια που ισχύει και εδώ. Το RAGAS (Retrieval Augmented Generation Assessment) είναι το πιο δημοφιλές framework αξιολόγησης RAG και μετράει τέσσερις μετρικές:
- Faithfulness — Πόσο πιστή είναι η απάντηση στο ανακτημένο πλαίσιο;
- Answer Relevancy — Πόσο σχετική είναι η απάντηση με την ερώτηση;
- Context Precision — Πόσο ακριβές είναι αυτό που ανέκτησε ο retriever;
- Context Recall — Καλύπτει το πλαίσιο όλα τα σχετικά σημεία;
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall
)
from datasets import Dataset
# Δημιουργία evaluation dataset
eval_data = {
"question": ["Τι είναι το RAG;", "Πώς λειτουργεί το reranking;"],
"answer": [answer1, answer2],
"contexts": [contexts1, contexts2],
"ground_truth": [truth1, truth2]
}
dataset = Dataset.from_dict(eval_data)
# Εκτέλεση αξιολόγησης
results = evaluate(
dataset,
metrics=[
faithfulness,
answer_relevancy,
context_precision,
context_recall
]
)
print(results)
# {'faithfulness': 0.92, 'answer_relevancy': 0.89,
# 'context_precision': 0.85, 'context_recall': 0.88}
Στοχεύεις σε βαθμολογίες πάνω από 0.85 σε κάθε μετρική. Αν η faithfulness είναι χαμηλή, κοίτα τα chunks και το prompt. Αν η context precision πέφτει, βελτίωσε τον retriever ή πρόσθεσε reranking.
Πίνακας Επιλογής Εργαλείων
Η σωστή επιλογή εργαλείων εξαρτάται από τη φάση του project. Ρίξε μια ματιά:
| Στάδιο | Prototyping | Production |
|---|---|---|
| Embedding Model | all-MiniLM-L6-v2 (δωρεάν) | BGE-M3 ή OpenAI text-embedding-3-large |
| Vector Store | FAISS (τοπικό) | Pinecone, Qdrant ή pgvector |
| LLM | GPT-4o-mini | GPT-4o ή Claude Sonnet |
| Framework | LangChain LCEL | LangChain + LangGraph |
| Caching | InMemoryCache | Redis Semantic Cache |
| Αξιολόγηση | Χειροκίνητη δοκιμή | RAGAS + LangSmith |
Κοινά Λάθη που Πρέπει να Αποφύγεις
Μετά από αρκετά projects με RAG pipelines, αυτά είναι τα λάθη που βλέπω ξανά και ξανά:
- Chunks χωρίς overlap — Χωρίς overlap, η πληροφορία στα σύνορα δύο chunks απλά χάνεται. Βάλε τουλάχιστον 50-100 tokens overlap. Σοβαρά, μη το παραλείψεις.
- Υπερβολικά μεγάλο k — Ανάκτηση 20+ chunks δεν βοηθά, απλά αυξάνει θόρυβο και κόστος. Πέντε με εφτά reranked chunks δίνουν καλύτερα αποτελέσματα από 20 χωρίς reranking.
- Αγνόηση metadata — Πρόσθεσε metadata (ημερομηνία, πηγή, τύπος εγγράφου) στα chunks. Μπορεί να χρησιμοποιηθεί για pre-filtering πριν την αναζήτηση, κι αυτό κάνει θαύματα.
- Μη αξιολόγηση — Χωρίς μετρικές, βελτιστοποιείς στα τυφλά. Χρησιμοποίησε RAGAS από την αρχή, ακόμα κι αν φαίνεται "υπερβολικό".
- Παραμέληση αξιοπιστίας — Θυμήσου: 0,95 × 0,95 × 0,95 = 0,81 συνολική αξιοπιστία. Κάθε στάδιο του pipeline πολλαπλασιάζει τα σφάλματα — δεν τα προσθέτει.
Συχνές Ερωτήσεις
Χρειάζομαι fine-tuning μαζί με RAG;
Στις περισσότερες περιπτώσεις, όχι. Το RAG δουλεύει εξαιρετικά χωρίς fine-tuning και με πολύ χαμηλότερο κόστος. Fine-tuning χρειάζεται μόνο αν θέλεις να αλλάξεις τη συμπεριφορά του μοντέλου σε βάθος, ή αν χειρίζεσαι τεράστιο όγκο ερωτημάτων όπου η μείωση tokens στο prompt γίνεται θέμα budget.
Ποιο vector store για production;
Εξαρτάται από τον όγκο. Για έως 1 εκατ. vectors, τα Qdrant και pgvector είναι εξαιρετικές open-source λύσεις. Για δισεκατομμύρια vectors (ναι, υπάρχουν τέτοια use cases), managed λύσεις όπως Pinecone ή Weaviate δίνουν P95 latency σε single-digit milliseconds.
Πώς μειώνω τα hallucinations;
Τρεις στρατηγικές: πρώτον, ξεκάθαρο prompt που λέει στο μοντέλο να απαντά ΜΟΝΟ βάσει του πλαισίου. Δεύτερον, reranking ώστε μόνο τα πιο σχετικά chunks να φτάνουν στο LLM. Τρίτον, contextual compression για να αφαιρέσεις θόρυβο. Κανένα από αυτά δεν είναι silver bullet, αλλά μαζί κάνουν μεγάλη διαφορά.
Τι chunk size να χρησιμοποιήσω;
Η γενική σύσταση είναι 256-512 tokens με 50-100 tokens overlap. Αλλά — και αυτό είναι σημαντικό — διαφορετικοί τύποι εγγράφων θέλουν διαφορετικά μεγέθη. Τεχνική τεκμηρίωση ωφελείται από μικρότερα chunks (256), ενώ αφηγηματικά κείμενα από μεγαλύτερα (512). Πειραματίσου και μέτρα με RAGAS.
Πόσο κοστίζει ένα production RAG system;
Το κόστος εξαρτάται κυρίως από τρεις παράγοντες: LLM API calls, embedding generation και vector store hosting. Η καλή είδηση; Με smart routing και semantic caching, μπορείς να εξοικονομήσεις 40-46% σε μηνιαία κόστη. Ένα τυπικό σύστημα με 100K ερωτήματα/μήνα κοστίζει $200-$500 με σωστή βελτιστοποίηση — πολύ πιο λογικό απ' ό,τι περιμένουν οι περισσότεροι.