This section uses two mathematical sub-Agents to demonstrate multi-Agent orchestration: the main Agent does not do everything directly, but uses specialized sub-Agents through the tool.
When an Agent takes on too many responsibilities at the same time, prompts become longer and tool selection becomes more confusing. The approach of Multi-Agent is to split the Put capability into multiple specialized Agents, and then the main Agent is responsible for selection and coordination.
Focus on a small task, such as just taking square root or squaring.
The logic of Put and Use sub-Agent is encapsulated into a common tool.
Choose which wrapper tool to adjust based on the User problem.
"Agent as Tool": The sub-Agent itself is not directly exposed to the User user, but is called Use by a tool function. The main Agent only needs to learn when to call Use this tool.
Notebook first defines two common tools: one to calculate square roots and one to calculate squares. They are child Agent exclusive tools.
@tool
def square_root(x: float) -> float:
"""Calculate the square root of a number"""
return x ** 0.5
@tool
def square(x: float) -> float:
"""Calculate the square of a number"""
return x ** 2The mathematical tools are small, but the structure is consistent with the real business: each sub-Agent has its own tool set and responsibility boundaries.
Each sub-Agent only gets the tools it needs. This way it has less space to move and its behavior is more predictable.
subagent_1 = create_agent(
model=deepseek_model(),
tools=[square_root],
)
subagent_2 = create_agent(
model=deepseek_model(),
tools=[square],
)| Agent | tool | Responsibilities |
|---|---|---|
subagent_1 | square_root | answer prescription questions |
subagent_2 | square | answer square question |
The master Agent cannot directly "see" the child Agent. Notebook uses two wrapper tools to adjust the Use sub-Agent, and Put the final message of the sub-Agent back to the main Agent.
@tool
def call_subagent_1(x: float) -> float:
"""Call subagent 1 in order to calculate the square root of a number"""
response = subagent_1.invoke(
{"messages": [HumanMessage(content=f"Calculate the square root of {x}")]}
)
return response["messages"][-1].content
@tool
def call_subagent_2(x: float) -> float:
"""Call subagent 2 in order to calculate the square of a number"""
response = subagent_2.invoke(
{"messages": [HumanMessage(content=f"Calculate the square of {x}")]}
)
return response["messages"][-1].contentWrapper tool preferably returns the final result or short explanation required by the main Agent. Do not put the entire sub-Agent trace back as it is, otherwise the main Agent context will expand.
After the main Agent gets the two wrapper tools, it will choose which sub-Agent to use based on the user problem. For example "square root" should be routed to call_subagent_1.
main_agent = create_agent(
model=deepseek_model(),
tools=[call_subagent_1, call_subagent_2],
)
question = "What is the square root of 456?"
response = main_agent.invoke({"messages": [HumanMessage(content=question)]})
print(response["messages"][-1].content)Asks a math question.
Decides a square-root operation is needed.
Calls the square-root sub-agent.
Calls the low-level tool and returns the result.
At the end, the notebook uses pprint(response) and model_dump() to inspect message details. This shows when the main Agent issued a tool call, what the tool returned, and how the final answer was formed.
pprint(response)
pprint(response["messages"][1])
pprint(response["messages"][0].model_dump(), sort_dicts=False)
pprint(response["messages"][1].model_dump(), sort_dicts=False)Multi-agent issues are often not "the model cannot answer" but "routing was wrong" or "the sub-agent return value does not fit the main Agent." Inspecting the trace is the fastest way to locate the problem.