Hey everyone, Alex here from agntai.net. It’s May 8th, 2026, and I’ve been wrestling with something pretty fundamental lately: how we actually *build* useful AI agents, especially when they need to handle complex, multi-step tasks. We’re past the “cool demo” phase, right? Now it’s about making these things reliable, debuggable, and genuinely helpful in a production setting. And for me, that brings us straight to the nitty-gritty of agent architecture.
Specifically, I want to talk about the often-overlooked power of *compositional architectures* for AI agents. Forget monolithic LLM calls trying to do everything at once. We’re talking about breaking down complex problems into smaller, manageable pieces, each handled by a specialized ‘sub-agent’ or tool, orchestrated by a higher-level controller. It sounds obvious, but the devil, as always, is in the implementation details and the mental model we adopt.
My Frustration with “One Model to Rule Them All”
I’ve been on a project recently – let’s call it “Project Chimera” – where the initial approach was to throw a large language model at a fairly intricate data analysis and report generation task. The idea was simple: give it the raw data, give it the prompt, and let it figure it out. Sound familiar? We’ve all been there. And for simple tasks, it works great. But for Chimera, which involved fetching data from multiple APIs, performing specific statistical analyses, generating charts, and then summarizing findings based on predefined report templates… well, it was a mess.
The LLM would hallucinate API calls, misinterpret data fields, get stuck in loops, or just flat-out ignore crucial constraints. Debugging was a nightmare. You’d tweak the prompt, and it would fix one issue but break two others. It felt like playing whack-a-mole with an invisible hammer. My colleague, Sarah, kept saying, “It’s like asking a brilliant but inexperienced intern to manage a whole department.” And she was right. The LLM is brilliant at language, at reasoning, at connecting ideas, but not necessarily at precise execution of a multi-step, constrained workflow.
The Shift: Embracing Specialization
That frustration led us to a complete architectural rethink. Instead of one giant model trying to do everything, we started thinking about how a human expert would approach Project Chimera. A human wouldn’t just “think” the report into existence. They’d:
- Identify the data sources.
- Use specific tools (Python scripts, SQL queries) to fetch and clean data.
- Apply statistical software to analyze it.
- Use a charting library to visualize.
- Then, and only then, would they write the report, drawing on all these prepared artifacts.
This is the core idea behind compositional agents: an orchestrator (often an LLM, but not always) that delegates specific tasks to specialized modules or tools. Each module is good at one thing, and the orchestrator’s job is to figure out *which* tool to use *when*, and how to combine their outputs.
Why This Matters Right Now
With models getting bigger and more capable, there’s a temptation to just rely on their raw intelligence. But even the best models have limitations in terms of context window, factual accuracy for specific data, and deterministic execution. Compositional architectures address these by:
- Reducing Hallucinations: When an LLM is asked to call a specific, predefined function with specific arguments, it’s far less likely to invent an API endpoint or a data field.
- Improving Debuggability: If something goes wrong, you can pinpoint whether it was the orchestrator’s decision, the tool’s execution, or the data passed between them.
- Enhancing Reliability: Tools can be rigorously tested and validated independently. Their behavior is predictable.
- Managing Complexity: Instead of one giant, unwieldy prompt, you have smaller, focused prompts for the orchestrator and clear interfaces for tools.
- Allowing for Specialization: You can integrate highly optimized, non-LLM components (e.g., a fast database query engine, a dedicated machine learning inference model for a specific task) without forcing the LLM to learn those capabilities from scratch.
A Simple Compositional Agent Architecture: The “Tool-Use” Pattern
Let’s look at a common, practical pattern: an LLM acting as an orchestrator, selecting from a set of predefined tools. This is what we ended up using for Project Chimera.
The Components:
- The Orchestrator (LLM): This is the brain. It takes the user’s request, understands the intent, and decides which tool (or sequence of tools) is needed to fulfill that request. It also interprets the results from the tools and synthesizes the final response.
- The Tool Registry: A collection of available tools. Each tool has a clear description of what it does, its input parameters, and its expected output. This description is crucial for the LLM to understand how and when to use the tool.
- The Tools: These are actual functions, APIs, or even other specialized models that perform specific, well-defined tasks. They are deterministic and reliable.
- Memory/State (Optional but Recommended): For multi-turn conversations or sequential tasks, the agent needs to remember previous interactions, tool outputs, and decisions.
How it Works (Simplified Flow):
- User asks a question.
- Orchestrator (LLM) receives the question and its current memory/state.
- Orchestrator thinks: “Based on this, what tool do I need? What arguments should I pass?” It generates a tool call (e.g., `call_tool(“search_database”, {“query”: “sales data for Q1 2026”})`).
- The system executes the tool call.
- The tool returns its result (e.g., JSON data, a file path, a success/failure message).
- Orchestrator receives the tool’s result, updates its memory/state, and thinks again: “Do I need another tool? Can I answer the user now?”
- This loop continues until the orchestrator decides it has enough information to formulate a final answer, or it determines it cannot fulfill the request.
Practical Example: A Data Analysis Agent
Let’s say we want an agent that can answer questions about sales data from a database and also generate simple plots. Instead of trying to make an LLM query a SQL database and then somehow generate a matplotlib chart, we’d build tools.
Tool 1: `query_sales_db`
def query_sales_db(sql_query: str) -> list[dict]:
"""
Executes a SQL query against the sales database and returns the results.
Use this to fetch sales figures, product information, or customer data.
Args:
sql_query (str): The SQL query to execute.
Returns:
list[dict]: A list of dictionaries, where each dictionary represents a row.
"""
# In a real scenario, this would connect to a DB
# For demonstration, let's mock some data
if "SELECT product_name, SUM(amount)" in sql_query and "GROUP BY product_name" in sql_query:
return [
{"product_name": "Widget A", "total_sales": 15000},
{"product_name": "Gadget B", "total_sales": 22000},
{"product_name": "Doodad C", "total_sales": 8000},
]
elif "SELECT customer_name, region" in sql_query:
return [
{"customer_name": "Alpha Corp", "region": "East"},
{"customer_name": "Beta Inc", "region": "West"},
]
return []
Tool 2: `generate_bar_chart`
import matplotlib.pyplot as plt
import pandas as pd
import io
import base64
def generate_bar_chart(data: list[dict], x_column: str, y_column: str, title: str) -> str:
"""
Generates a bar chart from provided data and returns it as a base64 encoded PNG image.
Args:
data (list[dict]): The data to plot. Each dict is a row, with keys as column names.
x_column (str): The name of the column to use for the X-axis (categories).
y_column (str): The name of the column to use for the Y-axis (values).
title (str): The title of the chart.
Returns:
str: A base64 encoded PNG string of the bar chart.
"""
df = pd.DataFrame(data)
if x_column not in df.columns or y_column not in df.columns:
return f"Error: Columns '{x_column}' or '{y_column}' not found in data."
plt.figure(figsize=(10, 6))
plt.bar(df[x_column], df[y_column])
plt.xlabel(x_column)
plt.ylabel(y_column)
plt.title(title)
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
buffer = io.BytesIO()
plt.savefig(buffer, format='png')
plt.close() # Close the plot to free memory
return base64.b64encode(buffer.getvalue()).decode('utf-8')
The orchestrator’s prompt would include descriptions of these tools, similar to how many LLM APIs accept function schemas:
# Orchestrator System Prompt Snippet (simplified for brevity)
You are an AI assistant that can answer questions about sales data and visualize it.
You have access to the following tools:
1. query_sales_db(sql_query: str) -> list[dict]:
Executes a SQL query against the sales database and returns the results.
Use this to fetch sales figures, product information, or customer data.
Example: query_sales_db("SELECT product_name, SUM(amount) FROM sales GROUP BY product_name")
2. generate_bar_chart(data: list[dict], x_column: str, y_column: str, title: str) -> str:
Generates a bar chart from provided data and returns it as a base64 encoded PNG image.
Args: data (list[dict]), x_column (str), y_column (str), title (str).
Example: generate_bar_chart([{'label': 'A', 'value': 10}], 'label', 'value', 'My Chart')
To use a tool, respond with a JSON object like:
{"tool_name": "tool_function_name", "args": {"arg1": "value1", "arg2": "value2"}}
After a tool runs, its output will be provided to you. Then you can decide if another tool is needed or if you can answer the user.
When a user asks, “What are the total sales for each product and can you show me a bar chart?”, the orchestrator would:
- Generate a `query_sales_db` call with a suitable SQL query.
- Receive the sales data from the tool.
- Then generate a `generate_bar_chart` call, passing the received data and appropriate column names.
- Receive the base64 encoded image string.
- Finally, it would synthesize a natural language response, possibly including the image by embedding the base64 string in an HTML `<img>` tag or similar for the frontend.
Beyond Simple Tools: Hierarchical Agents
This tool-use pattern is powerful, but we can take it further. What if a tool itself is another AI agent? This is where hierarchical agents come in. An orchestrator might delegate a complex sub-task to a “sub-agent” that itself uses a set of tools or even orchestrates other sub-sub-agents.
For Project Chimera, our initial “data fetching” tool was just a single SQL query. But then we realized sometimes we needed to fetch from multiple databases, combine data, and handle complex joins *before* analysis. We turned that into a “Data Integration Agent,” which had its own set of tools (SQL query, API fetcher, CSV parser) and its own internal LLM orchestrator. The main Chimera agent would then simply call `data_integration_agent.fetch_and_prepare_data(…)`, treating the entire sub-agent as a single, powerful tool.
This creates a tree-like structure, where responsibilities are cleanly separated. It’s software engineering 101 applied to AI agents: modularity, encapsulation, and clear interfaces. It makes scaling, maintenance, and debugging infinitely easier.
Challenges and Considerations
- Tool Description Quality: The LLM’s ability to use tools depends heavily on how well those tools are described in its prompt. Clear, concise, and accurate descriptions are paramount.
- Context Management: Keeping track of conversation history and tool outputs without exceeding the LLM’s context window is an ongoing challenge. Summarization techniques or external memory systems become essential.
- Error Handling: What happens if a tool fails? The orchestrator needs a strategy (retry, inform user, try an alternative tool).
- Safety and Guardrails: Especially with tools that interact with external systems (like databases or APIs), robust input validation and access controls are critical. The LLM shouldn’t be able to generate arbitrary SQL queries that could delete data, for instance.
- Latency: Each tool call adds latency. For real-time applications, you need efficient tools and careful orchestration to minimize steps.
Actionable Takeaways for Your Next AI Agent Project
- Start with a Problem Breakdown: Before writing any code, sketch out the task. How would a human break it down? What discrete steps are involved? This will help you identify potential tools or sub-agents.
- Define Tools with Precision: For each potential tool, define its purpose, inputs, and outputs clearly. Think of it like defining a robust API endpoint. Write docstrings!
- Favor Deterministic Tools: Whenever possible, make your tools deterministic. A `query_database` tool should always return the same data for the same query (given stable data). Leave the non-deterministic, creative parts to the LLM orchestrator.
- Iterate on Tool Descriptions: The prompt that describes your tools to the LLM will likely need refinement. Experiment with different phrasings, examples, and levels of detail to see what gets the best behavior.
- Implement Robust Error Handling: Build mechanisms for your orchestrator to detect and react to tool failures. This could involve retries, fallback tools, or communicating the issue to the user.
- Think Hierarchically for Complexity: If a single agent’s task becomes too broad or requires too many tools, consider creating sub-agents. Each sub-agent can be a specialist in its domain.
The journey with Project Chimera taught me a lot. We moved from frustrating, unpredictable LLM behavior to a system that, while still requiring careful attention, is far more stable, debuggable, and capable of handling intricate workflows. It’s not about making LLMs “smarter” in a monolithic sense, but about giving them the right set of hands to do their work effectively. That, to me, is the real path forward for building practical, production-ready AI agents today.
Alright, that’s my take for this week. Let me know your thoughts on compositional agents or any architectural patterns you’re finding success with in the comments below. Happy building!
đź•’ Published: