1. Introduction
Generative AI and Large Language Models (LLMs) are changing how we access and interact with information.
Traditional chatbots generally depend on predefined questions, answers or information available within their training data.
However, what if we want to build an AI assistant that can understand our own technical articles, retrieve relevant information and answer questions using our personal knowledge base?
In this project, we will build an AI-powered chatbot that connects directly to a personal Blogger website and answers questions based on published blog articles.
We will use Retrieval-Augmented Generation (RAG), LangChain, OpenAI embeddings, GPT-4.1-mini and ChromaDB.
The application will automatically:
Fetch published articles from a Blogger website using its RSS feed.
Extract article titles and content.
Convert articles into LangChain documents.
Split documents into smaller text chunks.
Generate vector embeddings using OpenAI.
Store embeddings in ChromaDB.
Retrieve relevant content based on user questions.
Generate contextual answers using an LLM.
Display the original article references.
Project results
During my implementation, the application successfully processed:
| Metric | Result |
|---|---|
| Blog posts retrieved | 276 |
| Text chunks generated | 2,773 |
| Vector database | ChromaDB |
| Embedding model | text-embedding-3-small |
| LLM | GPT-4.1-mini |
| Framework | LangChain |
2. Project Architecture
Before writing the code, let's understand the complete architecture.
High-level architecture
Blogger Website
|
v
Blogger RSS Feed
|
v
Python RSS Data Loader
|
v
BeautifulSoup HTML Content Extraction
|
v
LangChain Documents
|
v
Recursive Character Text Splitter
|
v
OpenAI Embeddings
|
v
ChromaDB Vector Database
|
v
Retriever
|
v
Relevant Document Chunks
|
v
GPT-4.1-mini
|
v
AI Generated Answer + Source References
Architecture components
1. Blogger RSS Feed
Provides access to published blog articles without manually downloading individual posts.
2. Python
Handles RSS fetching, content extraction, document processing and application execution.
3. LangChain
Provides the document loading, splitting, embedding, retrieval and LLM orchestration components.
4. OpenAI Embeddings
Converts text into numerical vector representations that can be searched semantically.
5. ChromaDB
Stores the vectors and associated document metadata.
6. Retriever
Finds the most relevant document chunks based on the user's question.
7. GPT-4.1-mini
Uses retrieved information to generate a contextual answer.
3. Technology Stack
We will use the following technologies.
| Technology | Purpose |
|---|---|
| Python 3.13 | Application development |
| LangChain | RAG orchestration |
| OpenAI API | LLM and embeddings |
| GPT-4.1-mini | Answer generation |
| text-embedding-3-small | Vector embeddings |
| ChromaDB | Vector database |
| feedparser | RSS feed parsing |
| BeautifulSoup | HTML extraction |
| Requests | HTTP requests |
| python-dotenv | API key management |
4. Prerequisites
Before starting, ensure you have:
Windows 11 or Linux.
Python 3.10 or newer.
VS Code.
OpenAI API key with API access and billing configured.
Internet connectivity.
A public Blogger website.
For this project, we will use:
Blog URL:
https://rajguptaaws.blogspot.com/
5. Create the Project Directory
Create a project directory:
mkdir day6-rag
cd day6-ragCreate the following project structure:
day6-rag/
│
├── main.py
│
├── .env
│
├── requirements.txt
│
└── chroma_db/Explanation:
main.py: Main Python application.
.env: Stores the OpenAI API key.
requirements.txt: Python dependencies.
chroma_db: Persistent vector database storage.
6. Create Python Virtual Environment
We will use an existing virtual environment called Old/.venv.
Run:
& "..\Old\.venv\Scripts\python.exe" --versionInstall the required dependencies:
& "..\Old\.venv\Scripts\python.exe" -m pip install requests beautifulsoup4 python-dotenv feedparser langchain langchain-community langchain-core langchain-openai langchain-chroma langchain-text-splittersVerify the installation:
& "..\Old\.venv\Scripts\python.exe" -m pip list7. Configure OpenAI API Key
Create a file named .env in the project directory.
Add:
OPENAI_API_KEY=your_openai_api_keyReplace the placeholder with your actual OpenAI API key.
Important security precautions:
Never publish your API key.
Never upload .env to GitHub.
Never hardcode API keys inside Python source code.
Add .env to .gitignore.
8. Understand the Blogger RSS Feed
Instead of scraping the complete Blogger website, we will use its RSS feed.
Our feed URL:
https://rajguptaaws.blogspot.com/feeds/posts/default?alt=rssThe feed contains information about published blog posts.
Examples of information available:
Article title
Article URL
Published date
Article content or summary
We can access the RSS feed programmatically using Python.
We will use feedparser to read the feed and Requests to download it.
9. Complete Python Implementation
Create main.py.
We will implement the application step by step.
Step 1: Import required libraries
import os
import hashlib
import requests
import feedparser
from bs4 import BeautifulSoup
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParserExplanation:
requests: Downloads RSS feed content.
feedparser: Parses Blogger RSS entries.
BeautifulSoup: Extracts readable text from HTML.
Document: Creates LangChain document objects.
RecursiveCharacterTextSplitter: Splits articles into chunks.
OpenAIEmbeddings: Generates vector embeddings.
Chroma: Stores and retrieves vectors.
ChatOpenAI: Generates AI answers.
Step 2: Load environment variables
load_dotenv()
if not os.getenv("OPENAI_API_KEY"):
raise ValueError("OPENAI_API_KEY is missing from .env")This loads the OpenAI API key from the .env file.
Step 3: Define Blogger configuration
BLOG_URL = "https://rajguptaaws.blogspot.com"
FEED_URL = BLOG_URL + "/feeds/posts/default"
PAGE_SIZE = 100We will retrieve articles in batches of 100.
Step 4: Load blog posts
Create a function to retrieve published blog posts.
def load_blog_posts():
documents = []
start_index = 1
seen_urls = set()
while True:
params = {
"alt": "rss",
"start-index": start_index,
"max-results": PAGE_SIZE
}
print(f"Fetching blog posts from index {start_index}...")
response = requests.get(
FEED_URL,
params=params,
timeout=30,
headers={"User-Agent": "Mozilla/5.0"}
)
response.raise_for_status()
feed = feedparser.parse(response.content)
entries = feed.entries
if not entries:
break
for entry in entries:
title = entry.get("title", "Untitled")
article_url = entry.get("link", BLOG_URL)
if article_url in seen_urls:
continue
seen_urls.add(article_url)
if entry.get("content"):
html_content = entry.content[0].value
else:
html_content = entry.get("summary", "")
soup = BeautifulSoup(
html_content,
"html.parser"
)
content = soup.get_text(
separator="\n",
strip=True
)
if not content:
continue
published = entry.get("published", "")
document = Document(
page_content=f"""
Title: {title}
Published: {published}
Content:
{content}
""",
metadata={
"title": title,
"source": article_url,
"published": published
}
)
documents.append(document)
if len(entries) < PAGE_SIZE:
break
start_index += PAGE_SIZE
return documentsExplanation:
The load_blog_posts() function performs the following operations:
Connects to the Blogger RSS feed.
Retrieves articles in batches.
Extracts the article title.
Retrieves the article URL.
Extracts article HTML content.
Converts HTML into plain text.
Creates LangChain Document objects.
Stores article metadata.
We use pagination because a blog may contain hundreds of articles.
Step 5: Load the blog documents
documents = load_blog_posts()
print(
f"\nTotal blog posts loaded: {len(documents)}"
)
if not documents:
raise RuntimeError(
"No blog posts found. Check the RSS feed."
)Expected output from my implementation:
Fetching blog posts from index 1...
Fetching blog posts from index 101...
Fetching blog posts from index 201...
Total blog posts loaded: 276The actual count depends on the number of published articles.
Step 6: Split documents into smaller chunks
Large articles should not be passed directly to the retrieval process as a single document.
We divide them into smaller chunks.
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=100
)
chunks = splitter.split_documents(documents)
print(
f"Total chunks created: {len(chunks)}"
)Explanation:
chunk_size=500
Each chunk contains approximately 500 characters.
chunk_overlap=100
Each consecutive chunk overlaps by approximately 100 characters.
Why do we need chunking?
Improves retrieval granularity.
Helps retrieve relevant sections.
Preserves some context between adjacent chunks.
Avoids sending entire articles to the LLM unnecessarily.
Expected output:
Total chunks created: 2773Step 7: Initialize OpenAI embeddings
Embeddings convert text into numerical vector representations.
Initialize the embedding model:
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small"
)Example:
Input:
"What is Kubernetes?"
The embedding model converts the text into a vector representation.
The resulting vector represents semantic information about the text.
We can use these vectors to find semantically related content.
Step 8: Initialize ChromaDB
ChromaDB is our vector database.
We will store the generated vectors and their corresponding document content.
vector_store = Chroma(
collection_name="raj_blog_rag",
embedding_function=embeddings,
persist_directory="./chroma_db"
)Explanation:
collection_name: Logical collection for our blog knowledge.
embedding_function: Converts documents into vectors.
persist_directory: Stores the database on local disk.
This allows us to reuse the vector database between application executions.
Step 9: Store documents in ChromaDB
We will create deterministic IDs for each chunk.
This prevents the same chunks from being repeatedly added with new random IDs.
chunk_ids = []
for chunk in chunks:
unique_string = (
chunk.metadata["source"]
+ "_"
+ chunk.page_content
)
chunk_id = hashlib.sha256(
unique_string.encode("utf-8")
).hexdigest()
chunk_ids.append(chunk_id)Now insert the documents:
vector_store.add_documents(
documents=chunks,
ids=chunk_ids
)
print(
"Blog data stored in ChromaDB successfully!"
)Note: For this initial tutorial, the code demonstrates ingestion into a persistent collection. For repeated production ingestion, implement collection synchronization, updating changed articles and deleting outdated chunks. Do not assume that deterministic IDs alone automatically synchronize existing records.
Step 10: Create the retriever
The retriever searches ChromaDB for relevant document chunks.
retriever = vector_store.as_retriever(
search_kwargs={
"k": 4
}
)The k=4 parameter tells the retriever to return four relevant chunks for each question.
Step 11: Initialize the LLM
We will use GPT-4.1-mini.
llm = ChatOpenAI(
model="gpt-4.1-mini",
temperature=0
)Temperature 0 helps make responses more consistent.
Step 12: Create the RAG prompt
The prompt instructs the LLM to answer using the retrieved blog content.
prompt = ChatPromptTemplate.from_template("""
You are an expert AI assistant for Raj Gupta's
technical blog.
Answer the user's question using the provided context.
Rules:
1. Use the retrieved blog content.
2. Do not invent technical information.
3. If the answer is unavailable, say you do not know.
4. Explain technical concepts clearly.
5. Include relevant source URLs when available.
Context:
{context}
Question:
{question}
Answer:
""")Step 13: Format retrieved documents
We need to combine retrieved document chunks into a single context string.
def format_docs(docs):
formatted_docs = []
for doc in docs:
content = f"""
Title: {doc.metadata.get('title', '')}
Source: {doc.metadata.get('source', '')}
Content:
{doc.page_content}
"""
formatted_docs.append(content)
return "\n\n".join(formatted_docs)This function also preserves the source URL so the LLM can reference the original article.
Step 14: Implement the RAG question-answering function
We will retrieve relevant documents first and then send them to the LLM.
def ask_question(question):
# Retrieve relevant documents
retrieved_docs = retriever.invoke(question)
if not retrieved_docs:
return "I could not find relevant information."
# Format retrieved documents
context = format_docs(retrieved_docs)
# Create prompt
messages = prompt.invoke({
"context": context,
"question": question
})
# Generate answer
response = llm.invoke(messages)
answer = StrOutputParser().invoke(response)
# Display source references
print("\nAI Answer:\n")
print(answer)
print("\nSources:")
sources = set()
for doc in retrieved_docs:
source = doc.metadata.get("source")
if source:
sources.add(source)
for source in sources:
print(source)This approach prints sources directly from the retrieved documents instead of relying entirely on the LLM to reproduce URLs accurately.
Step 15: Create the interactive chatbot
Finally, create an interactive terminal interface.
print("\n====================================")
print("Raj Gupta Blog AI Assistant")
print("====================================")
while True:
question = input(
"\nAsk a question (exit to stop): "
)
if question.lower().strip() == "exit":
print("Exiting chatbot...")
break
if not question.strip():
continue
try:
ask_question(question)
except Exception as error:
print(f"\nError: {error}")10. Complete main.py
All the previous steps are combined into one Python application.
The complete file contains:
RSS feed ingestion
Document extraction
Text chunking
Embedding generation
ChromaDB storage
Semantic retrieval
LLM integration
Interactive question answering
Source reference display
Use the complete code in the accompanying implementation section of this article.
11. Run the Application
Open PowerShell inside the day6-rag directory.
Execute:
& "..\Old\.venv\Scripts\python.exe" main.pyThe application will start fetching blog posts.
Expected output:
Fetching blog posts from index 1...
Fetching blog posts from index 101...
Fetching blog posts from index 201...
Total blog posts loaded: 276
Total chunks created: 2773
Blog data stored in ChromaDB successfully!
====================================
Raj Gupta Blog AI Assistant
====================================
Ask a question (exit to stop):12. Test the AI Chatbot
Let's test the application with a real question.
Question:
What is RAG?The chatbot retrieves relevant information from the blog and passes it to GPT-4.1-mini.
Example answer:
RAG stands for Retrieval-Augmented Generation.
It is a method used to improve the responses of a language model by incorporating relevant external information retrieved from a knowledge base or documents.
A typical RAG process is:
User Question → Document Retrieval → Context → LLM → Answer
The chatbot also displays the original blog article reference.
Example source:
https://rajguptaaws.blogspot.com/2026/10/part-2-building-production-style-rag.html
13. How RAG Works Internally
Let's understand the two important phases.
Phase 1: Data Ingestion
This happens before users ask questions.
Fetch blog articles.
Extract article content.
Create document objects.
Split articles into chunks.
Generate embeddings.
Store vectors in ChromaDB.
Phase 2: Query Execution
This happens whenever a user asks a question.
Receive the user question.
Convert the question into an embedding.
Search ChromaDB for similar vectors.
Retrieve the most relevant document chunks.
Construct the prompt with retrieved context.
Send the prompt to GPT-4.1-mini.
Generate the final answer.
Display relevant source URLs.
14. Common Errors and Troubleshooting
Error 1: ModuleNotFoundError: No module named 'feedparser'
Solution:
python -m pip install feedparserError 2: ModuleNotFoundError: No module named 'bs4'
Solution:
python -m pip install beautifulsoup4Error 3: ModuleNotFoundError: No module named 'langchain_chroma'
Solution:
python -m pip install langchain-chromaError 4: OpenAI API authentication error
Check whether the API key is correctly configured in the .env file.
Also verify that your OpenAI API account has access and available billing.
Error 5: Blogger RSS feed returns no articles
Verify:
Blog is publicly accessible.
RSS feed URL is correct.
Blogger feed settings permit access.
The feed is returning article entries.
Error 6: ChromaDB retrieval returns irrelevant answers
Possible improvements:
Experiment with chunk size.
Experiment with chunk overlap.
Increase the number of retrieved chunks.
Improve document metadata.
Use a reranker.
Evaluate retrieval quality.
15. Production Improvements
The current implementation is a learning-level RAG application.
For a production-grade implementation, consider:
Incremental ingestion instead of processing all articles every time.
Detecting modified and deleted articles.
Metadata filtering.
Hybrid search using keyword and vector retrieval.
Reranking retrieved documents.
Query rewriting.
RAG evaluation using Ragas.
FastAPI integration.
Authentication and authorization.
Logging, monitoring and error handling.
Rate limiting and API cost management.
Deployment using Docker and Kubernetes.
16. Future Enhancements
The next version of this project can include:
FastAPI backend.
Web-based chatbot UI.
LangGraph workflow orchestration.
Conversation memory.
Multi-user support.
Automated blog synchronization.
Advanced retrieval techniques.
MCP server integration.
AI agents for technical troubleshooting.
17. Conclusion
In this project, we successfully built an AI-powered chatbot that uses our own technical blog as its knowledge base.
We explored how Blogger RSS feeds, Python, LangChain, OpenAI embeddings, ChromaDB and GPT-4.1-mini work together to implement a complete Retrieval-Augmented Generation pipeline.
The project demonstrates how an LLM can access custom knowledge beyond its pretrained information and generate answers grounded in retrieved documents.
This is a practical foundation for building enterprise knowledge assistants, internal documentation chatbots, technical support agents and AI-powered information retrieval systems.
Author: Raj Gupta
Technologies: Python | LangChain | RAG | OpenAI | ChromaDB | Generative AI | Vector Database | LLM
Project: AI-Powered Blog Chatbot
No comments:
Post a Comment