TL;DR
- Aprenda os principais padrões de projeto que realmente importam para sistemas de IA.
- Veja como cada padrão mapeia para problemas reais de orquestração de IA.
- Use exemplos concretos de código para aplicar em sua stack.
- Entenda os trade-offs, não apenas a teoria.
- Aplique um roteiro simples para introduzir padrões sem engenharia excessiva.
O Problema da Falta de Estado: Por que a IA Precisa de Arquitetura
LLMs são stateless (sem estado).
Toda vez que você envia um prompt para o GPT, Claude ou qualquer modelo, ele esquece tudo fora da requisição atual. Ele não lembra da sua mensagem anterior, do plano que criou dois passos atrás, ou das ferramentas que acabou de chamar. Qualquer “memória” vem do sistema ao redor dele, não do modelo em si.
Isso significa que a camada de orquestração deve ser dona de:
- Histórico da conversa
- Regras de negócio
- Roteamento de ferramentas
- Estado de execução
Se você ignorar isso, acabará com uma pilha de scripts: chamadas ad-hoc para o modelo, templates de prompt espalhados e lógica duplicada em torno de ferramentas e memória. Funciona para uma demo. Quebra em produção.
Padrões de projeto ajudam a transformar essa bagunça em uma arquitetura clara e combinável.
Abaixo estão 7 padrões que mapeiam diretamente para problemas reais de IA, com exemplos que você pode adaptar.
1. Padrão Factory: Padronizando a Criação de Agentes
Problema
Você precisa de diferentes agentes: "Coder", "Researcher", "Critic", "Planner". Cada um precisa de:
- Prompts de sistema específicos
- Configuração do modelo (temperatura, nome do modelo)
- Acesso a ferramentas
- Conexão com vector store ou memória
Codificar essa configuração em todos os lugares leva a lógica duplicada e bugs sempre que você muda algo.
Abordagem ruim:
# configuração espalhada
researcher = Agent(
role="researcher",
model="gpt-4o",
tools=[web_search, retrieve_docs],
memory=DocStore("research_index"),
)
coder = Agent(
role="coder",
model="gpt-4o",
tools=[code_exec, unit_test],
memory=None,
)
Toda vez que você muda como um pesquisador deve funcionar, você edita vários lugares.
Padrão
Use uma Factory para centralizar a criação de agentes.
class AgentFactory:
def __init__(self, vector_store, tools, models):
self.vector_store = vector_store
self.tools = tools
self.models = models
def create(self, agent_type: str):
if agent_type == "researcher":
return Agent(
role="researcher",
model=self.models["deep_reasoning"],
tools=[self.tools["web_search"], self.tools["retrieval"]],
memory=self.vector_store["research"],
)
if agent_type == "coder":
return Agent(
role="coder",
model=self.models["code_first"],
tools=[self.tools["code_exec"], self.tools["tests"]],
memory=None,
)
if agent_type == "critic":
return Agent(
role="critic",
model=self.models["deep_reasoning"],
tools=[self.tools["style_checker"]],
memory=None,
)
raise ValueError(f"Unknown agent type: {agent_type}")
Uso:
factory = AgentFactory(vector_store, tools, models)
researcher = factory.create("researcher")
coder = factory.create("coder")
critic = factory.create("critic")
Quando Usar
- Você tem mais de um tipo de agente.
- Você quer atualizar o comportamento em um lugar.
- Você está construindo frameworks reutilizáveis ou plataformas internas.
Quando Não Usar
- Você tem um único agente e não espera variação.
- Seu caso de uso é experimental e muda diariamente.
2. Padrão Strategy: Trocando “Cérebros” Dinamicamente
Problema
Nem toda tarefa precisa do raciocínio nível GPT-4. Algumas tarefas são simples:
- Resumir um parágrafo curto
- Extrair alguns campos
- Reformatar texto
Se todas as requisições vão para um modelo pesado, você queima orçamento e aumenta a latência.
Uso de modelo hard-coded:
def answer(query: str) -> str:
# sempre usa o modelo mais caro
return call_llm("gpt-4o", query)
Padrão
Use Strategy para definir uma família de opções de “cérebro” e escolher a correta em tempo de execução.
class ModelStrategy:
def generate(self, prompt: str) -> str:
raise NotImplementedError
class CheapModelStrategy(ModelStrategy):
def generate(self, prompt: str) -> str:
return call_llm("gpt-4o-mini", prompt)
class ExpensiveModelStrategy(ModelStrategy):
def generate(self, prompt: str) -> str:
return call_llm("gpt-4o", prompt)
class Router:
def __init__(self, cheap: ModelStrategy, expensive: ModelStrategy):
self.cheap = cheap
self.expensive = expensive
def handle(self, task: dict) -> str:
if self._is_simple(task):
return self.cheap.generate(task["prompt"])
return self.expensive.generate(task["prompt"])
def _is_simple(self, task: dict) -> bool:
return len(task["prompt"]) < 300 and not task.get("requires_tools")
Uso:
router = Router(
cheap=CheapModelStrategy(),
expensive=ExpensiveModelStrategy(),
)
response = router.handle({"prompt": user_input, "requires_tools": False})
Benefícios
- Reduz custos roteando tarefas triviais para modelos mais baratos.
- Mantém o código de chamada estável enquanto você muda as regras de roteamento.
Quando Não Usar
- Você sempre usa um único modelo fixo.
- Você ainda não mede custo ou latência.
3. Padrão Proxy: O Guardião do Seu LLM
Problema
Chamadas diretas para APIs de LLM podem causar:
- Custo descontrolado
- Problemas de limite de taxa (rate-limit)
- Falta de observabilidade
- Riscos de conformidade e PII (Informações Pessoais Identificáveis)
Código sem um proxy:
def ask_llm(prompt: str) -> str:
return openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
Cada parte do seu código chama o provedor diretamente. Você não pode aplicar regras em um só lugar.
Padrão
Use um Proxy entre seu app e o provedor de LLM.
class LLMProxy:
def __init__(self, client, cache, limiter, pii_filter):
self.client = client
self.cache = cache
self.limiter = limiter
self.pii_filter = pii_filter
def generate(self, model: str, messages: list[dict]) -> str:
key = self._cache_key(model, messages)
cached = self.cache.get(key)
if cached:
return cached
self.limiter.check_quota()
safe_messages = self.pii_filter.strip(messages)
response = self.client.chat(model=model, messages=safe_messages)
self.cache.set(key, response)
return response
def _cache_key(self, model: str, messages: list[dict]) -> str:
# implementar um hash estável
...
Uso:
proxy = LLMProxy(client, cache, limiter, pii_filter)
def ask_llm(prompt: str) -> str:
return proxy.generate("gpt-4o", [{"role": "user", "content": prompt}])
Benefícios
- Controle central de custo, logs e segurança.
- Fácil integração com ferramentas como LiteLLM ou Helicone.
- Mantém a lógica de negócio limpa.
Quando Não Usar
- Experimentos apenas locais.
- Scripts únicos onde custo e segurança não são preocupações.
4. Padrão Decorator: Observabilidade Sem Poluição
Problema
Você quer logar:
- Entradas
- Saídas
- Uso de tokens
- Tempo de execução
Adicionar código de log dentro de cada função torna tudo ruidoso.
Abordagem ruim:
def search_web(query: str) -> str:
start = time.time()
print("search_web called with:", query)
result = call_api(query)
print("result:", result, "took", time.time() - start)
return result
Padrão
Use Decorators para envolver comportamento ao redor de funções.
def trace(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
log_event(
name=func.__name__,
args=args,
kwargs=kwargs,
duration_ms=int(duration * 1000),
)
return result
return wrapper
@trace
def search_web(query: str) -> str:
return call_api(query)
Todas as funções marcadas com @trace tornam-se observáveis sem mudar sua lógica principal.
Benefícios
- Separação limpa entre lógica e observabilidade.
- Fácil integração com ferramentas de rastreamento (ex: LangSmith, OpenTelemetry).
Quando Não Usar
- Caminhos extremamente críticos em performance onde o overhead do decorator não é aceitável.
- Casos onde você precisa ver toda a lógica inline para fins de auditoria.
5. Padrão Composite: Lidando com Fluxos Multi-Etapas como Unidades Únicas
Problema
Fluxos complexos de IA frequentemente têm etapas aninhadas:
- Pesquisar → Planejar → Codificar → Revisar
- Recuperar → Gerar → Validar → Persistir
Se você tratar cada etapa como uma entidade de nível superior separada, você perde estrutura e reuso.
Padrão
Use Composite para que tanto etapas atômicas quanto grupos de etapas compartilhem a mesma interface.
class Task:
def run(self, context: dict) -> dict:
raise NotImplementedError
class SimpleTask(Task):
def __init__(self, name, handler):
self.name = name
self.handler = handler
def run(self, context: dict) -> dict:
return self.handler(context)
class CompositeTask(Task):
def __init__(self, name, children: list[Task]):
self.name = name
self.children = children
def run(self, context: dict) -> dict:
state = context
for child in self.children:
state = child.run(state)
return state
Uso:
research = SimpleTask("research", research_handler)
draft = SimpleTask("draft", draft_handler)
review = SimpleTask("review", review_handler)
write_article = CompositeTask("write_article", [research, draft, review])
result = write_article.run({"topic": "multi-agent systems"})
Benefícios
- Trate fluxos de múltiplas etapas como tarefas únicas.
- Componha fluxos maiores a partir de unidades menores.
Quando Não Usar
- Fluxos lineares muito simples sem reuso.
- Casos onde a orquestração já é tratada por um motor externo (ex: n8n, Airflow) e você não precisa de uma representação em código.
6. Padrão Chain of Responsibility: Roteamento de Ferramentas e Agentes
Problema
Um único agente ou função tenta lidar com toda requisição:
- Algumas queries precisam de busca na web.
- Outras precisam de acesso ao banco de dados.
- Outras precisam de execução de código.
Lógica hard-coded vira um grande bloco if/else.
Padrão
Use Chain of Responsibility para passar uma requisição através de um pipeline de handlers até que um assuma a responsabilidade.
class Handler:
def __init__(self, next_handler=None):
self.next = next_handler
def handle(self, request: dict):
if self.next:
return self.next.handle(request)
return None
class WebSearchHandler(Handler):
def handle(self, request: dict):
if "search" in request["intent"]:
return search_web(request["query"])
return super().handle(request)
class DbQueryHandler(Handler):
def handle(self, request: dict):
if "db" in request["intent"]:
return query_db(request["query"])
return super().handle(request)
Uso:
pipeline = WebSearchHandler(
next_handler=DbQueryHandler(
next_handler=Handler() # fallback padrão
)
)
response = pipeline.handle({"intent": "search", "query": "vector databases"})
Benefícios
- Cada handler tem uma responsabilidade única.
- Fácil de adicionar, remover ou reordenar etapas.
Quando Não Usar
- Quando as regras de roteamento são simples e estáveis.
- Quando você já usa um componente de roteador dedicado.
7. Padrão Mediator: Coordenação em Sistemas Multi-Agentes
Problema
Em um setup multi-agente, agentes começam a chamar uns aos outros diretamente:
- Pesquisador chama Codificador
- Codificador chama Crítico
- Crítico chama Pesquisador de novo
Isso cria acoplamento oculto e loops de feedback complexos.
Padrão
Use um Mediator que gerencia a comunicação. Agentes falam com o mediador, não uns com os outros.
class Mediator:
def __init__(self, factory: AgentFactory):
self.factory = factory
def handle(self, task: dict) -> str:
researcher = self.factory.create("researcher")
coder = self.factory.create("coder")
critic = self.factory.create("critic")
research = researcher.run(task)
code = coder.run({"spec": research})
review = critic.run({"code": code})
if review["status"] == "approve":
return code
return self._iterate(task, review)
def _iterate(self, task: dict, review: dict) -> str:
# implementar loop de revisão
...
Agentes continuam simples. O mediador é dono da lógica do fluxo de trabalho.
Benefícios
- Lugar central para mudar a orquestração.
- Mais fácil de raciocinar sobre loops e modos de falha.
Quando Não Usar
- Sistemas de agente único sem colaboração.
- Fluxos extremamente simples que não justificam um coordenador.
Um Roteiro Prático de Adoção
Não tente implementar todos os padrões de uma vez. Uma sequência segura:
- Proxy: Coloque um proxy na frente do seu provedor de LLM para ganhar observabilidade, controle de custo e segurança.
- Factory: Padronize como agentes são criados para remover duplicação de setup.
- Strategy: Adicione roteamento de modelo para controlar custo e latência.
- Decorator: Adicione rastreamento e logs sem tocar na lógica de negócio.
- Composite / Chain of Responsibility / Mediator: Introduza orquestração estruturada conforme os fluxos crescem.
Se você já sente que sua base de código está virando “apenas scripts”, você está no ponto onde esses padrões valem a pena.