DEV Community

Rehbar Khan
Rehbar Khan

Posted on

How I Built a RAG Chatbot Into My Portfolio with LangGraph, PGVector & MCP

Most portfolios have a boring "About Me" paragraph. I replaced mine with something you can talk to — an AI terminal that answers questions about me, pulls my live GitHub activity, and remembers the conversation. You can try it right now on my portfolio: rehbarkhan.in.

I'm Rehbar Khan, a Full Stack & Gen-AI developer, and in this post I'll break down exactly how it works — the RAG pipeline, the LangGraph agent, the memory layer, and how I wired in real GitHub data with MCP. No fluff, just the architecture.

The problem

I wanted a portfolio assistant that could:

  1. Answer questions about my background accurately — no hallucinated jobs or fake projects.
  2. Fetch live data (my latest GitHub activity), not a stale snapshot.
  3. Remember the conversation across messages.
  4. Stream responses token-by-token like a real terminal.

That rules out "just prompt an LLM." You need retrieval for grounding, tools for live data, and state for memory. Here's the stack I landed on.

Architecture at a glance

Next.js 16 chat UI  ──►  FastAPI (streaming)  ──►  LangGraph agent
                                                     ├── RAG retriever  → pgvector (Neon Postgres)
                                                     ├── GitHub MCP tool → live GitHub data
                                                     └── Redis checkpointer → conversation memory
Enter fullscreen mode Exit fullscreen mode
  • Frontend: Next.js 16 (App Router, TypeScript) — a streaming terminal UI.
  • Backend: FastAPI with streaming responses.
  • Orchestration: LangGraph (StateGraph, ToolNode, tools_condition).
  • LLM: OpenAI gpt-4o-mini.
  • Retrieval: text-embedding-3-smallpgvector on Neon Postgres.
  • Memory: Redis checkpointer for per-session history.
  • Live data: GitHub via the Model Context Protocol (MCP).

1. The RAG pipeline

Everything the bot knows about me lives in a single reference.txt. I chunk it, embed it, and store it in Postgres with pgvector:

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_postgres import PGVector

splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80)
chunks = splitter.split_text(open("reference.txt").read())

vectorstore = PGVector.from_texts(
    texts=chunks,
    embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
    connection=CONNECTION_STRING,       # load from env, never hardcode!
    collection_name="profile",
)
Enter fullscreen mode Exit fullscreen mode

Why chunk_size=500 / overlap=80? My profile data is dense and fact-heavy — short chunks keep each fact retrievable without dragging in unrelated context. The overlap stops sentences from getting cut mid-fact across chunk boundaries.

At query time I retrieve with a score threshold, not just top-k, so irrelevant matches get dropped instead of padding the prompt:

retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"k": 2, "score_threshold": 0.4},
)
Enter fullscreen mode Exit fullscreen mode

This is the anti-hallucination trick: if nothing clears the threshold, the model gets no context and answers "I don't know" instead of inventing something.

2. The LangGraph agent

Instead of a linear chain, I use a graph so the model can decide whether to call a tool (like fetching GitHub data) or answer directly:

from langgraph.graph import StateGraph, START
from langgraph.prebuilt import ToolNode, tools_condition

graph = StateGraph(State)
graph.add_node("model", call_model)
graph.add_node("tools", ToolNode(tools))
graph.add_conditional_edges("model", tools_condition)
graph.add_edge("tools", "model")
graph.add_edge(START, "model")
Enter fullscreen mode Exit fullscreen mode

tools_condition routes to the tool node only when the model emits a tool call, then loops back. Clean, and no brittle if/else routing.

3. Live GitHub data via MCP

The bot can answer "what has Rehbar been building lately?" with real data because I connected the GitHub MCP server as a tool source:

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "Github": {
        "transport": "streamable_http",
        "url": "https://api.githubcopilot.com/mcp/",
        "headers": {"Authorization": f"Bearer {github_token}"},
    }
})
tools = await client.get_tools()
Enter fullscreen mode Exit fullscreen mode

MCP is the killer part — I get a whole toolset without hand-writing API wrappers. The agent picks the right tool on its own.

4. Memory with Redis

LangGraph checkpointers make conversation memory almost free. I use a Redis-backed saver keyed by session id, so each visitor gets their own thread:

graph = builder.compile(checkpointer=AsyncRedisSaver(...))
Enter fullscreen mode Exit fullscreen mode

What I learned

  • Score thresholds beat top-k for factual RAG — precision over recall when the cost of a wrong fact is high.
  • MCP removes glue code. Adding a live data source went from "write an API client" to "point at a server."
  • Graphs > chains the moment you need conditional tool use.

Try it

The bot is live on my portfolio — ask it anything about my work: rehbarkhan.in. Full stack is Next.js + FastAPI + LangGraph + pgvector + Redis.


I'm Rehbar Khan, a Full Stack & Gen-AI developer building intelligent systems with LangChain, RAG, and LLMs. More at rehbarkhan.in · GitHub · LinkedIn.

Top comments (0)