Skip to content

ChatGPT-Style Retrieval-Augmented Generation (RAG) with LangChain

Core Concept LearningAugust 3, 20265 min read

Ask ChatGPT a general-knowledge question and it answers from what it learned during training. Ask a support bot "what's your refund window for a damaged item bought during the holiday sale," and there is no training run that ever saw your company's actual refund policy document. Retrieval-augmented generation is the pattern that closes that gap: fetch the relevant passage from your own documents at request time, hand it to the model as context, and generate an answer grounded in that passage instead of the model's general training.

This guide builds one such bot with LangChain: a support assistant answering questions from a refund-policy document, following one question — "what's the refund window for a damaged item?" — through chunking, embedding, retrieval, and generation. Each section adds the piece that makes the difference between a demo that looks right on the happy path and a bot that admits "I don't know" instead of confidently answering from outside the document when retrieval comes up empty. For the deeper mechanics of chunk boundaries, see RAG chunking strategies, and for how embeddings represent meaning as vectors, see Embeddings explained.

Parts of a LangChain RAG chatbot
Parts of a LangChain RAG chatbot

The four moving parts

A LangChain RAG chain has four pieces with distinct responsibilities: a document loader + splitter that turns a policy PDF or markdown file into chunks small enough to embed meaningfully; an embedding model that converts each chunk (and later, each query) into a vector; a vector store that indexes those vectors for fast similarity search; and a chat model that receives the retrieved chunks plus the question and composes an answer.

The critical constraint that trips up first attempts: the embedding model used to index documents and the one used to embed the query at request time must be the same model. Mixing embedding models (or even different versions of the same model) puts query and document vectors in incompatible spaces — cosine similarity between them becomes meaningless, and retrieval silently returns unrelated chunks with no error.

Quick reference

  • Loader + splitter: PDF/markdown/HTML in, list of ~200–500 token chunks out, each tagged with source metadata.
  • Embedding model: same model, same version, for both indexing and query time — this is a contract, not a suggestion.
  • Vector store: pgvector, Chroma, or a managed service — the choice affects operational cost more than retrieval quality at small scale.
  • Chat model: receives retrieved chunks + question in the prompt; its job is composition, not knowledge recall.

Remember this

Retrieval silently breaks, without any error, if the query-time embedding model differs from the indexing-time embedding model — that consistency is a hard requirement, not a tuning knob.

Chunking and indexing the policy document

Split the refund policy into chunks before embedding — a whole multi-page document embedded as one vector loses the granularity to distinguish "damaged item" refunds from "changed my mind" refunds, because one vector can only represent the average of everything in it. A few hundred tokens per chunk, with modest overlap, keeps each chunk focused enough that its embedding actually represents one coherent idea.

Source metadata attached at chunking time (which document, which section) is what lets a later answer cite where it came from — that citation is the second half of the trust story: not just "answer grounded in the docs," but "answer traceable to a specific paragraph a human can go check."

One question through the RAG chain
One question through the RAG chain

Quick reference

  • Pin the embedding model name/version in one constant, not scattered across index and query code.
  • Keep chunk metadata (source file, section heading) — it's what makes a cited answer verifiable.
  • For a production store, replace MemoryVectorStore with pgvector or a managed vector DB — memory stores don't survive a restart.
  • Re-index whenever the source document changes; a stale index answers from last quarter's policy.
Load and split
1import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";2import { TextLoader } from "langchain/document_loaders/fs/text";3 4const loader = new TextLoader("docs/refund-policy.md");5const rawDocs = await loader.load();6 7const splitter = new RecursiveCharacterTextSplitter({8  chunkSize: 400,9  chunkOverlap: 60,10});11const chunks = await splitter.splitDocuments(rawDocs);12// Expected: chunks.length > 1 for a multi-section policy doc,13// each chunk carrying { pageContent, metadata: { source } }.
Embed and index into a vector store
1import { OpenAIEmbeddings } from "@langchain/openai";2import { MemoryVectorStore } from "langchain/vectorstores/memory";3 4const EMBEDDING_MODEL = "text-embedding-3-small"; // pin this — see the note above5 6const embeddings = new OpenAIEmbeddings({ model: EMBEDDING_MODEL });7const vectorStore = await MemoryVectorStore.fromDocuments(chunks, embeddings);8 9// Expected: a similarity search for "damaged item refund window"10// returns the chunk containing the damaged-goods clause, ranked first.11const results = await vectorStore.similaritySearch(12  "damaged item refund window",13  414);15console.log(results.map((r) => r.metadata.source));

Remember this

Chunk-level metadata is what turns a RAG answer from an assertion into something a user can verify — index it at chunking time, not as an afterthought at generation time.

Retrieval, generation, and the grounding rule

The retrieval step embeds the incoming question with the same embedding model and finds the top-k most similar chunks. The generation step then "stuffs" those chunks into the prompt alongside the question and asks the chat model to answer using only that context. The instruction to use only the provided context is the entire mechanism that keeps the bot from quietly falling back to its general training when the retrieved chunks don't actually answer the question.

Without that instruction, a question outside the document's scope ("what's your policy on international shipping duties" when the refund policy never mentions duties) gets a fluent, confident, and completely made-up answer — the model fills the gap from general knowledge because nothing told it not to. The fix is a system prompt that explicitly permits "I don't know" as a valid answer.

Zoom: retrieval scoring for one query
Zoom: retrieval scoring for one query

Quick reference

  • The grounding instruction is doing real work — test the out-of-scope case explicitly, don't assume the model infers it.
  • Return source citations from the retrieved chunk metadata alongside the answer so a user can verify it.
  • If similarity scores for all top-k chunks are low, treat that as "nothing relevant found" and skip generation entirely rather than stuffing weak matches.
  • Log question → retrieved chunks → answer triples; a wrong answer is almost always a retrieval problem, not a generation problem.
Stuffed prompt, no grounding boundary
1const chunks = await vectorStore.similaritySearch(question, 4);2const context = chunks.map((c) => c.pageContent).join("\n\n");3 4const prompt = `Context:\n${context}\n\nQuestion: ${question}\nAnswer:`;5// If the question is outside the doc's scope, the model still answers6// fluently — it just answers from training data instead of context.
Grounding instruction + explicit "not in the docs" path
1const SYSTEM = `Answer only using the CONTEXT below. If the context does2not contain enough information to answer, respond exactly:3"I don't have that in our documentation — I'll connect you with support."4Do not use outside knowledge, even if you know the general answer.`;5 6async function answer(question: string) {7  const chunks = await vectorStore.similaritySearch(question, 4);8  const context = chunks9    .map((c, i) => `[${i + 1}] (${c.metadata.source})\n${c.pageContent}`)10    .join("\n\n");11 12  return chatModel.invoke([13    { role: "system", content: SYSTEM },14    { role: "user", content: `CONTEXT:\n${context}\n\nQUESTION: ${question}` },15  ]);16}17 18// Expected: answer("refund window for a damaged item?") cites the19// damaged-goods clause and its source.20// Break it: ask answer("international shipping duties policy?") —21// expected: the exact fallback string, not an invented policy.

Remember this

An explicit instruction to say "I don't know" when the context doesn't answer the question is the single mechanism separating a grounded RAG bot from one that quietly reverts to hallucinating from general training on any out-of-scope question.

When RAG is the right tool

RAG earns its complexity when the answer source changes independently of any model release — a refund policy edited next week, a product catalog updated daily — and when you need traceable citations a compliance or support team can verify. It's the wrong tool when the knowledge is small enough to fit permanently in a system prompt, or when the task needs reasoning over a huge document all at once rather than a handful of relevant chunks.

That second case is where long-context prompting can outperform chunked retrieval — feeding the model the whole document instead of a handful of matching passages. Treat the choice as a workload question, not a loyalty to one architecture: measure retrieval hit rate and answer faithfulness on your own labeled question set before assuming the more complex pipeline is earning its cost.

Quick reference

  • Use RAG when the source document changes independently of model releases and answers need to be citable.
  • Skip RAG for a small, stable knowledge base — a system prompt with the whole document may be simpler and just as accurate.
  • Consider long context instead of chunked retrieval when a question needs the whole document's structure, not a handful of matching passages — see Long context vs RAG.
  • Evaluate with a labeled question set: track retrieval hit rate (right chunk in top-k) and answer faithfulness (grounded vs. invented) separately — they fail independently.

Remember this

RAG is the right tool exactly when the underlying document changes on its own schedule and answers need a verifiable source — for a small, stable knowledge base, a plain system prompt is often simpler and equally accurate.

Key takeaway

Build the pipeline against a short refund policy document: chunk and index it, then run the retrieval + generation chain against "what's the refund window for a damaged item?" and confirm the answer cites the correct clause and source. That's your expected success — a grounded, citable answer.

Then break it deliberately: ask a question the policy never addresses ("what's your international shipping duties policy?") and confirm the bot returns your exact fallback string rather than an invented answer. Pass criterion: the in-scope question returns a cited, grounded answer, and the out-of-scope question returns the explicit "not in our documentation" fallback — never a confident, uncited invention either way.

Share:

Related Articles

First-generation Retrieval-Augmented Generation (RAG) systems relied exclusively on naive Vector Search (semantic simila

Read

Jul 30, 2026 · 9 min read

RAG taxonomy gets confusing because people mix three different ideas: architecture levels, retrieval tricks, and product

Read

A search team upgrades their embedding model for better quality, re-embeds only newly added documents with it, and leave

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.