Module 3: Agents with create_agent

Legacy vs Modern: API Mapping

Capsule overview

If you search "LangChain tutorial" on Google, YouTube, or Medium, 80% of the results you find use APIs that no longer exist. LLMChain, SequentialChain, AgentExecutor, ConversationChain, ConversationBufferMemory — all of it was the official way to use LangChain up until October 2025. After LangChain v1.0 shipped, these APIs entered deprecation and were replaced by simpler, more powerful, better-designed alternatives.

The problem is that the internet has a long memory. Tutorials, courses, and Stack Overflow answers don't update themselves. If you copy code from a 2024 tutorial and paste it into your project running LangChain v1.2+, you're going to get deprecation warnings, import errors, or unexpected behavior. And worse: you're going to learn patterns you'll have to unlearn.

This capsule is your translation dictionary. It maps EVERY legacy API to its modern equivalent, with before/after code examples you can copy directly. After reading it, you'll be able to take any old LangChain tutorial and translate it into the modern style you learned in this module.


Why the legacy APIs exist

LangChain evolved extremely fast. It was created by Harrison Chase in October 2022, and in its first two years it went through radical architectural changes:

PeriodState of LangChainCharacteristic APIs
Oct 2022 - 2023Rapid experimentationLLMChain, SequentialChain, AgentExecutor, ConversationChain
2024TransitionLCEL (pipe operator), RunnableSequence, deprecations begin
Oct 2025LangChain v1.0 (stable)create_agent, init_chat_model, with_structured_output
2026+LangChain v1.2+Middleware system, @dynamic_prompt, consolidated APIs

The key is understanding that the legacy APIs weren't "bad" — they were the best options at the time. But LangChain discovered that:

  • Too many abstractions: LLMChain did very little (just prompt + model), yet it forced you to learn a whole new class
  • Memory as a separate object: ConversationBufferMemory was fragile and hard to debug
  • AgentExecutor was limited: it didn't support streaming well, had no middleware, and was hard to customize
  • Fragmented imports: from langchain.llms import OpenAI mixed completion models with chat models

v1.0 simplified all of this. Instead of learning 15 different classes, you now use 3-4 functions that cover 95% of the cases.


Complete equivalence table

This is the central reference of the capsule. Save it — you're going to need it every time you run into legacy code online.

Replaced APIs (don't use anymore)

Legacy APIModern APINotes
LLMChain(llm, prompt)model.invoke(prompt) or prompt | modelChains replaced by the pipe operator
SequentialChain([chain1, chain2])chain1 | chain2The pipe operator chains runnables
AgentExecutor(agent, tools)create_agent(model, tools)create_agent handles everything internally
ConversationChain(llm, memory=...)Agent + checkpointing (Module 8)Memory is now checkpointing
from langchain.llms import OpenAIinit_chat_model("openai:gpt-4.1")Chat models, not completion models
ConversationBufferMemoryAgent state + MemorySaverBuilt into LangGraph
OutputParserwith_structured_output()Native in chat models
CallbackHandlerMiddleware system (Module 4)More powerful and composable

APIs that are still current

APIStatusNotes
RunnablePassthrough✅ CurrentPart of LCEL, still supported
ChatPromptTemplate✅ CurrentPrompt templates still work
@tool decorator✅ CurrentThe standard way to create tools
bind_tools()✅ CurrentBinding tools to models
ToolMessage✅ CurrentTool response messages
Pipe operator (|)✅ CurrentLCEL is still core to LangChain
with_structured_output()✅ CurrentNative structured output

Rule of thumb: If the concept you see in a tutorial involves Chain in the class name (except ChatPromptTemplate), it probably has a simpler modern replacement.


Translation 1: LLMChain → invoke / pipe operator

LLMChain was the basic way to connect a prompt with a model. It required creating a class instance, handing it the LLM and the prompt, and then calling .run() or .invoke().

Legacy code

# ❌ LEGACY — Don't use with LangChain v1.2+
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

llm = OpenAI(temperature=0.7)
prompt = PromptTemplate(
    input_variables=["topic"],
    template="Write a joke about {topic}"
)

chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run("programming")
print(result)

Modern code (option 1: direct invoke)

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

model = init_chat_model("openai:gpt-4.1-mini", temperature=0.7)

result = model.invoke("Write a joke about programming")
print(result.content)
# Expected output: Why do programmers prefer dark mode?
# Because light attracts bugs.

Modern code (option 2: pipe operator with a template)

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate

model = init_chat_model("openai:gpt-4.1-mini", temperature=0.7)
prompt = ChatPromptTemplate.from_template("Write a joke about {topic}")

chain = prompt | model
result = chain.invoke({"topic": "programming"})
print(result.content)
# Expected output: Why do programmers prefer dark mode?
# Because light attracts bugs.

What changed?

AspectLegacyModern
ModelOpenAI() (completion model)init_chat_model() (chat model)
Chain classLLMChain(llm, prompt)prompt | model (pipe operator)
Executionchain.run("topic")chain.invoke({"topic": "topic"})
ResultA plain stringAIMessage (with .content, metadata)

Translation 2: SequentialChain → pipe operator

SequentialChain connected multiple LLMChain objects in sequence. The output of one was the input of the next.

Legacy code

# ❌ LEGACY — Don't use with LangChain v1.2+
from langchain.chains import LLMChain, SequentialChain
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

llm = OpenAI()

chain1 = LLMChain(
    llm=llm,
    prompt=PromptTemplate(
        input_variables=["topic"],
        template="Generate 3 ideas about {topic}. Reply with the ideas only."
    ),
    output_key="ideas"
)

chain2 = LLMChain(
    llm=llm,
    prompt=PromptTemplate(
        input_variables=["ideas"],
        template="Pick the best idea from these and explain why:\n{ideas}"
    ),
    output_key="best_idea"
)

sequential = SequentialChain(
    chains=[chain1, chain2],
    input_variables=["topic"],
    output_variables=["ideas", "best_idea"]
)

result = sequential({"topic": "AI applications"})
print(result["best_idea"])

Modern code

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model("openai:gpt-4.1-mini")

step1 = ChatPromptTemplate.from_template(
    "Generate 3 ideas about {topic}. Reply with the ideas only."
) | model | StrOutputParser()

step2 = ChatPromptTemplate.from_template(
    "Pick the best idea from these and explain why:\n{ideas}"
) | model | StrOutputParser()

ideas = step1.invoke({"topic": "AI applications"})
best = step2.invoke({"ideas": ideas})
print(best)
# Expected output: The best idea is [X] because...

The pipe operator (|) removes the need for a SequentialChain class. If you need to pass data between steps, you just use ordinary Python variables. It's more explicit, easier to debug, and doesn't require learning new classes.


Translation 3: AgentExecutor → create_agent

This is the most important change in LangChain v1.2+. AgentExecutor was the official way to run agents, but it had serious limitations: partial streaming, no middleware support, and it was hard to customize.

Legacy code

# ❌ LEGACY — Don't use with LangChain v1.2+
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def calculate(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))

llm = ChatOpenAI(model="gpt-4")

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    MessagesPlaceholder(variable_name="chat_history", optional=True),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_openai_tools_agent(llm, [calculate], prompt)
executor = AgentExecutor(agent=agent, tools=[calculate], verbose=True)

result = executor.invoke({"input": "What's 25 * 4?"})
print(result["output"])

Modern code

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langgraph.prebuilt import create_agent

@tool
def calculate(expression: str) -> str:
    """Evaluate a math expression."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")

agent = create_agent(model, [calculate])

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What's 25 * 4?"}]}
)
print(result["messages"][-1].content)
# Expected output: 25 × 4 = 100

What changed?

AspectLegacy (AgentExecutor)Modern (create_agent)
Setup15+ lines (prompt, agent, executor)1 line: create_agent(model, tools)
PromptManual, with MessagesPlaceholderAutomatic (or system_prompt=)
Executionexecutor.invoke({"input": "..."})agent.invoke({"messages": [...]})
StreamingPartial, with AgentAction chunksNative with agent.stream()
MiddlewareNot supported@wrap_model_call, @before_model, etc.
CustomizationSubclass AgentExecutorComposable middleware

Translation 4: ConversationChain + Memory → Agent with checkpointing

ConversationChain with ConversationBufferMemory was how you kept conversation history. It was fragile — the memory lived in a separate object, it was hard to persist, and it vanished on restart.

Legacy code

# ❌ LEGACY — Don't use with LangChain v1.2+
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4")
memory = ConversationBufferMemory()

conversation = ConversationChain(llm=llm, memory=memory)

print(conversation.predict(input="Hi, my name is Ana"))
print(conversation.predict(input="What's my name?"))

Modern code

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langgraph.prebuilt import create_agent
from langgraph.checkpoint.memory import MemorySaver

model = init_chat_model("openai:gpt-4.1-mini")

agent = create_agent(model, tools=[], checkpointer=MemorySaver())

config = {"configurable": {"thread_id": "conversation-1"}}

result1 = agent.invoke(
    {"messages": [{"role": "user", "content": "Hi, my name is Ana"}]},
    config=config,
)
print(result1["messages"][-1].content)

result2 = agent.invoke(
    {"messages": [{"role": "user", "content": "What's my name?"}]},
    config=config,
)
print(result2["messages"][-1].content)
# Expected output:
# Hi Ana! How can I help you?
# Your name is Ana.

With MemorySaver and a thread_id, the conversation persists automatically. You don't need to manage a separate Memory object. And in production, you can swap MemorySaver() for PostgresSaver to persist to a database (you'll see that in Module 8).


Translation 5: OutputParser → with_structured_output

OutputParser required the model to produce text in a specific format (JSON, YAML, etc.) and then parsed it by hand. It was error-prone because it depended on the model respecting the exact format.

Legacy code

# ❌ LEGACY — Don't use with LangChain v1.2+
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field

class Movie(BaseModel):
    title: str = Field(description="Movie title")
    year: int = Field(description="Release year")
    genre: str = Field(description="Main genre")

parser = PydanticOutputParser(pydantic_object=Movie)

prompt = PromptTemplate(
    template="Recommend a science fiction movie.\n{format_instructions}",
    input_variables=[],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

llm = ChatOpenAI(model="gpt-4")
chain = prompt | llm | parser
result = chain.invoke({})
print(result)

Modern code

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class Movie(BaseModel):
    title: str = Field(description="Movie title")
    year: int = Field(description="Release year")
    genre: str = Field(description="Main genre")

model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(Movie)

result = structured_model.invoke("Recommend a science fiction movie.")
print(result)
# Expected output: title='Blade Runner 2049' year=2017 genre='Science fiction'
print(f"{result.title} ({result.year}) — {result.genre}")
# Expected output: Blade Runner 2049 (2017) — Science fiction

with_structured_output() uses the model's native tool calling to guarantee the format. It doesn't depend on the model producing textual JSON — it's significantly more reliable.


How to read legacy tutorials

When you find a LangChain tutorial online, follow these steps to figure out whether it uses legacy APIs:

Step 1: Check the imports

# 🚩 Signs of legacy code:
from langchain.llms import OpenAI          # → use init_chat_model
from langchain.chains import LLMChain      # → use the pipe operator
from langchain.agents import AgentExecutor # → use create_agent
from langchain.memory import ...           # → use checkpointing
from langchain.output_parsers import ...   # → use with_structured_output

Step 2: Identify the pattern

If you see...The pattern is...Replace it with...
LLMChain(llm, prompt)Prompt + Modelprompt | model or model.invoke()
SequentialChain(chains=[...])Chaining stepsstep1 | step2 or Python variables
AgentExecutor(agent, tools)Agent with toolscreate_agent(model, tools)
ConversationChain(memory=...)Chat with memorycreate_agent + MemorySaver
PydanticOutputParserStructured outputmodel.with_structured_output(Schema)
CallbackHandlerIntercepting executionMiddleware (@before_model, etc.)

Step 3: Translate

Use the translations in this capsule as your reference. 90% of legacy tutorials fall into one of the 5 patterns above. If you find a case that isn't here, check the official migration guide.


Why migrate to the modern APIs

This isn't just about "staying current." There are concrete technical reasons to use the modern APIs:

1. Deprecation warnings and future breakage

The legacy APIs emit warnings every time you use them:

LangChainDeprecationWarning: The class `LLMChain` was deprecated in
LangChain 0.1.17 and will be removed in 1.0. Use RunnableSequence, e.g.,
`prompt | llm` instead.

In future versions of LangChain, these APIs will be removed, not just deprecated. Your code will stop working.

2. Features that don't exist in legacy

FeatureAvailable in legacyAvailable in modern
Native streaming❌ Partial✅ Complete
Middleware system❌ No@wrap_model_call, etc.
Dynamic prompts❌ No@dynamic_prompt
Dynamic tools❌ No✅ Middleware
Native structured output❌ Manual parserswith_structured_output
Checkpointing❌ Memory objectsMemorySaver, PostgresSaver
Multi-provider❌ One import per providerinit_chat_model("provider:model")

3. Simpler, more maintainable code

The same agent in legacy vs modern:

# Legacy: ~20 lines of setup
# prompt, agent, executor, memory, callbacks...

# Modern: ~5 lines
model = init_chat_model("openai:gpt-4.1-mini")
agent = create_agent(model, tools, prompt="You are a helpful assistant.")
result = agent.invoke({"messages": [{"role": "user", "content": "Hi"}]})

4. Better debugging and observability

The modern APIs integrate natively with LangSmith (Module 12). The traces show every step of the agent clearly. With the legacy APIs, debugging was significantly harder.


Connection with the project

In this module's project (Capsule 08), you'll build a research agent using modern APIs exclusively:

  • create_agent instead of AgentExecutor — setup in 1 line
  • init_chat_model instead of ChatOpenAI — multi-provider
  • with_structured_output instead of OutputParser — a reliable structured report
  • agent.stream() instead of callbacks — native streaming

If at some point you look something up online and find legacy code, you now know exactly how to translate it.


Troubleshooting

Problem 1: LangChainDeprecationWarning when using LLMChain

LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17

Cause: You're using LLMChain in your code, or importing a library that uses it internally.

Fix: Replace LLMChain(llm=model, prompt=prompt) with the pipe operator:

chain = prompt | model
result = chain.invoke({"variable": "value"})

Problem 2: ImportError: cannot import name 'OpenAI' from 'langchain.llms'

Cause: In LangChain v1.2+, completion models (langchain.llms) were removed or moved. Chat models are the standard.

Fix: Use init_chat_model instead of importing directly:

from langchain.chat_models import init_chat_model
model = init_chat_model("openai:gpt-4.1-mini")

Problem 3: AgentExecutor doesn't support streaming the way I expect

Cause: AgentExecutor.stream() emits AgentAction and AgentFinish chunks, which are awkward to process. The format isn't intuitive.

Fix: Migrate to create_agent, which supports native streaming:

from langgraph.prebuilt import create_agent

agent = create_agent(model, tools)
for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "Hi"}]},
    stream_mode="messages",
):
    # Chunks are standard messages, easy to process
    print(chunk)

Problem 4: ConversationBufferMemory doesn't persist across restarts

Cause: ConversationBufferMemory stores everything in RAM. When the process restarts, it's gone.

Fix: Use MemorySaver with create_agent for in-memory persistence, or PostgresSaver for durable persistence:

from langgraph.checkpoint.memory import MemorySaver

agent = create_agent(model, tools=[], checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "user-123"}}
result = agent.invoke({"messages": [...]}, config=config)

Problem 5: A tutorial uses create_openai_tools_agent, which I don't recognize

Cause: create_openai_tools_agent was an intermediate function between the legacy API and the modern one. It created an agent compatible with AgentExecutor.

Fix: Replace the whole create_openai_tools_agent + AgentExecutor pattern with create_agent:

# Legacy:
# agent = create_openai_tools_agent(llm, tools, prompt)
# executor = AgentExecutor(agent=agent, tools=tools)

# Modern:
agent = create_agent(model, tools, prompt="Your system prompt here")

Exercises

Exercise 1: Translate LLMChain to the pipe operator (Basic)

You have this legacy code that generates a haiku about a topic. Translate it to modern APIs using init_chat_model and the pipe operator.

# Legacy code to translate:
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

llm = OpenAI(temperature=0.9)
prompt = PromptTemplate(
    input_variables=["topic"],
    template="Write a haiku about {topic}. The haiku only, nothing else."
)
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run("the sea")
print(result)
See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model("openai:gpt-4.1-mini", temperature=0.9)
prompt = ChatPromptTemplate.from_template(
    "Write a haiku about {topic}. The haiku only, nothing else."
)

chain = prompt | model | StrOutputParser()
result = chain.invoke({"topic": "the sea"})
print(result)
# Expected output:
# Waves upon the shore
# salt and sea foam as day ends
# the tide always turns

Changes made:

  • OpenAI()init_chat_model("openai:gpt-4.1-mini") (chat model, not completion)
  • PromptTemplateChatPromptTemplate.from_template() (for chat models)
  • LLMChain(llm, prompt)prompt | model | StrOutputParser() (pipe operator)
  • chain.run("the sea")chain.invoke({"tema": "the sea"}) (a dict of inputs)
  • StrOutputParser() pulls the string out of the AIMessage

Exercise 2: Translate SequentialChain (Basic)

Translate this legacy code that generates ideas and then evaluates them:

# Legacy code to translate:
from langchain.chains import LLMChain, SequentialChain
from langchain.prompts import PromptTemplate
from langchain.llms import OpenAI

llm = OpenAI()
chain1 = LLMChain(
    llm=llm,
    prompt=PromptTemplate(input_variables=["product"], template="Generate 3 names for a {product} product"),
    output_key="names"
)
chain2 = LLMChain(
    llm=llm,
    prompt=PromptTemplate(input_variables=["names"], template="Pick the best name from: {names} and explain why"),
    output_key="winner"
)
seq = SequentialChain(chains=[chain1, chain2], input_variables=["product"], output_variables=["names", "winner"])
result = seq({"product": "organic coffee"})
print(result["winner"])
See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = init_chat_model("openai:gpt-4.1-mini")

step1 = (
    ChatPromptTemplate.from_template(
        "Generate 3 names for a {product} product"
    )
    | model
    | StrOutputParser()
)

step2 = (
    ChatPromptTemplate.from_template(
        "Pick the best name from: {names} and explain why"
    )
    | model
    | StrOutputParser()
)

names = step1.invoke({"product": "organic coffee"})
print(f"Generated names:\n{names}\n")

winner = step2.invoke({"names": names})
print(f"Winner:\n{winner}")
# Expected output:
# Generated names:
# 1. Pure Root  2. Living Earth  3. Noble Bean
#
# Winner:
# The best name is "Living Earth" because it evokes nature,
# freshness and the organic origin of the product...

Changes made:

  • SequentialChain is gone — the steps connect through Python variables
  • Each step is a pipe: prompt | model | StrOutputParser()
  • The result of step1 is passed to step2 by hand
  • More explicit, easier to debug

Exercise 3: Translate AgentExecutor to create_agent (Medium)

Translate this legacy agent with two tools to the modern API:

# Legacy code to translate:
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get the weather for a city."""
    data = {"Madrid": "22°C, sunny", "London": "14°C, cloudy"}
    return data.get(city, f"No data for {city}")

@tool
def calculator(expression: str) -> str:
    """Compute a math expression."""
    return str(eval(expression))

llm = ChatOpenAI(model="gpt-4", temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are an assistant with access to tools."),
    MessagesPlaceholder(variable_name="chat_history", optional=True),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_openai_tools_agent(llm, [get_weather, calculator], prompt)
executor = AgentExecutor(agent=agent, tools=[get_weather, calculator], verbose=True)
result = executor.invoke({"input": "Weather in Madrid, and what's 100*1.16?"})
print(result["output"])
See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langgraph.prebuilt import create_agent

@tool
def get_weather(city: str) -> str:
    """Get the weather for a city."""
    data = {"Madrid": "22°C, sunny", "London": "14°C, cloudy"}
    return data.get(city, f"No data for {city}")

@tool
def calculator(expression: str) -> str:
    """Compute a math expression."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini", temperature=0)

agent = create_agent(
    model,
    [get_weather, calculator],
    prompt="You are an assistant with access to tools.",
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Weather in Madrid, and what's 100*1.16?"}]}
)
print(result["messages"][-1].content)
# Expected output: It's 22°C and sunny in Madrid. 100 × 1.16 = 116.

Changes made:

  • 15 lines of setup → 3 lines
  • ChatOpenAI(model="gpt-4")init_chat_model("openai:gpt-4.1-mini")
  • ChatPromptTemplate with MessagesPlaceholderprompt="..." as a string
  • create_openai_tools_agent + AgentExecutorcreate_agent(model, tools)
  • executor.invoke({"input": "..."})agent.invoke({"messages": [...]})
  • result["output"]result["messages"][-1].content

Exercise 4: Translate OutputParser to with_structured_output (Medium)

Translate this code that uses PydanticOutputParser to extract structured information:

# Legacy code to translate:
from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import List

class BookReview(BaseModel):
    title: str = Field(description="Book title")
    author: str = Field(description="Author")
    rating: float = Field(description="Rating from 1 to 5")
    pros: List[str] = Field(description="Positive aspects")
    cons: List[str] = Field(description="Negative aspects")

parser = PydanticOutputParser(pydantic_object=BookReview)
prompt = PromptTemplate(
    template="Generate a review of the book '{book}'.\n{format_instructions}",
    input_variables=["book"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

llm = ChatOpenAI(model="gpt-4")
chain = prompt | llm | parser
result = chain.invoke({"book": "1984 by George Orwell"})
print(f"{result.title} by {result.author}: {result.rating}/5")
See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field

class BookReview(BaseModel):
    title: str = Field(description="Book title")
    author: str = Field(description="Author")
    rating: float = Field(description="Rating from 1 to 5")
    pros: list[str] = Field(description="Positive aspects")
    cons: list[str] = Field(description="Negative aspects")

model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(BookReview)

result = structured_model.invoke("Generate a review of the book '1984 by George Orwell'.")
print(f"{result.title} by {result.author}: {result.rating}/5")
print(f"Pros: {', '.join(result.pros)}")
print(f"Cons: {', '.join(result.cons)}")
# Expected output:
# 1984 by George Orwell: 4.8/5
# Pros: Gripping narrative, Deep reflection on power, Memorable characters
# Cons: Bleak tone that can be overwhelming, Slow pacing in some passages

Changes made:

  • PydanticOutputParser is gone — you no longer need an intermediate parser
  • PromptTemplate with format_instructions is gone — the model knows which fields to generate
  • prompt | llm | parsermodel.with_structured_output(BookReview) + .invoke()
  • More reliable: it uses native tool calling instead of depending on the model producing textual JSON
  • List[str]list[str] (Python 3.11+ syntax)

Exercise 5: Identify and translate mixed code (Hard)

This code mixes legacy and modern APIs. Identify which parts are legacy, which are modern, and rewrite the whole thing with modern APIs:

# Mixed code to translate:
from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain.prompts import MessagesPlaceholder

@tool
def search(query: str) -> str:
    """Search for information."""
    return f"Result for: {query}"

llm = ChatOpenAI(model="gpt-4", temperature=0)

summary_chain = LLMChain(
    llm=llm,
    prompt=ChatPromptTemplate.from_template("Summarize this: {text}")
)

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a researcher."),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_openai_tools_agent(llm, [search], prompt)
executor = AgentExecutor(agent=agent, tools=[search])

result = executor.invoke({"input": "Search about Python"})
summary = summary_chain.run(result["output"])
print(summary)
See solution

Legacy parts identified:

  • ChatOpenAI(model="gpt-4") → use init_chat_model
  • LLMChain(llm, prompt) → use the pipe operator
  • create_openai_tools_agent + AgentExecutor → use create_agent
  • MessagesPlaceholder("agent_scratchpad") → not needed with create_agent
  • chain.run() → use chain.invoke()
  • @tool decorator — current
  • ChatPromptTemplate — current
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langgraph.prebuilt import create_agent

@tool
def search(query: str) -> str:
    """Search for information."""
    return f"Result for: {query}"

model = init_chat_model("openai:gpt-4.1-mini", temperature=0)

agent = create_agent(model, [search], prompt="You are a researcher.")

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Search about Python"}]}
)
agent_output = result["messages"][-1].content

summary_chain = (
    ChatPromptTemplate.from_template("Summarize this: {text}")
    | model
    | StrOutputParser()
)
summary = summary_chain.invoke({"text": agent_output})
print(summary)
# Expected output: Python is a high-level, interpreted,
# general-purpose programming language...

Changes made:

  • ChatOpenAIinit_chat_model (multi-provider)
  • LLMChain → pipe operator (prompt | model | StrOutputParser())
  • create_openai_tools_agent + AgentExecutorcreate_agent
  • chain.run()chain.invoke({"text": ...})
  • The agent's result: result["output"]result["messages"][-1].content

Exercise 6: Build a modern agent from a legacy spec (Hard)

Read this spec, written for legacy APIs, and build it from scratch using modern APIs. Do NOT translate line by line — build it from scratch.

Spec:

"Create an AgentExecutor with ConversationBufferMemory that uses two tools: one to look up product prices and another to calculate discounts. The agent must remember products mentioned earlier. Use verbose=True to see the process."

See solution
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langgraph.prebuilt import create_agent
from langgraph.checkpoint.memory import MemorySaver

@tool
def get_price(product: str) -> str:
    """Get the price of a product."""
    prices = {
        "laptop": 999.99,
        "mouse": 29.99,
        "keyboard": 79.99,
        "monitor": 349.99,
        "headphones": 149.99,
    }
    product_lower = product.lower().strip()
    if product_lower not in prices:
        available = ", ".join(sorted(prices.keys()))
        return f"Product '{product}' not found. Available: {available}"
    return f"{product_lower}: ${prices[product_lower]:.2f}"

@tool
def calculate_discount(price: float, percent: float) -> str:
    """Calculate the price with the discount applied."""
    if percent < 0 or percent > 100:
        return f"Percentage {percent} is invalid. It must be between 0 and 100."
    discount = price * (percent / 100)
    final = price - discount
    return f"Original: ${price:.2f} → Discount: ${discount:.2f} ({percent}%) → Final: ${final:.2f}"

model = init_chat_model("openai:gpt-4.1-mini")

agent = create_agent(
    model,
    [get_price, calculate_discount],
    prompt="You are a shopping assistant. You help look up prices and calculate discounts.",
    checkpointer=MemorySaver(),
)

config = {"configurable": {"thread_id": "shopping-1"}}

result1 = agent.invoke(
    {"messages": [{"role": "user", "content": "How much does a laptop cost?"}]},
    config=config,
)
print(f"R1: {result1['messages'][-1].content}")

for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "Apply a 20% discount to it"}]},
    config=config,
    stream_mode="messages",
):
    msg, metadata = chunk
    if msg.content and metadata.get("langgraph_node") == "agent":
        print(msg.content, end="", flush=True)
print()

# Expected output:
# R1: The laptop costs $999.99.
# With a 20% discount, the laptop goes from $999.99 to $799.99 (you save $200.00).

How it maps to the legacy spec:

  • AgentExecutorcreate_agent
  • ConversationBufferMemorycheckpointer=MemorySaver() + thread_id
  • verbose=Trueagent.stream() with stream_mode="messages" (better than verbose)
  • "Remember products" → The thread_id persists the full history

Summary

In this capsule you learned:

  • LangChain evolved radically between 2022 and 2025 — the legacy APIs exist because they were the framework's original version
  • LLMChain is replaced by model.invoke() or the pipe operator (prompt | model)
  • SequentialChain is replaced by the pipe operator or Python variables between steps
  • AgentExecutor is replaced by create_agent(model, tools) — from ~20 lines to ~3
  • ConversationChain + Memory is replaced by create_agent + MemorySaver (checkpointing)
  • OutputParser is replaced by with_structured_output() — more reliable because it uses native tool calling
  • What's still current: @tool, bind_tools, the pipe operator, ChatPromptTemplate, RunnablePassthrough
  • How to read legacy tutorials: check the imports, identify the pattern, translate with the equivalence table
  • Why migrate: deprecation warnings, features exclusive to the modern APIs, simpler code, better debugging

Next capsule: Project — you'll build a research agent with tools using exclusively the modern APIs you learned throughout this module.


Additional resources

  1. LangChain Migration Guide — Official guide to migrating chains to LCEL and the modern APIs
  2. LangChain v1.0 Release Notes — Official v1.0 announcement with the list of changes
  3. create_agent API Reference — Reference for the modern agent API
  4. init_chat_model — Guide to universal model initialization
  5. Structured Output — Official structured output guide, no parsers
  6. LangGraph MemorySaver — Reference for modern checkpointing

Module 3 — LangChain & LangGraph: From Chains to Agents