Voltar ao Blog
AI & Development25 de fevereiro de 202510 min de leitura

Uma Estrutura de Monorepo para Agentes de IA Que Não Vai Virar Caos em 6 Meses

DR

Doug Ribeiro

10 min de leitura

O primeiro agente que você entrega é sempre um script. O segundo agente é uma cópia do primeiro com ferramentas diferentes. O terceiro agente é onde as coisas dão errado: alguém faz fork de um dos scripts anteriores, as dependências divergem, o logging fica inconsistente, e um trimestre depois você tem cinco agentes em quatro repos sem nenhum código compartilhado.

Se você sabe que vai construir mais de um agente, monte o monorepo antes de construir o segundo. Não antes do primeiro. Existe um custo real em estrutura prematura, e não vou fingir o contrário. Mas a janela entre o agente um e o agente dois é quando você paga menos e economiza mais.

A Premissa da Stack

Este post assume Angular no frontend, Python para o runtime do agente, Docker para dev local. Essa é a stack padrão que uso nos meus próprios projetos. Se você é Node/TypeScript de ponta a ponta, a maior parte disso se traduz diretamente. Se você é Go ou Rust no backend, a estrutura é a mesma, as escolhas de tooling mudam.

Por que Python para o agente em si. Os SDKs de IA (Anthropic, OpenAI) são mais maduros em Python, o ecossistema de ferramentas em torno de vector stores e pipelines de embedding é Python-first, e um loop ReAct fica mais legível em uma linguagem onde uma ferramenta é só uma função e um dict é só um dict. Mantenho o frontend em Angular porque é onde construo todo produto, e o dashboard para observar execuções de agentes é uma app Angular de verdade.

O Layout

text
my-agents/
├── apps/
│   ├── dashboard/              # Angular app, agent run viewer
│   └── api/                    # Python FastAPI, dashboard backend
├── agents/
│   ├── sonar_bridge/           # First agent
│   │   ├── agent.py
│   │   ├── tools.py
│   │   ├── playbook.py
│   │   └── tests/
│   ├── docs_drift/             # Second agent
│   └── bug_triage/             # Third agent
├── libs/
│   ├── base_agent/             # The shared ReAct loop
│   ├── memory/                 # pgvector client, Postgres schema, Redis client
│   ├── tools/                  # Reusable tools: github, slack, sonar, jira
│   └── telemetry/              # Logging, cost tracking, evaluation
├── infra/
│   ├── docker-compose.yml      # postgres, redis, sonarqube for local dev
│   ├── migrations/             # Alembic for Postgres schema
│   └── k8s/                    # deployment manifests if you need them
├── knowledge/                  # Markdown knowledge base (Obsidian-compatible)
│   ├── conventions/
│   ├── runbooks/
│   └── decisions/
├── pyproject.toml              # Workspace root, points to libs and agents
├── package.json                # For the dashboard
└── docker-compose.yml

Três zonas. apps/ é para código voltado ao usuário: o dashboard e sua API. agents/ é para agentes individuais, cada um um pacote pequeno. libs/ é código compartilhado do qual todo agente depende. Nada mais pertence a esse nível.

O Agente Base

libs/base_agent/ é a peça mais importante de toda a estrutura. Contém o loop ReAct, os padrões de acesso à memória, o rastreamento de custo e a telemetria. Todo agente em agents/ importa dele e define apenas o que é específico do seu domínio: o playbook (system prompt), a lista de ferramentas, as regras de escalonamento.

python
# libs/base_agent/agent.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Callable
from libs.memory import MemoryStack
from libs.telemetry import Telemetry

@dataclass
class AgentConfig:
    name: str
    version: str
    playbook: str
    tools: list[Callable]
    confidence_threshold: float = 0.7
    max_iterations: int = 20
    scratchpad_window: int = 6

class BaseAgent(ABC):
    """Owns the loop. The three hooks below are what each agent supplies."""

    def __init__(self, config: AgentConfig, memory: MemoryStack, telemetry: Telemetry):
        self.config = config
        self.memory = memory
        self.telemetry = telemetry

    @abstractmethod
    def _plan(self, trigger: dict, context: str | None,
              scratchpad: list) -> tuple[str, dict | None]:
        """One model call. Returns (thought, action), or action=None when done."""

    @abstractmethod
    def _execute(self, action: dict) -> dict:
        """Run one tool from config.tools and return the observation."""

    @abstractmethod
    def _decide(self, scratchpad: list) -> dict:
        """Turn the finished scratchpad into the run outcome."""

    def run(self, trigger: dict) -> dict:
        run_id = self.telemetry.start_run(self.config, trigger)

        similar = self.memory.vector_search(trigger, top_k=5)
        context = self.memory.summarize(similar) if similar else None

        scratchpad = []
        for iteration in range(self.config.max_iterations):
            thought, action = self._plan(trigger, context, scratchpad)
            if action is None:
                break
            observation = self._execute(action)
            scratchpad.append((thought, action, observation))
            # The scratchpad is what blows the context budget on a long
            # run. Compress the older entries instead of carrying 20
            # raw observations into the next plan call.
            if len(scratchpad) > self.config.scratchpad_window:
                scratchpad = self.memory.compress(
                    scratchpad, keep_last=self.config.scratchpad_window
                )

        outcome = self._decide(scratchpad)
        self.telemetry.end_run(run_id, outcome)
        self.memory.write(run_id, outcome)
        return outcome

O BaseAgent é dono do loop e de mais nada. _plan, _execute e _decide são abstratos de propósito: _plan é a chamada ao modelo, _execute é o dispatch de ferramentas, _decide transforma o scratchpad finalizado em um resultado. A implementação concreta desses três mora uma única vez em libs/base_agent/, então um agente novo é um objeto de configuração e um playbook, cerca de 30 linhas. Repare no passo de compressão dentro do loop. Vinte iterações de thought, action e observation crus são o jeito de você acabar com uma conta de tokens que não planejou.

python
# agents/sonar_bridge/agent.py
from libs.base_agent import ReActAgent, AgentConfig
from libs.memory import MemoryStack
from libs.telemetry import Telemetry
from libs.tools import sonar, github
from .playbook import PLAYBOOK

def build() -> ReActAgent:
    return ReActAgent(
        config=AgentConfig(
            name="sonar_bridge",
            version="0.1.0",
            playbook=PLAYBOOK,
            tools=[
                sonar.get_issues_for_pr,
                github.read_file_range,
                github.comment_on_pr,
            ],
            confidence_threshold=0.85,
        ),
        memory=MemoryStack.default(),
        telemetry=Telemetry.default(),
    )

O Dashboard Realmente Importa

O Angular está fazendo trabalho de verdade aqui. Você precisa de um lugar para ver toda execução de agente, filtrar por resultado, comparar decisões entre versões e reproduzir falhas. Isso não é um nice-to-have, é como você debuga um agente que decidiu escalar três vezes seguidas na noite passada.

A API Python em apps/api/ é um wrapper FastAPI fino que lê do mesmo Postgres em que os agentes escrevem. Sem magia, sem duplicação. O dashboard é um cliente burro dos mesmos dados que seus agentes já produzem.

Dev Local em um Único Comando

yaml
# docker-compose.yml at the root
services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_PASSWORD: local
      POSTGRES_DB: agents
    ports: ["5432:5432"]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

  sonarqube:
    image: sonarqube:community
    ports: ["9000:9000"]

  api:
    build: ./apps/api
    depends_on: [postgres, redis]
    environment:
      DATABASE_URL: postgres://postgres:local@postgres/agents
      REDIS_URL: redis://redis:6379
    ports: ["8000:8000"]

  dashboard:
    build: ./apps/dashboard
    depends_on: [api]
    ports: ["4200:4200"]

docker-compose up dá a qualquer pessoa que clone o repo um ambiente completo de trabalho: Postgres com pgvector, Redis, SonarQube, a API e o dashboard Angular. Sem mexer em instalações locais de Postgres, sem 'funciona na minha máquina' no primeiro dia.

O Diretório de Conhecimento

knowledge/ é uma pasta de arquivos markdown. Não é uma feature. Não é um plugin. Só markdown. O time edita direto, ou aponta um vault do Obsidian para ela. Um indexador em libs/memory/ sincroniza o conteúdo para o pgvector a cada mudança, e todo agente que precisa de um playbook ou de uma convenção lê da mesma fonte.

Separo a base de conhecimento do código deliberadamente. Conhecimento de negócio muda independentemente do código do agente. Você quer que não-engenheiros editem knowledge/ sem tocar em agents/. O Git ainda versiona. Reviews ainda acontecem. Mas a fronteira fica clara.

O Que Isso Te Dá

  • Novo agente = nova pasta em agents/, 30 linhas de Python. Sem debate de infraestrutura.
  • Todo agente produz o mesmo formato de telemetria. O dashboard não precisa de código por agente.
  • Memória, ferramentas e lógica de avaliação são compartilhadas. Um bug fix em libs/ corrige todos os agentes de uma vez.
  • Dev local é um único comando. Do clone à stack rodando, sem documento de setup para seguir.
  • Deploy em produção é o mesmo padrão de imagem de container para todo agente. Um pipeline, muitos agentes.

Onde Eu Traço o Limite

Nem tudo pertence ao monorepo. Se um agente tem um raio de impacto diferente, digamos que ele pode escrever em um sistema de produção enquanto os outros só comentam em pull requests, ele ganha seu próprio repo e sua própria cadência de release. Se um agente é experimental e eu não tenho certeza se sobrevive ao mês, eu prototipo fora e movo para dentro quando provar seu valor.

O monorepo é para os agentes de produção. Os estáveis. Aqueles dos quais seu time depende. Todo o resto pode começar como script e ser promovido quando conquistar seu lugar.

Um agente base compartilhado e uma camada de telemetria consistente valem mais do que qualquer agente individual. É esse código que torna o quinto agente fácil de entregar.

Referências

MonorepoArchitectureAngularPythonAI Agents