An agent is more than a workflow; it is an autonomous system that uses an LLM to decide which tools to use and how to handle unexpected results. In Python, the ecosystem is rapidly maturing, moving from experimental scripts to production-grade frameworks.
Choosing Your Framework
For a seasoned engineer, the choice of framework depends on the required balance between Autonomy and Control.
| Framework | Mental Model | When to Use |
|---|---|---|
| PydanticAI | Type-Safe Functions | Use this for Production. It feels like FastAPI for agents. It uses Pydantic for strict validation, making it highly reliable and easy to debug. |
| CrewAI | Role-Based Agents | Use this for Business Logic. If you need an “Accountant Agent” and a “Manager Agent” to collaborate, CrewAI’s role-playing abstraction is powerful. |
| LangGraph | Cyclic Graphs | Use this for Custom Architectures. If you need to build a bespoke reasoning loop that doesn’t fit the standard ReAct pattern. |
| AutoGen | Conversational Agents | Use this for Creative Problem Solving. Ideal for multi-agent brainstorming or coding tasks. |
The Tooling Revolution: Model Context Protocol (MCP)
In the past, giving an agent a tool meant writing a custom wrapper for every API. The Model Context Protocol (MCP) changes this. It is a universal standard that allows agents to connect to tools and data sources seamlessly.
Why MCP?
An API allows you to get data. MCP allows you to provide context. * FastMCP: A framework that allows you to turn any Python function into an MCP-compatible tool in minutes. * Motivation: APIs are often too rigid for LLMs. MCP servers can expose not just functions, but also local files, database schemas, and even browser sessions in a way that the LLM can “understand” and navigate.
Production Considerations for Agents
State Management
State is the “memory” of your agent. In production, you must handle: * Persistence: Saving the agent’s state to a database (Postgres/Redis) so it can resume after a crash. * Context Pruning: Agents can “forget” the original goal if the conversation gets too long. You must implement strategies to summarize old turns or “slide” the window of active context.
Failure Handling: The “Retry” isn’t enough
In agentic engineering, a “failure” might be the LLM calling a tool with the wrong arguments. * Self-Correction: Catch the error, pass it back to the LLM as a “Thought,” and let it try again with a different argument. * Fallback Models: If a cheap model (GPT-4o-mini) fails the reasoning task, automatically “escalate” the task to a more capable model (Claude 3.5 Sonnet).
Evaluation & Observability
You cannot “unit test” an agent’s path. You must use Traces. * LangSmith / Langfuse: These tools allow you to visualize the “Trajectory” of the agent. You can see exactly why an agent chose Tool A over Tool B. * Evals: Create a dataset of “Golden Trajectories”—expected sequences of tool calls for specific prompts—and run them against new versions of your agent to check for regressions.
Security: The “Sandboxing” Mandate
Never give an agent direct access to your host shell. 1. Code Interpreters: Use tools like E2B to run agent-generated code in a secure, isolated sandbox. 2. Least Privilege: Scoped API keys are mandatory. If an agent only needs to read a Google Sheet, do not give it access to the entire Google Drive.
Cost / Latency Tradeoffs
Agents are expensive. A single user request might trigger 10 LLM calls. * Parallelism: If the LLM determines it needs to call three tools, use a framework that supports parallel tool execution. * Prefetching: Use “Cheap” models for classification and routing, and “Expensive” models only for the final reasoning or tool argument generation.
Engineering Heuristics for Performance
- Prompt as Code: Treat your system prompts like code. Use version control. A one-word change can break an agent’s tool-calling capability.
- Explicit Tool Descriptions: The LLM’s “vision” of a tool is its description. Be verbose. Instead of
get_data, usefetches_user_billing_history_as_json_for_the_last_30_days. - Always Validate: If a tool returns data, validate it with Pydantic before the agent sees it. Garbage In, Garbage Out still applies to AI.
tep-by-Step Implementation: Pydantic AI + FastMCP
This section provides a concrete roadmap to building a production-ready agent that consumes external tools via MCP.
Precise Setup Steps
- Environment: Create a virtual environment and install dependencies:
bash pip install pydantic-ai[mcp] mcp python-dotenv - Build the Tool Server (FastMCP): Create a separate file
server.pyto host your tools. This isolates the “Execution Layer” from the “Intelligence Layer.” - Define the Agent: Create
agent_app.py, initialize theAgentwith Pydantic validation for structured outputs. - Connect via Stdio: Use
MCPServerStdioto allow the agent to launch and communicate with the tool server. - Inject Dependencies: Use Pydantic AI’s
RunContextto pass stateful objects (like database connections) into tools.
Code Implementation
Part A: The MCP Tool Server (server.py)
This follows the Least Privilege principle by only exposing specific functions.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("InventoryManager")
@mcp.tool()
async def check_stock(item_id: str) -> int:
"""Check the current stock level for a specific item ID."""
# Engineering Note: This is where you connect to your real DB
inventory_db = {"A101": 50, "B202": 0}
return inventory_db.get(item_id, 0)
if __name__ == "__main__":
mcp.run()Part B: The Pydantic AI Agent (agent_app.py)
This demonstrates State Management, Type Safety, and Observability.
import asyncio
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
from pydantic_ai.mcp import MCPServerStdio
from dotenv import load_dotenv
load_dotenv()
# 1. Structured Output Schema (Validation)
class PurchaseRecommendation(BaseModel):
item_id: str
should_buy: bool
reasoning: str
# 2. Define the Agent with Intelligence and Toolsets
# The 'toolsets' parameter is where the MCP integration happens
mcp_server = MCPServerStdio("python", args=["server.py"])
agent = Agent(
'openai:gpt-4o',
result_type=PurchaseRecommendation,
toolsets=[mcp_server],
system_prompt="Analyze inventory levels and recommend if we should restock."
)
# 3. Execution Loop with Lifecycle Management
async def main():
# Use 'async with' to handle the subprocess lifecycle of the MCP server
async with mcp_server:
result = await agent.run("Should we restock item A101?")
# Accessing validated data
print(f"Recommendation for{result.data.item_id}:")
print(f"Should Buy:{result.data.should_buy}")
print(f"Reason:{result.data.reasoning}")
if __name__ == "__main__":
asyncio.run(main())Factoring in Production Concepts
- Stateful Dependencies: Use
RunContextin Pydantic AI to pass aUserobject or aDatabaseSessionto your local tools. This ensures the agent’s tools are grounded in the current State. - Tool Validation: By using Pydantic models for
result_type, the agent is forced to “Reason” toward a structured schema. If it fails, Pydantic AI’s internal Failure Handling will automatically retry the prompt with the validation error. - Security: The FastMCP server runs in a separate process. You can further secure this by running the server in a Docker Sandbox while the agent remains in your primary application environment.
- Cost/Latency: Notice that
check_stockis a fast, local function. By using MCP, the agent only calls this tool when its “Intelligence” (GPT-4o) determines it is necessary, saving expensive tokens on unnecessary lookups.