Aditya
Deploy RAG benchmark dashboard
af383cf
Raw
History Blame Contribute Delete
5.75 kB
"""
Naive RAG System β€” Basic Retrieval-Augmented Generation
This is the simplest form of RAG (Retrieval-Augmented Generation).
It adds one key improvement over the Base LLM: before answering, it
searches a knowledge base to find relevant text chunks, then hands
those chunks to the LLM as context.
How it works:
Step 1 β€” Embed the question: Convert the question into a vector (list of numbers)
that captures its meaning.
Step 2 β€” FAISS search: Find the chunks in the knowledge base whose vectors
are closest to the question vector (semantic similarity).
Step 3 β€” Generate: Give the top-k chunks + the question to the LLM and ask
it to answer using ONLY what's in those chunks.
Why is this "naive"?
- It only uses one search method (vector similarity)
- It searches with the raw question as-is (no rephrasing)
- It uses all retrieved chunks equally (no reranking by relevance)
Later systems fix these limitations one by one.
This is the "entry-level RAG" baseline. Every advanced system should beat it.
"""
import asyncio
import logging
import time
from langchain_groq import ChatGroq
from langchain_core.prompts import ChatPromptTemplate
from eval_framework.config import get_settings
from eval_framework.systems.shared import SharedIndex
from eval_framework.types import QAPair, SystemOutput
logger = logging.getLogger(__name__)
# Rough cost per output token for Groq-hosted Llama models
_COST_PER_OUTPUT_TOKEN = 0.59 / 1_000_000
class NaiveRAGSystem:
"""
Standard RAG: embed question -> FAISS similarity search -> LLM generation.
The starting point that every advanced technique builds on top of.
Should score significantly better than BaseLLM on faithfulness and
hallucination rate, since the LLM is grounded by real retrieved context.
"""
def __init__(
self,
index: SharedIndex,
top_k: int = 3,
model_name: str = "llama-3.3-70b-versatile",
):
"""
Args:
index: The shared FAISS + BM25 index (built once, reused here).
top_k: How many chunks to retrieve and show to the LLM.
3 is a good balance β€” enough context, not too noisy.
model_name: Groq model for answer generation.
"""
self._index = index
self.top_k = top_k
self.model_name = model_name
settings = get_settings()
# LLM for generating answers β€” low temperature for factual consistency
self._llm = ChatGroq(
api_key=settings.groq_api_key,
model_name=model_name,
temperature=0.1,
max_tokens=512,
)
# Set up FAISS as a LangChain retriever.
# "similarity" means: embed the query and find the nearest chunks by cosine distance.
self._retriever = index.vectorstore.as_retriever(
search_type="similarity",
search_kwargs={"k": top_k},
)
# Prompt instructs the LLM to ONLY use the provided context.
# This "grounding" instruction is what reduces hallucinations in RAG systems.
self._prompt = ChatPromptTemplate.from_template(
"You are a precise question-answering assistant. "
"Answer the question using ONLY the information in the context below. "
"If the context does not contain enough information, say: "
"'The document does not contain enough information to answer this question.'\n\n"
"Context:\n{context}\n\n"
"Question: {question}\n\n"
"Answer:"
)
async def query(self, qa_pair: QAPair) -> SystemOutput:
"""
Retrieve relevant chunks with FAISS, then generate a grounded answer.
Args:
qa_pair: Contains the question to answer.
Returns:
SystemOutput with the answer, retrieved context, timing, and cost.
"""
start = time.time()
# FAISS similarity search is a synchronous (blocking) operation.
# We run it in a thread executor so it doesn't block the async event loop
# while other questions are being processed concurrently.
source_docs = await asyncio.get_event_loop().run_in_executor(
None, lambda: self._retriever.invoke(qa_pair.question)
)
# Join the retrieved chunks into one context block.
# The "---" separator helps the LLM see where each chunk starts and ends.
context = "\n\n---\n\n".join(doc.page_content for doc in source_docs)
# Send context + question to the LLM for answer generation
messages = await self._prompt.ainvoke({"context": context, "question": qa_pair.question})
response = await self._llm.ainvoke(messages)
answer = response.content
latency_ms = (time.time() - start) * 1000
# Store context on qa_pair so evaluators (faithfulness, etc.) can use it
if context:
qa_pair.context = context
# Rough cost estimate: word count * 1.3 approximates token count
output_tokens = len(answer.split()) * 1.3
estimated_cost = output_tokens * _COST_PER_OUTPUT_TOKEN
logger.info(f"NaiveRAG answered in {latency_ms:.0f}ms | {len(source_docs)} chunks")
return SystemOutput(
answer=answer,
latency_ms=latency_ms,
cost_usd=estimated_cost,
model=self.model_name,
metadata={
"system": "naive_rag",
"chunks_retrieved": len(source_docs),
# Store first 100 chars of each chunk for debugging
"source_snippets": [doc.page_content[:100] for doc in source_docs],
},
)