Migrating Langchain To Pydantic AI
PydanticOfficialPublished by Pydantic
- Skill
- Workflow
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured methodology for porting existing AI applications from the LangChain ecosystem to Pydantic AI. It emphasizes preserving application behavior over matching framework structures, advocating for a slice-based migration approach. The guide covers mapping core agent loops, handling complex state and persistence, and translating Deep Agents harness features into Pydantic AI primitives. It includes specific guidance on managing semantic gaps, verifying observability, and ensuring production readiness through characterization tests.
What it does
- Provides a step-by-step procedure for identifying and migrating functional slices of an application.
- Maps LangChain/LangGraph concepts to Pydantic AI primitives.
- Offers strategies for translating Deep Agents harness features like filesystem tools, subagents, and memory.
- Defines best practices for handling dependencies, typed invariants, and observability.
- Includes guidance on managing semantic differences and verifying parity through testing.
- Outlines the process for production cutover and verification.
When to use it
- When you need to migrate an existing LangChain or LangGraph application to Pydantic AI.
- When your application uses Deep Agents and requires a transition to Pydantic AI Harness.
- When you need to ensure behavioral parity during a framework migration.
- When you need to resolve semantic differences between LangChain and Pydantic AI implementations.
Advantages
- Focuses on behavioral preservation rather than superficial code matching.
- Provides a clear, risk-aware path for complex agentic systems.
- Includes specific mapping references for core loops and harness features.
- Encourages the use of typed dependencies for better reliability.
- Promotes a test-driven approach to ensure migration accuracy.
Usage examples
- “How do I migrate a simple LangChain agent to Pydantic AI?”
- The agent will guide you to identify the agent's tools and system prompt, then show you how to define a Pydantic AI Agent with typed dependencies and instructions, replacing the LangChain constructor.
- “How should I handle Deep Agents subagents in Pydantic AI?”
- The agent will explain how to map the subagent's filesystem, skills, and memory middleware to Pydantic AI Harness capabilities or custom application services.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/pydantic/migrating-langchain-to-pydantic-ai/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip migrating-langchain-to-pydantic-ai.zip -d .agents/skills
Documentation by Pydantic
Migrate LangChain, LangGraph, and Deep Agents to Pydantic AI
Preserve behavior, not framework shape. Migrate the smallest behaviorally complete slice and leave application infrastructure outside that slice unchanged. LangChain, LangGraph, and Deep Agents are one source ecosystem; Pydantic AI, pydantic_graph, and Pydantic AI Harness are the matching target ecosystem, and this skill maps across all of it.
Work from the running application
Read repository instructions, dependency files, tests, and the actual runtime entrypoints. Identify the installed LangChain, LangGraph, Deep Agents, Pydantic AI, and Harness versions.
Trace one representative request through prompts, retrieval, model and tool calls, state, persistence, interrupts, emitted events, tracing/metrics callbacks, and the public result. Inspect every caller and sibling endpoint that consumes the migrated component; a narrow implementation slice can still have several public contracts. Include keyword parameter names and the sync, async, callback, and streaming forms callers actually use. Record only contracts those paths actually use.
Run the cheapest useful baseline. When the migration surface is broad or unclear, search dependency files and source for
langchain,langgraph,langsmith, anddeepagents, then confirm findings against imports, factories, and call sites. Resolve wherecreate_deep_agentcomes from: the upstream package and a vendored copy that reproduces its contracts both count as Deep Agents.Classify the slice before choosing a target:
Chain or LCEL pipeline: keep deterministic retrieval and transformation in plain Python; use a Pydantic AI agent only where a model/tool loop adds value.
LangChain agent: normally use one reusable
pydantic_ai.Agentwith typed dependencies, tools, and outputs.Direct LangGraph workflow: use plain async Python for simple fixed control flow, or
pydantic_graphwhen explicit typed nodes and branching remain useful. Treat persistence as a separate design decision.Deep Agents harness:
create_deep_agentbundles file tools, shell execution, subagent delegation, summarization, skills, memory, permissions, prompt caching, and approval middleware on top of the LangChain agent loop, and a harness profile can add or hide more. Migrate the loop with the LangChain mappings, then map each bundled feature the slice actually uses to a Harness capability, a core primitive, or an application service with Deep Agents Mapping. Inventory implicit defaults the application code never mentions.Product runtime: retain queues, configured database backends, sandboxes, auth, schedulers, webhooks, tracing, and transport adapters unless the user placed them in scope. Extend an existing application seam before creating a parallel persistence or provider subsystem.
Add or preserve deterministic characterization tests, then migrate one vertical slice behind the existing public boundary.
Run the original tests and focused parity tests. Classify each observed contract by its evidence; never describe the migration as one-to-one merely because the happy path or trace shape looks similar.
Read Concept Mapping for the detected source features. Read Deep Agents Mapping whenever deepagents is in the slice. Read Semantic Gaps only for state, middleware, retries, approval, concurrency, streaming, or other behavior where similar-looking APIs may differ. Use Workaround Recipes after a concrete gap is identified, not as a mandatory checklist. Read Logfire Verification when adding observability, comparing source and target runs, or debugging a semantic difference. Read Verification and Cutover before a production cutover.
Explain semantic differences
When an observed source contract has no direct equivalent, explain it to the user before making a consequential design choice. State the source behavior, how the proposed Pydantic AI design differs, the user-visible or operational impact, and the available choices. Recommend one option and name its residual risk. Keep this proportional: do not turn ordinary import or naming changes into semantic warnings.
Do not stop at "unsupported." When nothing maps directly, recommend an existing core or Harness composition, a narrow adapter or application service, a new capability built from public primitives, or a core change, in that order of preference. Ask before choosing only when the options materially change behavior, architecture, public API, or scope.
Match rigor to risk
For a stateless chain or ordinary agent port, focused characterization tests and a short residual-risk note are enough. Do not require a semantic-gap register or durability exercise for behavior the source does not have.
For middleware, structured output transport, retrieval, tool retries, or streaming, probe the affected contract against the installed versions.
For checkpointed graphs, interrupts, approvals, durable execution, concurrent fan-out, or external side effects, create a migration ledger. Separate dependencies, messages, workflow state, checkpoint state, and long-term memory. Fit those owners into the repository's existing backend-selection and service interfaces where possible. Test restart, replay, correlation, authorization, and idempotency only to the extent the source promises them.
A
deepagentsdependency alone changes nothing. When the active slice callscreate_deep_agentor depends on its planning, filesystem, sandbox, skills, memory, subagent, permission, or deployment contracts, those are in-scope features with Harness or application owners, not a reason to stop. Files, sandboxes, permissions, subagents, and background work carry the same ledger and restart obligations as checkpointed graphs.
Pydantic AI defaults
Put authenticated identity, service clients, and configuration in typed dependencies, never model-chosen tool arguments.
Strengthen observed unstable seams, not the whole application: parameterize the agent's dependency and output types, and validate terminal choices, persisted workflow records, and framework adapters. Preserve stable public wire shapes and do not invent types for paths outside the migrated slice.
Preserve public request, response, error, and event shapes with a small adapter while callers migrate.
Keep retrieval, storage, provider, and transport integrations in place when they are outside the requested slice. Transitional LangChain integrations are acceptable when named and bounded.
Use Pydantic models for terminal structured output when that preserves the contract; retain an existing parser when changing the wire contract would expand the migration.
Do not force an
Agentonto deterministic LCEL orpydantic_graphonto everyStateGraph.Inspect the installed Pydantic AI API before choosing model classes, provider transports, hooks, streaming methods, or durable integrations.
Add
pydantic-ai-harnessonly for observed harness behavior: planning, workspace files, shell or sandboxed execution, Agent Skills, memory notebooks, repository context, model-directed subagents, model-agnostic compaction, tool-output limits, guardrails, spend limits, or step persistence. Add capabilities one at a time, import each from its owning public submodule, treatpydantic_ai_harness.experimental.*as version-sensitive, and run an import-and-construction smoke test in the target environment. Harness composes onto the core agent loop; it is not a second runtime and does not replace application infrastructure.When adding Pydantic AI, prefer a currently supported stable release. Use the newest compatible release unless that would expand the migration through an unrelated major/runtime upgrade; explain and pin any exception. Resolve the whole project from a clean environment and run an import probe because an existing environment can hide incompatible transitive versions. Prefer
pydantic-ai-slimwith only the required provider and integration extras when the dependency surface is bounded, and use the full distribution when its broader integrations are actually needed. Do not pin an older release merely to match a remembered example.If the source already uses LangSmith, Langfuse, or another observability system, do not replace it silently. Explain that Logfire is the first-party Pydantic AI integration and normally provides the most direct agent, model, tool, retry, error, usage, and timing experience. Contrast that with the continuity of retaining the current system, including its dashboards, alerts, evaluations, retention, and export pipeline; recommend a choice and obtain agreement before switching. Offer Logfire at application startup when no tracing system exists or the user chooses it, make content capture an explicit privacy decision, and keep executable contract tests as the authority for parity.
Completion
The slice is complete when every observed contract is either preserved by an executable check, intentionally changed by an accepted decision, or explicitly not applicable. An untested contract is unverified, not equivalent; an unresolved requested contract is unfinished work, not completion evidence. Constrain the slice or ask the user to accept the deferral. Remove LangChain, LangGraph, or Deep Agents dependencies only after no retained path needs them.
Files
- ATTRIBUTION.mdAdded by AutoGineer885 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SKILL.md9.9 KiB
- agents/openai.yaml394 B
- references/CONCEPT-MAPPING.md16.2 KiB
- references/DEEP-AGENTS-MAPPING.md24.5 KiB
- references/LOGFIRE-VERIFICATION.md10.2 KiB
- references/SEMANTIC-GAPS.md21.6 KiB
- references/VERIFICATION-AND-CUTOVER.md12.3 KiB
- references/WORKAROUND-RECIPES.md18.0 KiB
references/CONCEPT-MAPPING.md
LangChain and Pydantic AI Concept Mapping
Use this reference to select Pydantic AI primitives. Preserve behavior rather than matching class names. For create_deep_agent and its bundled middleware, backends, profiles, and hosts, continue with Deep Agents Mapping after this file.
Contents
Core agent loop
Typed invariants at unstable seams
Tools and runtime context
Structured output
Middleware and lifecycle
State, memory, and persistence
Graphs and multi-agent systems
Streaming, testing, and observability
Transitional bridges
Migration traps
Core agent loop
| LangChain / LangGraph | Pydantic AI default | Migration note |
|---|---|---|
| LCEL prompt/retriever/parser pipeline | plain Python around a model call or Agent | Keep deterministic retrieval, formatting, and routing explicit. Do not introduce an agent loop when the source performs one model call. |
create_agent(model, tools, system_prompt=...) | Agent(model, tools=..., instructions=...) | Keep one reusable agent unless construction genuinely varies per request. |
agent.invoke({"messages": ...}) | await agent.run(prompt, deps=..., message_history=...) | Consume result.output; do not expose Pydantic AI message objects at the public boundary. |
ainvoke | await agent.run(...) | Pydantic AI is async-first; use run_sync only at synchronous edges. |
RunnableConfig.configurable | typed dependency object and explicit run arguments | Split application configuration from model-visible content. |
context_schema / ToolRuntime.context | deps_type / RunContext.deps | Put DB clients, authenticated identity, config, and service gateways in dependencies. |
| prompt templates | instructions or system_prompt | Use instructions for current-agent policy and system_prompt only when prior prompts must survive history; multiple LangChain dynamic prompts may replace rather than compose. |
init_chat_model | provider-prefixed model string or model instance | Preserve provider settings explicitly; verify the installed provider API. |
Preserve model transport as well as the model name. LangChain's ChatOpenAI, an OpenAI Responses model, an Azure deployment, and an OpenAI-compatible local endpoint can share a model label while using different request protocols and settings. Inspect the installed Pydantic AI provider constructors and test every configured branch without making a live request; leave unknown custom endpoints as an explicit integration gap.
Minimal translation:
# ruff: noqa: F704, F821, PLE1142, Q000
# LangChain
from langchain.agents import create_agent
agent = create_agent(
model="provider:model",
tools=[lookup_order],
system_prompt="Help authenticated customers with orders.",
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "Where is order 123?"}]},
context=runtime_context,
)# ruff: noqa: F704, F821, PLE1142
# Pydantic AI
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
@dataclass
class Deps:
customer_id: str
orders: 'OrderService'
agent = Agent(
'provider:model',
deps_type=Deps,
instructions='Help authenticated customers with orders.',
)
@agent.tool
async def lookup_order(ctx: RunContext[Deps], order_id: str) -> str:
return await ctx.deps.orders.lookup_for_customer(ctx.deps.customer_id, order_id)
result = await agent.run('Where is order 123?', deps=deps)
print(result.output)The dependency boundary is a security boundary: the model chooses order_id, but cannot choose customer_id or the service client.
Typed invariants at unstable seams
Use types to make an observed migration decision executable, not to remodel stable application code. The following seams caused real ambiguity in migrated applications:
| Observed source ambiguity | Target invariant | Evidence to require |
|---|---|---|
| free-form config or runtime context mixes trusted identity, services, and model inputs | a dependency dataclass plus Agent[DepsT, OutputT]; only model-chosen values appear in tool parameters | static checking plus a captured tool schema and model request |
| a string, dictionary, or graph control value can mean several terminal states, such as answer versus request-more-input | a Pydantic model or explicit output union for the states the source actually reaches | every variant validates and every application branch is exercised; remember that including str permits plain text to terminate the run |
| graph state mixes public messages, model protocol history, pending resume state, owner, and persistence version | distinct public DTOs, list[ModelMessage], and a typed workflow record; serialize model history with ModelMessagesTypeAdapter when the installed version provides it | storage round-trip, continuation, ownership, and resume tests against the real backend |
| provider names and settings are passed through loose strings and dictionaries | retain the source enum or typed configuration and map it to a concrete Pydantic AI Model | construct every configured branch without a live request and probe provider-specific settings |
Do not claim semantic parity from static typing alone. Types can expose missing cases and validate boundaries; they cannot prove timing, retries, persistence, side effects, or framework lifecycle behavior.
Tools and runtime context
| Source pattern | Target pattern |
|---|---|
@tool plain function | @agent.tool_plain or Tool(fn) |
tool with ToolRuntime | @agent.tool with first parameter RunContext[Deps] |
BaseTool subclass | Prefer a normal typed function; use Tool or Tool.from_schema only when dynamic schema is necessary. |
| toolkit | FunctionToolset, another AbstractToolset, or a focused capability |
| MCP adapter | Pydantic AI MCP toolset or MCP capability |
| tools filtered by user/state | tool prepare=..., PrepareTools, a wrapper toolset, or capability loading |
| runtime-discovered large catalog | deferred tools/tool search; avoid rebuilding the schema every turn |
| tool retry middleware | Hooks.on.tool_execute or service-client retry for same-handler local retry; ModelRetry, tool retries=..., validators, and transport retry remain separate domains |
| approval middleware | DeferredToolRequests and DeferredToolResults |
| tool artifacts | ToolReturn with distinct model content, return value, and metadata |
Preserve tool name, description, JSON schema, concurrency, idempotency, timeout, retry, approval, auth, and error-to-model semantics. A successful wrapper import does not prove tool parity.
Structured output
| LangChain | Pydantic AI |
|---|---|
response_format=Schema | output_type=Schema, but select the output transport explicitly when parity matters |
| provider strategy | NativeOutput(Schema) when native enforcement is required |
| tool strategy | ToolOutput(Schema) when tool transport is required |
| manual parser / non-JSON text | TextOutput(parser) |
| dynamic JSON schema | StructuredDict(schema, name=...) |
| retry after invalid response | output validation and ModelRetry / configured retries |
Do not include str in a union when the run must end with structured output; plain text would be a valid terminal result.
LangChain may auto-select provider-native structured output for a bare schema. A bare Pydantic AI structured output_type follows the selected model profile's default, which may be tool, native, or prompted output. Use NativeOutput, ToolOutput, PromptedOutput, or TextOutput explicitly when wire behavior matters, and characterize the chosen provider/model.
Middleware and lifecycle
Map each middleware by the behavior it owns:
| LangChain middleware behavior | Pydantic AI target |
|---|---|
| dynamic system prompt | dynamic @agent.instructions |
| before/after model or tool | Hooks lifecycle hook; wrapper decorators include on.model_request and on.tool_execute |
| reusable prompt + tools + hooks + settings | custom AbstractCapability |
| trim/summarize messages | ProcessHistory with an explicitly tested summary and pairing policy |
| filter/rename tool definitions | tool prepare, PrepareTools, or wrapper toolset |
| validate tool arguments | args_validator or tool-validation hook |
| dynamic model selection/fallback | model instance/wrapper such as FallbackModel, or select the model at the app boundary |
| model call limit | UsageLimits and explicit application limits |
| tool error conversion | catch the expected exception in the tool and raise ModelRetry only for model-correctable failures |
| logging/tracing | Logfire instrumentation or hooks for application metrics |
| guardrail | input/output validation hook; keep authorization inside tools/services |
| inject a mid-run user message | RunContext.enqueue or AgentRun.enqueue |
| Deep Agents built-in middleware (filesystem, subagents, summarization, skills, memory, permissions, prompt caching, human-in-the-loop) | the Harness capability or application service named in Deep Agents Mapping |
Reproduce hook ordering explicitly. Combining several source middleware objects into one opaque hook makes parity harder to inspect and test.
State, memory, and persistence
Separate four concepts that LangGraph often stores together:
Run dependencies: immutable or service-like values used during one run ->
deps_typeandRunContext.deps.Conversation messages: model request/response history -> persist serialized Pydantic AI messages and pass
message_history.Workflow state: plan, counters, fan-out results, approvals, domain progress -> typed application state or
pydantic_graphstate.Long-term memory: cross-thread facts -> an explicit repository/service in dependencies.
| LangGraph feature | Migration choice |
|---|---|
MessagesState | Pydantic AI message history plus separately typed workflow state |
| custom reducers | plain update functions or pydantic_graph joins/reducers |
| checkpointer/thread | application persistence or a durable execution integration |
| store | explicit storage service in typed dependencies |
| time travel/fork | durable workflow-specific implementation; do not infer this from message history |
interrupt() for missing user input | application-owned pending conversational state and resume |
interrupt() or HITL around a protected tool | deferred tool request plus authenticated, durable application correlation and resume |
| replay after failure | Temporal, DBOS, Prefect, Restate, or another explicit durable boundary |
Never label a migration complete because chat messages survive if the old system also promised checkpoint replay, pending writes, thread forks, or exactly-once side-effect protection.
Pydantic AI may repair dangling tool calls and orphaned tool results before the model request, while LangGraph add_messages merges by message ID. Compare the actual model-visible history when converting stored threads.
Graphs and multi-agent systems
Choose by topology:
Use one Pydantic AI
Agentfor a normal model/tool loop.Use plain async Python for a fixed sequence, bounded loop, or
asyncio.gatherfan-out.Use delegation via tools when a parent agent remains in control and consumes child output.
Use programmatic hand-off when application code chooses the next specialist.
Use
pydantic_graphwhen explicit typed nodes, branches, joins, or inspectable workflow state add value.Use parent tools that call typed child agents for model-selected delegation; define child history, dependencies, usage, result, and failure propagation explicitly.
Add a durable execution integration when the workflow must survive process failure; a graph alone is not durability.
Translate Command(goto=..., update=...) into an explicit next-node value plus a typed state update. Preserve fan-out limits, cancellation, exception aggregation, and ordering when translating LangGraph Send or parallel branches. For Pydantic Graph fan-out, return branch-local results, carry a source index through the join when order matters, and use ReducerContext.cancel_sibling_tasks() only when early completion is the intended reducer contract.
Streaming, testing, and observability
| LangChain ecosystem | Pydantic ecosystem |
|---|---|
stream / astream values, updates, messages | run_stream, run_stream_events, event_stream_handler, or iter |
| fake chat models | TestModel or FunctionModel under agent.override(...) |
| trajectory/eval datasets | pydantic_evals cases, datasets, and evaluators |
| LangSmith traces | Pydantic AI Logfire instrumentation and application-owned OpenTelemetry spans; LangSmith can export source traces to the same backend for comparison |
| graph state inspection | typed state plus application persistence/graph inspection |
Define an application-owned event schema at the UI/API boundary. Adapt both implementations to it during migration; do not make clients depend directly on either framework's event classes.
Use Logfire to inspect model calls, tools, retries, errors, usage, and timing, then prove public contracts with executable tests. In particular, model time to first chunk is not client time to first event. See Logfire-Assisted Migration Verification.
Do not substitute run_stream() for run() without a separate trajectory test. run_stream() commits the first matching output as it streams; co-emitted tools and retries can therefore produce a different terminal result from a complete run().
Transitional bridges
Pydantic AI can wrap LangChain tools:
# ruff: noqa: F821
from pydantic_ai import Agent
from pydantic_ai.ext.langchain import LangChainToolset, tool_from_langchain
single_tool = tool_from_langchain(existing_langchain_tool)
toolset = LangChainToolset(existing_toolkit.get_tools())
agent = Agent('provider:model', tools=[single_tool], toolsets=[toolset])Use this only to create a vertical slice while tool internals are ported. The wrapper delegates argument validation to the LangChain tool, retains LangChain dependencies, and can conceal framework-specific callbacks or runtime assumptions. Add a removal issue and a parity test for every bridge.
Audit LangChain-only flags such as return_direct: the wrapper invokes the tool but does not automatically preserve the source agent's stop-after-tool routing. When the call is a model-selected terminal action, represent it as a named output function with ToolOutput and prove one execution plus the source model-call count.
Keep an existing retriever or LCEL pipeline behind a narrow tool/service interface if rewriting it would block agent migration. Port it later when the agent boundary is stable.
Migration traps
Translating
state_schematodeps_typeand then mutating dependencies as workflow state.Passing authenticated identity, tenant, or credentials as model-chosen tool parameters.
Treating all middleware as hooks even when behavior belongs in a tool or model wrapper.
Reusing LangChain message objects in Pydantic AI history.
Moving deterministic branches into prompts to avoid learning
pydantic_graph.Replacing checkpointers with an in-memory message list.
Keeping both observability SDKs without defining trace ownership and correlation.
Testing only final text while tool trajectory, approval, and side effects changed.
Treating a
deepagentsimport as out of scope instead of mapping the harness features the slice actually uses.
Primary references: Pydantic AI agents, tools, hooks, third-party tools, multi-agent patterns, durable execution, and Pydantic AI Harness.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Pydantic
- Publisher
- Pydantic (Official)
- Source
- pydantic/skills · skills/migrating-langchain-to-pydantic-ai · commit 98f591c
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Also distributed in
- pydantic/skills
AutoGineer is not affiliated with or endorsed by Pydantic or Pydantic.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download migrating-langchain-to-pydantic-ai.zip