# LangChain Essentials — The Only Things You Need Before LangGraph

> Source: <https://dev.to/santanu_mohanta_29/langchain-essentials-the-only-things-you-need-before-langgraph-4ci8>
> Published: 2026-07-31 20:03:09+00:00

Note:This article was written in July 2026 using LangChain 0.3.x. APIs may change in future versions — check the[LangChain docs]if something doesn't work.

When I started learning LangChain, I got overwhelmed. There are chains, agents, memory, retrievers, output parsers — dozens of abstractions. Then I realized: **most of them are deprecated.**

LangGraph (by the same team) has replaced the orchestration layer. But you still need LangChain's core building blocks inside LangGraph nodes. So I figured out the **minimum** you actually need to learn.

Here's everything, in one file.

| # | Topic | Why It Matters for LangGraph |
|---|---|---|
| 1 | LLM Setup | You need to talk to a model |
| 2 | Prompt Templates + LCEL | The LCEL pipe syntax carries over |
| 3 | Structured Output | Pydantic models replace the old output parsers |
| 4 | Tool Calling |
This is the big one — LangGraph agents are built around the tool-calling loop |

Let's walk through each one.

``` python
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="llama-3.3-70b-versatile",
    temperature=0.0,
    api_key=os.environ["GROQ_API_KEY"],
    base_url="https://api.groq.com/openai/v1",
)
```

`ChatOpenAI`

works with any OpenAI-compatible API. Here I'm using Groq for free, fast inference with Llama 3.3.

``` python
from langchain_core.prompts import ChatPromptTemplate

template_string = """Translate the text that is delimited by triple backticks \
into a style that is {style}.
text: ```

{text}
```

"""

prompt_template = ChatPromptTemplate.from_template(template_string)

chain = prompt_template | llm

response = chain.invoke({"style": "formal English", "text": "yo what's up"})```

The pipe (`|`

) syntax is called **LCEL** (LangChain Expression Language). It replaces the old `LLMChain`

, `SequentialChain`

, etc. Simple and composable.

The old way required `ResponseSchema`

, `StructuredOutputParser`

, and injecting format instructions into your prompt. The new way is just Pydantic:

``` python
from pydantic import BaseModel, Field

class ReviewInfo(BaseModel):
    """Information extracted from a product review."""
    gift: bool = Field(description="Was the item purchased as a gift?")
    delivery_days: int = Field(description="How many days to arrive? -1 if unknown.")
    price_value: list[str] = Field(description="Sentences about value or price.")

structured_llm = llm.with_structured_output(ReviewInfo, method="function_calling")
result = (prompt | structured_llm).invoke({"text": review})

print(result.gift)           # True  (a real bool, not the string "true")
print(result.delivery_days)  # 2     (a real int)
```

Who does what:

| Step | Who |
|---|---|
| Converting Pydantic → JSON schema | LangChain |
| Understanding schema & producing JSON | The LLM |
| Parsing JSON back into Pydantic obj | LangChain |

No more format instructions in your prompt. The LLM never sees "return JSON" — it uses function calling under the hood.

This is what LangGraph automates. Understanding it manually first makes LangGraph click instantly.

**Define tools** — just Python functions with the `@tool`

decorator:

``` python
from langchain_core.tools import tool

@tool
def get_current_weather(city: str) -> str:
    """Get the current weather for a given city."""
    return {"berlin": "17°C, cloudy"}.get(city.lower(), "No data")

@tool
def get_population(city: str) -> int:
    """Get the approximate population of a city."""
    return {"berlin": 3_700_000}.get(city.lower(), -1)
```

The `@tool`

decorator transforms these into `BaseTool`

objects. The docstring becomes the description the LLM reads to decide when to call it.

**Bind tools and call:**

```
tools = [get_current_weather, get_population]
llm_with_tools = llm.bind_tools(tools)

messages = [HumanMessage("What's the weather and population in Berlin?")]
ai_response = llm_with_tools.invoke(messages)

print(ai_response.tool_calls)
# [{"name": "get_current_weather", "args": {"city": "Berlin"}, "id": "..."},
#  {"name": "get_population", "args": {"city": "Berlin"}, "id": "..."}]
```

The LLM **does not execute** the tools. It returns a structured request asking *you* to run them.

**Execute tools and send results back:**

```
tool_map = {t.name: t for t in tools}

messages.append(ai_response)
for tc in ai_response.tool_calls:
    result = tool_map[tc["name"]].invoke(tc["args"])
    messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

final_response = llm_with_tools.invoke(messages)
print(final_response.content)
# "The weather in Berlin is 17°C and cloudy, with a population of 3.7 million."
```

This loop — **LLM decides → you execute → send result back → repeat** — is exactly what LangGraph's `ToolNode`

automates.

If you're heading to LangGraph, **don't bother learning** these legacy LangChain abstractions:

`LLMChain`

→ replaced by LCEL (`prompt | llm`

)`SequentialChain`

→ replaced by LCEL pipes`RouterChain`

→ replaced by LangGraph branching`ConversationChain`

→ replaced by LangGraph state`AgentExecutor`

→ replaced by LangGraph agent loopLangGraph is **not** a replacement for LangChain — it builds on top of it:

LangGraph came in early 2024 because LangChain's original agent/chain abstractions were too rigid — hard to customize, no support for cycles or branching, and memory was bolted on rather than built in.

The entire thing is one file: **github.com/santanu2908/langchain-essentials**

```
git clone https://github.com/santanu2908/langchain-essentials.git
cd langchain-essentials
uv sync
# add your GROQ_API_KEY to .env
uv run main.py
```

*Next up: LangGraph. If this helped you, follow along — I'll be sharing that journey too.*

I'm **Santanu Mohanta** — connect with me on [LinkedIn](https://www.linkedin.com/in/santanu29/) or check out my projects on [GitHub](https://github.com/santanu2908).
