Supervisor e worker: instradare le sotto-attività verso specialisti
Un supervisor scompone una richiesta in sotto-attività tipizzate, assegna ciascuna a uno specialista identificato per nome, con una propria istruzione e una propria porzione di contesto, e un synthesiser unifica i risultati preservando i disaccordi.
Fatti chiave
- Livello
- Avanzato
- Runtime
- Python • API OpenAI
- Pattern
- Flusso ispezionabile con confini di sistema visibili
- Interazione
- Sandbox live • Script
- Aggiornato
- 28 luglio 2026
Naviga questo esempio
Pattern
Relazioni con la doctrineQuali principi questo pattern esprime nel codice, e quali la sua forma predefinita infrange.Libreria
Sfoglia gli esempiRiapri la libreria completa per confrontare pattern vicini e percorsi collegati.Interazione
Esegui ora nel sandboxProva l'interazione direttamente nella superficie guidata di questo esempio.Sorgente
Apri codice completoLeggi l'implementazione reale, i punti evidenziati e i requisiti runtime.MCP
Chiama via MCPUsa la stessa risorsa dentro agenti, export deterministici e setup MCP.
Principi collegati
Relazioni con la doctrine
Queste sono forme di ragionamento: come un agente decide, quando agisce, cosa ricorda. Per comporre l'interfaccia intorno a esso, vedi l'AI Pattern Blueprint. Per trigger, pianificazione, caricamento del contesto e ripristino, vedi Agent runtime architecture.
Questo pattern esprime questi principi direttamente nel codice.
P9 · Rappresentare il lavoro delegato come un sistema, non solo come una conversazione
Questo è il pattern che rende concreto P9. Il lavoro esiste come Plan of Subtasks tipizzato, con owner identificati per nome, non come flusso di messaggi: quale specialista è stato coinvolto, su quale questione e che cosa ha restituito sono tutti oggetti indirizzabili separatamente. Un supervisor che trasferisce tra agent una trascrizione di chat in continua crescita ha la forma, ma non la proprietà.
supervisor_workers.py:Plan
La forma classica di questo pattern li viola per impostazione predefinita. Ognuno riporta la correzione.
P2 · Assicurarsi che il lavoro in background rimanga percepibile
Gli specialisti vengono eseguiti uno dopo l'altro, senza emettere nulla tra il piano e la sintesi, a meno che non sia il chiamante a stamparlo. Un'esecuzione con più specialisti è la configurazione con la latenza più elevata in questo corpus e quella che più facilmente viene scambiata per un blocco. Correzione applicata qui: emettere quale specialista è in esecuzione e qual è la sua domanda prima di ogni chiamata, anziché dopo.
supervisor_workers.py:runP6 · Esporre uno stato operativo significativo, non la complessità interna
L'output grezzo di ogni specialista viene esposto per impostazione predefinita, così l'operatore vede il chatter interno invece dello stato del lavoro. Con tre specialisti è leggibile; con dieci diventa rumore e nasconde quale prospettiva sia stata effettivamente rilevante. Correzione: riportare l'avanzamento come numero di specialisti che hanno completato il lavoro, insieme agli eventuali disaccordi rilevati, e mantenere il testo di ciascuno specialista dietro un passaggio di ispezione.
supervisor_workers.py:runP8 · Rendere espliciti i passaggi, le approvazioni e i blocchi
Il piano del supervisor viene eseguito non appena viene prodotto. Non c'è alcuna pausa tra la decisione di coinvolgere quattro specialisti e l'esecuzione di quattro chiamate al modello. Correzione: trattare il piano come una proposta, indicandone il costo, e subordinare l'esecuzione all'approvazione quando il fan-out o il relativo costo supera una soglia definita.
supervisor_workers.py:run
Le relazioni con la doctrine sono analisi di prima parte, non il risultato di una validazione. Esegui il validatore sul tuo codice per ottenere un verdetto con punteggio.
supervisor_workers.py
"""
Supervisor and workers: route subtasks to specialists
A supervisor decomposes a request into subtasks, assigns each to a named
specialist, and a synthesiser merges the results. Each specialist runs with
its own instruction and its own slice of context.
Request -> Plan (typed subtasks) -> Specialists -> Synthesis
The real reason to reach for this is context isolation, not intelligence:
each worker sees only what its subtask needs, so one long transcript does not
drag every step. It is not a general upgrade. Most requests are better served
by one loop, and a team of agents that does not need to be a team is just a
larger bill and more places to fail.
Reach for it when the subtasks are genuinely independent, or when they need
materially different instructions. Not when they merely sound different.
Run: python supervisor_workers.py
"""
from typing import Literal
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel, Field
load_dotenv()
client = OpenAI()
MODEL = "gpt-5.4-mini"
MAX_SUBTASKS = 4
#: Each specialist is an instruction, not a model. Adding one is a line here.
SPECIALISTS: dict[str, str] = {
"technical": (
"You assess technical feasibility and implementation risk. Name the "
"specific mechanism that would break, not general caution."
),
"financial": (
"You assess cost and commercial impact. Give ranges and state what "
"drives them. Never invent a figure that was not provided."
),
"operational": (
"You assess who has to run this once it exists, and what they need. "
"Focus on the ongoing burden rather than the build."
),
}
class Subtask(BaseModel):
specialist: Literal["technical", "financial", "operational"]
question: str = Field(
description="The single question this specialist should answer."
)
class Plan(BaseModel):
subtasks: list[Subtask] = Field(
description=(
"One subtask per specialist that is genuinely needed. Do not "
"include a specialist whose lens adds nothing to this request."
)
)
reasoning: str = Field(description="Why these specialists and not the others.")
def plan(request: str) -> Plan:
completion = client.beta.chat.completions.parse(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Decompose the request into subtasks for the specialists "
"listed. Use only the specialists that genuinely add "
"something. Fewer is better: a specialist with nothing "
"useful to say costs a call and dilutes the synthesis.\n\n"
"Specialists:\n"
+ "\n".join(f"- {name}: {role}" for name, role in SPECIALISTS.items())
),
},
{"role": "user", "content": request},
],
response_format=Plan,
)
return completion.choices[0].message.parsed
def run_specialist(name: str, question: str) -> str:
"""One worker. Sees its own question, not the whole transcript."""
completion = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": SPECIALISTS[name]},
{"role": "user", "content": question},
],
)
return completion.choices[0].message.content or ""
def synthesise(request: str, findings: list[tuple[str, str]]) -> str:
body = "\n\n".join(f"[{name}]\n{text}" for name, text in findings)
completion = client.chat.completions.create(
model=MODEL,
messages=[
{
"role": "system",
"content": (
"Merge the specialist findings into one answer. Attribute "
"each point to the specialist it came from. Where two "
"specialists disagree, say so rather than averaging them."
),
},
{"role": "user", "content": f"Request: {request}\n\n{body}"},
],
)
return completion.choices[0].message.content or ""
def run(request: str) -> str:
print(" planning...")
proposal = plan(request)
print(f" rationale: {proposal.reasoning}")
subtasks = proposal.subtasks[:MAX_SUBTASKS]
if not subtasks:
# A plan with no subtasks is a real answer: this request did not need
# a team. Say so rather than inventing work for the specialists.
message = "No specialist lens applied to this request. Answer it directly."
print(f" {message}")
return message
findings: list[tuple[str, str]] = []
for index, subtask in enumerate(subtasks, start=1):
# Printed before the call, so a slow specialist reads as working.
print(f" [{index}/{len(subtasks)}] {subtask.specialist}: {subtask.question}")
answer = run_specialist(subtask.specialist, subtask.question)
print(f" -> {answer[:80]}...")
findings.append((subtask.specialist, answer))
print(" synthesising...")
result = synthesise(request, findings)
print(f" {result}")
return result
if __name__ == "__main__":
request = (
"We are considering moving our nightly batch reconciliation to a "
"streaming pipeline. Should we?"
)
print(f"\n> {request}")
run(request)
Principi correlati
- P2visibilityAssicurarsi che il lavoro in background rimanga percepibileQuando il sistema opera in modo asincrono o al di fuori del focus immediato dell'utente, dovrebbe fornire segnali persistenti e proporzionati che il lavoro sta continuando.Apri il principio →
- P6visibilityEsporre uno stato operativo significativo, non la complessità internaPresentare lo stato del sistema in linguaggio e strutture rilevanti per l'utente, piuttosto che esporre dettagli interni di basso livello che non supportano l'azione o la comprensione.Apri il principio →
- P8trustRendere espliciti i passaggi, le approvazioni e i blocchiQuando il sistema non può procedere, la ragione dovrebbe essere immediatamente visibile, insieme a qualsiasi azione richiesta dall'utente o da un'altra dipendenza.Apri il principio →
- P9orchestrationRappresentare il lavoro delegato come un sistema, non solo come una conversazioneDove il lavoro coinvolge più passaggi, agenti, dipendenze o attività concorrenti, dovrebbe essere rappresentato come un sistema strutturato piuttosto che solo come un flusso di messaggi.Apri il principio →