Week 7: Hierarchical Work!
April 10, 2026
Hello everybody and welcome back to my senior project blog! This week is important because it’s where I’ve started (at mostly finalized) my hierarchical pipeline! I’m excited to share that with you guys, so let’s get into it.
The Hierarchical Pipeline Overview
The sequential pipeline was an agentic framework where every agent in the sequence was invoked without question. Although this is a surefire way to ensure accuracy across all levels of agents, it’s not necessarily the most efficient or scalable method due to the token cost of invoking four instances of Gemini API calls. As a result, I decided to also use a hierarchical structure that essentially allows the agent to skip agent 2 if it believes that the persona it is dealing with has no need to do so.
In order to enable this, I’m going to create an orchestrator agent. An orchestrator agent is simply an overarching agent that has access to invoke tool calls if necessary. In order not to give the orchestrator too much freedom and have it stick to the general pipeline, it is mandated to start with agent 1 (convert the input into a JSON), and end with agent 4 (the quality control agent).
The ADK Integration
Initially, my goal with the hierarchical agent was to develop it natively with Google ADK, because it would be more scalable and, as a result, easier to implement into a web app. However, due to the issues with ADK’s implementation I ran into too many bugs, and instead have made a temporary python script that achieves the same pipeline with API calls.
Firstly, ADK works by establishing what is known as a “root agent”. Whenever you run the call “adk web” in your terminal, the package searches for an init python file that imports the root agent. We can then assign sub-agents to the root agent with the script shown below.
root_agent = LlmAgent(
name="financial_planning_orchestrator",
model="gemini-2.0-flash",
instruction=ORCHESTRATOR_PROMPT,
sub_agents=[
persona_profiler,
heuristic_strategist,
goal_optimizer,
quality_control
]
)
Each of these sub-agents can be defined with a similar framework (using the LlmAgent constructor) as shown below.
goal_optimizer = LlmAgent(
name="goal_optimizer",
model="gemini-2.0-flash",
instruction=AGENT3_PROMPT,
description="""Takes a persona profile and baseline plan and
produces a goal-optimized financial plan with specific deviations
from the baseline. Call this after heuristic_strategist, or
directly after persona_profiler if heuristic_strategist was
skipped."""
)
The “description” tag is what the root_agent (the orchestrator) uses to allocate which agents it wants to use. In our case, there are only two options it has: either to run agent 2 and then run agent 3, or just run agent 3 while skipping agent 2.
After this, I decided to test the system out using the ADK Web UI. At first, I was confused because after I initiated a prompt it seemed as if only agent 1 was run. In reality, the way the ADK web version works is it adds a “human in the loop” every single run. Essentially, you are the one who is giving the orchestrator the final say. On top of that, after agent 3 had run, agent 4 was never being called despite it being clear in the orchestrator’s system prompt that agent 4 was a non-negotiable.
Due to time constraints as I was travelling for spring break, I was unable to debug these fully, so I instead decided to use a makeshift python script with API calls that achieves the same goal. I gave the script a hard set at 3 applicable heuristics if it was to run agent 2, in order to give it a clear concrete understanding of when to skip agent 2 and when to run it. I also added two hard cases to skip agent 2: when the user has a FIRE (Financial Independence, Retire Early) goal or is a freelance/gig worker, where none of the heuristics in my document apply.
After this, I added a reasoning step to every instance call where the orchestrator would skip agent 2, and put a section for it to explain its reasoning. Below is an example where agent 2 was invoked.
"routing_decisions": {
"persona_profiler": {
"called": true,
"reason": "Always called first"
},
"heuristic_strategist": {
"called": true,
"reason": "Standard persona with sufficient applicable heuristics"
},
"goal_optimizer": {
"called": true,
"reason": "Always called"
},
"quality_control": {
"called": true,
"reason": "Always called last"
}
And here is an example of where it is not invoked:
"routing_decisions": {
"persona_profiler": {
"called": true,
"reason": "Always called first"
},
"heuristic_strategist": {
"called": false,
"reason": "Fewer than 3 applicable heuristics \u2014 baseline would not be meaningful"
},
"goal_optimizer": {
"called": true,
"reason": "Always called"
},
"quality_control": {
"called": true,
"reason": "Always called last"
}
Just for reference the \u2014 is an em-dash (we all know that AI loves to use those), but in the JSON schema it appears in its unicode format. Apart from what is shown above, the outputs are formatting identically to the sequential pipeline, with the outputs from agents 1, 3, 4, and 2 if applicable.
Conclusion
Due to the fact I was travelling for Spring Break, this week was purposefully a lighter week when it comes to progress. In the coming weeks I hope to set up my account on Prolific and begin with data collection as soon as possible. I also hope to debug the ADK implementation as ADK allows me to scale my data better and works as a more robust backend when I plan to create a web app. As always, the code for this week is available on my GitHub. Thanks for reading and I’ll see you guys next week!
Reader Interactions
Comments
Leave a Reply
You must be logged in to post a comment.


I really liked how you clearly explained the tradeoff between the sequential and hierarchical pipelines, especially the focus on efficiency vs. accuracy. The routing logic with the orchestrator was interesting, I like how it save token cost but still has “human overview” through the mandated first and fourth agent. One question I had was: do you think the hardcoded rules (like the “3 heuristics” threshold) might limit flexibility later on, and are you planning to make that more dynamic?
After seeing the process of how the sequential pipeline was created, I definitely find it cool seeing the development process through the lens of this new hierarchical architecture. I did have a few questions about this new setup though: like Aanya mentioned, do you think that these hardcoded rules will limit flexibility and potentially create a system that is unable to adapt? In addition, how do you plan on ensuring that the orchestrator agent and the hardcoded rules will call the other agents in the best way possible?