Bitdoze Logo

Google ADK Multi-Agent Research Team with Tavily & Crawl4AI

Build a multi-agent research team with Google ADK. Combine Tavily web search, Crawl4AI content extraction, and AI summarization into one agent workflow.

DragosDragos30 min read
Google ADK Multi-Agent Research Team with Tavily & Crawl4AI

Building a multi-agent research team with Google ADK is the natural next step after getting your first Google ADK agent running. ADK’s architecture makes it straightforward to wire up specialized agents (one for web search, one for content extraction, one for summarization) and have a coordinator agent orchestrate them into a single workflow.

This tutorial has been fully rewritten for Google ADK 2.x (tested against 2.7.1, Aug 2026). The original version targeted ADK 0.x/1.x, and several APIs have changed since then: the model name, the Tavily LangChain integration, the Crawl4AI result API, and the multi-agent delegation pattern. Everything below works with the current versions.

Tested with google-adk 2.7.1

This tutorial was written and tested against Google ADK 2.7.1 (Aug 2026), Crawl4AI 0.9.x, and langchain-tavily. If you’re using an older ADK version, see the Troubleshooting section for migration notes.

What you’ll build: a multi-agent research team

We’ll create a system with four agents working together:

Agent Role Tools How it works
Coordinator Routes user requests to the right specialist sub_agents delegation LLM decides which specialist to invoke based on the query
Search Agent Web search for finding information Tavily Search (via LangChain) Returns structured results with links and snippets
Content Extractor Deep analysis of web pages Crawl4AI (custom FunctionTool) Extracts, parses, and structures page content
Summarizer Condenses information into clear summaries None (pure LLM reasoning) Creates summaries at different detail levels

The coordinator uses ADK 2.x’s sub_agents pattern. The coordinator’s LLM sees each specialist’s description and decides which one to delegate to. This is the modern ADK idiom for coordinator-over-specialists, replacing the older AgentTool wrapper pattern.

Here’s how data flows through the system:

User Query


┌─────────────────────┐
│  Coordinator Agent   │  ← LLM decides routing
│  (gemini-flash-latest)│
└──────┬──┬──┬────────┘
       │  │  │
       ▼  ▼  ▼
   ┌───────┐ ┌──────────┐ ┌───────────┐
   │Search │ │Extractor │ │Summarizer │
   │Agent  │ │Agent     │ │Agent      │
   └───┬───┘ └────┬─────┘ └─────┬─────┘
       │          │              │
       ▼          ▼              ▼
   Tavily API  Crawl4AI     LLM reasoning
       │          │              │
       └──────────┴──────────────┘

            Session State
            (shared memory)


            Final Response

Prerequisites

Cost note

Tavily’s free tier gives 1,000 API credits/month, enough for prototyping. The Gemini API has a free tier on AI Studio. Crawl4AI is open-source but runs a headless browser (needs ~500 MB RAM). You can build and test this entire system for $0.

Setting up your project

Create the project and install dependencies:

# Create project directory
mkdir adk-research-team
cd adk-research-team

# Initialize with uv
uv init
uv venv

# Install required packages
uv add google-adk langchain-tavily python-dotenv crawl4ai

Breaking change from older tutorials

If you followed an earlier version of this tutorial, replace langchain-community with langchain-tavily in your dependencies. The TavilySearchResults class from langchain_community is deprecated and will not work with current Tavily APIs.

Create a .env file in your project root:

# .env file
GOOGLE_API_KEY=your_google_ai_studio_api_key
TAVILY_API_KEY=your_tavily_api_key
# Optional: force the non-enterprise Gemini API path
GOOGLE_GENAI_USE_ENTERPRISE=False

The GOOGLE_GENAI_USE_ENTERPRISE variable replaces the deprecated GOOGLE_GENAI_USE_VERTEXAI. If you’re using a plain AI Studio key (not Vertex AI), you can omit it entirely. ADK defaults to the Gemini API path.

ADK’s CLI auto-loads .env from the project root, so manual load_dotenv() calls are optional but harmless.

Using a different model provider?

If you want to use models from OpenRouter or other providers instead of Google’s Gemini, see how to use any OpenRouter model with Google ADK.

Project structure

ADK expects a specific layout for adk web to discover your agents:

adk-research-team/
├── .env                       # API keys
└── agent_module/              # Agent package
    ├── __init__.py            # Makes it a Python package
    ├── agent.py               # Main file with root_agent
    ├── search_agent.py        # Search specialist
    ├── content_extractor.py   # Content analysis specialist
    └── summarizer.py          # Summarization specialist

Create the directory structure:

mkdir -p agent_module
touch agent_module/__init__.py

Add the following to agent_module/__init__.py:

# This file makes agent_module a proper Python package
# The import below ensures that agent.py is accessible
from . import agent

Agent and folder names must be valid Python identifiers (letters, digits, underscores only). Names with dashes like my-agent will cause ADK to silently skip loading the agent.

Creating the specialized agents

Let’s implement each specialist one by one, starting with the leaf agents and building up to the coordinator.

touch agent_module/search_agent.py
from dotenv import load_dotenv
import os
from google.adk.agents import LlmAgent
from google.adk.integrations.langchain import LangchainTool
from langchain_tavily import TavilySearch

load_dotenv()


def create_search_agent():
    """
    Creates an agent specialized in web searching using Tavily Search.
    """
    if not os.getenv("TAVILY_API_KEY"):
        raise ValueError("TAVILY_API_KEY not found in environment variables")

    # Create Tavily Search tool (langchain-tavily package)
    tavily_tool = TavilySearch(
        max_results=5,
        topic="general",
        search_depth="advanced",
        include_answer=True,
        include_raw_content=True,
        include_images=False,
    )

    # Wrap for ADK
    adk_tavily_tool = LangchainTool(tool=tavily_tool)

    return LlmAgent(
        name="search_agent",
        model="gemini-flash-latest",
        description="A specialized agent that searches the web for information using Tavily Search API.",
        instruction="""You are a web research specialist.

        When asked to find information about a topic, craft an effective search query and use the TavilySearch tool.

        After receiving search results:
        1. Parse the response which may contain a direct answer and multiple search results.
        2. Format the results in a clear, structured way, with each result showing the title, link, and a brief preview of the content.
        3. Highlight the most relevant results based on the original query.
        4. If Tavily provided a direct answer, present that first as the most likely answer.

        If the search doesn't return useful results, suggest refined search terms for a follow-up search.

        Avoid making up information - only report what is found in the search results.
        """,
        tools=[adk_tavily_tool],
    )

Key changes from the old version:

  • from google.adk.agents import LlmAgent instead of from google.adk import Agent
  • from google.adk.integrations.langchain import LangchainTool (moved from google.adk.tools.langchain_tool)
  • from langchain_tavily import TavilySearch instead of TavilySearchResults from langchain_community
  • Model: gemini-flash-latest (the old gemini-2.0-flash was shut down in June 2026)

Tavily free tier

Tavily gives 1,000 free API credits/month with no credit card. One search typically costs 1 credit. Advanced search with include_raw_content may cost more per query.

If you need a search option that doesn’t require an API key, you can look at free web search without API keys using DuckDuckGo. For a managed web search API built for AI agents, TinyFish web search API is another option worth evaluating.

2. The Content Extractor with Crawl4AI

touch agent_module/content_extractor.py
from dotenv import load_dotenv
import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig
from google.adk.agents import LlmAgent
from google.adk.tools import FunctionTool
from google.adk.tools.tool_context import ToolContext

load_dotenv()


async def extract_content_from_url(url: str, tool_context: ToolContext = None) -> dict:
    """
    Extracts content from a URL using Crawl4AI.

    Args:
        url (str): The URL to extract content from.
        tool_context (ToolContext, optional): Tool context for ADK session state.

    Returns:
        dict: Extracted content, metadata, and status.
    """
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(url=url, config=CrawlerRunConfig())

    # Crawl4AI 0.9.x: result.markdown is a MarkdownGenerationResult object
    md = result.markdown.raw_markdown if result.markdown else ""

    # Title: try metadata first, fall back to first markdown heading
    title = (result.metadata or {}).get("title")
    if not title and md:
        for line in md.split("\n"):
            if line.startswith("# "):
                title = line[2:]
                break

    response = {
        "status": "success" if result.success else "error",
        "title": title,
        "url": result.url or url,
        "markdown_content": md[:10000],  # Limit to 10k chars for context window
        "content_length": len(md),
        "word_count": len(md.split()),
        "has_truncated_content": len(md) > 10000,
        "error_message": result.error_message if not result.success else None,
    }

    # Store full content in session state for the summarizer
    if tool_context and md:
        tool_context.state["temp:extracted_content"] = md

    return response


def create_content_extractor_agent():
    """
    Creates an agent specialized in extracting and analyzing content from URLs.
    """
    extract_tool = FunctionTool(func=extract_content_from_url)

    return LlmAgent(
        name="content_extractor",
        model="gemini-flash-latest",
        description="A specialized agent that extracts and analyzes content from web pages using Crawl4AI.",
        instruction="""You are a web content analysis specialist.

        When given a URL, use the extract_content_from_url tool to fetch and analyze its content.

        After extracting content:
        1. Report the page title and basic metadata (word count, if content was truncated).
        2. Highlight key information found in the content that's most relevant to the user's request.
        3. Note if there were any errors during extraction.

        When extracting content from multiple URLs, organize the information clearly by URL.

        If the extraction fails, explain the error and suggest possible solutions.
        """,
        tools=[extract_tool],
    )

Crawl4AI breaking changes

Crawl4AI 0.9.x changed result.markdown from a string to a MarkdownGenerationResult object. Use .raw_markdown to get the text. The result.title and result.headers attributes no longer exist. Use result.metadata.get("title") instead. The include_headers parameter was removed from arun().

A few things to note about this code:

  • We use CrawlerRunConfig() to configure the crawl (replaces the old include_headers=True kwarg)
  • result.markdown.raw_markdown gives us the actual text (the old result.markdown string access would crash)
  • State keys use the temp: prefix. ADK auto-discards these after the invocation, which is the right behavior for intermediate handoffs
  • Exceptions are allowed to propagate instead of being swallowed in a broad except Exception. ADK 2.0 uses exceptions for automatic retries and Human-in-the-Loop pauses.

For enterprise-scale web data extraction, Bright Data web scraping platform handles proxy rotation and anti-bot bypass. For a lighter alternative to Crawl4AI, see free Firecrawl alternative for AI agents. And if you’re building your own site and want to control what AI crawlers can access, here’s how to block AI crawlers on your own site.

3. The Summarizer Agent

touch agent_module/summarizer.py
from google.adk.agents import LlmAgent


def create_summarizer_agent():
    """
    Creates an agent specialized in summarizing content.
    Uses output_key to automatically save the summary to session state.
    """
    return LlmAgent(
        name="summarizer",
        model="gemini-flash-latest",
        description="A specialized agent that summarizes content at various detail levels.",
        instruction="""You are a professional content summarizer.

        Summarize the content provided to you according to the requested length:

        - "short": A concise summary in 1-3 sentences, capturing only the essential point.
        - "medium": A balanced summary in 1-3 paragraphs, covering key points and some supporting details.
        - "long": A comprehensive summary in multiple paragraphs, preserving nuances and important contexts.

        If no length is specified, default to "medium".

        Always structure summaries with clear headings and bullet points when appropriate.

        Prioritize accuracy over brevity - never include information not found in the original text.

        For technical or complex content, preserve the key terminology used in the original text.
        """,
        output_key="summary",
    )

Simpler with output_key

ADK 2.x lets you set output_key="summary" on an LlmAgent to automatically save its final response to session state. No custom tool needed. The old version used a hand-rolled summarize_content function to write to state; output_key does the same thing in one line.

4. The Coordinator: wiring up the root_agent

Finally, the main agent file that ADK discovers:

touch agent_module/agent.py
from google.adk.agents import LlmAgent

# Import our specialized agents
from agent_module.search_agent import create_search_agent
from agent_module.content_extractor import create_content_extractor_agent
from agent_module.summarizer import create_summarizer_agent

# Define the root agent that ADK web will use
root_agent = LlmAgent(
    name="research_coordinator",
    model="gemini-flash-latest",
    description="A coordinator agent that manages a team of specialized research agents.",
    instruction="""You are the coordinator of a research assistant team with specialized agents.

    Your team includes:
    - search_agent: Finds information on the web using Tavily Search.
    - content_extractor: Analyzes and extracts content from specific URLs.
    - summarizer: Creates concise summaries of content at different levels of detail.

    Based on user requests, delegate tasks to the appropriate specialist:

    1. If the user needs to find information on a topic, delegate to search_agent.
    2. If the user provides a specific URL or wants to analyze a web page, delegate to content_extractor.
    3. If the user needs a summary of content, delegate to summarizer.
    4. For complex research tasks, coordinate multiple agents in sequence:
       - First search for relevant information
       - Then extract detailed content from the most promising URLs
       - Finally summarize the findings

    Always present the results from your specialists in a clear, organized manner.
    When coordinating multi-step research, explain the research process to the user.

    Remember: You are responsible for the final response to the user, so ensure it fully addresses their request.
    """,
    sub_agents=[
        create_search_agent(),
        create_content_extractor_agent(),
        create_summarizer_agent(),
    ],
)

The key change here is sub_agents=[...] instead of tools=[AgentTool(...)]. With sub_agents, the coordinator’s LLM sees each specialist’s name and description and decides which one to invoke based on the user’s request. This is the idiomatic ADK 2.x pattern for coordinator-over-specialists.

The old AgentTool pattern still works, but it’s now better suited for tightly-controlled single-turn sub-agents (mode="single_turn"). For a coordinator that should freely route between specialists, sub_agents is the right choice.

Running the multi-agent team

There are three ways to run and test your agents.

Option 1: adk web (interactive UI)

# From the project root (where .env is located)
adk web

This starts a local web server at http://127.0.0.1:8000. Open it in your browser, select “research_coordinator” from the agent list, and start chatting.

Local only

adk web is unauthenticated and binds to 127.0.0.1 by default. Do not expose it to the public internet without adding authentication and TLS.

Verify it works: The agent list should show research_coordinator. Try a simple search query first:

“What are the latest advancements in quantum computing?”

Then try a multi-step query:

“Research the environmental impact of electric vehicles, analyze the top result in detail, and provide a short summary of the key findings.”

Option 2: adk run (CLI REPL)

Good for smoke-testing a single agent without the web UI:

# Test the search agent standalone
adk run agent_module/search_agent.py

This drops you into an interactive REPL. Type a query and see if the agent responds correctly. Use this to verify each specialist works before wiring the coordinator.

Option 3: adk api_server (HTTP endpoints)

For scripting and curl-based verification:

adk api_server

Then verify with curl:

# Health check
curl http://127.0.0.1:8000/health

# List available agents
curl http://127.0.0.1:8000/list-apps

This is useful for integrating the agent team into other applications or testing from CI.

How the multi-agent system works

Let’s break down the architecture:

  1. Coordinator as orchestrator: All user requests go to the coordinator. Its LLM analyzes the request and decides which specialist to delegate to based on each sub_agent’s description.

  2. Sub-agents delegation: ADK 2.x’s sub_agents pattern lets the coordinator’s LLM see the specialists’ names and descriptions. It decides routing through LLM reasoning, no hardcoded if/else logic needed.

  3. Communication via session state: Agents share data through tool_context.state. The content extractor stores extracted text with temp:extracted_content, and the summarizer (or coordinator) can read it. The temp: prefix means ADK auto-discards the data after the invocation.

  4. LangChain integration: The LangchainTool wrapper from google.adk.integrations.langchain bridges LangChain tools (like Tavily) into ADK. This means any LangChain tool can be used in your ADK agents.

  5. Custom tools with FunctionTool: The Crawl4AI integration shows how to wrap any async Python function as an ADK tool using FunctionTool.

Note that InMemorySessionService (the default) is not persistent. Session state resets when the server restarts. That’s fine for development and testing. For production, you’d swap in a persistent SessionService.

Extending the system

The modular design makes it easy to add capabilities:

Enhancement Implementation approach
PDF document analysis Add a specialist agent with a PDF parsing tool
Translation services Create a translation agent with a language API
Data visualization Add an agent that generates charts from extracted data
Sentiment analysis Implement a specialized sentiment analysis agent
Citation management Add a tool to extract and format citations from academic sources
Tavily Extract for simple URLs Use TavilyExtract from langchain-tavily instead of Crawl4AI for basic URL content extraction (fewer moving parts)
Using Tavily's MCP server instead of LangChain

Tavily provides an official MCP server (mcp-tavily on PyPI, or hosted at mcp.tavily.com). Instead of wrapping Tavily through LangChain, you can connect it directly via ADK’s McpToolset. This removes the LangChain dependency entirely and follows the Model Context Protocol standard.

The tradeoff: MCP integration is newer and less documented than the LangChain path. If you’re already using LangChain for other tools, the LangchainTool wrapper is simpler. If you’re building a pure ADK stack, MCP is cleaner.

For another MCP tooling example, see Agno with Context7 MCP.

Upgrading to ADK 2.0 Workflow Runtime

ADK 2.0 introduced a graph-based Workflow Runtime for deterministic agent orchestration. If your research pipeline is always “search → extract → summarize” (no branching), a Workflow graph is more predictable than LLM-driven sub_agents routing.

The sub_agents pattern shown in this tutorial is better when you want the coordinator to decide which specialists to call based on the query. A Workflow is better when the sequence is fixed and you want guaranteed execution order.

See the ADK 2.0 documentation for Workflow examples.

If you’re exploring other frameworks for multi-agent systems, you can also build an AI research squad with Agno or build an AI agent with Mastra. Both take different approaches to agent orchestration.

Troubleshooting common issues

Problem Cause Fix
429 RESOURCE_EXHAUSTED on Gemini free tier Free tier has rate limits Configure http_options.retry_options on the model or request higher quota. See the accordion below.
Agent not showing in adk web Folder/agent name has dashes or __init__.py doesn’t import agent Use only letters, digits, underscores in names. Ensure __init__.py has from . import agent.
ModuleNotFoundError: langchain_tavily Installed langchain-community instead of langchain-tavily Run uv add langchain-tavily. The old package’s Tavily tool is deprecated.
AttributeError: 'MarkdownGenerationResult' has no attribute 'split' Using old Crawl4AI code that treats result.markdown as a string Use result.markdown.raw_markdown. It’s now a MarkdownGenerationResult object.
Crawl4AI browser not found / timeouts Headless Chromium not installed Run crawl4ai-setup to install Chromium. Increase page_timeout in CrawlerRunConfig(page_timeout=60000).
Broad except Exception masks retries ADK 2.0 uses exceptions for automatic retries and HITL pauses Let exceptions propagate from tools instead of catching everything.
State “lost” between sessions InMemorySessionService resets on restart Expected for dev. Use a persistent SessionService for production.
.env not loading File is inside agent_module/ instead of project root Move .env to the project root (next to agent_module/). ADK CLI auto-loads it.
Fixing 429 errors on Gemini free tier

The Gemini API free tier has rate limits that can return 429 RESOURCE_EXHAUSTED errors. You can configure retry behavior on the model:

from google.adk.agents import LlmAgent
from google.genai import types

root_agent = LlmAgent(
    name="research_coordinator",
    model="gemini-flash-latest",
    generate_content_config=types.GenerateContentConfig(
        http_options=types.HttpOptions(
            retry_options=types.HttpRetryOptions(
                attempts=5,
                initial_delay=1.0,
                max_delay=10.0,
            )
        )
    ),
    # ... rest of config
)

If you hit 429s consistently, consider requesting a quota increase on AI Studio or switching to a paid Gemini API tier.

Cost and free-tier summary

Component Free tier Paid pricing Notes
Google ADK Free / open-source N/A Python package, no API cost
Gemini API (AI Studio) Free tier available Varies by model 429 errors common on free tier
Tavily Search 1,000 credits/month $0.008/credit PAYG One search ≈ 1 credit. No card required for free tier.
Crawl4AI Free / open-source N/A Runs headless Chromium (~500 MB RAM). Docker option available.

Zero cost to prototype

All four components have free tiers. You can build and test this entire multi-agent research team without spending a cent.

One thing to watch: include_raw_content=True on Tavily and storing 10k-character markdown per URL can blow up your context window fast. The truncation in the content extractor handles this, but if you’re processing many URLs, consider include_raw_content=False to save tokens.

Conclusion

You now have a working multi-agent research team built on Google ADK 2.x. The system demonstrates several key ADK patterns:

  • sub_agents delegation for coordinator-over-specialists routing
  • LangChain integration via LangchainTool for using Tavily Search
  • Custom tools with FunctionTool for the Crawl4AI content extractor
  • output_key for automatic state writes (the summarizer)
  • Session state with temp: prefix for inter-agent data sharing
  • adk web for rapid interactive testing without building a frontend

From here, you can extend the team with new specialists, swap in different models, or upgrade to ADK’s Workflow Runtime for deterministic pipelines.

Start with Part 1: Your First Google ADK Agent

To use models beyond Gemini (like Claude or GPT via OpenRouter), see how to use any OpenRouter model with Google ADK.