Writing

Genkit vs LangChain vs ADK: Python Edition 2026

Genkit makes a different bet: one primitive, minimum abstraction, everything else is your code.

LangChain. LangGraph. LangSmith. DeepAgents. Four separate abstraction towers for building AI agents in Python. Here’s the problem: they don’t compose. Outgrow one and you start over in the next. Build a working agent with AgentExecutor, then need persistent memory, and you’re not iterating — you’re rewriting in LangGraph with a different import, a different class, a different mental model, and a different execution engine from scratch.

Genkit makes a different bet: one primitive, minimum abstraction, everything else is your code.

The primitive is the generate loop:

model call → [optional tool call] → recurse until answer OR max_depth

An agent is this loop plus session state. Genkit formalizes the loop as generate(), the agent harness as define_agent(), and adds tracing because code alone can’t give you that. Everything else — auth, retry, routing, data models — stays regular Python.

Here’s what that means for a developer building a production agent in 2026.


Concept Inventory

Before writing a single line of application code, here are the framework concepts you must learn to build a production agent with tool use:

LangChain (simple, stateless agent):

  • Runnables / LCEL pipe operator (|)
  • create_tool_calling_agent
  • AgentExecutor
  • ChatPromptTemplate, MessagesPlaceholder
  • @tool decorator
  • ConversationBufferMemory (or its LangGraph replacement)
  • Callbacks

When you need persistent memory, add LangGraph:

  • StateGraph
  • add_node, add_edge, add_conditional_edges
  • ToolNode
  • MemorySaver / BaseCheckpointSaver
  • compile()
  • TypedDict state schema
  • Config dict with thread_id

Genkit:

  • Genkit instance

  • generate() / generate_stream()

  • @ai.flow()

  • @ai.tool()

  • middleware (optional, for cross-cutting concerns)

  • define_agent() + InMemorySessionStore (when you need state)

  • Core concepts · LangChain (stateless): Runnable, LCEL, AgentExecutor, PromptTemplate, Tool, Memory · + LangGraph (stateful): + StateGraph, Node, Edge, ToolNode, Checkpointer, compile() · Genkit: Genkit, generate(), flow, tool

  • Mental model shift to add state · LangChain (stateless): Full rewrite in LangGraph · Genkit: Add store= param + agent protocol

  • Framework glue vs your code · LangChain (stateless): ~40% framework · + LangGraph (stateful): ~60% framework · Genkit: ~10% framework


The Same Agent, Three Ways

The best way to see the difference is to build the same thing in each framework. Here’s a weather bot with tool use — stateless first, then stateful.

LangChain: Stateless Path

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

@tool
def get_weather(city: str) -> str:
    """Get the weather for a city."""
    return f"Sunny and 72°F in {city}"

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a weather assistant."),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, [get_weather], prompt)
executor = AgentExecutor(agent=agent, tools=[get_weather], verbose=True)
result = executor.invoke({
    "input": "What's the weather in Chicago?",
    "chat_history": [],
})
print(result["output"])

This works. The LCEL syntax is readable. The friction starts when you need to remember history.

LangChain: Stateful Path (requires LangGraph rewrite)

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from typing import TypedDict, Annotated
import operator

class AgentState(TypedDict):
    messages: Annotated[list, operator.add]

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
llm_with_tools = llm.bind_tools([get_weather])

def should_continue(state):
    return "tools" if state["messages"][-1].tool_calls else END

def call_model(state):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

workflow = StateGraph(AgentState)
workflow.add_node("agent", call_model)
workflow.add_node("tools", ToolNode([get_weather]))
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue)
workflow.add_edge("tools", "agent")
app = workflow.compile(checkpointer=MemorySaver())

result = app.invoke(
    {"messages": [HumanMessage("What's the weather in Chicago?")]},
    config={"configurable": {"thread_id": "1"}},
)
print(result["messages"][-1].content)

Count what changed: new import set, new TypedDict state schema, new graph construction idiom, new compile(), new invocation format with config. The get_weather tool definition didn’t change — everything around it did.

Genkit: Stateless → Stateful

from pydantic import BaseModel
from genkit import Genkit
from genkit.plugins.google_genai import GoogleAI
from genkit.agent import InMemorySessionStore, AgentInit

ai = Genkit(plugins=[GoogleAI()], model='googleai/gemini-2.0-flash')

class WeatherInput(BaseModel):
    city: str

@ai.tool()
async def get_weather(input: WeatherInput) -> str:
    """Get the weather for a city."""
    return f"Sunny and 72°F in {input.city}"

# ── Stateless: one generate() call ─────────────────────────────────────────
response = await ai.generate(
    prompt="What's the weather in Chicago?",
    tools=[get_weather],
)
print(response.text)

# ── Stateful: same tool, same model, add store ─────────────────────────────
agent = ai.define_agent(
    name='weatherAgent',
    model='googleai/gemini-2.0-flash',
    system='You are a weather assistant.',
    tools=[get_weather],              # ← same tool, unchanged
    store=InMemorySessionStore(),     # ← this line adds persistence
)

conn = await agent.stream_bidi()     # start a session
await conn.send_text("What's the weather in Chicago?")
await conn.close()                   # signal: no more input this turn

async for chunk in conn.receive():   # stream response chunks
    if chunk.model_chunk:
        for part in chunk.model_chunk.content:
            if hasattr(part.root, 'text'):
                print(part.root.text, end='', flush=True)

The get_weather tool didn’t change. The model string didn’t change. define_agent() wraps the same primitives and adds session tracking. The agent protocol (stream_bidisend_textclosereceive) is Genkit’s bidi-streaming interface — you learn it once and it works for every agent.

The graduation cliff: Going from stateless to stateful in LangChain means switching frameworks entirely. You move from LangChain’s AgentExecutor world to LangGraph’s StateGraph world — different import paths, different concepts, different execution model. In Genkit, you add one parameter (store=) and switch from generate() to define_agent(). Same generate loop underneath, just with session state attached.


Adding Production Features

Retry on Rate Limits

At production scale, you will hit RESOURCE_EXHAUSTED errors. Here’s how each framework handles it.

LangChain has no standard retry primitive. The typical approach is wrapping your LLM call with tenacity:

from tenacity import retry, wait_exponential, stop_after_attempt
from langchain_google_genai import ChatGoogleGenerativeAI

@retry(wait=wait_exponential(min=1, max=60), stop=stop_after_attempt(3))
def call_with_retry(llm, messages):
    return llm.invoke(messages)

This works, but it’s not composable: you have to apply it per-call, it doesn’t integrate with LangChain’s callback system, and it won’t appear in LangSmith traces. Every team implements this differently.

Genkit:

from genkit.plugins.middleware import Retry

response = await ai.generate(
    prompt=user_input,
    tools=[get_weather],
    use=[Retry(max_retries=3)],   # retries RESOURCE_EXHAUSTED by default
)

One line. Standard. Composable with every other middleware. Shows up in the Dev UI. Retry retries RESOURCE_EXHAUSTED, UNAVAILABLE, DEADLINE_EXCEEDED, and INTERNAL by default — the four errors you’ll actually see in production. You can customize which statuses to retry:

use=[Retry(max_retries=5, statuses=['RESOURCE_EXHAUSTED'])]

FastAPI Deploy

LangChain requires manual wiring: create a route, call executor.invoke() in the handler, handle exceptions yourself, build SSE if you want streaming. No standard pattern.

Genkit has a decorator:

from fastapi import FastAPI
from genkit.plugins.fastapi import genkit_fastapi_handler

app = FastAPI()

@app.post('/weather', response_model=None)
@genkit_fastapi_handler(ai)
@ai.flow()
async def weather_flow(city: str) -> str:
    response = await ai.generate(
        prompt=f"What's the weather in {city}?",
        tools=[get_weather],
    )
    return response.text

@genkit_fastapi_handler(ai) exposes the flow as an HTTP endpoint with SSE streaming support and Genkit protocol compatibility. Dev UI reflection starts automatically when GENKIT_ENV=dev is set — no lifespan wiring needed. In production, remove GENKIT_ENV=dev and deploy normally.


The ADK Angle

Google ADK is a different product with a different target. ADK is enterprise-first, built around Google Workspace, and designed for multi-agent orchestration at Google’s scale — handoffs between agents, sub-agent spawning, Sheets/Drive/Gmail integrations, enterprise auth.

If your agents need to read from Google Drive, write to Sheets, or operate inside Google Workspace workflows, ADK has first-class integrations that no other framework matches. If you’re building a Python service that calls a model and needs production tracing, Genkit is the right call.

Using ADK for a single-service weather bot is like using Kubernetes for a cron job. The abstraction cost is real and it compounds across your team.


Decision Guide

This is the part where most comparison articles say “it depends.” Here’s the actual decision:

  • Your situation: Building on Google Cloud Run or Firebase Functions · Use this: Genkit Python
  • Your situation: Have JS and Python teams sharing infra · Use this: Genkit Python
  • Your situation: Need production OTel tracing without a SaaS subscription · Use this: Genkit Python
  • Your situation: Need 50+ third-party integrations (Pinecone, Weaviate, Cohere, etc.) · Use this: LangChain
  • Your situation: Need mature RAG pipeline tooling out of the box · Use this: LangChain
  • Your situation: Team already knows LangChain, project is live · Use this: LangChain (migration cost isn’t worth it)
  • Your situation: Agents need to operate on Google Workspace (Sheets, Drive, Gmail) · Use this: Google ADK
  • Your situation: Enterprise multi-agent orchestration at scale · Use this: Google ADK

The one situation where Genkit wins clearly and decisively: you’re on Google Cloud, you want production observability from day one, and you want a codebase where the framework glue is thin enough that a coding agent (Claude Code, Copilot, Codex) can read the whole thing and modify it without hallucinating abstractions.


The Lightness Bet

AI agent code is evolving fast. New models, new tool-calling patterns, new multi-agent architectures — every month, something changes. The developers who move fastest right now are the ones whose “AI layer” is thin. The ones who understand every line of their agent code.

Heavy abstractions trap you. You become dependent on the framework catching up to the new primitive, or you rewrite around it. Genkit’s bet is that minimum abstraction means maximum longevity: when the next pattern shift happens, you update a generate() call, not a graph topology.

Flask made this bet against Django. FastAPI made it against Flask-plus-marshmallow. In the AI agent space, that bet is Genkit.

The plugin ecosystem is still growing. The PyPI packages aren’t fully published yet — install from git. There are fewer Stack Overflow answers. Those are real tradeoffs. But if you’re building a new service on Google Cloud and you want the framework to be the least interesting part of your codebase, Genkit is where that bet pays off.


Genkit Python install:

uv pip install "genkit[google-genai] @ git+https://github.com/firebase/genkit.git#subdirectory=py/packages/genkit"

Set GEMINI_API_KEY from Google AI Studio. Dev UI: genkit start -- uvicorn main:app --reload.