# Install required packages
# !pip install langchain langchain-openai langgraph chromadb
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_functions_agent
from langchain.tools import Tool
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.memory import ConversationBufferMemory
load_dotenv()
print("β
Setup complete")
Part 1: Framework Overview (2026 Landscape)ΒΆ
The agent ecosystem now spans fully managed cloud APIs, open-source SDKs/frameworks, and interop protocols that let agents share tools and talk to each other.
Fully Managed Agent APIsΒΆ
These platforms host and run agents for you β bring your prompt and tools, they handle infra, scaling, and monitoring.
Service |
Vendor |
Model Lock-in |
Key Differentiator |
|---|---|---|---|
Anthropic Managed Agents |
Anthropic |
Claude only |
Versioned agent configs, hosted infra, built-in tool suite |
LangGraph Cloud |
LangChain |
Model-agnostic |
Hosted deployment of LangGraph agents, persistence, streaming |
AWS Bedrock Agents |
AWS |
Multi-model (Bedrock) |
Native AWS integrations (S3, Lambda, Knowledge Bases) |
Azure AI Agent Service |
Microsoft (Foundry) |
Multi-model (Azure) |
Tool connectors (Azure Search, Code Interpreter, Bing), enterprise RBAC |
Vertex AI Agent Builder |
Gemini-native |
GCP-native, integrated with Vertex AI Search and Conversation |
|
Salesforce Agentforce |
Salesforce |
Multi-model |
Enterprise agents embedded in CRM workflows |
Open-Source SDKs & FrameworksΒΆ
Framework |
Best For |
Learning Curve |
Key Differentiator |
|---|---|---|---|
LangChain |
General-purpose agents, quick prototypes |
Easy |
Largest ecosystem of tools and integrations |
LangGraph |
Complex workflows, cyclic graphs |
Medium-Hard |
Graph-based orchestration, most mature for stateful agents |
OpenAI Agents SDK |
Lightweight multi-agent handoffs |
Easy |
Minimal abstractions, built-in tracing, provider-agnostic via LiteLLM |
Google ADK |
GCP-native agents, multi-language |
Medium |
4 language SDKs (Python, Java, Go, Node), A2A protocol |
CrewAI |
Multi-agent teams, role-based agents |
Easy |
Easiest onboarding, role-based collaboration model |
AutoGen |
Multi-agent conversation/debate |
Medium |
Microsoft-backed, multi-agent conversation and debate patterns |
Semantic Kernel |
Enterprise .NET + Python agents |
Medium |
Deep Azure integration, .NET first-class support, plugin system |
SmolAgents |
Minimal code-first agents |
Easy |
Hugging Face, <100 lines to a working agent, code-generation agents |
Llama-stack |
Meta/Llama ecosystem |
Medium |
Safety built-in, Llama-native tooling, on-device support |
Haystack |
RAG-heavy agent pipelines |
Medium |
deepset, pipeline-based, strong retrieval integration |
Bee Agent Framework |
Production enterprise agents |
Medium |
IBM-backed, observability and compliance focus |
Custom |
Full control, specific requirements |
Hard |
No abstractions, maximum flexibility |
Interop Protocols & StandardsΒΆ
Protocol |
Purpose |
Status (2026) |
|---|---|---|
MCP (Model Context Protocol) |
Standardized tool/resource connectivity for agents |
Broadly adopted β Anthropic, OpenAI, Google, Microsoft all support it |
A2A (Agent-to-Agent, Google) |
Agent interoperability β agents discovering and delegating to other agents |
Early adoption β Google ADK native, growing cross-vendor support |
See Notebook 06 for deep coverage of MCP.
Visual / No-Code Agent BuildersΒΆ
For teams that want to build agent workflows without writing Python, several platforms provide drag-and-drop graph editors:
Platform |
Type |
Key Feature |
Best For |
|---|---|---|---|
Langflow |
Open-source |
Visual LangChain/LangGraph builder, runs locally or on Datastax cloud |
Prototyping LangChain flows, export to Python |
Flowise |
Open-source |
Low-code LLM app builder, Node.js, Docker-ready |
Quick chatbot/agent demos, internal tools |
Dify |
Open-source |
Full RAG + agent platform with visual workflow editor |
End-to-end LLM apps, teams with mixed skill levels |
n8n AI |
Open-source + cloud |
Workflow automation with AI agent nodes |
Connecting agents to 400+ SaaS integrations |
When to choose no-code: Rapid prototyping, non-developer stakeholders defining workflows, or simple agent use cases that donβt require custom Python logic.
When to Use EachΒΆ
LangChain:
β Quick prototypes
β Standard agent patterns
β Rich ecosystem of tools
β Limited control over agent loop
LangGraph:
β Complex state machines
β Cyclic workflows
β Human-in-the-loop
β Steepest learning curve
OpenAI Agents SDK:
β Lightweight, minimal boilerplate
β Multi-agent handoffs built-in
β Provider-agnostic (LiteLLM)
β Less mature ecosystem than LangChain
Google ADK:
β Multi-language (Python, Java, Go, Node)
β Native A2A protocol support
β GCP integration
β Smaller community than LangChain/OpenAI
CrewAI:
β Role-based collaboration
β Easiest onboarding
β Less flexible for non-team patterns
Semantic Kernel:
β .NET first-class support
β Deep Azure/Microsoft 365 integration
β Smaller Python community
Custom Implementation:
β Full control
β Optimized for specific use case
β More development time
Managed vs. Self-Hosted: Key QuestionsΒΆ
Cost vs. control β Are you willing to pay for fully managed agent infra (Anthropic, Azure, AWS), or do you prefer owning the stack?
Lock-in β Anthropic Managed Agents ties you to Claude. Azure ties you to Azure. Does model/cloud flexibility matter?
Multi-agent in prod β Running multi-agent setups? LangGraph and AutoGen handle this best. CrewAI is easiest to start.
Interop β Does your architecture need MCP (tool sharing) or A2A (agent delegation)? These are becoming table stakes for multi-vendor setups.
Part 2: LangChain AgentsΒΆ
Building Your First LangChain AgentΒΆ
LangChain abstracts the agent loop into two core components: an Agent (the LLM reasoning engine that decides which tool to call) and an AgentExecutor (the runtime that manages the observe-think-act cycle, handles tool dispatch, and enforces iteration limits). The create_openai_functions_agent constructor wires the LLM to OpenAIβs native function-calling API, so the model returns structured JSON tool invocations rather than free-text that must be parsed with brittle regex.
How LangChain Agents WorkΒΆ
The executor runs a loop: (1) pass the conversation plus a scratchpad of prior tool calls/results to the LLM, (2) if the LLM returns a function call, execute the matching Tool and append the result to the scratchpad, (3) repeat until the LLM returns a plain text answer or max_iterations is reached. This is functionally equivalent to the ReAct pattern from Notebook 03, but the framework handles prompt formatting, output parsing, and error recovery. The Tool wrapper maps a Python callable to a name and natural-language description that the LLM uses to decide when invocation is appropriate, making tool registration a one-liner rather than a manual JSON schema.
# Initialize LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)
# Define tools
def get_word_length(word: str) -> int:
"""Returns the length of a word."""
return len(word)
def multiply_numbers(a: float, b: float) -> float:
"""Multiply two numbers together."""
return a * b
# Create LangChain tools
tools = [
Tool(
name="get_word_length",
func=get_word_length,
description="Get the length of any word. Input should be a single word."
),
Tool(
name="multiply",
func=lambda x: multiply_numbers(*map(float, x.split(','))),
description="Multiply two numbers. Input should be two numbers separated by comma, e.g., '5,3'"
)
]
print(f"β
Created {len(tools)} tools")
# Create agent prompt
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant with access to tools."),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
# Create agent
agent = create_openai_functions_agent(llm, tools, prompt)
# Create agent executor
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=5
)
print("β
LangChain agent ready")
# Test the agent
response = agent_executor.invoke({
"input": "What is the length of the word 'LangChain' multiplied by 3?"
})
print(f"\nπ€ Agent Response: {response['output']}")
Real-World Example: Research AgentΒΆ
A research agent demonstrates how multiple tools compose to answer questions that no single tool could handle alone. The agent below has access to Wikipedia for factual retrieval, a calculator for arithmetic, and a date tool for temporal context. When the user asks a compound question like βHow many years ago was Python created?β, the agent autonomously chains tool calls: first querying Wikipedia for the creation date, then calling the calculator to subtract from todayβs date.
Why This Pattern MattersΒΆ
Production LLM applications rarely need just one capability. By registering heterogeneous tools with clear, descriptive docstrings, you let the modelβs function-calling mechanism serve as an implicit router that selects the right tool based on semantic understanding of the query. The quality of tool descriptions directly affects routing accuracy β vague descriptions cause the agent to pick the wrong tool or hallucinate answers instead of calling any tool at all. Each Tool objectβs description field is effectively part of the prompt, so treat it with the same care you would give a system message.
import wikipedia
from datetime import datetime
# Wikipedia search tool
def search_wikipedia(query: str) -> str:
"""Search Wikipedia and return a summary."""
try:
return wikipedia.summary(query, sentences=3)
except:
return f"Could not find information about '{query}'"
# Current date tool
def get_current_date() -> str:
"""Get the current date."""
return datetime.now().strftime("%B %d, %Y")
# Calculator tool
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression."""
try:
result = eval(expression)
return str(result)
except:
return "Invalid expression"
# Create research tools
research_tools = [
Tool(
name="wikipedia",
func=search_wikipedia,
description="Search Wikipedia for information. Input should be a search query."
),
Tool(
name="current_date",
func=get_current_date,
description="Get today's date. No input required."
),
Tool(
name="calculator",
func=calculate,
description="Calculate mathematical expressions. Input should be a valid math expression."
)
]
# Create research agent
research_prompt = ChatPromptTemplate.from_messages([
("system", "You are a research assistant. Answer questions using available tools."),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
research_agent = create_openai_functions_agent(llm, research_tools, research_prompt)
research_executor = AgentExecutor(
agent=research_agent,
tools=research_tools,
verbose=True
)
print("β
Research agent ready")
# Test research agent
result = research_executor.invoke({
"input": "Who invented Python programming language and when?"
})
print(f"\nπ Research Result:\n{result['output']}")
Part 3: LangGraph WorkflowsΒΆ
From Linear Chains to Stateful GraphsΒΆ
LangGraph extends LangChain by modeling agent logic as a directed graph where nodes are processing steps and edges define transitions. Unlike a simple sequential chain, LangGraph supports cycles (an agent can loop back to re-plan after receiving new information), conditional branching (route to different nodes based on state), and human-in-the-loop checkpoints. The StateGraph class manages a typed state dictionary that flows through the graph, with each node reading and updating shared state via TypedDict annotations.
Why Graph-Based Orchestration MattersΒΆ
Many real-world agent tasks are not linear pipelines. A coding assistant might plan, write code, run tests, discover a bug, and loop back to rewrite β a cyclic workflow that cannot be expressed as a simple chain. LangGraphβs add_conditional_edges method lets you define routing functions that inspect the current state and choose the next node, enabling patterns like retry loops, parallel fan-out/fan-in, and early termination. The compile() step converts the graph definition into an executable Runnable that supports streaming, async execution, and state persistence for long-running workflows.
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator
# Define state
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
next_step: str
final_answer: str
# Node functions
def planning_node(state: AgentState) -> AgentState:
"""Plan the approach"""
print("π§ Planning...")
state["messages"].append("Created plan")
state["next_step"] = "research"
return state
def research_node(state: AgentState) -> AgentState:
"""Conduct research"""
print("π Researching...")
state["messages"].append("Gathered information")
state["next_step"] = "synthesis"
return state
def synthesis_node(state: AgentState) -> AgentState:
"""Synthesize findings"""
print("βοΈ Synthesizing...")
state["final_answer"] = "Research complete with findings"
state["next_step"] = "end"
return state
# Build graph
workflow = StateGraph(AgentState)
# Add nodes
workflow.add_node("planning", planning_node)
workflow.add_node("research", research_node)
workflow.add_node("synthesis", synthesis_node)
# Add edges
workflow.set_entry_point("planning")
workflow.add_edge("planning", "research")
workflow.add_edge("research", "synthesis")
workflow.add_edge("synthesis", END)
# Compile
app = workflow.compile()
print("β
LangGraph workflow created")
# Run the workflow
initial_state = {
"messages": ["Starting research task"],
"next_step": "planning",
"final_answer": ""
}
final_state = app.invoke(initial_state)
print("\nπ Workflow Result:")
print(f"Messages: {final_state['messages']}")
print(f"Final Answer: {final_state['final_answer']}")
Part 4: Memory IntegrationΒΆ
Giving Agents Persistent ContextΒΆ
Without memory, every agent invocation is stateless β the LLM has no knowledge of prior turns. ConversationBufferMemory solves this by storing the full message history and injecting it into the prompt via the chat_history placeholder. This enables multi-turn interactions where the agent can reference earlier context (βWhat was my name?β or βUse the same format as beforeβ).
Memory Strategies and Trade-offsΒΆ
Buffer memory is the simplest approach but grows linearly with conversation length, eventually exceeding the modelβs context window. LangChain provides alternatives: ConversationSummaryMemory compresses older turns into a running summary (trading fidelity for token efficiency), ConversationBufferWindowMemory keeps only the last \(k\) turns, and VectorStoreMemory embeds messages for semantic retrieval of relevant history. Choosing the right memory strategy depends on your token budget, conversation length, and whether the agent needs exact recall or just topical awareness of past interactions.
from langchain.memory import ConversationBufferMemory
# Create memory
memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True
)
# Create agent with memory
memory_prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant. Remember previous conversation."),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad")
])
memory_agent = create_openai_functions_agent(llm, research_tools, memory_prompt)
memory_executor = AgentExecutor(
agent=memory_agent,
tools=research_tools,
memory=memory,
verbose=True
)
print("β
Agent with memory ready")
# Test memory
print("First message:")
response1 = memory_executor.invoke({"input": "My name is Alice"})
print(response1['output'])
print("\nSecond message (should remember name):")
response2 = memory_executor.invoke({"input": "What's my name?"})
print(response2['output'])
Part 5: Framework ComparisonΒΆ
LangChain vs Custom ImplementationΒΆ
The fundamental trade-off in agent frameworks is development speed versus control. LangChain lets you build a functional agent in 5-10 lines by composing pre-built abstractions (Tool, AgentExecutor, Memory), but those abstractions impose opinions about prompt formatting, error handling, and the agent loop that may not suit every use case. A custom implementation requires writing the full observe-think-act loop, tool dispatch, output parsing, and retry logic yourself β easily 50+ lines β but gives you complete visibility into every decision the agent makes.
When Custom WinsΒΆ
Custom implementations become worthwhile when you need fine-grained control over token budgets (e.g., dynamically pruning tool descriptions based on context), non-standard reasoning patterns (e.g., tree-of-thought with backtracking), or tight integration with proprietary infrastructure. In production systems where latency and cost matter, the overhead of framework abstractions β extra prompt tokens from verbose templates, unnecessary serialization steps β can add up. Profile both approaches on your actual workload before committing.
import time
# LangChain approach (5-10 lines)
def langchain_agent():
tools = [Tool(name="calc", func=lambda x: eval(x), description="Calculator")]
agent = create_openai_functions_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
return executor.invoke({"input": "What is 15 + 27?"})
# Custom approach (50+ lines)
def custom_agent():
# Would need: prompt engineering, tool execution, loop control,
# error handling, parsing, etc.
pass
print("β
LangChain: Quick to build, less control")
print("β
Custom: More code, full control")
Decision Matrix (2026)ΒΆ
Criteria |
LangChain |
LangGraph |
OpenAI Agents SDK |
Google ADK |
CrewAI |
Semantic Kernel |
Custom |
|---|---|---|---|---|---|---|---|
Development Speed |
βββββ |
βββ |
ββββ |
βββ |
βββββ |
βββ |
β |
Flexibility |
βββ |
βββββ |
βββ |
ββββ |
ββ |
ββββ |
βββββ |
Multi-Agent |
ββ |
βββββ |
ββββ |
βββ |
ββββ |
βββ |
βββββ |
MCP Support |
ββββ |
ββββ |
βββββ |
ββββ |
ββ |
ββββ |
ββ |
Model Agnostic |
βββββ |
βββββ |
ββββ |
βββ |
ββββ |
ββββ |
βββββ |
Documentation |
βββββ |
ββββ |
ββββ |
βββ |
ββββ |
ββββ |
N/A |
Community |
βββββ |
ββββ |
ββββ |
βββ |
βββ |
βββ |
β |
Production Ready |
ββββ |
βββββ |
ββββ |
βββ |
βββ |
ββββ |
βββββ |
Part 6: Production PatternsΒΆ
Error Handling and RetriesΒΆ
Production agents face failures that never appear in tutorials: API rate limits, malformed tool outputs, infinite reasoning loops, and context window overflows. Wrapping the AgentExecutor in a custom subclass lets you intercept exceptions at the execution boundary, log diagnostic information, and return graceful fallback responses instead of crashing. The pattern below catches any exception during the agent loop and converts it into a structured error response that downstream code can handle.
Why Defensive Agent Design MattersΒΆ
An unhandled exception in an agent loop can leave the system in an inconsistent state β partial tool calls executed, memory corrupted, or user-facing errors exposed. Production agents should implement circuit breaker patterns (stop calling a failing tool after \(n\) consecutive errors), timeout guards (abort if the agent hasnβt converged within a time budget), and graceful degradation (fall back to a simpler model or direct response when tools are unavailable). The max_iterations parameter in AgentExecutor is your first line of defense against infinite loops, but application-level error handling provides the safety net.
from langchain.callbacks import StdOutCallbackHandler
# Add custom error handling
class SafeAgentExecutor(AgentExecutor):
def _call(self, inputs, **kwargs):
try:
return super()._call(inputs, **kwargs)
except Exception as e:
return {
"output": f"Error occurred: {str(e)}",
"error": True
}
print("β
Safe agent executor ready")
Monitoring and LoggingΒΆ
Structured logging transforms an opaque agent into an observable system. By subclassing AgentExecutor and logging inputs and outputs at the execution boundary, you create an audit trail that answers critical production questions: which tool was called, what arguments were passed, how long each step took, and whether the agentβs final answer addressed the userβs intent. LangChainβs callback system (StdOutCallbackHandler, LangSmithTracer) provides built-in hooks for tracing every intermediate step β tool invocations, LLM calls, token counts β without modifying agent code. In production, pipe these traces to an observability platform (Datadog, Weights & Biases, LangSmith) to monitor latency distributions, error rates, and cost per query across your agent fleet.
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class LoggingAgentExecutor(AgentExecutor):
def _call(self, inputs, **kwargs):
logger.info(f"Agent input: {inputs}")
result = super()._call(inputs, **kwargs)
logger.info(f"Agent output: {result}")
return result
print("β
Logging configured")
π― Knowledge CheckΒΆ
Q1: Name three fully managed agent API services and their vendor lock-in trade-offs.
Q2: When should you choose LangGraph over the OpenAI Agents SDK?
Q3: What are MCP and A2A, and why do they matter for multi-vendor agent architectures?
Q4: How does memory work in LangChain agents?
Click for answers
A1: Anthropic Managed Agents (Claude-only), AWS Bedrock Agents (multi-model on Bedrock), Azure AI Agent Service (multi-model on Azure). Lock-in increases with vendor-specific tool integrations.
A2: LangGraph when you need complex stateful workflows with cycles, conditional branching, human-in-the-loop checkpoints, or persistent state. OpenAI Agents SDK for lightweight multi-agent handoffs with minimal boilerplate.
A3: MCP (Model Context Protocol) standardizes how agents connect to tools/resources β broadly adopted across Anthropic, OpenAI, Google, Microsoft. A2A (Agent-to-Agent) enables agents to discover and delegate tasks to other agents β early but growing adoption. Both reduce vendor lock-in.
A4: Memory stores conversation history and passes it to the agent as context. Strategies include buffer (full history), summary (compressed), window (last k turns), and vector store (semantic retrieval).
π Next StepsΒΆ
Complete the Agent Framework Challenge
Read Notebook 5: Multi-Agent Systems
Read Notebook 6: MCP (Model Context Protocol)
Read Notebook 7: OpenAI Agents SDK + LangGraph deep dive
Build a production agent with your chosen framework
Experiment with LangGraph for complex workflows
Great work! You now have a complete map of the 2026 agent framework landscape! π