Artigo
· 2 hr atrás 6min de leitura

Agentes de IA do Zero Parte 1: Forjando o Cérebro

image1

Alguns conceitos fazem todo sentido no papel, enquanto outros exigem que você coloque a mão na massa.
Veja dirigir, por exemplo. Você pode memorizar cada componente da mecânica do motor, mas isso não significa que você realmente saiba dirigir.

Você só consegue compreender de verdade quando está no banco do motorista, sentindo fisicamente o ponto de fricção da embreagem e a vibração da estrada sob você.
Embora alguns conceitos de computação sejam intuitivos, Agentes Inteligentes são diferentes. Para entendê-los, você precisa sentar no banco do motorista.

Em meus artigos anteriores sobre agentes de IA, discutimos ferramentas como CrewAI e LangGraph. Neste guia, porém, vamos construir um micro-framework de agentes de IA do zero. Escrever um agente vai além de mera sintaxe; é uma jornada que todo desenvolvedor deveria percorrer para tentar resolver problemas do mundo real.

Ainda assim, além da experiência em si, existe outro motivo fundamental para fazer isso, melhor resumido por Richard Feynman:

"O que eu não consigo criar, eu não entendo."

Então… O Que É um Agente de IA?

Vamos ser específicos. Um agente é essencialmente um código que persegue um objetivo. Ele não apenas conversa. Ele “lê o ambiente” e executa diversas tarefas, que vão desde classificar e-mails até gerenciar agendas complexas.

Diferentemente de scripts rígidos, agentes possuem agência. Scripts convencionais quebram no momento em que a realidade foge das regras codificadas. Agentes não. Eles se adaptam. Se um voo é cancelado, eles não falham com um erro; simplesmente recalculam a rota.

Gosto de visualizar a arquitetura como um sistema biológico:

  • As Mãos: As Ferramentas. Sem ambientes de execução ou APIs, o cérebro fica preso em um frasco.
  • O Sistema Nervoso: Sua camada de orquestração. Ela gerencia estado e registra memória.
  • O Corpo: A infraestrutura de implantação que garante confiabilidade e disponibilidade.

Um único agente pode ser impressionante, mas Agentic AI é onde o verdadeiro poder está.
É todo o sistema em que múltiplos agentes especializados colaboram para alcançar um objetivo compartilhado.

É essencialmente uma agência digital: você tem um agente conduzindo pesquisas, outro redigindo textos e um nó “gerente” garantindo que ninguém atrapalhe o trabalho do outro.

Ainda assim, sendo honesto, a teoria só nos leva até certo ponto — e eu estou louco para realmente construir isso.
Vamos colocar a mão na massa. Eu chamei este projeto de MAIS, que tem um propósito duplo: tecnicamente, significa Multi-Agent Interoperability Systems. Porém, em português, simplesmente significa “mais”. É uma referência à nossa busca constante por capacidades extras.

O Cérebro: Inteligência Agnóstica com LiteLLM

Para alimentar nossos agentes, precisamos de flexibilidade, mas codificar rigidamente um provedor específico como a OpenAI nos limita. E se quisermos testar o Gemini 3.0? E se um cliente preferir rodar o Llama 3 localmente via Ollama?

Para isso, prefiro confiar em uma biblioteca que se tornou um pilar nos projetos dos Musketeers: LiteLLM.

A beleza do LiteLLM está na sua padronização. Ele atua como um adaptador universal, normalizando requisições e respostas entre mais de 100+ provedores.
Essa abstração é crucial para um Sistema Multi-Agente, pois nos permite combinar modelos de acordo com as necessidades específicas de cada agente.

Vamos imaginar o seguinte cenário:

  • O primeiro agente usa um modelo rápido e econômico (ex.: gpt-4o-mini).
  • O segundo agente utiliza um modelo com alta capacidade de raciocínio e uma grande janela de contexto (ex.: claude-3-5-sonnet) para analisar dados complexos.

Com nossa arquitetura, podemos definir qual modelo um agente usará simplesmente alterando uma string nas configurações.

Segurança em Primeiro Lugar: Lidando com Chaves de API

Conectar-se a esses provedores exige chaves de API, e certamente não queremos codificar segredos diretamente no código-fonte.
A “forma InterSystems” de lidar com isso é por meio de Production Credentials.

Para garantir que nossas chaves permaneçam protegidas, o LLM Adapter atua como uma ponte para o armazenamento seguro de credenciais do IRIS.
Utilizamos uma propriedade chamada APIKeysConfig para gerenciar essa passagem.
Você deve preenchê-la com os nomes das chaves específicas exigidas pelo LiteLLM (ex.: OPENAI_API_KEY, AZURE_API_KEY), separadas por vírgula.

Quando o adapter é inicializado, ele busca os segredos reais no armazenamento seguro e os atribui como variáveis de ambiente, permitindo que o LiteLLM se autentique sem nunca expor chaves brutas no código:

Method OnInit() As %Status
{
    Set tSC = $$$OK
    Try {
        Do ..Initialize()
    } Catch e {
        Set tSC = e.AsStatus()
    }
    Quit tSC
}

/// Configure API Keys em ambiente Python 
Method Initialize() [ Language = python ]
{
    import os
    for tKeyName in self.APIKeysConfig.split(','): 
        credential = iris.cls("Ens.Config.Credentials")._OpenId(tKeyName.strip())
        if not credential:
            continue
        os.environ[tKeyName] = credential.PasswordGet()
}

Agora que a camada de segurança está pronta, vamos focar no núcleo de raciocínio do nosso Adapter.
É aqui que definimos qual modelo chamar e como estruturar a mensagem.
O Adapter possui uma propriedade de configuração que determina o modelo padrão:

/// Modelo Default para usar se não for especificado na requisição
Property DefaultModel As %String [ InitialExpression = "gpt-4o-mini" ]; 

No entanto, a mágica acontece em tempo de execução. A mensagem de entrada dc.mais.messages.LLMRequest possui uma propriedade opcional Model.
Se o orquestrador (BPL) enviar essa propriedade preenchida, o Adapter respeita a escolha dinâmica.
Caso contrário, ele utiliza o DefaultModel.

Separando Instruções da Entrada

Outra decisão importante de design foi como enviamos texto para o LLM. Em vez de enviar apenas uma string bruta, dividi o conceito em dois campos na requisição:

  1. Content: Onde ficam o “System Prompt” ou as instruções do agente atual (ex.: “Você é um garçom especialista em vinhos…”).
  2. UserContent: Onde entra a entrada real do usuário (ex.: “Qual vinho combina com peixe?”).

Isso nos permite construir um array de mensagens limpo para o LiteLLM, garantindo que a IA diferencie claramente sua persona da pergunta do usuário.

Aqui está como o método principal CallLiteLLM monta essa estrutura utilizando Python diretamente dentro do IRIS:

Method CallLiteLLM(pRequest As dc.mais.messages.LLMRequest) As dc.mais.messages.LLMResponse [ Language = python ]
{
    import litellm
    import json
    import time
    import iris

    t_attempt = 0
    max_retries = self.MaxRetries
    retry_delay = self.RetryDelay

    last_error = None
    pResponse = iris.cls("dc.mais.messages.LLMResponse")._New()

    while t_attempt <= max_retries:
        t_attempt += 1

        try:
            model = pRequest.Model
            if not model:
                model = self.GetDefaultModel()

            messages = [{"role": "user", "content": pRequest.Content}]

            if pRequest.UserContent:
                messages.append({"role": "user", "content": pRequest.UserContent})

            response = litellm.completion(model=model, messages=messages)

            pResponse.Model = response.model

            choices_list = []
            if hasattr(response, 'choices'):
                for choice in response.choices:
                    if hasattr(choice, 'model_dump'):
                        choices_list.append(choice.model_dump())
                    elif hasattr(choice, 'dict'):
                        choices_list.append(choice.dict())
                    else:
                        choices_list.append(dict(choice))

            pResponse.Choices = json.dumps(choices_list)
            if (len(response.choices) > 0 ):
                pResponse.Content = response.choices[0].message.content

            if hasattr(response, 'usage'):
                if hasattr(response.usage, 'model_dump'):
                    pResponse.Usage = json.dumps(response.usage.model_dump())
                else:
                    pResponse.Usage = json.dumps(dict(response.usage))

            if hasattr(response, 'error') and response.error:
                pResponse.Error = json.dumps(dict(response.error))

            return pResponse

        except Exception as e:
            last_error = str(e)

            class_name = "dc.mais.adapter.LiteLLM"
            iris.cls("Ens.Util.Log").LogError(class_name, "CallLiteLLM", f"LiteLLM call attempt {t_attempt} failed: {last_error}")

            if t_attempt > max_retries:
                break

            time.sleep(retry_delay)

    error_payload = {
        "message": "All LiteLLM call attempts failed",
        "details": last_error
    }
    pResponse.Error = json.dumps(error_payload)

    return pResponse
}

Para aqueles que preferem a sintaxe clássica, também incluí uma versão em ObjectScript do mesmo método:

Method CallLiteLLMObjectScript(pRequest As dc.mais.messages.LLMRequest, Output pResponse As dc.mais.messages.LLMResponse) As %Status
{
    Set tSC = $$$OK
    Set tAttempt = 0
    Set pResponse = ##class(dc.mais.messages.LLMResponse).%New()

    While tAttempt <= ..MaxRetries {
        Set tAttempt = tAttempt + 1

        Try {
            Set model = $Select(pRequest.Model '= "": pRequest.Model, 1: ..GetDefaultModel())

            // Prepare a history
            Set jsonHistory = [].%FromJSON(pRequest.History)
            Set:(jsonHistory="") jsonHistory = []

            // Injete o Tool Output se houver (Fecha a lógica de loop)
            If (pRequest.ToolCallId '= "") && (pRequest.ToolOutput '= "") {
                Set tToolMsg = {}
                Set tToolMsg.role = "tool"
                Set tToolMsg.content = pRequest.ToolOutput
                Set tToolMsg."tool_call_id" = pRequest.ToolCallId
                Do jsonHistory.%Push(tToolMsg)
            }

            // Adicione conteúdo e prompt do usuário atual se não estiver vazio
            If (pRequest.Content '= "") {
                Do jsonHistory.%Push({"role": "user", "content": (pRequest.Content)})
            }

            // Adicione campos extra de conteúdo do usuário, se houver
            If (pRequest.UserContent'="") {
                Do jsonHistory.%Push({"role": "user", "content": (pRequest.UserContent)})
            }

            Set strMessages = jsonHistory.%ToJSON()

            // Chame o Python Helper
            Set tResponse = ..PyCompletion(model, strMessages, pRequest.Parameter, 1)

            // Mapeie a respsota
            Set pResponse.Model = tResponse.model

            // Converta as escolhas (choices) Python para IRIS DynamicArray
            Set choices = []
            For i=0:1:tResponse.choices."__len__"()-1 {
                Do choices.%Push({}.%FromJSON(tResponse.choices."__getitem__"(i)."to_json"()))
            }
            Set pResponse.Choices = choices.%ToJSON()

            // Processe a última escolha
            If (choices.%Size()>0) {
                Set choice = choices.%Get(choices.%Size()-1)

                If ($IsObject(choice.message)){
                    Set pResponse.Content = choice.message.content

                    // Extraia as chamadas Tool Calls
                    // Check if 'tool_calls' exists and is a valid Object (DynamicArray)
                    Set tToolCalls = choice.message."tool_calls"

                    // Verifique que é um Objeto (Array) e não uma string vazia
                    If $IsObject(tToolCalls) {
                        Do ..GetToolCalls(tToolCalls, .pResponse)
                    }
                }
            }

            // Mapeie o uso
            If ..hasattr(tResponse, "usage") {
                Set pResponse.Usage = {}.%FromJSON(tResponse.usage."to_json"()).%ToJSON()
            }

            // Sucesso - Saia do Loop
            Quit 

        } Catch e {
            Set tSC = e.AsStatus()
            $$$LOGERROR("LiteLLM call attempt "_tAttempt_" failed: "_$System.Status.GetOneErrorText(tSC))
            If tAttempt > ..MaxRetries Quit
            Hang ..RetryDelay
        }
    }

    If ($$$ISERR(tSC)) {
        Set pResponse.Error = {
            "message": "All LiteLLM call attempts failed",
            "details": ($System.Status.GetOneErrorText(tSC))
        }.%ToJSON()
    }
    Quit tSC
}

Você deve ter notado a chamada para ..PyCompletion(...) dentro da versão ObjectScript desta lógica.
Não é um método padrão do sistema; mas sim um auxiliar customizado projetado para lidar com o Marshaling de Dados entre as duas linguagens.
Embora o IRIS permita chamadas diretas ao Python, passar estruturas aninhadas complexas (ex: listas de objetos contendo tipos de dados específicos) às vezes pode exigir conversão manual.
O método PyCompletion atua como uma camada de tradução. Ele aceita os dados do ObjectScript como strings JSON serializadas. Em seguida, ele os desserializa em dicionários e listas nativos do Python (usando json.loads) dentro do ambiente Python. Finalmente, ele executa a requisição LiteLLM.
Esta abordagem "Híbrida" mantém nosso código ObjectScript limpo e legível, focando puramente na lógica de negócio (looping, gerenciamento de histórico), enquanto transfere o trabalho pesado de conversão de tipos de dados e interação com bibliotecas para um pequeno wrapper Python dedicado.

Essa estrutura simples nos dá um controle tremendo. Enquanto o BPL pode trocar o "cérebro" da operação (o Modelo) ou a personalidade (Conteúdo) dinamicamente em cada etapa do fluxo, o Adapter cuida do "encanamento" técnico.

O Palco está Montado, mas está Vazio

Já percorremos um longo caminho até agora. Construímos uma ponte segura e agnóstica a provedores para o LLM usando Python e LiteLLM. Resolvemos os problemas complicados de interoperabilidade com **kwargs e estabelecemos uma forma segura de lidar com credenciais com a ajuda do armazenamento nativo do IRIS.

No entanto, se você olhar de perto, verá que temos um belo carro com um motor potente, mas sem motorista.

Estabelecemos a conexão com o "Cérebro", mas falta uma persona definida. Podemos invocar o GPT, mas sem instruções específicas, ele não sabe se deve agir como um recepcionista prestativo ou um engenheiro de suporte técnico. No momento, ele é apenas um processador sem estado, sem memória, sem um objetivo e desconectado de qualquer ferramenta.

Na Parte 2, daremos uma Alma a este cérebro. Faremos o seguinte:

  1. Construir a classe dc.mais.adapter.Agent para definir personas.
  2. Dominar a Engenharia de Prompt Dinâmica para aplicar regras de negócio.
  3. Implementar a lógica de "Allow List" para comunicação entre agentes.
  4. Mergulhar na teoria do Paradigma ReAct que torna os agentes verdadeiramente inteligentes.

Eu compliquei demais o adapter? Você tem uma maneira mais limpa de lidar com as variáveis de ambiente?
Se sim, ou se você notar uma falha na minha lógica antes de chegarmos à Parte 2, aponte nos comentários abaixo! Estou escrevendo isso tanto para aprender com você quanto para compartilhar.

Agradecimentos: Um agradecimento especial ao meu colega Mosqueteiro, @José Pereira, que me apresentou às maravilhas do LiteLLM.

Fique ligado. Estamos apenas começando.

Discussão (0)1
Entre ou crie uma conta para continuar