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:
| Period | State of LangChain | Characteristic APIs |
|---|---|---|
| Oct 2022 - 2023 | Rapid experimentation | LLMChain, SequentialChain, AgentExecutor, ConversationChain |
| 2024 | Transition | LCEL (pipe operator), RunnableSequence, deprecations begin |
| Oct 2025 | LangChain 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:
LLMChaindid very little (just prompt + model), yet it forced you to learn a whole new class - Memory as a separate object:
ConversationBufferMemorywas 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 OpenAImixed 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 API | Modern API | Notes |
|---|---|---|
LLMChain(llm, prompt) | model.invoke(prompt) or prompt | model | Chains replaced by the pipe operator |
SequentialChain([chain1, chain2]) | chain1 | chain2 | The 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 OpenAI | init_chat_model("openai:gpt-4.1") | Chat models, not completion models |
ConversationBufferMemory | Agent state + MemorySaver | Built into LangGraph |
OutputParser | with_structured_output() | Native in chat models |
CallbackHandler | Middleware system (Module 4) | More powerful and composable |
APIs that are still current
| API | Status | Notes |
|---|---|---|
RunnablePassthrough | ✅ Current | Part of LCEL, still supported |
ChatPromptTemplate | ✅ Current | Prompt templates still work |
@tool decorator | ✅ Current | The standard way to create tools |
bind_tools() | ✅ Current | Binding tools to models |
ToolMessage | ✅ Current | Tool response messages |
Pipe operator (|) | ✅ Current | LCEL is still core to LangChain |
with_structured_output() | ✅ Current | Native 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?
| Aspect | Legacy | Modern |
|---|---|---|
| Model | OpenAI() (completion model) | init_chat_model() (chat model) |
| Chain class | LLMChain(llm, prompt) | prompt | model (pipe operator) |
| Execution | chain.run("topic") | chain.invoke({"topic": "topic"}) |
| Result | A plain string | AIMessage (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?
| Aspect | Legacy (AgentExecutor) | Modern (create_agent) |
|---|---|---|
| Setup | 15+ lines (prompt, agent, executor) | 1 line: create_agent(model, tools) |
| Prompt | Manual, with MessagesPlaceholder | Automatic (or system_prompt=) |
| Execution | executor.invoke({"input": "..."}) | agent.invoke({"messages": [...]}) |
| Streaming | Partial, with AgentAction chunks | Native with agent.stream() |
| Middleware | Not supported | @wrap_model_call, @before_model, etc. |
| Customization | Subclass AgentExecutor | Composable 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 + Model | prompt | model or model.invoke() |
SequentialChain(chains=[...]) | Chaining steps | step1 | step2 or Python variables |
AgentExecutor(agent, tools) | Agent with tools | create_agent(model, tools) |
ConversationChain(memory=...) | Chat with memory | create_agent + MemorySaver |
PydanticOutputParser | Structured output | model.with_structured_output(Schema) |
CallbackHandler | Intercepting execution | Middleware (@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
| Feature | Available in legacy | Available 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 parsers | ✅ with_structured_output |
| Checkpointing | ❌ Memory objects | ✅ MemorySaver, PostgresSaver |
| Multi-provider | ❌ One import per provider | ✅ init_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_agentinstead ofAgentExecutor— setup in 1 lineinit_chat_modelinstead ofChatOpenAI— multi-providerwith_structured_outputinstead ofOutputParser— a reliable structured reportagent.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)PromptTemplate→ChatPromptTemplate.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 theAIMessage
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:
SequentialChainis gone — the steps connect through Python variables- Each step is a pipe:
prompt | model | StrOutputParser() - The result of
step1is passed tostep2by 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")ChatPromptTemplatewithMessagesPlaceholder→prompt="..."as a stringcreate_openai_tools_agent+AgentExecutor→create_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:
PydanticOutputParseris gone — you no longer need an intermediate parserPromptTemplatewithformat_instructionsis gone — the model knows which fields to generateprompt | llm | parser→model.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")→ useinit_chat_model - ❌
LLMChain(llm, prompt)→ use the pipe operator - ❌
create_openai_tools_agent+AgentExecutor→ usecreate_agent - ❌
MessagesPlaceholder("agent_scratchpad")→ not needed withcreate_agent - ❌
chain.run()→ usechain.invoke() - ✅
@tooldecorator — 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:
ChatOpenAI→init_chat_model(multi-provider)LLMChain→ pipe operator (prompt | model | StrOutputParser())create_openai_tools_agent+AgentExecutor→create_agentchain.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:
AgentExecutor→create_agentConversationBufferMemory→checkpointer=MemorySaver()+thread_idverbose=True→agent.stream()withstream_mode="messages"(better than verbose)- "Remember products" → The
thread_idpersists 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
- LangChain Migration Guide — Official guide to migrating chains to LCEL and the modern APIs
- LangChain v1.0 Release Notes — Official v1.0 announcement with the list of changes
- create_agent API Reference — Reference for the modern agent API
- init_chat_model — Guide to universal model initialization
- Structured Output — Official structured output guide, no parsers
- LangGraph MemorySaver — Reference for modern checkpointing
Module 3 — LangChain & LangGraph: From Chains to Agents