LangChain RAG Tutorial 2026: Rewriting the Pipeline After the chains Module Disappeared

2026-10-22 10 min read

Every RAG walkthrough still opens with an import from langchain.chains, and on langchain 1.4.6 that line raises ModuleNotFoundError. Here is the rebuilt pipeline, with chunk counts measured at three settings and a hit@4 loop that tells you whether retrieval or generation is broken.

💡 What You Will Learn

Every RAG walkthrough still opens with an import from langchain.chains, and on langchain 1.4.6 that line raises ModuleNotFoundError. Here is the rebuilt pipeline, with chunk counts measured at three s

📜 Table of Contents

Paste almost any RAG tutorial into a fresh terminal and it dies on line one: ModuleNotFoundError: No module named 'langchain.chains'. That is not a broken tutorial. That is what happens when you install langchain==1.4.6 and run code written for the 0.x line, where retrieval chains lived in langchain.chains. The module is gone. Every walkthrough that opens with from langchain.chains import create_retrieval_chain is now a dead end.

So here is the pipeline rewritten against what is actually on PyPI today, with the parts most tutorials skip: how splitting behaves when you measure it, how to check retrieval before you blame the model, and where the legacy import hides if you genuinely need it.

What a clean install contains now

These are the versions a fresh virtualenv pulled when this page was written (verified 22 October 2026, Python 3.11):

PackageVersionWhat it gives you
langchain1.4.6Agent API (create_agent), middleware, structured output. No chains module.
langchain-core1.6.12Runnables, prompts, parsers, InMemoryVectorStore
langchain-text-splitters1.1.3RecursiveCharacterTextSplitter and friends
langchain-openai1.7.2ChatOpenAI, OpenAIEmbeddings
langchain-postgres0.0.19PGVector store for production
langchain-classic1.0.8Optional. Where the old chains now live.

Two things worth noticing. First, langchain requires Python >=3.10,<4.0, so a 3.9 environment cannot run it at all. Second, the splitter library is a separate distribution now: if pip install langchain succeeded but RecursiveCharacterTextSplitter is missing, you skipped langchain-text-splitters.

The legacy import, and why it still fails

If you have old code and no time to rewrite it, install langchain-classic. But the import path moved one level down, which catches people twice:

# fails on langchain-classic 1.0.8
from langchain_classic.chains import create_stuff_documents_chain

# works
from langchain_classic.chains.combine_documents import create_stuff_documents_chain
from langchain_classic.chains.retrieval import create_retrieval_chain

The top-level langchain_classic.chains package only exposes an importer shim, so the familiar one-line import raises ImportError even after you install the right package. Import from the submodule and the old chains behave as before.

Better option: skip chains entirely. In the 1.x line the whole retrieval graph is four runnables, and you can read all four.

Step 1: split, and actually measure it

Chunk settings are the biggest single cause of "my RAG answers are garbage", and the usual advice (1000 characters, 200 overlap) gets repeated without numbers attached. So here are numbers. The test document was 5,600 characters of plain prose, split with RecursiveCharacterTextSplitter at three settings:

chunk_size / overlapchunksavg charsminmax
1000 / 2007969829999
500 / 5013473210499
300 / 3021288108299

The max column is the useful one. Chunks never exceed chunk_size, but they can be far smaller: at 500/50 the smallest chunk was 210 characters, less than half the target. That happens because the splitter prefers to break at a paragraph boundary rather than fill the budget. The default separator order is blank line, newline, space, then a hard character split. A stray short section in your document therefore produces a stub chunk that embeds as noise.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    add_start_index=True,   # keeps the char offset so you can cite a location
)
chunks = splitter.create_documents([raw_text], metadatas=[{"source": "manual.pdf"}])
print(len(chunks), sum(len(c.page_content) for c in chunks) / len(chunks))

Two settings matter more than the exact size. add_start_index=True gives every chunk its offset in the source, which is what lets you show a citation like "page 12, character 4,180" instead of just a filename. And drop chunks under roughly 100 characters before embedding: they are almost always headings or table fragments that retrieve badly and push better chunks out of the top-k.

Step 2: the retrieval chain, in four runnables

No chains module, no legacy objects. A store, a formatter, a prompt, a model:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

store = InMemoryVectorStore.from_documents(chunks, OpenAIEmbeddings(model="text-embedding-3-small"))
retriever = store.as_retriever(search_kwargs={"k": 4})

def format_docs(docs):
    return "\n\n".join(
        f"[{i+1}] ({d.metadata.get('source','?')} @ {d.metadata.get('start_index','?')})\n{d.page_content}"
        for i, d in enumerate(docs)
    )

prompt = ChatPromptTemplate.from_messages([
    ("system",
     "Answer only from the numbered context. Cite the bracket number after each claim. "
     "If the context does not contain the answer, say so and stop."),
    ("human", "Context:\n{context}\n\nQuestion: {question}"),
])

chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | ChatOpenAI(model="gpt-4.1-mini", temperature=0)
    | StrOutputParser()
)

print(chain.invoke("How many stages does the pipeline have?"))

InMemoryVectorStore lives in langchain_core, so this runs with no extra service. It also exposes similarity_search_with_score and max_marginal_relevance_search, which you will want in the next step. Past a few thousand chunks, swap it for PGVector from langchain-postgres; the retriever interface does not change.

Step 3: score retrieval before you touch the prompt

Most people debug RAG by rewriting the system prompt. That assumes the right passage was in the context, which is exactly the assumption worth testing first. Twenty labelled questions are enough to tell you whether the problem is retrieval or generation:

import json

# questions.json: [{"q": "...", "must_contain": "phrase that only the right chunk has"}, ...]
cases = json.load(open("questions.json"))
hits, mrr = 0, 0.0

for case in cases:
    docs = retriever.invoke(case["q"])
    rank = next((i + 1 for i, d in enumerate(docs)
                 if case["must_contain"].lower() in d.page_content.lower()), None)
    if rank:
        hits += 1
        mrr += 1 / rank

print("hit@4 =", hits, "/", len(cases))
print("MRR  =", round(mrr / len(cases), 2))

Interpretation, and this is the part that saves days:

Step 4: moving to Postgres without changing the chain

The in-memory store loses everything on restart, so the production swap is one constructor:

from langchain_postgres import PGVector

store = PGVector(
    embeddings=OpenAIEmbeddings(model="text-embedding-3-small"),
    collection_name="handbook",
    connection="postgresql+psycopg://app:secret@localhost:5432/knowledge",
    use_jsonb=True,
)
store.add_documents(chunks)
retriever = store.as_retriever(search_kwargs={"k": 4})

PGVector in langchain-postgres 0.0.19 gives you from_documents, from_texts, add_documents and as_retriever, so the rest of the script is untouched. Keep collection_name stable across deploys; if you change the embedding model, change the collection name too, because mixing two embedding spaces inside one collection silently ruins similarity scores.

Step 5: when the job is agentic, not retrieval

If a question needs two or three lookups instead of one, the 1.x answer is the agent API rather than a retrieval chain:

from langchain.agents import create_agent

agent = create_agent(
    model="openai:gpt-4.1-mini",
    tools=[search_docs],          # a plain function wrapped with the tool decorator
    system_prompt="Search before answering. Cite the source name for every claim.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "Compare the refund rules in both PDFs."}]})

create_agent is exported from langchain.agents in 1.4.6, alongside middleware and structured_output. Everything previously expressed as a multi-vector or router chain is now just more tools, which is easier to debug: you see the tool call, the arguments and the returned text in one trace.

Failure modes, mapped

SymptomMost likely causeFirst thing to change
ModuleNotFoundError: langchain.chains0.x code on a 1.x installRewrite as runnables, or install langchain-classic and import from submodules
Right answer exists in the docs, never citedChunk boundary cut the passage, or a 100-character stub won a slotIncrease overlap, filter short chunks, raise k to 6
Confident answer with no numbersModel filling gaps; context empty or off-topicRun the hit@4 loop; add the "if the context does not contain it, say so" line
The same two sources cited for every questionOverlap too high, near-duplicate chunks cluster at the topLower overlap, enable MMR search for diversity
Answers degrade after a reindexEmbedding model changed inside the same collectionNew collection_name, re-embed everything
Fast locally, slow in the containerEmbedding calls re-run per requestCache embeddings by content hash; embed once at ingest

An honest note on whether you need the framework

The pipeline above is roughly 40 lines of LangChain. The same pipeline with httpx, a Postgres client and your own SQL is roughly 120 lines you fully own, with no version churn. LangChain earns its place when you want the ecosystem: document loaders for a dozen file types, retrievers like parent-document and multi-query, and the agent middleware in 1.x. If your system is one index over one document set, the framework is a dependency you will keep re-reading release notes for. Pick deliberately, not by default.

FAQ

Why did the chains module disappear instead of being deprecated?

The 1.x rewrite rebuilt the package around runnables and agents. The old chain classes moved to a separate distribution, langchain-classic, so installs stay small and legacy code keeps working without pinning the main package back to 0.x.

Does installing langchain-classic fix my old script completely?

Only after you fix the import path. In 1.0.8 the chains are importable from submodules such as langchain_classic.chains.retrieval, but not from the package root. Change those imports and old code runs on Python 3.10 or newer.

What chunk size should I start with?

Start at 1000 characters with 200 overlap for prose, 300 to 500 for API docs and tables, then measure. In the run above the same 5,600-character input produced 7 to 21 chunks depending on settings, and that count decides how many near-duplicates compete for your k slots.

Is InMemoryVectorStore usable in production?

For a demo or a test suite, yes. It lives in process memory, so it disappears on restart and is not shared between workers. Switch to PGVector, Qdrant or pgvector directly once the corpus passes a few thousand chunks.

Do I need a reranker?

Add one after measuring. If hit@4 is already above 0.9 but MRR is under 0.5, a cross-encoder reranker is the cheapest fix available. If hit@4 is under 0.6, a reranker cannot rescue passages the retriever never returned.

Related reading

Related Articles
2026-10-21
Best AI Call Transcription Tool for Independent Insurance Agents 2026: E&O Paperwork Without a CRM Overhaul
2026-10-19
Best AI Payroll Software in Stockholm 2026: Collective Agreements, AGI and What AI Cannot Do
2026-10-22
Ollama in Docker With a GPU: The Compose Block, the Env Defaults, and the VRAM Math Behind Silent CPU Offload
2026-10-21
Best AI Invoice Software in Poznan 2026: KSeF Deadlines, 23% VAT and a Two-Person Studio
2026-10-20
Best AI Title Search Tool for Small Title Agencies 2026: What Is Real Automation and What Is a Demo
2026-10-20
Best AI Expense Management Software in Bern 2026: 8.1% VAT, Paper Receipts and a Treuhänder-Ready Report

Written by our editorial team; tools listed here are tested or verified against public sources. Links point to official sites or GitHub repos for reference only — no paid placements.

💬 Comments (0)

No comments yet. Be the first!

Login to comment