DEV Community

Marcos Vinicius
Marcos Vinicius

Posted on

Do Zero ao Grafo Multiagente em .NET com Microsoft Agent Framework e DuckDB

Muito se fala em Python quando o tema é Inteligência Artificial Generativa. No entanto, quando entramos no território de sistemas corporativos, tipagem estrita, alta performance, governança e microsserviços, o ecossistema .NET (C#) oferece uma base arquitetural sólida e madura.

Neste artigo, vamos analisar a engenharia por trás do DecisionFlow.cs, o núcleo inteligente do projeto Olist.Agents. Trata-se de um projeto acadêmico desenvolvido para a entrega do Tech Challenge da pós-graduação, modelado para demonstrar maturidade de produção e padrões arquiteturais reais com IA agêntica.

Você vai aprender:

  1. Como abstrair modelos de linguagem usando Microsoft.Extensions.AI.
  2. O que é um AIAgent e por que ele precisa de ferramentas (tools).
  3. Por que usamos Executors se já instanciamos os agentes.
  4. Como o agente Cadu implementa RAG Estruturado sobre 100 mil registros reais.
  5. Como orquestrar um fluxo em grafo com bifurcações e supervisão humana (Human-in-the-Loop).
  6. Por que usamos RequestPort para receber decisões humanas sem travar threads.

1. O Alicerce: Desacoplamento com Microsoft.Extensions.AI

Muitos desenvolvedores acoplam suas aplicações diretamente a SDKs proprietários. No .NET moderno, a Microsoft padronizou a interação com modelos através da interface unificada IChatClient.

No trecho inicial do DecisionFlow.cs, instanciamos o cliente do Google Gemini, mas o encapsulamos sob essa abstração:

IChatClient chatClient = new Client(
        apiKey: _googleAIConfigs.ApiKey,
        httpOptions: new HttpOptions
        {
            RetryOptions = new HttpRetryOptions { Attempts = 5, InitialDelay = 2, MaxDelay = 30 }
        })
    .AsIChatClient(_googleAIConfigs.Model);
Enter fullscreen mode Exit fullscreen mode

Por que isso é importante?

  • Desacoplamento de Provedor: O fluxo de trabalho não se importa se o motor de inferência é o Gemini, Azure OpenAI, Claude ou um modelo local rodando via Ollama.
  • Resiliência Nativa: O HttpRetryOptions trata erros de rede transitórios, códigos 503 (sobrecarga) e 429 (limites de requisição por minuto) com backoff exponencial configurado.

2. Ferramentas e Protocolo Aberto: Model Context Protocol (MCP)

Antes de instanciar os agentes, o código inicializa um cliente Model Context Protocol (MCP):

await using McpClient learnMcpClient = await McpClient
    .CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
    {
        Endpoint = new Uri(_mcpServerConfig.McpServerUrl),
        Name = "Microsoft Learn MCP",
    }));

IList<AITool> researcherTools = [.. (await learnMcpClient.ListToolsAsync()).Cast<AITool>()];
Enter fullscreen mode Exit fullscreen mode

O MCP padroniza como modelos de IA descobrem e acionam ferramentas externas via JSON-RPC/HTTP. No .NET, essas ferramentas são convertidas diretamente para instâncias de AITool, ficando disponíveis para consumo agêntico sem SDKs intermediários.


3. O que é um AIAgent e Como Criá-lo

Um AIAgent no Microsoft Agent Framework combina três pilares:

  1. Identidade / Nome: Como ele é identificado no fluxo.
  2. Instruções (System Prompt): Regras de negócio e limites de atuação.
  3. Ferramentas (Tools): Métodos que o modelo pode acionar sob demanda via function calling.
AIAgent cadu = chatClient
    .AsAIAgent(
        name: "Cadu",
        instructions: AgentHelper.ReadablePromptAgent("Agents/Prompts/Cadu.md"),
        tools: billing ? _sqlConsultTool.GetCaduTools() : []
    );
Enter fullscreen mode Exit fullscreen mode

Boas Práticas: Os prompts não ficam hardcoded em classes C#. Eles residem em arquivos .md externos, permitindo versionamento, refinamento e manutenção sem necessidade de recompilar a aplicação.


4. Por que Usar Executors se Já Temos os AIAgent?

Uma dúvida frequente de quem está iniciando no framework é: "Se eu já criei a instância do AIAgent, por que preciso de classes como SantiExecutor, LeoExecutor ou CaduExecutor?"

A resposta está no Princípio da Responsabilidade Única (SRP) e na arquitetura interna de workflows em grafo:

  • AIAgent (O Cérebro / Raciocínio): Cuida exclusivamente da cognição com a LLM — interpreta o prompt, executa ferramentas e devolve texto ou JSON. Ele não conhece o grafo, não sabe quem o antecedeu e não sabe para onde enviar o resultado.
  • Executor (O Nó Operacional do Grafo): É o nó de infraestrutura que herda do framework (como Executor<TInput, TOutput>). Suas responsabilidades são:
    1. Adaptação de Contratos: Recebe os dados de entrada do nó anterior e formata o contexto para o agente.
    2. Chamada e Desserialização: Executa o AIAgent e converte a resposta em texto/JSON para um objeto C# fortemente tipado (SantiTriagem, LeoTriagem, CaduTriagem).
    3. Regras de Negócio Determinísticas: Se o modelo falhar, alucinar ou retornar dados incompletos, o Executor aplica validações defensivas e fallbacks antes de propagar o estado.
    4. Telemetria: Emite o ExecutorInvokedEvent, permitindo que o painel de controle acompanhe a execução em tempo real.
SantiExecutor santiExecutor = new(santi);
LeoExecutor leoExecutor = new(leo);
CaduExecutor caduExecutor = new(cadu);
Enter fullscreen mode Exit fullscreen mode

5. RAG no Mundo Real: Como o Cadu Audita o Catálogo

Muitos tutoriais resumem RAG (Retrieval-Augmented Generation) a dividir arquivos PDF em blocos (chunks) e buscar similaridade vetorial. No mercado corporativo, RAG também opera sobre dados estruturados de missão crítica.

O Cenário do Cadu (Catalog Guard)

O Cadu audita inconsistências em anúncios de produtos sobre o dataset público da Olist (mais de 100 mil registros de pedidos e produtos).

  • O Desafio: Como um modelo de linguagem pode validar se as dimensões, o peso ou o histórico de um anúncio são coerentes sem estourar o limite de tokens da janela de contexto?
  • A Solução (RAG Estruturado / Text-to-SQL in-process):
    1. O nó do Cadu recebe a identificação do anúncio (ProductId).
    2. O agente decide acionar a sua ferramenta: _sqlConsultTool.GetCaduTools().
    3. A ferramenta executa consultas analíticas via DuckDB + Dapper diretamente sobre os arquivos .csv locais (tabelas products, order_items, sellers), sem a complexidade de subir um banco de dados relacional dedicado.
    4. Os dados factuais retornam da base (Retrieval) e são incorporados ao contexto do Cadu.
    5. O agente sintetiza a sua avaliação determinando se o anúncio está conforme (Augmented Generation).

Essa abordagem garante custo reduzido, zero alucinação com dados numéricos e altíssima velocidade de auditoria.


6. O Grafo de Decisão (WorkflowBuilder)

Para conectar todos os nós de forma auditável e paralela, utilizamos o WorkflowBuilder:

Workflow workflow = new WorkflowBuilder(orchestrator)
    .WithName("OlistDecisionFlow")
    .AddFanOutEdge(orchestrator, [santiExecutor, leoExecutor, caduExecutor])
    .AddSwitch(santiExecutor, s => s
        .AddCase<SantiTriagem>(t => t?.Gravidade == Gravidade.Moderada, [santiAuto])
        .WithDefault([analistaCX]))
    .AddSwitch(leoExecutor, s => s
        .AddCase<LeoTriagem>(t => t?.Risco == RiscoAtraso.Baixo, [leoAuto])
        .WithDefault([analistaLogistica]))
    .AddSwitch(caduExecutor, s => s
        .AddCase<CaduTriagem>(t => t?.Parecer == ParecerCatalogo.Conforme, [caduAuto])
        .WithDefault([analistaCatalogo]))
    .AddEdge(analistaCX, humanGate)
    .AddEdge(analistaLogistica, humanGate)
    .AddEdge(analistaCatalogo, humanGate)
    .WithOutputFrom(santiAuto, leoAuto, caduAuto, humanGate)
    .Build();
Enter fullscreen mode Exit fullscreen mode

Entendendo a Topologia:

  • orchestrator: Consulta a base DuckDB e extrai os casos pendentes de triagem.
  • AddFanOutEdge: Dispara a análise em paralelo para os três agentes especialistas (Santi, Léo e Cadu).
  • AddSwitch: Bifurcação condicional com tipagem estrita do C#. Se o parecer do Cadu for Conforme, ele desvia para caduAuto (ação automática). Se houver inconformidade, segue para a porta do analista.
flowchart TD
  Orquestrador --> Santi & Leo[Léo] & Cadu
  Santi -- moderada --> SantiAuto[Ação automática]
  Santi -- grave --> AnalistaCX
  Leo -- risco baixo --> LeoAuto[Ação automática]
  Leo -- risco alto --> AnalistaLogistica
  Cadu -- conforme --> CaduAuto[Ação automática]
  Cadu -- não conforme --> AnalistaCatalogo
  AnalistaCX & AnalistaLogistica & AnalistaCatalogo --> HumanGate[Decisão humana]

7. Por que Usar RequestPort se o Agente Já Devolve um Objeto C#?

Quando o agente conclui a inferência, ele devolve um objeto tipado (como CaduTriagem). Então, por que declarar um RequestPort?

RequestPort<CaduTriagem, bool> analistaCatalogo = RequestPort.Create<CaduTriagem, bool>("AnalistaCatalogo");
Enter fullscreen mode Exit fullscreen mode

Se tentássemos recolher o input humano com um simples Console.ReadLine() dentro da execução de um nó, bloquearíamos a thread do runtime, impedindo o paralelismo do AddFanOutEdge e travando a fila de streaming de eventos.

O RequestPort<TRequest, TResponse> implementa o padrão Human-in-the-Loop assíncrono:

  1. Pausa Controlada de Estado: Ao receber o caso não conforme, o ramo específico do workflow é suspenso.
  2. Emissão de Evento Desacoplada: O workflow emite um RequestInfoEvent contendo o payload CaduTriagem.
  3. Recolha Não Bloqueante: A interface de apresentação (neste projeto, um terminal visual com Spectre.Console) exibe os detalhes para o operador humano.
  4. Retomada com SendResponseAsync: Assim que o analista confirma a sua decisão (true ou false), o valor é enviado de volta e o workflow retoma a sua execução imediatamente:
case RequestInfoEvent requestEvt when requestEvt.Request.TryGetDataAs(out CaduTriagem caduTriagem):
    bool decisao = dashboard.AskAnalyst(caduTriagem);
    await run.SendResponseAsync(requestEvt.Request.CreateResponse(decisao));
    break;
Enter fullscreen mode Exit fullscreen mode

8. Execução Orientada a Eventos

A inicialização e o consumo do grafo acontecem em modo streaming através do método InProcessExecution.RunStreamingAsync:

await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, scanRequest);
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        case ExecutorInvokedEvent invokedEvt:
            // Notifica o início de processamento no dashboard
            break;

        case WorkflowOutputEvent { Data: CaduTriagem caduAutomatico }:
            dashboard.AutoResolved("Cadu", caduAutomatico.ProductId, "o anúncio foi publicado na loja");
            break;

        case ExecutorFailedEvent failedEvt:
            dashboard.Error($"Falha no executor '{failedEvt.ExecutorId}': {failedEvt.Data}");
            break;
    }

    dashboard.Render();
}
Enter fullscreen mode Exit fullscreen mode

Essa estrutura transforma a aplicação num sistema reativo, auditável e resiliente, onde a interface de usuário permanece desacoplada da mecânica do grafo.


Conclusão

Projetos de conclusão de curso e provas de conceito ganham muito em relevância quando combinam fundamentação teórica com padrões de engenharia de software corporativa. O ecossistema .NET entrega tipagem rigorosa, ciclo de vida com injeção de dependência e arquitetura agêntica estruturada com o Microsoft Agent Framework.

Para conferir a implementação completa, incluindo prompts, diagramas e configuração de ambiente, acesse o repositório:

🔗 Repositório no GitHub: https://github.com/marcosvnl/Workflow-MAF-DOTNET

Como você tem estruturado sistemas de IA agêntica em .NET? Compartilhe suas experiências e dúvidas nos comentários!

Top comments (0)