LangChain: Introduction

LangChain 1.x deleted LLMChain and ConversationBufferMemory. What replaced them is not a new API — it is the observation that neither needed to be a class.
AI
NLP
LLM
Author

Ravi Kalia

Published

March 16, 2025

LangChain: Introduction

A language model is a pure function: text in, text out, no memory of the last call. Everything that makes a chatbot feel like a conversation lives outside the model — in whatever assembles the prompt. LangChain is a library for building that outside part, and its central claim is that assembling it needs composition rather than classes.

That claim is easier to see now than when this post was first written, because LangChain 1.x removed the classes. LLMChain, ConversationBufferMemory, ConversationChain and langchain.chat_models are gone — not renamed, deleted. What replaced them is the pipe operator and a wrapper, and the deletion is the argument: none of those objects were doing anything that composition could not.

What broke, concretely

If you have code from the 0.1 era, this is what it hits now:

Code
import warnings

warnings.filterwarnings("ignore")

import importlib

for module, symbol in [
    ("langchain.prompts", "PromptTemplate"),
    ("langchain.memory", "ConversationBufferMemory"),
    ("langchain.chains", "LLMChain"),
    ("langchain.text_splitter", "RecursiveCharacterTextSplitter"),
    ("langchain_core.prompts", "ChatPromptTemplate"),
    ("langchain_text_splitters", "RecursiveCharacterTextSplitter"),
]:
    try:
        getattr(importlib.import_module(module), symbol)
        print(f"ok      {module}.{symbol}")
    except (ImportError, AttributeError) as exc:
        print(f"GONE    {module}.{symbol}  ({type(exc).__name__})")
GONE    langchain.prompts.PromptTemplate  (ModuleNotFoundError)
GONE    langchain.memory.ConversationBufferMemory  (ModuleNotFoundError)
GONE    langchain.chains.LLMChain  (ModuleNotFoundError)
GONE    langchain.text_splitter.RecursiveCharacterTextSplitter  (ModuleNotFoundError)
ok      langchain_core.prompts.ChatPromptTemplate
ok      langchain_text_splitters.RecursiveCharacterTextSplitter

Four ModuleNotFoundErrors, not deprecation warnings. The top-level langchain package no longer holds prompts, memory, chains or splitters at all: primitives moved to langchain_core, and integrations moved to per-provider packages like langchain_huggingface and langchain_text_splitters.

A model, running locally

The original version of this post used ChatOpenAI and an API key. Everything below runs on a small local model instead, so the post is reproducible from its own requirements.txt with no account, no key and no per-token cost — which also means the outputs you see were genuinely produced by the code above them.

Qwen2.5-0.5B-Instruct is about half a billion parameters, small enough to run on CPU in seconds and weak enough that its limitations show. That is useful here: the post is about plumbing, and a small model makes it obvious when the plumbing rather than the model is doing the work.

Code
from transformers import pipeline
from langchain_huggingface import ChatHuggingFace, HuggingFacePipeline

generator = pipeline(
    "text-generation",
    model="Qwen/Qwen2.5-0.5B-Instruct",
    max_new_tokens=48,
    do_sample=False,
    return_full_text=False,
)
chat_model = ChatHuggingFace(llm=HuggingFacePipeline(pipeline=generator))
print(type(chat_model).__name__)
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
[transformers] Passing `generation_config` together with generation-related arguments=({'max_new_tokens', 'do_sample'}) is deprecated and will be removed in future versions. Please pass either a `generation_config` object OR all generation parameters explicitly, but not both.
ChatHuggingFace

Two wrappers, and the reason for each is worth knowing. HuggingFacePipeline adapts a transformers pipeline to LangChain’s interface. ChatHuggingFace then wraps that to apply the model’s chat template, turning a list of role-tagged messages into the exact token format Qwen was fine-tuned on. Skip the second and you get a plain text-completion model that will happily invent both sides of the dialogue.

return_full_text=False matters too: without it the pipeline echoes the prompt back and every response arrives with the entire templated conversation glued to the front.

A chain is a pipe

Here is the whole of what LLMChain used to be:

Code
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a concise assistant. Answer in one short sentence."),
        ("placeholder", "{history}"),
        ("human", "{input}"),
    ]
)

chain = prompt | chat_model | StrOutputParser()
print(chain.invoke({"input": "What is a vector database?", "history": []}))
[transformers] The following generation flags are not valid and may be ignored: ['temperature', 'top_p', 'top_k']. Set `TRANSFORMERS_VERBOSITY=info` for more details.
[transformers] Both `max_new_tokens` (=48) and `max_length`(=20) seem to have been set. `max_new_tokens` will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)
[transformers] Ignoring clean_up_tokenization_spaces=True for BPE tokenizer Qwen2Tokenizer. The clean_up_tokenization post-processing step is designed for WordPiece tokenizers and is destructive for BPE (it strips spaces before punctuation). Set clean_up_tokenization_spaces=False to suppress this warning, or set clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output=True to force cleanup anyway.
A vector database stores and organizes data using vectors, which represent points or lines in space.

prompt | chat_model | StrOutputParser() — format the messages, call the model, take the string off the response object. Each stage implements the same Runnable interface, which is what makes | meaningful, and the composite is itself a Runnable with the same invoke, batch and stream methods as its parts.

That uniformity is the actual design. LLMChain was a class that hard-coded this specific three-stage arrangement; the pipe lets you build any arrangement and get streaming and batching for free.

Memory is a wrapper, not a model feature

The chain above is stateless — note that we had to pass history: [] explicitly. Conversation memory is the observation that “remembering” just means replaying previous turns into the prompt each time, which is a job for a wrapper around the chain:

Code
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

sessions: dict[str, InMemoryChatMessageHistory] = {}


def get_history(session_id: str) -> InMemoryChatMessageHistory:
    """One transcript per session id."""
    return sessions.setdefault(session_id, InMemoryChatMessageHistory())


conversation = RunnableWithMessageHistory(
    chain,
    get_history,
    input_messages_key="input",
    history_messages_key="history",
)

config = {"configurable": {"session_id": "demo"}}
print("1:", conversation.invoke({"input": "Hi, my name is Ravi."}, config=config).strip())
print("2:", conversation.invoke({"input": "What is my name?"}, config=config).strip())
print("\nstored messages:", len(sessions["demo"].messages))
/Users/ravikalia/Code/github.com/ml-blog/.venv-huggingface/lib/python3.12/site-packages/IPython/core/interactiveshell.py:3775: LangChainDeprecationWarning: RunnableWithMessageHistory is deprecated. Use LangGraph's built-in persistence instead.
  exec(code_obj, self.user_global_ns, self.user_ns)
[transformers] Both `max_new_tokens` (=48) and `max_length`(=20) seem to have been set. `max_new_tokens` will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)
[transformers] Both `max_new_tokens` (=48) and `max_length`(=20) seem to have been set. `max_new_tokens` will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)
1: Hello! My name is Ravi.
2: Your name is Ravi.

stored messages: 4

The second answer knows the name from the first. Four messages are stored — two human, two AI — and the {history} placeholder in the prompt is where they get replayed on each call.

To show that this is entirely the wrapper’s doing rather than anything the model retains, ask the same question through the bare chain with an empty history:

Code
print(chain.invoke({"input": "What is my name?", "history": []}).strip())
[transformers] Both `max_new_tokens` (=48) and `max_length`(=20) seem to have been set. `max_new_tokens` will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)
I am an artificial intelligence designed to respond to your questions and provide information on various topics, but I do not have a personal name.

No idea. Same model object, same weights, same process — the only difference is whether prior turns were pasted into the prompt. That is what “memory” means for an LLM, and stating it plainly is more useful than a ConversationBufferMemory class that made it look like a property of the model.

Caveat: the replacement is already deprecated

Running the cell above emits LangChainDeprecationWarning: RunnableWithMessageHistory is deprecated. Use LangGraph's built-in persistence instead.

So the successor to ConversationBufferMemory is itself on the way out, in favour of a separate library with its own state model. It still works and it is still the smallest thing that demonstrates the idea, which is why this post uses it — but if you are building something to keep, look at LangGraph’s checkpointers before adopting the API above.

This is worth flagging rather than hiding, because it is the post’s own argument arriving on schedule: the composition primitives in langchain_core have held steady across the 1.x transition, while the convenience objects wrapped around them have now been replaced twice.

Caveat: replaying the whole transcript does not scale

InMemoryChatMessageHistory keeps everything and replays all of it. Since cost and latency scale with prompt length, and every model has a hard context limit, a long enough conversation gets expensive and then simply fails.

Production systems trim, summarise, or retrieve selectively — and LangChain offers those as different history implementations precisely because the naive one has this ceiling. It is also why the deleted ConversationBufferMemory was a poor default: it named the simplest strategy as though it were the strategy.

The components that remain

The vocabulary that survived the 1.x transition, and where it now lives:

Component Package Purpose
ChatPromptTemplate langchain_core.prompts Structure messages for the model
Runnable / | langchain_core.runnables Compose steps; gives invoke/batch/stream
StrOutputParser langchain_core.output_parsers Take text off the response object
RunnableWithMessageHistory langchain_core.runnables.history Replay a transcript into the prompt
ChatHuggingFace langchain_huggingface Provider adapter + chat template
RecursiveCharacterTextSplitter langchain_text_splitters Chunk documents for retrieval

The last one is the entry point to retrieval, where the same composition applies — a retriever is a Runnable, so RAG is the same pipe with a document-fetching stage in front. That is the subject of the companion post on classifying PDF documents.

Whether the abstraction earns its keep

The claim was that LangChain’s value is composition, and that deleting LLMChain demonstrated it. The evidence is that the replacement is shorter than what it replaced: three piped objects instead of a class, and a wrapper that makes explicit what memory always was.

The fair objection is that for a single prompt and a single model, none of this is necessary — chat_model.invoke(messages) would have done, and the pipe is ceremony. The abstraction starts paying when there are several stages that need to stream as a unit, batch as a unit, or be swapped independently, and it keeps paying when the provider changes and only the adapter does.

Where it stops holding is version churn. A framework whose central objects were deleted between minor-looking releases is one whose tutorials go stale faster than the models do — as this post did. Pin your versions, and prefer the langchain_core primitives, which are the part with a stability commitment.