A maioria dos times de desenvolvimento que conheço ainda usa LLMs da mesma forma que usa o Google: abrem o ChatGPT, colam o problema, copiam a resposta. Funciona para tarefas isoladas. Não funciona para integrar inteligência dentro de um produto.
Integrar um LLM de verdade — com streaming, tool calling, gestão de contexto e controle de custo — é uma habilidade técnica distinta. Este artigo cobre do zero ao avançado para times .NET que querem parar de usar IA como calculadora e começar a usá-la como componente de sistema.
Vamos cobrir os três principais provedores: Anthropic (Claude), OpenAI (GPT-4) e Google (Gemini) — com exemplos reais em C# para cada um.
Por que integrar LLMs via SDK, não via UI
Quando você integra um LLM diretamente na sua aplicação, você ganha:
- Contexto do sistema: injete dados do usuário, histórico, preferências e regras de negócio no prompt automaticamente
- Fluxo controlado: o LLM vira um componente como qualquer outro — com retry, timeout, fallback e observabilidade
- Tool calling: o modelo pode chamar funções do seu sistema (buscar dados, executar ações) de forma estruturada
- Custo gerenciável: você controla tokens por request, escolhe o modelo certo por tarefa e implementa cache
Comparativo rápido dos provedores
| Provider | Modelo top | Ponto forte | SDK .NET |
|---|---|---|---|
| Anthropic | Claude Opus 4 | Raciocínio complexo, contexto longo (200k tokens), seguir instruções precisas | Pacote oficial Anthropic |
| OpenAI | GPT-4o | Ecossistema maduro, multimodal, fine-tuning disponível | Pacote oficial OpenAI |
| Gemini 2.5 Pro | Contexto 1M tokens, grounding com Google Search, custo menor em volume | Google.Cloud.AIPlatform ou REST |
Para a maioria dos projetos empresariais no Brasil, a recomendação é começar com Claude para tarefas de raciocínio e análise e GPT para geração de conteúdo e multimodal. Gemini faz sentido quando você já está na stack Google Cloud ou precisa do contexto de 1M tokens.
Setup — Instalando os SDKs
# Anthropic SDK
dotnet add package Anthropic
# OpenAI SDK oficial
dotnet add package OpenAI
# Para Azure OpenAI
dotnet add package Azure.AI.OpenAI
No appsettings.json:
{
"LLM": {
"AnthropicApiKey": "",
"OpenAiApiKey": "",
"GeminiApiKey": ""
}
}
Sempre use variáveis de ambiente ou Azure Key Vault em produção — nunca coloque as chaves diretamente no appsettings.
Chamada básica — os três provedores
Claude (Anthropic SDK)
using Anthropic;
var client = new AnthropicClient(apiKey);
var response = await client.Messages.CreateAsync(new MessageCreateParams
{
Model = "claude-opus-4-7",
MaxTokens = 1024,
Messages = [
new() { Role = "user", Content = "Analise este contrato e liste as cláusulas de risco." }
]
});
var text = response.Content[0].Text;
GPT-4 (OpenAI SDK)
using OpenAI.Chat;
var client = new ChatClient("gpt-4o", apiKey);
var completion = await client.CompleteChatAsync(
new UserChatMessage("Analise este contrato e liste as cláusulas de risco.")
);
var text = completion.Value.Content[0].Text;
Gemini (via REST com HttpClient)
var request = new
{
contents = new[]
{
new { role = "user", parts = new[] { new { text = "Analise este contrato e liste as cláusulas de risco." } } }
}
};
var response = await httpClient.PostAsJsonAsync(
$"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={apiKey}",
request
);
var result = await response.Content.ReadFromJsonAsync<GeminiResponse>();
Streaming de resposta
Para aplicações com interface de usuário, streaming é obrigatório. Usuários abandonam interfaces que ficam congeladas por 5-10 segundos esperando a resposta completa. Com streaming, o texto aparece progressivamente — a UX muda completamente.
Streaming com Claude
await foreach (var ev in client.Messages.CreateStreamingAsync(new MessageCreateParams
{
Model = "claude-sonnet-4-6",
MaxTokens = 2048,
Messages = [new() { Role = "user", Content = prompt }]
}))
{
if (ev is ContentBlockDeltaEvent delta && delta.Delta is TextDelta textDelta)
{
// Envie para o cliente via SignalR, SSE ou yield return
await hubContext.Clients.User(userId).SendAsync("token", textDelta.Text);
}
}
Streaming com OpenAI
await foreach (var update in client.CompleteChatStreamingAsync(messages))
{
foreach (var part in update.ContentUpdate)
{
await hubContext.Clients.User(userId).SendAsync("token", part.Text);
}
}
Para SSE (Server-Sent Events) em Minimal APIs, o padrão é retornar IAsyncEnumerable<string> com Content-Type: text/event-stream. Funciona muito bem com React no frontend usando a API EventSource.
Tool Calling — o recurso que muda tudo
Tool calling (ou function calling) é o que transforma um LLM em um agente. Em vez de apenas gerar texto, o modelo pode sinalizar que quer chamar uma função da sua aplicação — e você decide se executa ou não.
Casos de uso reais:
- Chatbot de atendimento que consulta o banco de dados do cliente em tempo real
- Assistente de análise que busca dados do ERP antes de gerar o relatório
- Agente de onboarding que cria registros no sistema conforme o usuário responde
Exemplo com Claude
var tools = new List<Tool>
{
new()
{
Name = "buscar_pedido",
Description = "Busca informações de um pedido pelo número. Use quando o usuário perguntar sobre status de pedido.",
InputSchema = new InputSchema
{
Type = "object",
Properties = new Dictionary<string, PropertyDefinition>
{
["numero_pedido"] = new() { Type = "string", Description = "Número do pedido (formato: PED-XXXXX)" }
},
Required = ["numero_pedido"]
}
}
};
var response = await client.Messages.CreateAsync(new MessageCreateParams
{
Model = "claude-sonnet-4-6",
MaxTokens = 1024,
Tools = tools,
Messages = [new() { Role = "user", Content = "Qual o status do meu pedido PED-00123?" }]
});
// Verificar se o modelo quer usar uma tool
if (response.StopReason == "tool_use")
{
var toolUse = response.Content.OfType<ToolUseContent>().First();
var numeroPedido = toolUse.Input["numero_pedido"].GetString();
// Executar a função real
var pedido = await pedidoService.BuscarAsync(numeroPedido);
// Continuar a conversa com o resultado
var followUp = await client.Messages.CreateAsync(new MessageCreateParams
{
Model = "claude-sonnet-4-6",
MaxTokens = 1024,
Tools = tools,
Messages = [
new() { Role = "user", Content = "Qual o status do meu pedido PED-00123?" },
new() { Role = "assistant", Content = response.Content },
new() { Role = "user", Content = new List<ContentBlock>
{
new ToolResultContent
{
ToolUseId = toolUse.Id,
Content = JsonSerializer.Serialize(pedido)
}
}}
]
});
}
Prompt Caching — reduzindo custo em até 90%
Se você tem um system prompt grande (instruções do sistema, documentos de contexto, exemplos few-shot), pode estar pagando por ele toda vez que faz uma chamada. O Claude suporta prompt caching nativo: a primeira chamada com um bloco marcado como cacheável é mais cara, mas as subsequentes custam 10% do preço normal.
var response = await client.Messages.CreateAsync(new MessageCreateParams
{
Model = "claude-sonnet-4-6",
MaxTokens = 1024,
System = new List<SystemPrompt>
{
new()
{
Text = instrucoesSistema, // 5.000 tokens de contexto
CacheControl = new() { Type = "ephemeral" } // Marcar para cache
}
},
Messages = [new() { Role = "user", Content = perguntaDoUsuario }]
});
Em aplicações com muitas chamadas por usuário (chatbots, assistentes de suporte), o cache pode reduzir o custo de tokens em 60-90%. A latência da primeira chamada aumenta um pouco; das subsequentes, diminui.
A OpenAI tem prompt caching automático para conversas longas (sem marcação explícita). O Google tem caching explícito via API separada.
Saída estruturada — JSON mode
Para integrar LLMs com sistemas reais, você precisa de saída previsível. Pedir "retorne um JSON com os campos X, Y, Z" no prompt funciona, mas é frágil. O modo estruturado garante que o JSON sempre valide contra um schema.
Com OpenAI (modo mais robusto)
[Description("Análise de sentimento de texto")]
class SentimentoAnalise
{
[JsonPropertyName("sentimento")]
[Description("Sentimento: positivo, negativo ou neutro")]
public string Sentimento { get; init; } = "";
[JsonPropertyName("confianca")]
[Description("Score de confiança de 0 a 1")]
public float Confianca { get; init; }
[JsonPropertyName("resumo")]
public string Resumo { get; init; } = "";
}
var completion = await chatClient.CompleteChatAsync(
messages,
new ChatCompletionOptions
{
ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(
"sentimento_analise",
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(
JsonSchemaExporter.GetJsonSchemaAsNode(typeof(SentimentoAnalise))
))
)
}
);
var analise = JsonSerializer.Deserialize<SentimentoAnalise>(completion.Value.Content[0].Text);
O Claude tem suporte a tool use para saída estruturada (declare uma tool que representa seu schema e instrua o modelo a "usar" essa tool). Mais verbose, mas muito confiável.
Gerenciamento de erros e retry
APIs de LLM falham. Rate limits, timeouts, erros 500 ocasionais — tudo isso vai acontecer em produção. Use Polly (ou Microsoft.Extensions.Resilience no .NET 8+) para criar pipelines de resiliência:
services.AddHttpClient<ILlmService, ClaudeLlmService>()
.AddResilienceHandler("llm-pipeline", builder =>
{
builder.AddRetry(new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = args => args.Outcome switch
{
{ Exception: HttpRequestException } => PredicateResult.True(),
{ Result.StatusCode: HttpStatusCode.TooManyRequests } => PredicateResult.True(),
{ Result.StatusCode: HttpStatusCode.ServiceUnavailable } => PredicateResult.True(),
_ => PredicateResult.False()
}
});
builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(30),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(60)
});
builder.AddTimeout(TimeSpan.FromSeconds(30));
});
Observabilidade — rastreando tokens e custo
Sem métricas, você vai receber uma fatura inesperada de API no fim do mês. Instrumente suas chamadas com OpenTelemetry:
public class InstrumentedLlmService : ILlmService
{
private readonly ILlmService _inner;
private readonly Meter _meter;
private readonly Counter<long> _tokenCounter;
private readonly Histogram<double> _latencyHistogram;
public InstrumentedLlmService(ILlmService inner)
{
_inner = inner;
_meter = new Meter("Neryx.LLM");
_tokenCounter = _meter.CreateCounter<long>("llm.tokens.total", "tokens");
_latencyHistogram = _meter.CreateHistogram<double>("llm.request.duration", "ms");
}
public async Task<LlmResponse> CompleteAsync(LlmRequest request)
{
var sw = Stopwatch.StartNew();
try
{
var response = await _inner.CompleteAsync(request);
_tokenCounter.Add(response.InputTokens, new("type", "input"), new("model", request.Model));
_tokenCounter.Add(response.OutputTokens, new("type", "output"), new("model", request.Model));
return response;
}
finally
{
_latencyHistogram.Record(sw.Elapsed.TotalMilliseconds, new("model", request.Model));
}
}
}
Com isso você consegue dashboards no Grafana mostrando tokens por usuário, custo estimado por feature, latência por modelo e taxa de erro — tudo que você precisa para tomar decisões de custo vs qualidade.
Quando usar qual modelo
A regra prática que eu uso:
- Claude Sonnet — tarefas de produção do dia a dia: classificação, extração, sumarização, geração com instrução precisa. Melhor custo-benefício para volume
- Claude Opus — raciocínio complexo, análise de documentos longos, decisões que precisam de profundidade. Use com parcimônia
- GPT-4o — análise de imagem, integração com ferramentas do ecossistema OpenAI (Assistants, Files, Vision), fine-tuning
- Gemini Flash — alto volume, baixo custo, quando a tarefa é simples e a integração com Google Workspace faz sentido
- Claude Haiku / GPT-4o-mini — classificação, triagem, tarefas ultra-simples onde custo é crítico
Uma arquitetura comum em produção: Haiku para triagem inicial (classifica a intenção do usuário, ~0.001 USD por request), Sonnet para resposta principal (~0.01 USD), Opus apenas para casos escalados que precisam de análise profunda (~0.15 USD). O custo médio fica bem próximo do Haiku porque a maioria das requests para no primeiro modelo.
Próximos passos
Integração básica é a parte fácil. O que separa implementações amadoras de robustas em produção é:
- Gestão de contexto de conversa: armazenar e recuperar histórico com window sliding para não explodir o context window
- RAG (Retrieval-Augmented Generation): integrar vector search para o modelo consultar documentos específicos da empresa
- Avaliação de outputs: definir como você vai medir se o LLM está respondendo bem — antes de ir pra produção
- Guardrails: validação de input e output para evitar jailbreaks, informações confidenciais no contexto e alucinações em campos críticos
Se você está construindo algo com LLMs na stack .NET e quer uma revisão de arquitetura antes de escalar, é exatamente o tipo de projeto que trabalhamos na Neryx — da integração inicial ao pipeline de LLMOps em produção.