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 &

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. I wanted a portfolio assistant that could: Answer questions about my background accurately — no hallucinated jobs or fake projects. Fetch live data (my latest GitHub activity), not a stale snapshot. Remember the conversation across messages. 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. Next.js 16 chat UI ──► FastAPI (streaming) ──► LangGraph agent ├── RAG retriever → pgvector (Neon Postgres) ├── GitHub MCP tool → live GitHub data └── Redis checkpointer → conversation memory 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-small → pgvector on Neon Postgres. Memory: Redis checkpointer for per-session history. Live data: GitHub via the Model Context Protocol (MCP). 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", ) 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}, ) 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. 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") 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. 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() MCP is the killer part — I get a whole toolset without hand-writing API wrappers. The agent picks the right tool on its own. 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(...)) 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. 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.
Key Takeaways
- •Most portfolios have a boring "About Me" paragraph
- •This story was reported by Dev.to, covering developments in the dev space.
- •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.
📖 Continue reading the full article:
Read Full Article on Dev.to →

