Artigo
· Fev. 17 12min de leitura

Agentes de IA do Zero - Parte 2: Dando um Corpo ao Cérebro

cover

Na Parte 1, estabelecemos a base técnica do MAIS (Multi-Agent Interoperability Systems). Conectamos com sucesso o "Cérebro", construímos um Adapter robusto usando LiteLLM, protegemos nossas chaves de API com o IRIS Credentials e, finalmente, deciframos o código do quebra-cabeça da interoperabilidade com Python.

No entanto, neste momento, nosso sistema é apenas um cano bruto para um LLM. Ele processa texto, mas carece de identidade.

Hoje, na Parte 2, definiremos a Anatomia de um Agente. Passaremos de simples chamadas de API para Personas estruturadas. Aprenderemos como envolver o LLM em uma camada de lógica de negócios, dando-lhe um nome, um papel e, o mais importante, a capacidade de conhecer seus "vizinhos".

Vamos construir a "Alma" da nossa máquina.

A Anatomia de um Agente: Mais do que Apenas um Prompt

Agora que temos uma conexão com o "Cérebro" (o LLM), precisamos conceder-lhe uma personalidade. Um erro comum é pensar que um Agente é simplesmente um prompt de sistema, por exemplo: "Você é um assistente prestativo". Isso é apenas um chatbot.

A IA Agêntica verdadeira se destaca porque não precisa de uma "babá". Ela combina autonomia com um forte impulso para concluir o trabalho. Ela olha para frente, por exemplo, verificando o estoque antes de reservar uma venda e, se encontrar um obstáculo, descobre uma solução alternativa em vez de simplesmente desistir.

Para encapsular essa complexidade dentro do IRIS, desenvolvi a classe dc.mais.adapter.Agent. Ela atua como uma "Definição de Persona", envolvendo efetivamente o LLM bruto dentro de limites operacionais estritos.

Cada agente é construído sobre um conjunto de configurações específico. Sempre começamos com o Name (Nome) e o Role (Papel) para estabelecer um identificador único e um domínio de especialidade (por exemplo, um "Especialista em Culinária Francesa"). Para evitar alucinações ou desvios de escopo, definimos um Goal (Objetivo) fixo e um checklist detalhado de Tasks (Tarefas). Também impomos padrões de comunicação via OutputInstructions, dizendo ao agente para ser conciso ou evitar caracteres específicos, e fornecemos as Tools (Ferramentas - definições JSON) que ele está autorizado a executar.

Por que o "Target" é Importante

Não importa como você olhe, o componente mais crítico nesta configuração é o Target. Esta propriedade permite o que eu chamo de Handoff Descentralizado.

Em vez de rotear tudo por meio de um supervisor central, a propriedade Target fornece uma lista separada por vírgulas de próximos passos válidos na cadeia. Por exemplo, um agente MenuExpert sabe que seu trabalho é ajudar a escolher a comida. No entanto, graças à propriedade Target, ele também entende que, assim que o usuário disser "Quero a conta", ele deve passar a bola para o CashierAgent.

Isso cria um "motor de raciocínio" onde o LLM compreende seus próprios limites: "Eu sou o especialista em comida, mas não tenho permissão para processar pagamentos. Preciso chamar o Caixa."

Abaixo, você pode ver como a classe está até agora:

Class dc.mais.adapter.Agent Extends Ens.OutboundAdapter
{

/// Controla quais propriedades são visíveis nas configurações da produção
Parameter SETTINGS = "Name:Basic,Role:Basic,Goal:Basic,Tasks:Basic:textarea?rows=5&cols=50,OutputInstructions:Basic:textarea?rows=5&cols=50,Tools:Basic:textarea?rows=5&cols=50,Target:Basic,Model:Basic,MaxIterations,Verbose";

/// Identificador único para o agente
Property Name As %String;

/// Objetivo principal que o agente deve atingir
Property Goal As %String(MAXLEN = 100);

/// Descrição da função e expertise do agente. i.e. "Esse assistente é sábio, solícito e sugere questões para acompanhamento."
Property Role As %String(MAXLEN = 350);

/// Regras para como o agente deve formatar e apresentar respostas
Property OutputInstructions As %String(MAXLEN = 1000);

/// Lista ordenada de responsabilidades e ações que o agente deve performar
Property Tasks As %String(MAXLEN = 1000);

/// Lista de funções que o agente pode chamar
Property Tools As %String(MAXLEN = 10000);

/// Nome dos próximos agentes permiditos (Separados por vírgula, ex., "OrderTaker,OrderSubmitter")
Property Target As %String(MAXLEN = 1000);

// Separado por vírgula ou JSON para escalabilidade

/// Nome do modelo LLM model name (permite qeu diferentes agentes usem diferentes modelos)
Property Model As %String;

/// Número máximo de chamadas de ferramentas antes de parar
Property MaxIterations As %Integer [ InitialExpression = 3 ];

}

Essa estratificação é o "ingrediente secreto". Ela nos eleva de uma abordagem genérica de "espero que a IA entenda" para um sistema estruturado de "Planejar, Atribuir e Monitorar". Essencialmente, estamos envolvendo a imprevisibilidade bruta de um LLM em um cobertor de segurança de lógica de negócios.

Ainda assim, ter essas propriedades em uma classe de banco de dados não é suficiente. Também precisamos traduzir essas configurações estritas em instruções de linguagem natural que o LLM respeite.

É aqui que a Engenharia de Prompt Dinâmica entra em cena.

Implementei um método chamado GetAgentInstructions que atua como uma fábrica para a personalidade do agente. Ele não simplesmente concatena strings; ele constrói todo o modelo mental para a camada de IA, camada por camada.

O "Conhecimento dos Vizinhos" (Handoff)

Preste atenção na lógica dentro do bloco If (..Target '= ""), pois ela é a cola que mantém a rede unida. Na verdade, estamos dizendo ao agente exatamente quem são seus vizinhos.

Isso funciona como uma "Lista de Permissão" (Allow List). Isso evita que o MenuExpert tente transferir um cliente para um ParkingAttendant inexistente. Ele impõe o fluxo do processo de negócio no nível do prompt. Embora o mecanismo de transferência real pertença ao Orchestrator (que cobriremos em breve), a consciência da transferência começa aqui.

Prompting Defensivo

Você também deve notar a seção sobre Diretrizes de Uso de Ferramentas. Comandamos explicitamente o modelo: "NÃO adivinhe ou invente dados."

É programação defensiva aplicada ao inglês (ou português). Estamos impedindo preventivamente o modelo de alucinar um menu ou falsificar uma confirmação de pedido e forçando-o a utilizar as ferramentas nativas que fornecemos.

Confira a implementação abaixo:

Method GetAgentInstructions(Output oPrompt As %String) As %String
{
    Set tSC = $$$OK
    Set oPrompt = "" // Garante que não seja nulo
    Try {
        Set prompt = "Você é "_..Name_", um agente especializado."_$C(10)
        Set:(..Role '= "") prompt = prompt_"## Seu Papel: "_..Role_$C(10)
        Set:(..Goal '= "") prompt = prompt_"## Seu Objetivo: "_..Goal_$C(10)
        Set:(..Tasks '= "") prompt = prompt_"## Suas Tarefas: "_..Tasks_$C(10)
        Set:(..OutputInstructions'= "") prompt = prompt_"## Instruções de Saída: "_..OutputInstructions_$C(10)

        // --- Lógica de Handoff: Apresentando os vizinhos ---
        If (..Target '= "") {
            Set prompt = prompt_"## Capacidades de Handoff:"_$C(10)
            Set prompt = prompt_"- Você pode transferir a conversa APENAS para os seguintes agentes: "_..Target_$C(10)
            Set prompt = prompt_"- Use a ferramenta 'handoff_to_agent' com um desses nomes exatos."_$C(10)
        }

        // --- Prompting Defensivo para Ferramentas ---
        If (..Tools '= "") {
            Set prompt = prompt_"## Diretrizes de Uso de Ferramentas:"_$C(10)
            Set prompt = prompt_"- Você tem acesso a funções (ferramentas) para obter dados reais."_$C(10)
            Set prompt = prompt_"- Você DEVE chamar a função nativamente quando necessário."_$C(10)
            Set prompt = prompt_"- NÃO adivinhe ou invente dados. Use a função."_$C(10)
            Set prompt = prompt_"- NUNCA escreva o JSON da chamada da função no texto de resposta. Apenas acione a função."_$C(10)
        }

        Set prompt = prompt_"# Lembre-se: Você faz parte de um sistema multiagente."
        Set oPrompt = prompt

    } Catch ex {
        Set tSC=ex.AsStatus()
        $$$LOGERROR("Erro ao gerar instruções: "_ex.DisplayString())
    }
    Return tSC
}

Agora que nossos agentes têm suas ordens e reconhecem seus vizinhos, precisamos de um Comandante para garantir que eles realmente sigam o roteiro. Então, vamos entrar no Orchestrator.

O Sistema Nervoso: Orquestrando a Equipe do Bistrô

É hora de passarmos para o sistema nervoso: O Orchestrator.

Acho significativamente mais fácil entender o Orchestrator criando um projeto tangível do que discutindo teoria abstrata. Então, vamos para o nosso pacote dc.samples e construir uma "Equipe de Bistrô" para validar nossa estrutura.

O conceito é direto: estabeleceremos uma equipe de atendentes para um pequeno bistrô. Precisaremos de um Greeter para receber os convidados e um Menu Expert para lidar com os detalhes culinários.

1. Contratando a Equipe (Configurando Agentes)

Como projetamos nossa classe dc.mais.operation.Agent para ser reutilizável, não precisamos escrever código novo para esses agentes. Devemos simplesmente adicioná-los à Produção e configurar as definições.

[Legenda: Configurando a equipe: Adicionando uma Agent Operation reutilizável à Produção. Nenhum código novo necessário, apenas configuração.]

Vamos adicionar o primeiro, Agent.Greeter. Nos Parâmetros Básicos, devemos definir sua "alma":

Nome: Greeter (Anfitrião)
Papel: Receber os clientes e fornecer informações iniciais sobre o menu
Objetivo: Fazer com que os clientes se sintam bem-vindos e guiá-los ao especialista apropriado
Tarefas: 
- Receber os clientes com uma saudação calorosa e profissional
- Fornecer uma breve visão geral das especialidades do restaurante
- Identificar as necessidades do cliente (informações do menu, pedidos ou dúvidas gerais)
- Fazer o handoff (transferência) para o MenuExpert quando o cliente desejar informações detalhadas do menu
Instruções de Saída: 
- Saudar os clientes de forma calorosa e profissional
- Manter as respostas concisas e convidativas (máximo de 2-3 frases)
- Sempre terminar com uma pergunta para engajar o cliente

noice

Retornaremos ao MenuExpert em breve. Primeiro, vamos garantir que eles tenham um cérebro. Assim como fizemos para o Agente, criei uma Business Operation genérica dc.mais.operation.LLM usando o adapter que construímos anteriormente. Basta adicioná-la à produção e estamos prontos.

Ótimo!!

2. Construindo o Fluxo (O BPL)

Agora, vamos criar um Business Process chamado Orchestrator.

Este processo requer uma mensagem de Request contendo o seguinte:

  • Sender: O nome do agente enviando a mensagem (se houver).
  • Assignee: O agente específico que queremos atingir.
  • Content: O texto de interação do usuário.

Para a resposta, uma simples propriedade Content para manter a resposta do agente é suficiente.

Prefiro usar Context Variables para manter o estado limpo. Assim, a primeira coisa que fazemos no BPL é atribuir as propriedades do Request recebido ao Contexto.

A Lógica de "Cold Start": Se esta for a primeiríssima execução, o Assignee estará vazio. Precisamos decidir quem inicia a conversa. Por esse motivo, adicionei uma condição If simples: se o Assignee estiver vazio, defina o Target como 'greeter'.

O Roteamento: Neste ponto, traçamos a rota usando um Switch baseado no Target.

  • Case 'greeter': Chama Agent.Greeter.
  • Case 'menu_expert': Chama Agent.MenuExpert.

Importante: Estamos fazendo chamadas síncronas aqui (desative a flag Async). Por quê? Porque não estamos pedindo ao Agente para responder ao usuário ainda. Estamos solicitando que a Agent Operation retorne seu System Prompt (sua personalidade).

Lembre-se de salvar este resultado em context.CurrentSystemPrompt.

A Sinapse: Finalmente, após a rota ser determinada e termos o prompt correto, podemos chamar o LLM.

  • Request.Content: context.CurrentSystemPrompt (As Regras)
  • Request.UserContent: O texto real do usuário.

Com este fluxo simples — Router -> Get Persona -> Call LLM — podemos rodar nosso primeiro teste.

Enviei um "Olá" para o processo, e...

Et voilà! O Greeter respondeu com uma recepção calorosa e profissional, exatamente como configurado. Está vivo!

Dando Mãos aos Agentes: Ferramentas e o Loop ReAct

Vamos voltar para a nossa equipe. Paramos no Greeter, que é charmoso, mas, francamente, um pouco inútil quando se trata da comida real.

Apresentamos o MenuExpert.

A principal distinção entre este agente e o Greeter é que o MenuExpert não depende meramente de seus dados de treinamento (que alucinam preços). Ele requer acesso a dados em tempo real. Ele precisa de uma Ferramenta (Tool).

Para manter as coisas simples nesta exemplo, criei uma Business Operation padrão chamada Tool.GetMenu.

Nesta fase, nas configurações do MenuExpert, em Tools, devemos colar a definição da função que segue o padrão JSON schema da OpenAI:

[{
    "type": "function",
    "function": {
        "name": "get_menu",
        "description": "Obter o menu completo do bistrô",
        "parameters": {
            "type": "object",
            "properties": {},
            "required": []
        }
    }
}]

Isso atua como a documentação da API para o cérebro. Estamos basicamente dizendo ao LLM: "Se você precisar do menu, existe uma função chamada get_menu que não recebe argumentos. Use-a."

Uma Pausa Rápida: O Paradigma ReAct

Antes de implementar a conexão, vale a pena entender a teoria por trás dela. Todo o sistema depende de uma estrutura conhecida como ReAct (abreviação de Reason plus Act — Raciocinar mais Agir). Introduzida em um artigo de 2022, ela mudou fundamentalmente a maneira como construímos agentes de IA.

Antes do ReAct, os LLMs eram exclusivamente mecanismos de completamento de texto. Com o ReAct, forçamos o modelo a alternar entre raciocínio verbal (Pensamento) e ações (Ferramentas). Em resumo, parece-se com o seguinte monólogo interno:

Pensamento: O usuário quer o preço do Coq au Vin. Eu não sei.
Ação: get_menu()
Observação: {"Coq au Vin": 28.00}
Pensamento: Eu tenho o preço. Posso responder agora.
Resposta Final: "O Coq au Vin custa $28.00."

Pense na Memória como "O que eu lembro" (histórico de contexto) e no ReAct como "Como eu resolvo problemas" (o loop de raciocínio e ação). Sem o ReAct, o agente é apenas um observador passivo. Com ele, torna-se um resolvedor de problemas ativo.

A Peça que Falta: O Sistema Nervoso

Percorremos um terreno considerável. Temos uma conexão segura com o LLM (Parte 1). Também definimos nossos Agentes com Funções, Objetivos e Ferramentas estritos, e exploramos a teoria ReAct que impulsiona seu raciocínio (Parte 2).

No entanto, após revisar nosso código, identificamos um problema: Tudo é estático.

Temos as definições do MenuExpert e da ferramenta get_menu, mas nada os conecta. Não há um loop para capturar a chamada da ferramenta, executar o SQL e alimentar o resultado de volta para o cérebro. Também não há um mecanismo para lidar com o Handoff quando o agente diz: "Preciso de ajuda".

Temos o talento (os Atores) e as instruções (o Roteiro), mas eles não têm onde se apresentar. Também não há um Palco ainda.

Na final Parte 3, construiremos o Sistema Nervoso. Faremos o seguinte:

  1. Implementar o Orchestrator usando InterSystems BPL.
  2. Construir a Arquitetura de Loop Duplo para gerenciar ciclos de vida autônomos.
  3. Executar as ferramentas e lidar com o sinal de "Handoff" dinamicamente.
  4. Utilizar Visual Tracing para observar nossos agentes pensando em tempo real.

Prepare seu café, pois a próxima parte trará tudo isso à vida!

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