Skip to content

Commit 7331620

Browse files
sjrljulian-risch
andauthored
docs: Add dedicated page for multi agent docs (#11279)
Co-authored-by: Julian Risch <julian.risch@deepset.ai>
1 parent de9264e commit 7331620

10 files changed

Lines changed: 712 additions & 170 deletions

File tree

docs-website/docs/concepts/agents.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Key capabilities include:
4343
- **State management**: Share typed data between tools, accumulate results across iterations, and surface them in the result dict using `state_schema`. See [State](../pipeline-components/agents-1/state.mdx).
4444
- **Streaming**: Stream token-by-token output with a `streaming_callback`.
4545
- **Human-in-the-loop**: Intercept tool calls for human review before execution. See [Human in the Loop](../pipeline-components/agents-1/human-in-the-loop.mdx).
46-
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](../pipeline-components/agents-1/agent.mdx#multi-agent-systems).
46+
- **Multi-agent systems**: Wrap an `Agent` as a `ComponentTool` to build coordinator/specialist architectures. See [Multi-Agent Systems](./agents/multi-agent-systems.mdx).
4747

4848
Check out the [Agent](../pipeline-components/agents-1/agent.mdx) documentation, or the [example](#tool-calling-agent) below to get started.
4949

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
---
2+
title: "Multi-Agent Systems"
3+
id: multi-agent-systems
4+
slug: "/multi-agent-systems"
5+
description: "Learn how to build multi-agent systems in Haystack by spawning agents as tools. Use the @tool decorator or ComponentTool to connect specialist agents to a coordinator."
6+
---
7+
8+
# Multi-Agent Systems
9+
10+
Multi-agent systems let you compose multiple `Agent` instances into larger architectures where a **coordinator** agent delegates to **specialist** agents.
11+
Each specialist focuses on a specific task with its own tools and system prompt - the coordinator plans and routes work without needing to know how each task gets done.
12+
13+
Spawning agents as tools is useful when:
14+
15+
- A task is too broad for a single agent to handle reliably,
16+
- You want to isolate different capabilities into focused, reusable agents,
17+
- You need to keep the coordinator's context lean for better decisions and lower token usage.
18+
19+
In Haystack, you spawn a specialist agent as a tool using either the `@tool` decorator (recommended) or `ComponentTool`.
20+
21+
## Converting an Agent to a Tool
22+
23+
### `@tool` Decorator (Recommended)
24+
25+
Wrapping an agent inside a `@tool` function gives you full control over what the coordinator LLM sees:
26+
27+
- **Simplified parameters**: define explicit `Annotated` arguments instead of exposing `agent.run()`'s full interface
28+
- **Formatted output**: extract and return only what the coordinator needs, rather than the full result dict
29+
- **Error handling**: catch exceptions and return a clean message so the coordinator can recover
30+
31+
This approach works better with smaller LLMs because the tool has a clean, minimal signature.
32+
The coordinator only needs to provide a query string - all the `ChatMessage` construction and result unpacking is hidden inside the function.
33+
34+
```python
35+
from typing import Annotated
36+
from haystack.components.agents import Agent
37+
from haystack.components.generators.chat import OpenAIChatGenerator
38+
from haystack.components.generators.utils import print_streaming_chunk
39+
from haystack.dataclasses import ChatMessage
40+
from haystack.tools import ComponentTool, tool
41+
from haystack.components.websearch import SerperDevWebSearch
42+
from haystack.utils import Secret
43+
44+
45+
research_agent = Agent(
46+
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
47+
tools=[
48+
ComponentTool(
49+
component=SerperDevWebSearch(
50+
api_key=Secret.from_env_var("SERPERDEV_API_KEY"),
51+
top_k=3,
52+
),
53+
name="web_search",
54+
description="Search the web for current information on any topic",
55+
),
56+
],
57+
system_prompt="You are a research specialist. Search the web to find information.",
58+
)
59+
60+
61+
@tool
62+
def research(query: Annotated[str, "The research question to investigate"]) -> str:
63+
"""Research a topic and return a summary of findings."""
64+
try:
65+
result = research_agent.run(messages=[ChatMessage.from_user(query)])
66+
return result["last_message"].text
67+
except Exception as e:
68+
return f"Research failed: {e}"
69+
70+
71+
coordinator = Agent(
72+
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
73+
tools=[research],
74+
system_prompt="You are a coordinator. Delegate research tasks to the research tool.",
75+
streaming_callback=print_streaming_chunk,
76+
)
77+
78+
result = coordinator.run(
79+
messages=[
80+
ChatMessage.from_user("What are the latest developments in Haystack AI?"),
81+
],
82+
)
83+
```
84+
85+
### `ComponentTool`
86+
87+
`ComponentTool` wraps an agent directly without a wrapper function.
88+
Choose it when you want **declarative configuration**: the full specialist setup (model, tools, system prompt) lives in one serializable object alongside the coordinator.
89+
90+
Use `outputs_to_string={"source": "last_message"}` to surface only the specialist's final reply to the coordinator rather than the full result dict.
91+
92+
```python
93+
from haystack.tools import ComponentTool
94+
95+
research_tool = ComponentTool(
96+
component=research_agent,
97+
name="research_specialist",
98+
description="A specialist that researches topics on the web",
99+
outputs_to_string={"source": "last_message"},
100+
)
101+
102+
coordinator = Agent(
103+
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
104+
tools=[research_tool],
105+
system_prompt="You are a coordinator. Delegate research tasks to the research specialist.",
106+
streaming_callback=print_streaming_chunk,
107+
)
108+
109+
result = coordinator.run(
110+
messages=[
111+
ChatMessage.from_user("What are the latest developments in Haystack AI?"),
112+
],
113+
)
114+
```
115+
116+
The full specialist configuration is captured inline when serialized.
117+
Wrap the coordinator in a `Pipeline` and call `pipeline.dumps()` to get the YAML, which can be loaded back with `Pipeline.loads()`.
118+
119+
<details>
120+
<summary>View YAML</summary>
121+
122+
```yaml
123+
components:
124+
coordinator:
125+
init_parameters:
126+
chat_generator:
127+
init_parameters:
128+
api_base_url: null
129+
api_key:
130+
env_vars:
131+
- OPENAI_API_KEY
132+
strict: true
133+
type: env_var
134+
generation_kwargs: {}
135+
http_client_kwargs: null
136+
max_retries: null
137+
model: gpt-5.4-nano
138+
organization: null
139+
streaming_callback: null
140+
timeout: null
141+
tools: null
142+
tools_strict: false
143+
type: haystack.components.generators.chat.openai.OpenAIChatGenerator
144+
confirmation_strategies: null
145+
exit_conditions:
146+
- text
147+
max_agent_steps: 100
148+
raise_on_tool_invocation_failure: false
149+
required_variables: null
150+
state_schema: {}
151+
streaming_callback: null
152+
system_prompt: You are a coordinator. Delegate research tasks to the research
153+
specialist. Keep your final answer concise.
154+
tool_invoker_kwargs: null
155+
tools:
156+
- data:
157+
component:
158+
init_parameters:
159+
chat_generator:
160+
init_parameters:
161+
api_base_url: null
162+
api_key:
163+
env_vars:
164+
- OPENAI_API_KEY
165+
strict: true
166+
type: env_var
167+
generation_kwargs: {}
168+
http_client_kwargs: null
169+
max_retries: null
170+
model: gpt-5.4-nano
171+
organization: null
172+
streaming_callback: null
173+
timeout: null
174+
tools: null
175+
tools_strict: false
176+
type: haystack.components.generators.chat.openai.OpenAIChatGenerator
177+
confirmation_strategies: null
178+
exit_conditions:
179+
- text
180+
max_agent_steps: 100
181+
raise_on_tool_invocation_failure: false
182+
required_variables: null
183+
state_schema: {}
184+
streaming_callback: null
185+
system_prompt: You are a research specialist. Search the web to find
186+
information. Return a concise summary of your findings in 3-5 sentences.
187+
tool_invoker_kwargs: null
188+
tools:
189+
- data:
190+
component:
191+
init_parameters:
192+
allowed_domains: null
193+
api_key:
194+
env_vars:
195+
- SERPERDEV_API_KEY
196+
strict: true
197+
type: env_var
198+
exclude_subdomains: false
199+
search_params: {}
200+
top_k: 3
201+
type: haystack.components.websearch.serper_dev.SerperDevWebSearch
202+
description: Search the web for current information on any topic
203+
inputs_from_state: null
204+
name: web_search
205+
outputs_to_state: null
206+
outputs_to_string: null
207+
parameters: null
208+
type: haystack.tools.component_tool.ComponentTool
209+
user_prompt: null
210+
type: haystack.components.agents.agent.Agent
211+
description: A specialist that researches topics on the web
212+
inputs_from_state: null
213+
name: research_specialist
214+
outputs_to_state: null
215+
outputs_to_string:
216+
source: last_message
217+
parameters: null
218+
type: haystack.tools.component_tool.ComponentTool
219+
user_prompt: null
220+
type: haystack.components.agents.agent.Agent
221+
connection_type_validation: true
222+
connections: []
223+
max_runs_per_component: 100
224+
metadata: {}
225+
```
226+
227+
</details>
228+
229+
## Coordinator / Specialist Pattern
230+
231+
The coordinator/specialist pattern cleanly splits responsibilities: the coordinator handles planning and delegation, while each specialist owns a focused toolset and a targeted system prompt.
232+
233+
This is also a form of **context engineering**: deliberately controlling what each agent sees.
234+
A specialist accumulates its own tool call trace, but the coordinator only needs the final answer.
235+
Returning just `result["last_message"].text` (with `@tool`) or using `outputs_to_string` (with `ComponentTool`) surfaces only the specialist's final reply, keeping the coordinator's context lean.
236+
237+
When covering multiple topics, the coordinator can call the same specialist tool several times in a single response.
238+
All tool calls from one LLM response are executed concurrently using a thread pool.
239+
Control the level of parallelism with `max_workers` in `tool_invoker_kwargs` (default: `4`).
240+
241+
The example below asks the coordinator about two topics: it calls `research` twice and both specialists run in parallel.
242+
243+
`HTMLToDocument` uses [Trafilatura](https://trafilatura.readthedocs.io) to extract clean text from HTML pages.
244+
Install it before running:
245+
246+
```shell
247+
pip install trafilatura
248+
```
249+
250+
```python
251+
from typing import Annotated
252+
from haystack.components.agents import Agent
253+
from haystack.components.converters import HTMLToDocument
254+
from haystack.components.fetchers.link_content import LinkContentFetcher
255+
from haystack.components.generators.chat import OpenAIChatGenerator
256+
from haystack.components.generators.utils import print_streaming_chunk
257+
from haystack.components.websearch import SerperDevWebSearch
258+
from haystack.dataclasses import ChatMessage
259+
from haystack.tools import ComponentTool, tool
260+
from haystack.utils import Secret
261+
262+
263+
search_tool = ComponentTool(
264+
component=SerperDevWebSearch(
265+
api_key=Secret.from_env_var("SERPERDEV_API_KEY"),
266+
top_k=3,
267+
),
268+
name="web_search",
269+
description="Search the web for current information on any topic",
270+
)
271+
272+
273+
@tool
274+
def fetch_page(url: Annotated[str, "The URL of the web page to fetch"]) -> str:
275+
"""Fetch the content of a web page given its URL."""
276+
try:
277+
streams = LinkContentFetcher().run(urls=[url])["streams"]
278+
if not streams:
279+
return "No content found."
280+
documents = HTMLToDocument().run(sources=streams)["documents"]
281+
return documents[0].content if documents else "No content extracted."
282+
except Exception as e:
283+
return f"Failed to fetch page: {e}"
284+
285+
286+
research_agent = Agent(
287+
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
288+
tools=[search_tool, fetch_page],
289+
system_prompt=(
290+
"You are a research specialist. Search the web to find relevant pages, "
291+
"then fetch their full content for detailed information. "
292+
"Return a concise summary of your findings in 3-5 sentences."
293+
),
294+
)
295+
296+
297+
@tool
298+
def research(query: Annotated[str, "The research question to investigate"]) -> str:
299+
"""Research a topic and return a summary of findings."""
300+
try:
301+
result = research_agent.run(messages=[ChatMessage.from_user(query)])
302+
return result["last_message"].text
303+
except Exception as e:
304+
return f"Research failed: {e}"
305+
306+
307+
coordinator = Agent(
308+
chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
309+
tools=[research],
310+
system_prompt=(
311+
"You are a coordinator. Delegate research tasks to the research tool. "
312+
"For questions covering multiple topics, research each one independently. "
313+
"Keep your final answer concise."
314+
),
315+
streaming_callback=print_streaming_chunk,
316+
tool_invoker_kwargs={"max_workers": 4}, # run up to 4 specialist calls in parallel
317+
)
318+
319+
result = coordinator.run(
320+
messages=[
321+
ChatMessage.from_user(
322+
"What are the latest developments in large language models and retrieval-augmented generation?",
323+
),
324+
],
325+
)
326+
```
327+
328+
## Additional References
329+
330+
📖 Related docs:
331+
332+
- [Agent](../../pipeline-components/agents-1/agent.mdx)
333+
- [State](../../pipeline-components/agents-1/state.mdx)
334+
- [ComponentTool](../../tools/componenttool.mdx)
335+
336+
📚 Tutorials:
337+
338+
- [Creating a Multi-Agent System](https://haystack.deepset.ai/tutorials/45_creating_a_multi_agent_system)

0 commit comments

Comments
 (0)