.NET - Orquestração Híbrida de IA com MAF, Ollama e OpenAI
 Hoje veremos como constrir um pipeline unindo nuvem e inferência local com Microsoft Agent Framework, Ollama, OpenAI e Tool Calling usando C# e os recursos do Microsoft.Extensions.AI e MAF.

A integração de Inteligência Artificial em sistemas corporativos frequentemente oscila entre dois extremos: depender inteiramente de APIs em nuvem com faturamento elástico e preocupações de conformidade, ou insistir em modelos locais menores que sofrem com alucinações em tarefas de raciocínio rigoroso.

 

Com o amadurecimento do Microsoft Agent Framework (MAF) e as abstrações unificadas do Microsoft.Extensions.AI, o ecossistema C#/.NET passa a contar com um modelo arquitetural pragmático: a orquestração híbrida de agentes.

Neste artigo, veremos como construir um pipeline funcional de catálogo técnico que extrai dados brutos, consulta um repositório via Tool Calling, garante a saída em objetos C# fortemente tipados e gera o conteúdo final de apresentação sem excessos de engenharia.

1. Conceitos Fundamentais

Antes de ir ao código, é importante nivelar os conceitos centrais do ecossistema moderno de IA no .NET:
  • Agente de IA (AI Agent): Diferente de uma simples chamada a um endpoint de completude de texto, um agente encapsula persona/instruções, gerencia estado conversacional e tem capacidade autônoma de inspecionar parâmetros, invocar ferramentas (Tools) e reagir a retornos antes de entregar a resposta final.
  • IChatClient (Microsoft.Extensions.AI): Interface padronizada que abstrai o provedor de linguagem (OpenAI, Azure OpenAI, Ollama, Anthropic ou modelos locais). Permite trocar ou intercalar motores de inferência via injeção de dependência sem alterar uma única linha da lógica de negócio.
  • Saídas Estruturadas (Structured Outputs): Técnica em que o modelo adere a um schema JSON rígido mapeado diretamente para tipos do C# (record ou class), eliminando parses frágeis por expressões regulares e reduzindo falhas sintáticas. O próprio Microsoft Agent Framework oferece isso nativamente através de AIAgent.RunAsync<T>, sem exigir instrução manual de formato no prompt nem desserialização manual.
  • Chamada de Funções / Ferramentas (Tool Calling / Function Calling): Mecanismo pelo qual o agente decide invocar métodos locais do C# (como consultas a bancos de dados ou APIs REST) durante o ciclo de inferência para embasar suas respostas com dados reais.
2. O Cenário: Pipeline Híbrido de Catálogo de Produtos

Em vez de criar fluxos artificiais com entradas estáticas, o pipeline resolve um fluxo de negócio comum:
  • Entrada: O sistema recebe notas desorganizadas ou consultas técnicas de produtos.

  • Agente Especialista (Nuvem - OpenAI / gpt-4o-mini):
    Analisa a requisição, invoca uma ferramenta local do C# para consultar a disponibilidade do item no repositório de estoque e retorna um objeto C# estrito (EspecificacaoProduto) via saída estruturada nativa do agente.
  • Agente Redator (Local - Ollama / llama3.2): Recebe o objeto C# tipado e gera uma descrição promocional formatada, rodando localmente na máquina, com custo zero de tokens de saída e baixa latência.


3. Configuração do Projeto e Dependências

Crie uma aplicação do tipo console na plataforma .NET usando o Visual Studio 2026 e o template padrão ou usando o seguinte comando .NET CLI:

dotnet new console -n DemoAgenteHibrido
cd DemoAgenteHibrido

Instale os pacotes NuGet via Tools->Nuget Package Manager no VS 2026 ou via comando CLI :

# Abstração de IA oficial da Microsoft
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI

# Framework de Agentes (pacote estável, não requer --prerelease)
dotnet add package Microsoft.Agents.AI

# Provedor do Ollama para .NET
dotnet add package OllamaSharp

# Configuração e Injeção de Dependência
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.DependencyInjection

Nota: O pacote Microsoft.Extensions.Configuration.UserSecrets já vem transitivamente com o Microsoft.Extensions.Hosting, que o projeto já referencia e não precisa ser instalado.

Ao final o arquivo de projeto deverá estar assim:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net11.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Agents.AI" Version="1.21.0" />
    <PackageReference Include="Microsoft.Extensions.AI" Version="10.10.0" />
    <PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="10.10.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="11.0.0-rc.1.26425.128" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="11.0.0-rc.1.26425.128" />
    <PackageReference Include="OllamaSharp" Version="5.4.30" />
  </ItemGroup>

</Project>

Certifique-se de que o Ollama esteja em execução local com o modelo Llama 3 ou mais atual carregado:

ollama run llama3

Nota: Para projetos locais com Ollama, recomenda-se utilizar ao menos variantes como llama3.2 ou modelos mais recentes para melhor suporte a instruções e menor taxa de alucinações.


4. Implementação Pragmática


A implementação a seguir utiliza injeção de dependência clássica do .NET, declaração de ferramentas via atributos e tipagem com record.

4.1. Modelos de Domínio e Ferramenta de Estoque

namespace DemoAgenteHibrido.Dominio;

// Modelo fortemente tipado para a saída do primeiro agente
public record EspecificacaoProduto(
    string Nome,
    string Conectividade,
    string Carregamento,
    string DuracaoBateria,
    bool EmEstoque
);
using System.ComponentModel;
namespace DemoAgenteHibrido.Servicos;

// Serviço local que será exposto ao Agente como uma Tool
public class ServicoEstoque
{
    private static readonly Dictionary<string, bool> BancoDadosEstoque =
        new(StringComparer.OrdinalIgnoreCase)
        {
            ["Teclado Sem Fio Pro"] = true,
            ["Mouse Ergonômico USB"] = false,
            ["Monitor UltraWide 29"] = true
        };

    [Description("Consulta a disponibilidade do produto no estoque da empresa pelo nome.")]
    public bool VerificarDisponibilidade(
        [Description("Nome exato ou aproximado do produto")] string nomeProduto)
    {
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.WriteLine($"   [FERRAMENTA EXECUTADA] Consultando estoque para: '{nomeProduto}'");
        Console.ResetColor();

        // Simulação de consulta ao banco de dados
        return BancoDadosEstoque.TryGetValue(nomeProduto, out var emEstoque) && emEstoque;
    }
}

Entendendo o código:

record EspecificacaoProduto
Define a estrutura de dados imutável usada como contrato para o Structured Output do primeiro agente.
Mapeia de forma estrita as propriedades técnicas extraídas do texto bruto, como conectividade, recarga e bateria.
Garante tipagem forte no C#, eliminando a necessidade de validações manuais de strings ou formatos JSON.

Classe ServicoEstoque
Encapsula a lógica de negócio local de controle de estoque que será exposta ao agente como ferramenta.
Simula um banco de dados relacional em memória por meio de um dicionário estático que ignora maiúsculas/minúsculas.
Funciona como o serviço de domínio que isola a persistência de dados das rotinas de inferência de IA.

Método VerificarDisponibilidade
Atua como a ferramenta de Tool Calling decorada com atributos [Description] para guiar a decisão do modelo.
Recebe o nome do produto identificado pelo agente, registra a consulta visualmente e busca o registro no dicionário.
Retorna um valor booleano determinístico indicando se o item solicitado está disponível para pronta entrega.

4.2. Fluxo Principal da Aplicação


using System.ClientModel;
using System.ComponentModel;
using DemoAgenteHibrido.Dominio;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OllamaSharp;
using OpenAI;

var builder = Host.CreateApplicationBuilder(args);

// 1. Registro de Serviços e Ferramentas
builder.Services.AddSingleton<ServicoEstoque>();

// 2. Configuração do Cliente em Nuvem (OpenAI / gpt-4o-mini)
string chaveOpenAi =
    Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? "sua-chave-aqui";

IChatClient clienteOpenAi = new OpenAIClient(new ApiKeyCredential(chaveOpenAi))
    .GetChatClient("gpt-4o-mini")
    .AsIChatClient();

// 3. Configuração do Cliente Local (Ollama / Llama 3)
IChatClient clienteOllama =
    new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.2");

var app = builder.Build();
var servicoEstoque = app.Services.GetRequiredService<ServicoEstoque>();

Console.WriteLine("=== Pipeline Híbrido: MAF + OpenAI + Ollama ===\n");

// 4. Criação do Agente Analista (Nuvem) com suporte a Tool Calling
var ferramentaEstoque = AIFunctionFactory.Create(servicoEstoque.VerificarDisponibilidade);

var agenteAnalista = new ChatClientAgent(
    clienteOpenAi,
    name: "AnalistaDeCatalogo",
    instructions: """
        Você é um analista de produtos rigoroso.
        Sua função é identificar os detalhes técnicos do produto informado e checar a
        disponibilidade usando a ferramenta de estoque antes de responder.
        """,
    tools: [ferramentaEstoque]
);

// 5. Criação do Agente Redator (Local)
var agenteRedator = new ChatClientAgent(
    clienteOllama,
    name: "RedatorMarketing",
    instructions: """
        Você é um redator de e-commerce experiente.
        Você receberá os dados estruturados de um produto. Redija uma descrição atraente
        de 2 a 3 frases. Se o item estiver em estoque, convide o cliente a comprar
        imediatamente. Caso contrário, informe que o produto está indisponível para
        entrega imediata.
        """
);

// 6. Execução do Cenário
string notaBruta =
    "Cadastrar Teclado Sem Fio Pro. Conexão via 2.4GHz e Bluetooth, carregamento " +
    "USB-C rápido e bateria que dura 1 ano.";

Console.WriteLine($"[1] Entrada Bruta:\n\"{notaBruta}\"\n");

// Etapa A: Extração Estruturada (Structured Output nativo) e Execução de Ferramenta
Console.WriteLine("[2] Executando Agente Analista (OpenAI Cloud)...");

AgentResponse<EspecificacaoProduto> respostaAnalista =
    await agenteAnalista.RunAsync<EspecificacaoProduto>(notaBruta);

EspecificacaoProduto? especificacao = respostaAnalista.Result;

if (especificacao is null)
{
    Console.WriteLine("Falha ao gerar especificações estruturadas.");
    return;
}

Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("\n[3] Objeto C# Estruturado com Sucesso:");
Console.WriteLine($"   Produto: {especificacao.Nome}");
Console.WriteLine($"   Conectividade: {especificacao.Conectividade}");
Console.WriteLine($"   Carregamento: {especificacao.Carregamento}");
Console.WriteLine($"   Bateria: {especificacao.DuracaoBateria}");
Console.WriteLine($"   Em Estoque: {(especificacao.EmEstoque ? "Sim" : "Não")}");
Console.ResetColor();

// Etapa B: Redação Criativa no Modelo Local
Console.WriteLine("\n[4] Executando Agente Redator (Ollama Local)...");

string promptParaRedacao = $"""
    Produto: {especificacao.Nome}
    Conexão: {especificacao.Conectividade}
    Recarga: {especificacao.Carregamento}
    Duração Bateria: {especificacao.DuracaoBateria}
    Disponível em Estoque: {(especificacao.EmEstoque ? "Sim" : "Não")}
    """;

var respostaRedator = await agenteRedator.RunAsync(promptParaRedacao);

Console.WriteLine("\n[5] Descrição de Marketing Gerada Localmente:\n");
Console.WriteLine(respostaRedator.Text);

Habilitando o User Secrets no projeto para armazenar a chave de API

Em vez de ler a chave de uma variável de ambiente (Environment.GetEnvironmentVariable), a recomendação é que o app passa a lê-la do sistema de configuração do .NET (IConfiguration), com o User Secrets como fonte para desenvolvimento local. A chave nunca fica no código nem em nenhum arquivo do repositório.

No terminal, dentro da pasta do projeto: dotnet user-secrets init

Isso adiciona ao .csproj um <UserSecretsId> (um GUID) que vincula o projeto a um arquivo secrets.json guardado fora da pasta do projeto (em %APPDATA%\Microsoft\UserSecrets\<id>\secrets.json no Windows, ou ~/.microsoft/usersecrets/<id>/secrets.json no Linux/macOS) — por isso ele nunca é versionado por acidente.

Para guardar a chave use o comando :  dotnet user-secrets set "OpenAI:ApiKey" "sk-sua-chave-aqui"

O : cria uma chave hierárquica (OpenAI → ApiKey), que é a convenção do IConfiguration.

Para conferir o que foi salvo:  dotnet user-secrets list

A seguir basta substituir o código da fase 2 por este código:
// 2. Configuração do Cliente em Nuvem (OpenAI / gpt-4o-mini)
string chaveOpenAi = builder.Configuration["OpenAI:ApiKey"]
                              ?? throw new InvalidOperationException(
                              "A chave da API da OpenAI não foi configurada.");

IChatClient clienteOpenAi = new OpenAIClient(new ApiKeyCredential(chaveOpenAi))
                                .GetChatClient("gpt-4o-mini")
                                .AsIChatClient();

Precisamos ter certeza de que estamos no ambiente de desenvolvimento. Para isso abra um terminal na pasta do projeto e aplique os  comandos:

set DOTNET_ENVIRONMENT=Development
dotnet run

Você também pode configurar o ambiente criando no projeto uma pasta Properties e nesta pasta o arquivo launchsettings.json :
{
  "profiles": {
    "demogentehibrido": {
      "commandName": "Project",
      "environmentVariables": {
        "DOTNET_ENVIRONMENT": "Development"
      }
    }
  }
}

Agora vamos entender o código usado:

Fase 1: Registro de Serviços e Ferramentas
Registra o ServicoEstoque no contêiner de injeção de dependência como singleton.
Essa classe contém as regras de negócio de consulta de itens no inventário.
A instância será resolvida posteriormente para ser exposta como ferramenta ao agente.

Fase 2: Configuração do Cliente em Nuvem (OpenAI)
Obtém a chave de API da OpenAI a partir das variáveis de ambiente do sistema.
Instancia o OpenAIClient apontando para o modelo desejado (ex: gpt-4o-mini).
Converte o cliente para a abstração unificada IChatClient via .AsIChatClient().

Fase 3: Configuração do Cliente Local (Ollama)
Aponta para o endpoint do servidor local do Ollama (http://localhost:11434).
Define o modelo local a ser utilizado na inferência (neste caso, o llama3.2)
Instancia o OllamaApiClient, que implementa a interface IChatClient nativamente.

Fase 4: Criação do Agente Analista (Nuvem)
Transforma o método C# de verificação de estoque em ferramenta com AIFunctionFactory.
Instancia o ChatClientAgent configurado com o modelo em nuvem da OpenAI.
Atribui o papel de extração técnica rigorosa e anexa a ferramenta de consulta.

Fase 5: Criação do Agente Redator (Local)
Instancia o segundo ChatClientAgent apontando para o modelo local do Ollama.
Define instruções focadas em redação publicitária e apelo comercial de e-commerce.
Orienta o redator a destacar expressamente se o produto possui pronta entrega.

Fase 6: Execução do Cenário
Declara a string notaBruta contendo os dados desorganizados do produto informado.
Exibe a entrada textual no console para demonstrar o início do processamento.
Prepara o pipeline para acionar os agentes especializados em sequência lógica.

Etapa A: Extração Estruturada e Tool Calling
O agente da nuvem processa a nota bruta e executa a ferramenta de estoque local.
O método RunAsync<T> preenche a classe C# EspecificacaoProduto via Structured Output.
O código valida se a desserialização do objeto tipado foi concluída com sucesso.

Etapa B: Redação Criativa no Modelo Local
Monta um prompt contendo os campos já estruturados do objeto C# resultante.
Dispara a execução do agente redator local via RunAsync sem custo de token.
Exibe no console o texto final persuasivo gerado acessando a propriedade .Text.

Executando o projeto iremos obter o seguinte resultado:



5. Por que esta Abordagem é Equilibrada?


Esta arquitetura adota o meio-termo adequado para engenharia de software aplicada à IA:

Critério Abordagem Proposta
Integração C# Abstração padronizada via IChatClient
Integridade dos Dados Saída estruturada nativa do agente (RunAsync<T>), tipada em record C#
Acesso a Dados Tool Calling real com reflexão via AIFunctionFactory
Custo e Privacidade Nuvem usada estritamente onde o raciocínio é crítico; geração volumosa feita localmente
Conclusão

Projetos modernos de Inteligência Artificial no .NET não exigem a escolha arbitrária entre "apenas APIs pagas na nuvem" ou "apenas modelos locais".

A combinação do Microsoft Agent Framework com o Microsoft.Extensions.AI estabelece uma base extensível: os agentes delegam tarefas lógicas críticas a modelos adequados na nuvem e tarefas de redação e processamento intensivo a instâncias locais do Ollama.

Tudo isso mantendo a segurança de tipos, injeção de dependências nativa, saída estruturada validada pelo próprio framework e controle estrito sobre o fluxo da aplicação.

Observação - O arquivo Program.cs ficou sobrecarregado porque está acumulando quatro responsabilidades distintas: configuração de clientes de IA, instanciação direta de agentes, orquestração manual do fluxo e formatação de saídas de consola.

Para otimizar o código de acordo com as boas práticas de arquitetura em .NET, uma alternativa poderia ser:

  1. Modularizar a Injeção de Dependências: Isolar o registo dos clientes e agentes num método de extensão (ConfiguracaoIaExtensions.cs).

  2. Encapsular o Fluxo de Negócio num Serviço Orquestrador: Criar uma classe (OrquestradorCatalogo.cs) responsável pela execução da sequência entre os agentes.

  3. Reduzir o Program.cs: Manter apenas o arranque do anfitrião (Host) e a invocação do serviço principal.

E estamos conversados..

Pegue o projeto aqui:  DemoAgenteHibrido.zip e  o programa ajustado :  DemoAgenteHibrido_ajustado.zip

"Bem sei que tudo podes, e nenhum dos teus planos pode ser frustrado."
Jó 42:2
  

Referências:


José Carlos Macoratti