|
| 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