Construindo Subagentes para Pesquisa e Delegação Paralelas
Inicie subagentes paralelos para pesquisar subtarefas independentes e reportar de volta a um coordenador.
Resumo
Tarefas de pesquisa são um dos casos mais claros para um sistema multiagente: uma única pergunta frequentemente se decompõe em várias subperguntas independentes, cada uma das quais precisa de sua própria exploração antes que algo possa ser concluído.
Em vez de um agente pesquisar o concorrente A, depois o concorrente B, depois o concorrente C em sequência, você pode iniciar um subagente por concorrente, executá-los todos simultaneamente e pagar apenas o custo de latência do ramo mais lento em vez da soma de todos eles.
A forma do "fan-out" (distribuição) deve vir da forma da entrada: um subagente por entidade nomeada, por fonte de dados ou por seção de um documento, não um número arbitrário escolhido antecipadamente.
Uma vez que os ramos terminem, uma etapa de coordenação precisa transformar vários conjuntos de descobertas independentes, às vezes parciais ou contraditórias, em um relatório coerente, que é um trabalho distinto da pesquisa em si e merece sua própria etapa.
Esta página cobre como decidir a contagem e a forma dos subagentes a partir da entrada, como fazer o "fan-out" e "fan-in" (distribuição e agregação) com asyncio.gather, e como sintetizar descobertas divergentes sem apenas concatená-las.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
import asyncio
from claude_agent_sdk import query, AgentOptions, SubagentConfig
async def research(topic: str) -> str:
options = AgentOptions(
subagents=[
SubagentConfig(
name=f"research-{topic}",
description=f"Pesquisa {topic} e reporta suas descobertas.",
allowed_tools=["web_search"],
),
],
)
result = ""
async for message in query(
prompt=f"Use o subagente research-{topic} para investigar {topic}.",
options=options,
):
if message.get("type") == "subagent_result":
result = message["result"]
return result
async def main():
topics = ["tamanho do mercado", "concorrentes", "risco regulatório"]
findings = await asyncio.gather(*(research(t) for t in topics))
# Passe as descobertas para uma chamada query() separada do coordenador para sintetizar.
print(dict(zip(topics, findings)))
asyncio.run(main())Quando usar isso:
- A entrada se decompõe naturalmente em várias perguntas de pesquisa, fontes ou entidades independentes: concorrentes, fontes de dados, seções de documentos.
- Você deseja que a pesquisa em cada peça ocorra simultaneamente em vez de uma após a outra.
- As descobertas precisam ser sintetizadas em um relatório, não apenas concatenadas na ordem em que terminam.
- Alguns ramos de pesquisa podem falhar ou retornar resultados parciais, e a saída final ainda precisa dizer algo coerente.
Exemplo de Trabalho
import asyncio
from claude_agent_sdk import query, AgentOptions, SubagentConfig
async def research_one(topic: str, question: str) -> str:
"""Executa um único subagente de pesquisa em um tópico e retorna suas descobertas."""
options = AgentOptions(
allowed_tools=["web_search", "web_fetch"],
subagents=[
SubagentConfig(
name=f"research-{topic}",
description=f"Pesquisa {topic} e responde: {question}",
allowed_tools=["web_search", "web_fetch"],
),
],
)
findings = []
async for message in query(
prompt=f"Use o subagente research-{topic} para responder: {question}",
options=options,
):
if message.get("type") == "subagent_result":
findings.append(message["result"])
return "\n".join(findings) or "(nenhuma descoberta retornada)"
async def research_and_synthesize(topics: list[str], question: str) -> str:
# Fan out: uma invocação de subagente por tópico, despachada simultaneamente.
results = await asyncio.gather(
*(research_one(topic, question) for topic in topics),
return_exceptions=True,
)
# Alguns ramos podem falhar (timeout, sem resultados, uma fonte ruim). Continue
# com o que retornou em vez de perder todo o lote devido a um erro.
findings_by_topic = {}
for topic, result in zip(topics, results):
if isinstance(result, Exception):
findings_by_topic[topic] = f"(pesquisa falhou: {result})"
else:
findings_by_topic[topic] = result
# Fan in: uma chamada de coordenador sintetiza as descobertas, que podem ser
# parciais ou conflitantes, em um único relatório em vez de apenas juntá-las
# na ordem em que foram concluídas.
coordinator_prompt = "Sintetize estas descobertas de pesquisa em um relatório coerente.\n"
for topic, findings in findings_by_topic.items():
coordinator_prompt += f"\n## {topic}\n{findings}\n"
coordinator_prompt += (
"\nChame a atenção para qualquer acordo, desacordo ou lacunas entre as fontes. "
"Se a pesquisa de um tópico falhou, diga isso explicitamente em vez de omiti-la."
)
report_chunks = []
async for message in query(
prompt=coordinator_prompt,
options=AgentOptions(allowed_tools=[]),
):
if message.get("type") == "text":
report_chunks.append(message["text"])
return "".join(report_chunks)
asyncio.run(
research_and_synthesize(
topics=["Concorrente A", "Concorrente B", "Concorrente C"],
question="Qual é o modelo de precificação atual e o mercado-alvo deles?",
)
)O que isso demonstra:
- Gerar um subagente por tópico de pesquisa, aqui um por concorrente, para que cada ramo explore independentemente e simultaneamente.
- Usar
asyncio.gathercomreturn_exceptions=Truepara que um ramo de pesquisa falho não derrube todo o lote. - Rotular cada conjunto de descobertas por seu tópico de origem antes da síntese, para que o coordenador (e o leitor) possam atribuir as afirmações a uma fonte.
- Uma chamada
query()separada do coordenador, com escopoallowed_tools=[], cujo único trabalho é sintetizar texto já coletado, não fazer mais pesquisas. - Instruir explicitamente o coordenador a expor desacordos e lacunas em vez de simplesmente escolher uma versão da verdade silenciosamente.
Mergulho Profundo
Como Funciona
- Cada ramo de pesquisa é sua própria invocação de subagente com seu próprio contexto isolado, então o que um ramo lê ou raciocina nunca vaza para os resultados de outro ramo.
asyncio.gatherdespacha todas as corrotinas de ramo ao mesmo tempo e retorna seus resultados na mesma ordem da entrada, independentemente de qual deles realmente termina primeiro, que é o que tornazip(topics, results)seguro.return_exceptions=Truetransforma a exceção de um ramo em um valor na lista de resultados em vez de propagá-la e cancelar as outras tarefas ainda em execução; sem isso, uma falha mata todo ogather.- A etapa do coordenador é uma chamada
query()simples, não outro subagente, porque seu trabalho (ler texto já coletado e escrever uma síntese) não precisa de ferramentas ou exploração adicional, apenas um contexto limpo com todas as descobertas à sua frente. - Como o coordenador recebe apenas resultados de texto finais, não o raciocínio intermediário de cada ramo, ele sintetiza a partir dos mesmos resumos limpos que um revisor humano veria.
Dimensionando Subagentes de Pesquisa a Partir da Entrada
| Forma da entrada | Forma do subagente | Exemplo |
|---|---|---|
| Uma lista de entidades nomeadas | Um subagente por entidade | 5 concorrentes -> 5 subagentes |
| Várias fontes de dados independentes para uma pergunta | Um subagente por fonte | docs + API + tickets de suporte -> 3 subagentes |
| Um documento longo revisado peça por peça | Um subagente por seção | um relatório de 6 capítulos -> 6 subagentes |
| Uma única pergunta em aberto | Geralmente sem divisão | um subagente, ou nenhum, é suficiente |
O número de subagentes deve sempre vir da contagem das unidades independentes na entrada, concorrentes, fontes ou seções, não de escolher um número que pareça razoável. Se a entrada não se decompõe em peças independentes, o "fan-out" adiciona sobrecarga sem adicionar paralelismo.
Sintetizando Descobertas Divergentes
- Rotule cada bloco de descobertas com sua fonte antes que ele chegue ao coordenador; uma parede de texto sem rótulo não pode ser atribuída ou verificada.
- Peça explicitamente ao coordenador para chamar a atenção para acordo, desacordo e lacunas, em vez de apenas "resumir", o que o convida a escolher silenciosamente uma versão quando as fontes entram em conflito.
- Passe ramos falhos ou parciais como descobertas explícitas ("pesquisa falhou: timeout") em vez de descartá-los, para que o relatório final reflita o que é realmente conhecido versus o que está faltando.
- Trate a saída do coordenador como o entregável, não os resultados brutos dos ramos; os ramos são notas de trabalho, a síntese é o relatório.
Notas de Python
# return_exceptions=True é o que torna a falha parcial sobrevivente.
# Sem isso, uma exceção cancela todas as outras tarefas em andamento.
results = await asyncio.gather(
*(research_one(topic, question) for topic in topics),
return_exceptions=True,
)
# zip() emparelha cada tópico com seu resultado na ordem de entrada, que asyncio.gather
# preserva, mesmo que os ramos completem em ordem arbitrária.
for topic, result in zip(topics, results):
...Parâmetros e Valores de Retorno
| Parâmetro | Tipo | Descrição |
|---|---|---|
name | str | Identificador que o pai usa para invocar este subagente de pesquisa |
description | str | Diz ao modelo pai quando este subagente se aplica; escreva como uma descrição de ferramenta |
allowed_tools | list[str] | Escopo de ferramentas para este subagente, tipicamente web_search/web_fetch para pesquisa |
tool_config | dict | Escopo opcional por ferramenta, como restringir domínios ou comandos permitidos |
Armadilhas
- Fazer "fan-out" de um subagente por elemento de uma lista ilimitada. Uma lista de 5 concorrentes é um bom "fan-out"; uma lista de 500 linhas é um incidente de custo. Correção: limite o número de subagentes de pesquisa concorrentes e agrupe ou amostre além desse limite.
- Chamar
asyncio.gathersemreturn_exceptions=True. A exceção de um ramo cancela todos os outros ramos em andamento, então uma única fonte instável derruba um lote que estava bem. Correção: sempre passereturn_exceptions=Truee trate as exceções explicitamente ao desempacotar os resultados. - Pular a etapa do coordenador e retornar os resultados brutos dos ramos. Descobertas concatenadas e sem rótulo de vários subagentes leem como ruído, não como um relatório, e escondem desacordos entre as fontes. Correção: sempre execute uma etapa de síntese sobre as descobertas coletadas antes de tratar a saída como final.
- Dar ao coordenador o mesmo acesso a ferramentas que os subagentes de pesquisa. O trabalho do coordenador é ler e escrever texto, não pesquisar ou buscar, então conceder a ele
web_search/web_fetchamplia seu raio de explosão sem benefício. Correção: escopo o coordenador ao mínimo necessário, geralmenteallowed_tools=[]. - Não rotular as descobertas por fonte antes da síntese. Se o prompt do coordenador for apenas um bloco de texto mesclado, ele não pode atribuir uma afirmação ao tópico ou fonte de onde veio, o que torna o desacordo invisível. Correção: estruture o prompt do coordenador com uma seção claramente rotulada por tópico.
- Assumir que um ramo falho deve ser simplesmente descartado do relatório. Omitir silenciosamente um tópico cuja pesquisa falhou faz com que o relatório final pareça completo quando não está. Correção: passe as falhas como descobertas explícitas ("pesquisa falhou: ...") para que o coordenador e o leitor vejam a lacuna.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Padrão Orquestrador/trabalhador (despacho e mesclagem geral) | Itens de trabalho são tarefas heterogêneas, não perguntas de pesquisa paralelas sobre a mesma questão | Você precisa especificamente de N ramos de pesquisa paralelos mesclados em um único relatório |
| Um único subagente fazendo pesquisa sequencial | As perguntas de pesquisa dependem genuinamente das respostas umas das outras | As perguntas são independentes e poderiam ser executadas simultaneamente |
| Um único agente com uma ferramenta de busca, sem subagentes | A tarefa é uma pergunta de pesquisa, não vários ramos independentes | A entrada se decompõe naturalmente em múltiplos tópicos ou fontes independentes |
Chamadas query() sequenciais por tópico, sem asyncio.gather | Você precisa de limitação de taxa estrita ou garantias de ordenação entre os ramos | A latência é importante e os tópicos são independentes |
FAQs
Qual a diferença disso para o padrão geral de orquestrador/trabalhador?
Orquestrador/trabalhador é a forma geral: despachar itens de trabalho, mesclar resultados. Esta página é o caso específico de pesquisa: os itens de trabalho são perguntas de pesquisa independentes, fontes ou seções de documentos, e a etapa de mesclagem é uma síntese que precisa lidar com descobertas parciais e conflitantes, não apenas agregar saídas.
Quantos subagentes devo criar para uma determinada entrada?
Um por unidade independente na entrada, não um número escolhido antecipadamente.
- Uma lista de entidades nomeadas -> um subagente por entidade.
- Múltiplas fontes para uma pergunta -> um subagente por fonte.
- Um documento revisado seção por seção -> um subagente por seção.
O que acontece se um subagente de pesquisa falhar?
Com return_exceptions=True em asyncio.gather, a falha retorna como um objeto de exceção no slot desse ramo em vez de cancelar os outros. Converta-o em uma descoberta explícita "pesquisa falhou" e passe-o para o coordenador em vez de descartá-lo.
asyncio.gather garante algo sobre a ordem dos resultados?
Sim: os resultados retornam na mesma ordem das corrotinas de entrada, independentemente de qual termina primeiro. É isso que torna zip(topics, results) uma maneira segura de reassociar cada resultado ao seu tópico.
O coordenador que sintetiza as descobertas deve ser um subagente também?
Pode ser uma chamada query() simples em vez de um SubagentConfig. Seu trabalho é ler texto já coletado e escrever uma síntese, não explorar ou usar ferramentas, então ele não precisa do isolamento ou escopo de ferramentas para os quais os subagentes existem.
Quais ferramentas cada subagente de pesquisa deve receber?
Apenas o que sua pesquisa realmente requer, comumente web_search e web_fetch. Não conceda o escopo de ferramentas do pai ou do coordenador por padrão.
Como evito que o "fan-out" fique muito grande ou caro?
Limite o número de subagentes concorrentes que você despacha e defina um orçamento de tokens por ramo. Uma lista ilimitada na entrada deve ser agrupada ou amostrada, não transformada em um número ilimitado de subagentes.
E se dois subagentes de pesquisa retornarem informações conflitantes?
Passe ambas as descobertas, rotuladas por fonte, para o prompt do coordenador e instrua-o explicitamente a expor o desacordo em vez de resolvê-lo silenciosamente em uma direção.
Um subagente de pesquisa pode gerar seus próprios subagentes?
O modelo do SDK suporta aninhamento em princípio, já que cada subagente executa seu próprio loop interno de uso de ferramentas, mas para "fan-out" de pesquisa, um nível (coordenador mais subagentes por tópico) geralmente é suficiente; aninhamentos mais profundos adicionam sobrecarga sem adicionar clareza.
Preciso do SubagentConfig, ou posso apenas executar chamadas `query()` separadas por tópico?
SubagentConfig é o que escopa as ferramentas de cada ramo e dá ao modelo pai uma capacidade nomeada e descrita para invocar. Você ainda precisa de asyncio.gather no nível do aplicativo para executar os ramos simultaneamente; a configuração do subagente e o mecanismo de concorrência são preocupações separadas.
Como evito que o relatório final pareça uma concatenação de saídas de subagentes?
Não retorne as descobertas dos ramos diretamente. Execute uma etapa dedicada de coordenador, instrua-o a identificar acordo, desacordo e lacunas entre as descobertas rotuladas e trate sua saída, não os ramos brutos, como o entregável.
Este padrão é útil apenas para pesquisa ou ele se generaliza?
A forma de "fan-out"/"fan-in" se generaliza para qualquer lote de subtarefas independentes, mas as preocupações de síntese aqui, atribuindo descobertas a fontes e expondo desacordos em vez de mesclar saídas, são específicas para tarefas de pesquisa e investigação.
Relacionado
- Noções Básicas de Orquestração de Agentes - o modelo mental em que este padrão se baseia
- Padrões Orquestrador/Trabalhador com o Claude Agent SDK - o padrão geral de despacho e mesclagem que este especializa
- Guardrails para Sistemas Multiagentes - limitando a profundidade do "fan-out", o acesso a ferramentas e o gasto de tokens
- Delegando Trabalho para Subagentes no Claude Agent SDK - as mecânicas subjacentes de subagentes usadas aqui
Versões da Stack: Escrito contra a linha de modelos Claude atual por volta de Junho de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (o padrão), e Claude Haiku 4.5 - e o Claude Agent SDK (última versão). Nomes de modelos, versões de SDK e preços mudam rapidamente - verifique os detalhes atuais em platform.claude.com/docs antes de confiar neles.