Pydantic AI vs LangGraph: Understanding the Two Different Problems They Solve
Introduction
Pydantic AI vs LangGraph: Understanding the Two Different Problems They Solve
Photo by Igor Omilaev on Unsplash
Introduction
Building AI agents used to mean writing a single prompt and hoping for a usable answer. That approach breaks down quickly once an application
- needs to call tools
- keep track of state across multiple steps
- recover from failures, or
- produce output that the rest of the system can trust.
Two frameworks have become popular for solving this next stage of agent development: Pydantic AI and LangGraph. They are often mentioned together, but they are not solving the same problem, and understanding that difference is the key to using either one correctly.
This article explains what each framework does, how it works internally, where it fits in a real system, and how the two relate to each other.
Why These Two Frameworks Matter
The shift from “one prompt, one answer” to “agent system” introduces four new requirements:
- The agent must be able to plan and call tools, not just respond.
- The output must be predictable enough for other code to consume it.
- The system must keep state across multiple steps or turns.
- The system must recover from failure and, in some cases, wait for a human decision.
Pydantic AI and LangGraph each address a different subset of this list.
- Pydantic AI focuses on requirements 1 and 2: it makes the agent’s interface clean, typed, and validated.
- LangGraph focuses on requirements 3 and 4: it makes the workflow explicit, stateful, and durable.
For a beginner, this means less time spent writing fragile prompt-parsing code and more time writing normal Python. For an experienced developer, this means a choice between a compact agent abstraction (Pydantic AI) and a full orchestration runtime (LangGraph), depending on how much control the application needs.
Pydantic AI: Making Each Agent Response Reliable
Core Idea
Pydantic AI is built around a single object: the Agent. When you create an Agent, you define:
- the model
- the instructions
- the tools it can call
- its dependencies, and
- most importantly the shape of its output, using a Pydantic model.
The main value of this design is structured output. Instead of asking the model to “please respond in JSON” and then hoping the string can be parsed, Pydantic AI pushes the model toward a response that matches a schema you defined in advance. If the response does not match, the framework can retry or raise an error, so your application never receives data in an unexpected shape.
Where It Fits
Pydantic AI is most useful when the task is largely linear:
- Extraction (pull structured fields out of unstructured text)
- Classification (assign a label from a fixed set)
- Routing (decide which downstream process should handle a request)
- Short, focused assistants that return one clear answer per call
Because it is built on ordinary Python classes and functions, code written with Pydantic AI reads like a normal application module. This makes it easy to test, easy to review, and easy to maintain in a production codebase — properties that matter more once a team, not just one developer, owns the code.
Pydantic AI also supports tools (functions the agent can call during its run) and multi-agent patterns, so it is not restricted to single-turn chatbots. The important architectural point is that validation happens at the boundary: the language model itself can still be flexible or creative internally, but the data that leaves the agent and enters your application is guaranteed to match the schema you defined.
Example: CoffeeShop Agent in Pydantic AI
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
class CoffeeShopResult(BaseModel):
name: str
area: str
open_now: bool
reason: str
class CoffeeDeps:
city: str
coffee_agent = Agent(
model="openai:gpt-5",
deps_type=CoffeeDeps,
output_type=CoffeeShopResult,
instructions="You help users find a suitable coffee shop."
)
@coffee_agent.tool
async def get_city(ctx: RunContext[CoffeeDeps]) -> str:
return ctx.deps.city
result = await coffee_agent.run(
"Suggest a calm coffee shop for a work call.",
deps=CoffeeDeps(city="Bengaluru")
)
print(result.output)
How this works, step by step:
CoffeeShopResultdefines the exact shape of the answer the application expects: a name, an area, an open/closed flag, and a reason. Nothing else is accepted as a valid response.CoffeeDepsis a dependency object — data the agent needs while running but that is not part of the conversation itself (here, the user's city).- The
Agentis created once, with the model, the dependency type, the output type, and instructions describing its role. @coffee_agent.toolregisters a function the agent can call during its reasoning process. In this example,get_cityjust returns a value from the dependency object, but in a real system it could call a maps API or a search service.coffee_agent.run(...)executes the agent with a specific user query and a specific set of dependencies. The result is guaranteed to be an instance ofCoffeeShopResult, soresult.output.name,result.output.open_now, and so on can be used directly without manual parsing or validation.
This pattern — define the schema first, then let the agent fill it using tools and instructions — is the core habit to learn with Pydantic AI.
LangGraph: Making the Whole Workflow Explicit and Durable
Core Idea
LangGraph uses a graph model for agent systems. In graph terms, a workflow is represented as G = (V, E), where each node V performs a unit of work and each edge E decides what happens next, based on the current state. State is not hidden inside a conversation history; it is an explicit, typed object that flows through the graph from node to node.
This design exists because real agent systems are frequently not linear. They may need to branch based on a classification result, loop until a condition is satisfied, pause and wait for a human decision, or resume after an interruption (such as a server restart or a long-running background task).
Key Features and Why They Matter
- State management: State is a defined structure (in Python, typically a
TypedDictor similar) that every node reads from and writes to. This makes it clear at every point in the workflow exactly what data is available. - Checkpointing and persistence: Checkpointers save the state of a thread so a run can resume later, even after a failure or restart. Stores can keep long-term data across multiple threads or sessions. This is what allows a LangGraph-based agent to survive beyond a single request-response cycle.
- Conditional edges: Instead of always moving to the next step, an edge can inspect the state and decide which node runs next. This is how branching logic (if/else at the workflow level) is implemented.
- Human-in-the-loop control: The graph can pause at a node and wait for a human to approve, reject, or modify a decision before continuing. This is critical for high-stakes actions such as financial approvals, escalations, or irreversible operations.
- Streaming: Intermediate results can be streamed out as the graph executes, rather than only returning a final answer.
Because of these features, LangGraph behaves less like a single-agent helper library and more like an application runtime for multi-step processes. That is the main reason it is considered strong for long-running agents, complex business workflows, and any system where reliability over time matters more than simplicity of code.
Example: CoffeeShop Agent in LangGraph
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
query: str
city: str
result: str
def classify(state: State):
state["city"] = "Bengaluru"
return state
def answer(state: State):
state["result"] = f"Coffee shop search for {state['city']} ready."
return state
graph = StateGraph(State)
graph.add_node("classify", classify)
graph.add_node("answer", answer)
graph.add_edge(START, "classify")
graph.add_edge("classify", "answer")
graph.add_edge("answer", END)
app = graph.compile()
print(app.invoke({"query": "Find a quiet coffee shop for work", "city": "", "result": ""}))
How this works, step by step:
Statedefines the shape of the data that will pass through the entire graph: the original query, the resolved city, and the final result.classifyandanswerare nodes. Each is a plain function that takes the current state and returns an updated state. In a production system,classifymight call a language model to determine the user's intent, andanswermight call a search API before formatting the response.StateGraph(State)creates the graph and binds it to theStatetype, so LangGraph knows what shape of data flows through it.add_noderegisters each function as a node in the graph.add_edgeconnects nodes in sequence:START → classify → answer → END. In a more complex system,add_conditional_edgescould be used instead, so the next node depends on a value in the state (for example, routing to different handlers based on intent).graph.compile()turns the definition into a runnable application.app.invoke(...)runs the graph once with an initial state and returns the final state after all nodes have executed.
Compared with the Pydantic AI version, this code is longer, but it makes the flow of control explicit. Adding a retry, a branch, or a checkpoint later means adding a node or an edge — the structure of the workflow is visible in the code rather than implied by prompt logic.
Deep Agents: A Layer on Top of LangGraph
“Deep agents” is a term for agents designed to work over longer periods: planning multi-step tasks, delegating work to subagents, keeping context manageable, and sometimes continuing background work while other tasks proceed. This is different from a single tool-calling loop that answers one question and stops.
In the LangGraph ecosystem, there is a create_deep_agent-style factory function. It is important to understand where this sits:
- LangGraph core is the low-level orchestration runtime — you define state, nodes, edges, and checkpoints yourself, with full control.
- Deep agents on LangGraph is a higher-level package built on top of that runtime. It assembles a ready-made deep agent — with subagent delegation and task decomposition already wired up — so you do not have to build that structure manually.
So create_deep_agent does not replace LangGraph; it is built using LangGraph underneath, which means the same production features (streaming, checkpointing, human-in-the-loop) are still available through it.
Practical takeaway: if you need fine control over every step of a workflow, use LangGraph directly. If you want a more opinionated, ready-to-use autonomous agent with subagent support, create_deep_agent is the faster starting point.
Pydantic AI addresses a similar long-running-task problem differently — through structure and validation at each agent’s boundary, rather than through a full orchestration graph. The practical distinction stated plainly: Pydantic AI helps each individual agent behave reliably. LangGraph (and deep agents built on it) helps the whole system behave reliably over time.
Is Pydantic AI Built on Top of LangGraph?
No. This is a common point of confusion, so it is worth stating directly: Pydantic AI is not built on top of LangGraph. They are separate, independently designed frameworks with different goals.
- Pydantic AI is centered on typed, validated agent outputs and a Pythonic developer experience.
- LangGraph is centered on stateful orchestration, branching workflows, persistence, and multi-step control.
A simple way to remember the distinction:
- Pydantic AI = “make each agent response reliable and structured.”
- LangGraph = “make the whole agent workflow explicit, stateful, and controllable.”
They can be used in the same application, and often are, but one is not a layer running on top of the other. The correct relationship is not “Pydantic AI runs on LangGraph” — it is “they solve different parts of the agent problem, and can complement each other.”
Use Cases: Two Ways to Combine Pydantic AI and LangGraph
Pattern A — LangGraph as Orchestrator, Pydantic AI at the Node Level
In this pattern, LangGraph controls the overall flow — deciding which step runs next, saving state, handling retries and approvals — while Pydantic AI is used inside individual nodes to guarantee that each agent’s output is a valid, typed object. The graph manages the process; the agent guarantees the data.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from pydantic import BaseModel
from pydantic_ai import Agent
class ClassificationResult(BaseModel):
intent: str
confidence: float
classifier_agent = Agent(
model="openai:gpt-5",
output_type=ClassificationResult,
instructions="Classify the user's intent."
)
class State(TypedDict):
query: str
intent: str
result: str
async def classify_node(state: State):
output = (await classifier_agent.run(state["query"])).output
state["intent"] = output.intent # guaranteed valid field
return state
def route_node(state: State):
state["result"] = f"Routed to handler for: {state['intent']}"
return state
graph = StateGraph(State)
graph.add_node("classify", classify_node)
graph.add_node("route", route_node)
graph.add_edge(START, "classify")
graph.add_edge("classify", "route")
graph.add_edge("route", END)
app = graph.compile()
When to use this pattern: the business process itself is the primary complexity — multiple steps, conditional branches, retries, or a required human approval before an action is taken. LangGraph’s checkpointing and persistence protect the whole run, while each Pydantic AI agent inside a node removes the risk of a malformed or unparseable response breaking the next step.
Pattern B — Pydantic AI as Orchestrator, LangGraph Agents as Subagents
This is the reverse arrangement, and it is equally valid. Here, a Pydantic AI Agent sits at the top level and acts as the orchestrator. Its job is to guarantee that the caller of the system always receives a valid, typed response — for example, an API endpoint that must never return malformed JSON. Internally, one or more of its tools invoke a compiled LangGraph graph to carry out a task that genuinely needs multi-step reasoning, branching, or state (a "deep agent" style subagent).
python
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
# --- LangGraph subagent: does the multi-step research work ---
class ResearchState(TypedDict):
topic: str
findings: str
def gather(state: ResearchState):
state["findings"] = f"Findings on {state['topic']}"
return state
research_graph = StateGraph(ResearchState)
research_graph.add_node("gather", gather)
research_graph.add_edge(START, "gather")
research_graph.add_edge("gather", END)
research_app = research_graph.compile()
# --- Pydantic AI orchestrator: guarantees the final typed response ---
class ResearchSummary(BaseModel):
topic: str
summary: str
orchestrator = Agent(
model="openai:gpt-5",
output_type=ResearchSummary,
instructions="Summarize research findings for the user."
)
@orchestrator.tool
async def run_research(ctx: RunContext[None], topic: str) -> str:
result = research_app.invoke({"topic": topic, "findings": ""})
return result["findings"]
result = await orchestrator.run("Research the topic: agent frameworks")
print(result.output) # always a valid ResearchSummary
How this works: the orchestrator is a normal Pydantic AI Agent with output_type=ResearchSummary, so no matter how complicated the internal work is, the caller always receives a validated ResearchSummary object. The run_research tool is the bridge — when called, it invokes the compiled LangGraph app (research_app.invoke(...)) as a black box, letting that subagent handle branching, state, or checkpointing internally, and returns a plain string back to the orchestrator.
Conclusion
The core mental model to take away is this: Pydantic AI makes an agent correct at the output boundary. LangGraph makes the whole agent system correct over time.
- If you are building a beginner-friendly agent that needs to return a predictable, typed answer — such as the CoffeeShop Agent example — start with Pydantic AI.
- If you are building a production system that must remember state, branch based on conditions, recover from failure, and coordinate multiple steps or subagents, LangGraph is the stronger foundation.
- If you want an autonomous, long-running agent without building the orchestration graph yourself,
create_deep_agentgives you that on top of LangGraph.
For many serious applications, the best architecture is not choosing one framework permanently, but combining both: LangGraph for the workflow, Pydantic AI for the agents inside it or vice versa.
메타데이터
- post_id
- 6a0292cd7478
- slug
- pydantic-ai-vs-langgraph-understanding-the-two-different-problems-they-solve-6a0292cd7478
- url
- https://pub.towardsai.net/pydantic-ai-vs-langgraph-understanding-the-two-different-problems-they-solve-6a0292cd7478
- canonical_url
- https://pub.towardsai.net/pydantic-ai-vs-langgraph-understanding-the-two-different-problems-they-solve-6a0292cd7478
- author_url
- https://medium.com/@ganeshrbajaj
- status
- ok
- fetched_at
- 2026-07-13 06:23:13