Integração: LLM API¶
Visão Geral¶
O Ciba suporta múltiplos providers de LLM (Large Language Models) para geração de respostas de IA. Atualmente suportados:
- Anthropic (Claude): Claude Sonnet, Haiku, Opus
- OpenAI (GPT): GPT-4.1, GPT-4o-mini, GPT-5, GPT-5-mini
Configuração¶
appsettings.json¶
API keys são configuradas via appsettings ou variáveis de ambiente. Os demais valores (modelos, timeouts, etc.) são hardcoded na classe LlmConfig.
Variáveis de Ambiente (Produção)¶
Valores Hardcoded (LlmConfig.cs)¶
| Propriedade | Valor Default | Descrição |
|---|---|---|
DefaultModel |
claude-sonnet-4-5-20250929 |
Modelo padrão |
MaxTokens |
1024 |
Limite de tokens na resposta |
TimeoutSeconds |
30 |
Timeout da request |
Providers |
anthropic, openai | Lista de providers com modelos disponíveis |
PromptCachingEnabled |
true |
Habilita prompt caching na Anthropic |
PromptCacheTtl |
1h |
TTL do cache de prompt (5m ou 1h) |
Interface¶
public interface ILlmClient
{
Task<LlmResponse> GenerateResponseAsync(
string systemPrompt,
List<ChatMessage> messages,
string? modelOverride = null,
int? maxTokensOverride = null,
double? temperatureOverride = null,
CancellationToken cancellationToken = default);
Task<LlmResponse> GenerateResponseAsync(
List<SystemPromptBlock> systemBlocks,
List<ChatMessage> messages,
string? modelOverride = null,
int? maxTokensOverride = null,
double? temperatureOverride = null,
CancellationToken cancellationToken = default);
}
Seleção de Provider¶
O sistema seleciona automaticamente o provider baseado no modelo:
- Se
modelOverrideespecificado, usa o modelo solicitado - Caso contrário, usa
DefaultModel - Provider é determinado pela lista de modelos de cada provider
Modelos por Instância¶
Cada WhatsAppInstance pode ter um modelo específico configurado via AiModel. Se não especificado, usa o DefaultModel global.
var modelOverride = instance.AiModel?.ModelId;
var response = await _llm.GenerateResponseAsync(
systemPrompt,
chatMessages,
modelOverride, // null = usa DefaultModel
ct);
Anthropic (Claude)¶
Estrutura da Request (com Prompt Caching)¶
Quando PromptCachingEnabled = true, o system prompt é enviado como array de blocos com cache_control:
{
"model": "claude-sonnet-4.5",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "Regras do sistema + Agent Prompt + Behavior + Attachments + Response Format",
"cache_control": { "type": "ephemeral", "ttl": "1h" }
},
{
"type": "text",
"text": "Etapas do atendimento (se configuradas)",
"cache_control": { "type": "ephemeral", "ttl": "1h" }
},
{
"type": "text",
"text": "Base de Conhecimento (sem cache — muda a cada mensagem)"
}
],
"messages": [
{ "role": "user", "content": "Qual o horário?" },
{ "role": "assistant", "content": "Funcionamos das 8h às 18h." },
{ "role": "user", "content": "E no sábado?" }
]
}
Quando PromptCachingEnabled = false, o system prompt é enviado como string simples (concatenação de todos os blocos).
Estrutura da Response¶
{
"id": "msg_...",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "No sábado funcionamos das 9h às 14h."
}
],
"model": "claude-sonnet-4.5",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 150,
"output_tokens": 25,
"cache_creation_input_tokens": 1200,
"cache_read_input_tokens": 0
}
}
Headers HTTP¶
POST https://api.anthropic.com/v1/messages
Content-Type: application/json
x-api-key: sk-ant-...
anthropic-version: 2023-06-01
OpenAI (GPT)¶
Estrutura da Request¶
{
"model": "gpt-4.1",
"max_tokens": 1024,
"messages": [
{
"role": "system",
"content": "Você é um assistente...\n\n## Base de Conhecimento\n\n..."
},
{ "role": "user", "content": "Qual o horário?" },
{ "role": "assistant", "content": "Funcionamos das 8h às 18h." },
{ "role": "user", "content": "E no sábado?" }
]
}
Estrutura da Response¶
{
"id": "chatcmpl-...",
"object": "chat.completion",
"model": "gpt-4.1",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "No sábado funcionamos das 9h às 14h."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 150,
"completion_tokens": 25,
"total_tokens": 175
}
}
Headers HTTP¶
POST https://api.openai.com/v1/chat/completions
Content-Type: application/json
Authorization: Bearer sk-...
Montagem do Contexto (System Prompt Blocks)¶
O system prompt é montado em blocos ordenados do mais estável ao mais dinâmico, otimizando o reuso de cache prefix:
┌─────────────────────────────────────────────────┐
│ BLOCO 1 — ESTÁVEL (cache breakpoint) │
│ 1. Regras do Sistema (ESTÁTICO) │
│ 2. Agent Prompt (semi-estático) │
│ 3. Regras de Comportamento (semi-estático) │
│ 4. Anexos Disponíveis (semi-estático) │
│ 5. Response Format (ESTÁTICO) │
├─────────────────────────────────────────────────┤
│ BLOCO 2 — SEMI-VOLÁTIL (cache breakpoint) │
│ 6. Etapas do Atendimento (muda em transição) │
├─────────────────────────────────────────────────┤
│ BLOCO 3 — DINÂMICO (sem cache) │
│ 7. Base de Conhecimento (muda por mensagem) │
└─────────────────────────────────────────────────┘
// Monta blocos com marcadores de cache
var blocks = new List<SystemPromptBlock>();
blocks.Add(new SystemPromptBlock(stablePrompt, CacheControl: true));
if (stepSection != null)
blocks.Add(new SystemPromptBlock(stepSection, CacheControl: true));
if (knowledgeSection != null)
blocks.Add(new SystemPromptBlock(knowledgeSection, CacheControl: false));
// Envia para o LLM
var response = await _llm.GenerateResponseAsync(blocks, chatMessages, modelOverride, ct: ct);
Tratamento de Erros¶
try
{
var response = await _llm.GenerateResponseAsync(...);
}
catch (Exception ex)
{
_logger.LogError(ex, "LLM request failed");
// Fallback para mensagem de erro do agente
return agent.ErrorMessage ?? "Desculpe, tente novamente.";
}
Métricas e Custos¶
O tracking de tokens é feito via LlmUsage (entidade dedicada), incluindo tokens de cache.
Operações Rastreadas¶
LlmOperationType |
Provider/Modelo | Descrição |
|---|---|---|
Chat |
Configurável por instância | Resposta principal da IA |
Vision |
Configurável por instância | Descrição de imagens |
Transcription |
OpenAI (whisper) | Transcrição de áudio |
Embedding |
OpenAI (text-embedding-3-small) | Geração de embeddings para busca vetorial |
Chunking |
Configurável (KnowledgeRetrieval) | Divisão de blocos de conhecimento em chunks |
QueryRewrite |
OpenAI (gpt-4o-mini) | Reformulação de mensagens anafóricas em queries standalone |
Playground |
Configurável por instância | Teste de prompts no playground |
_usageTracker.TrackChat(agentId, inputTokens, outputTokens,
messageId, conversationId, modelOverride,
cacheCreationInputTokens, cacheReadInputTokens);
Cálculo de Custo com Cache¶
| Tipo de Token | Multiplicador |
|---|---|
| Input tokens (sem cache) | 1x preço base |
| Output tokens | 1x preço base |
| Cache write (5 min TTL) | 1.25x preço input |
| Cache write (1 hora TTL) | 2x preço input |
| Cache read | 0.1x preço input |
Logs estruturados:
_logger.LogDebug(
"Claude API response: {Chars} chars | Input: {InputTokens}, Output: {OutputTokens}, CacheCreation: {CacheCreation}, CacheRead: {CacheRead}",
generatedText.Length, inputTokens, outputTokens, cacheCreation, cacheRead);
Timeout e Retry¶
- Timeout: 30 segundos (configurável via
TimeoutSeconds) - Retry: 2 retries com backoff exponencial (Polly)
- Fallback: Usa
ErrorMessagedo agente
Configuração de Retry¶
// Retry policy com Polly
.WaitAndRetryAsync(
retryCount: 2,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
Boas Práticas¶
- System prompt estável - Não alterar frequentemente (maximiza cache hit)
- Busca semântica - Usar blocos relevantes em vez de todos
- Histórico limitado - 20 mensagens evita estourar limite de contexto
- Monitorar tokens - Acompanhar custos via
LlmUsagee dashboard - Testar no playground - Validar prompts antes de produção
- Escolher modelo apropriado - Haiku/mini para casos simples, Sonnet/GPT-4 para complexos
- Prompt caching - Ordenar blocos do mais estável ao mais dinâmico para maximizar cache prefix