25.7 Retrievers and VectorStore Integration
Right, so you’ve got your LLM, this brilliant, over-educated parrot that can say anything but knows nothing. It has no memory, no context beyond its last training run. To build something useful, you need to give it access to your data. That’s where retrievers come in. Think of them as the world’s fastest, most literal librarians for your AI. You ask a question, they sprint through the library of your documents, find the most relevant pages, and hand them to the LLM to craft an answer. No more making stuff up (well, less making stuff up).
The heart of this operation is the VectorStore. This is a fancy term for a database that stores data as vectors—numerical representations of meaning, also known as embeddings. When you add a document, it’s chopped up, converted into vectors, and stored. When you query it, your question is also converted into a vector, and the database performs a similarity search to find the vectors (and thus the text chunks) that are mathematically “closest” to your question. It’s finding conceptually similar things, not just keyword-matching like a simple Ctrl+F. Magic.
The Basic Retrieval Chain
Let’s start with the simplest possible workflow. You have some text, you stuff it into a vector store, and you ask it questions. Here’s how you do it with Chroma, one of the simpler persistent vector stores.
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
# Load your document. Let's pretend it's your company's thrilling HR policy.
loader = TextLoader("./hr_policy.txt")
documents = loader.load()
# This is CRITICAL: You can't just shove a 100-page PDF in whole. You have to split it.
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
texts = text_splitter.split_documents(documents)
# Create the vector store. This will generate the embeddings and persist them locally.
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents=texts, embedding=embeddings, persist_directory="./hr_policy_db")
vectorstore.persist() # Don't forget this, or you'll be re-embedding everything on the next run!
# Now, create your retriever. This is the object that queries the vector store.
retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) # Retrieve top 4 most relevant chunks
# Finally, plug it into a chain that handles the LLM part.
qa_chain = RetrievalQA.from_chain_type(
llm=OpenAI(temperature=0), # Temperature 0 for less creativity, more factuality
chain_type="stuff", # The simplest method: just "stuff" all retrieved docs into the prompt
retriever=retriever,
return_source_documents=True # So you can see what the LLM actually used
)
# Ask it something!
result = qa_chain({"query": "How many vacation days do I get in my first year?"})
print(result["result"])
print("\nSource Documents:")
for doc in result["source_documents"]:
print(f"- {doc.page_content[:100]}...")
Why Chunk Size and Overlap Are Your Secret Weapons
I just glossed over those chunk_size and chunk_overlap parameters, but they are arguably the most important knobs to tune. Set the chunk_size too large, and your retrieved documents will be full of irrelevant noise that distracts the LLM. Set it too small, and you lose crucial context. A chunk about “vacation days” that ends right before the sentence “This does not apply to probationary employees” is useless. That’s what chunk_overlap is for. It creates sliding windows of text, ensuring that key concepts that fall on a boundary are preserved across chunks. Start with 1000-1500 for chunk_size and 100-200 for overlap, but be prepared to experiment. Your specific data will dictate the ideal values.
Going Beyond Simple Retrieval: MMR
The default search type is similarity. It gives you the most similar chunks. This is often great, but sometimes it gives you four chunks that all say the exact same thing in slightly different words. This is a waste of tokens and context window. Enter Maximum Marginal Relevance (MMR). MMR balances similarity with diversity. It finds relevant chunks but prioritizes ones that contain new information.
# You configure this when creating the retriever
retriever = vectorstore.as_retriever(
search_type="mmr", # Enable MMR
search_kwargs={"k": 4, "fetch_k": 10} # Return 4 diverse results, but consider the top 10 similar ones first
)
Use MMR when you’re querying a large knowledge base where you suspect there might be redundant information. It’s a bit more expensive (fetch_k must be > k) but often worth it for the quality boost.
The Pitfalls: Where This All Goes Pear-Shaped
This isn’t magic, it’s math, and sometimes the math is dumb.
- The False Positive: The retriever will find chunks that are semantically similar but factually irrelevant. Your query about “server downtime” might retrieve a chunk about “waiting tables at a downtown server job” if the word “server” is prominent enough. Your embeddings model isn’t perfect. Always, and I mean always, use
return_source_documents=Trueto audit what the LLM is actually seeing. - The Out-of-Context Problem: The retriever fetches chunks without any understanding of the broader document structure. It might pull a sentence that says “this policy is deprecated” without the next sentence that says “and replaced with the new policy below.” You are responsible for providing enough context in the chunk itself through smart splitting.
- The Curse of the Empty Result: What happens if the retriever finds nothing? Your LLM will be asked to answer a question with zero context. You need to handle this edge case. Check the length of
result['source_documents']and have a fallback response. “I couldn’t find any relevant information in the knowledge base” is infinitely better than the LLM confidently hallucinating an answer.
The key takeaway? The retriever is your first and most important line of defense against LLM hallucination. A good retriever with well-prepared data does 80% of the work. The LLM is just a fancy synthesizer. So treat your retriever with respect: tune its parameters, audit its results, and never assume it’s right just because it’s clever.