This section builds a minimal RAG process from the employee handbook PDF: loading the document, segmenting the text, generating embedding, writing to the vector library, and packaging the Put retrieval capability into an Agent tool.
The complete process is listed at the beginning of the Notebook. The essence of RAG is: first put external data into a searchable index, when users ask questions, first retrieve relevant fragments, and then let the model answer based on the fragments.
Prepare PDF file.
Put long documents into chunks.
Put chunk into vector.
Write to vector store.
Do a similarity search by question.
Agent makes Use search results answer.
This lesson uses PyPDFLoader to read resources/acmecorp-employee-handbook.pdf. The loading result is a set of LangChain Documents, each Document containing text and metadata.
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("resources/acmecorp-employee-handbook.pdf")
data = loader.load()
print(data)A LangChain Document usually contains page_content and metadata. RAG retrieval mainly uses page_content.
LLMs and embedding models are not suited to processing the whole handbook at once, so the notebook first uses a recursive splitter to cut the text into chunks. chunk_overlap reduces the chance that cross-section information is cut off.
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
add_start_index=True,
)
all_splits = text_splitter.split_documents(data)
print(len(all_splits))| Parameter | Meaning |
|---|---|
chunk_size=1000 | Target length of about 1000 characters per text chunk |
chunk_overlap=200 | Keep 200 overlapping characters between adjacent chunks |
add_start_index=True | Record each chunk's start position in metadata |
The notebook defines a local HashingEmbeddings implementation. It does not depend on an external embedding API, so it suits course demos and offline environments, but its semantic quality is lower than a real embedding model.
import hashlib
import math
import re
from langchain_core.embeddings import Embeddings
class HashingEmbeddings(Embeddings):
def __init__(self, size: int = 384):
self.size = size
def _embed(self, text: str) -> list[float]:
# Convert tokens into a fixed-size hashed vector
...
embeddings = HashingEmbeddings()Hashing embeddings can run the RAG flow end to end, but production systems usually switch to OpenAI, Voyage, Cohere or a local semantic embedding model.
This lesson uses InMemoryVectorStore. It is simple and needs no server, which suits a notebook, but data disappears when the process ends.
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)
ids = vector_store.add_documents(documents=all_splits)
results = vector_store.similarity_search(
"How many days of vacation does an employee get in their first year?"
)
print(results[0])A RAG Agent's answer quality first depends on retrieving the right snippets. If results[0] is irrelevant, even a strong downstream LLM can answer incorrectly.
Finally, vector retrieval is wrapped as the search_handbook tool and given to the Agent. When the user asks about the handbook, the model can call the tool first, then answer from the returned snippet.
@tool
def search_handbook(query: str) -> str:
"""Search the employee handbook for information"""
results = vector_store.similarity_search(query)
return results[0].page_content
agent = create_agent(
model=deepseek_model(),
tools=[search_handbook],
system_prompt="You are a helpful agent that can search the employee handbook for information.",
)
response = agent.invoke(
{"messages": [HumanMessage(content="How many days of vacation does an employee get in their first year?")]}
)
print(response["messages"][-1].content)You can return multiple top-k snippets, include source page numbers, return an explicit error when there are no results, and replace InMemoryVectorStore with a persistent vector database.