- Published on
- • 5 min read
Simplifying Multi-Agent Systems: Reducing Root Agent Complexity
Simplifying Multi-Agent Systems: Reducing Root Agent Complexity
The Complex Root Agent I Started With
A while ago, my ADK root orchestrator agent was a 150-line prompt that tried to handle too much complexity. It had intricate conditional logic, state management, and transfer rules that spanned multiple nested if-else statements. The system was slower than needed and sometimes confused users with incorrect agent transfers.
# The pattern I was using - too complex
system_prompt = """
You are a workflow coordinator that must understand user intent deeply.
If user asks about requirements but they're not complete:
Check if they're asking new requirements or clarifying existing ones
If new requirements: transfer to requirement_agent
If clarifying: handle directly
elif user has seen search results:
Analyze if they want more results, want to select, or have questions
If they want to "tell me more": transfer to product_agent
If they want to "compare": handle directly
If they want new search: transfer to search_agent
# ... 30+ more lines of conditional logic
"""
root_agent = LlmAgent(
model=constants.ORCHESTRATOR_MODEL,
name="coordinator",
description="Complex workflow coordinator with deep intent analysis",
instruction=system_prompt,
tools=[
FunctionTool(complex_state_checker),
FunctionTool(intent_analyzer),
FunctionTool(workflow_manager)
]
)
The result? Slower response times and a system that was harder to maintain. Adding new features meant understanding complex conditional branches.
Radical Simplification
The solution wasn't more logic - it was dramatically less. I reduced my root agent's prompt from 150 lines to 20 lines by making it truly dumb and letting specialists be smart.
The New Root Agent: Simple and Focused
Here's the pattern I use now:
from google.adk.agents import LlmAgent
from .shared_libraries import constants
# Simplified fictional agents for illustration
requirement_agent = LlmAgent(model=constants.PRO_MODEL, name="requirement_agent")
search_agent = LlmAgent(model=constants.PRO_MODEL, name="search_agent")
selection_agent = LlmAgent(model=constants.PRO_MODEL, name="selection_agent")
specs_agent = LlmAgent(model=constants.PRO_MODEL, name="specs_agent")
quote_agent = LlmAgent(model=constants.PRO_MODEL, name="quote_agent")
# Simplified root agent with clean workflow logic
system_prompt = """
You are a workflow coordinator.
Your role is simple: manage workflow progression and let specialists handle everything else.
**WORKFLOW STATES:**
1. Requirements → 2. Search → 3. Selection → 4. Specifications → 5. Quotes
**REQUIRED TRANSFERS (Only These):**
- No requirements → requirement_agent
- Requirements complete + no search → search_agent
- Selections complete → specs_agent (automatic)
- Specs complete + user wants quotes → quote_agent
**NEVER TRANSFER:**
- If a specialist agent can handle the user's request
- More than twice per conversation turn
**YOUR SIMPLE JOB:**
1. Check session state workflow progress
2. Transfer only for required workflow progression
3. Let specialist agents handle all user questions
4. Provide workflow summaries when complete
"""
root_agent = LlmAgent(
model=constants.ORCHESTRATOR_MODEL,
name="coordinator",
description="Simple workflow coordinator that manages progression and delegates to specialists",
instruction=system_prompt,
sub_agents=[
requirement_agent,
search_agent,
selection_agent,
specs_agent,
quote_agent,
]
)
That's the entire orchestrator logic. 87% less code than the previous version. All the complex conditional logic is gone.
What Actually Changed: The Architecture Shift
Before (Complex Root Agent):
- 150 lines of conditional logic
- Complex state checking with nested if-else statements
- Root agent tried to understand user intent deeply
- Transfer rules spanned dozens of conditions
- High cognitive load on the orchestrator
After (Simple Root Agent):
- 20 lines total
- Simple declarative transfer rules
- Root agent only checks workflow state
- Clear "required transfers" vs "never transfer" rules
- Heavy lifting delegated to specialists
The key insight: The root agent shouldn't try to be smart. It should just know when to pass control to someone who is.
Better Sub-Agent Descriptions
One crucial discovery I made: well-written sub-agent descriptions make the root agent's job much easier. Instead of complex conditional logic, I now focus on writing clear, descriptive sub-agent definitions.
# Before: Generic description
search_agent = LlmAgent(
model=constants.PRO_MODEL,
name="search_agent",
description="Searches for products",
instruction="Search the catalog and return results"
)
# After: Detailed, proactive description
search_agent = LlmAgent(
model=constants.PRO_MODEL,
name="search_agent",
description="Use PROACTIVELY when user provides search queries, search criteria, specifications, technical details, filters, or search refinement requests. Handles text search, image search, and search refinement.",
instruction="""
You are a product search specialist...
# ... the rest of the agent prompt
""",
tools=[
FunctionTool(product_search_tool),
FunctionTool(image_search_tool),
FunctionTool(filter_results_tool)
]
)
This descriptive approach means the root agent doesn't need complex logic to decide when to use each specialist - the sub-agents essentially advertise their own use cases.
Specialists Handle the Complexity
With the root agent simplified, the specialist agents became more capable and self-contained. Each specialist is now responsible for its own domain complexity, including handling user questions within that domain.
The Performance Impact: Why Simplicity Wins
What Actually Improved
The architectural simplification delivered better results in practice:
What's Improved:
- Response Quality: More consistent and appropriate agent behavior
- System Reliability: Fewer confused responses and transfer loops
- Development Experience: Much easier to understand and modify the system
- Code Maintainability: Simpler logic meant fewer bugs and easier debugging
Code Reduction:
- Root Agent: 150 lines → 20 lines (87% reduction)
- Complexity: Eliminated nested conditional logic
- Cognitive Load: Much easier for new developers to understand
Why This Works Better:
- Reduced Cognitive Load: The LLM processes fewer tokens and simpler logic
- Clearer Decision Paths: No nested conditional branches to evaluate
- Better Context Preservation: Fewer unnecessary transfers
- Specialist Ownership: Each agent owns its domain completely
What I Learned About Agent Simplification
1. The "Dumb Coordinator" Principle
The root agent shouldn't try to understand user intent deeply. It should only:
- Check workflow state
- Follow simple transfer rules
- Let specialists handle complexity
# Bad: Complex intent analysis
"Analyze user intent based on conversation context..."
# Good: Simple state checking
"Requirements complete? → Search agent needed."
2. Specialist Ownership
Each specialist owns their domain completely:
- They handle domain-specific questions
- They determine when their work is done
- They manage their own tools and logic
- They only transfer when workflow progression is required
3. Minimal Transfer Logic
Instead of complex conditional branching, use:
- Clear workflow states
- Simple transfer triggers
- Explicit "never transfer" rules
How to Apply This Approach
Simplification Checklist
Before Refactoring:
- Identify complex conditional logic in root agent
- Map current transfer patterns
During Refactoring:
- Move domain logic to specialists
- Simplify root to state-based transfers only
- Add explicit "never transfer" rules
After Refactoring:
- Measure performance improvements
- Monitor transfer success rates
TL;DR: The Key Takeaway
I reduced my root orchestrator from 150 lines to 20 lines by making it truly dumb and letting specialists handle complexity. This resulted in more consistent behavior, easier maintenance, and a much simpler system to understand and modify. Root agents should coordinate, not comprehend.
What's Next
I'm trying to write up what I learned from building production AI agents with Google ADK, it'll be a series of posts coming up in the next few weeks. They will likely cover architecture, deployment, monitoring, and the lessons learned from running this system in production.
