Pular para conteúdo

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.

{
  "Llm": {
    "AnthropicApiKey": "sk-ant-...",
    "OpenAiApiKey": "sk-..."
  }
}

Variáveis de Ambiente (Produção)

- Llm__AnthropicApiKey=${ANTHROPIC_API_KEY}
- Llm__OpenAiApiKey=${OPENAI_API_KEY}

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:

  1. Se modelOverride especificado, usa o modelo solicitado
  2. Caso contrário, usa DefaultModel
  3. Provider é determinado pela lista de modelos de cada provider
// Exemplo: "claude-haiku-4.5" → Anthropic provider
// Exemplo: "gpt-4.1" → OpenAI 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 ErrorMessage do agente

Configuração de Retry

// Retry policy com Polly
.WaitAndRetryAsync(
    retryCount: 2,
    sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

Boas Práticas

  1. System prompt estável - Não alterar frequentemente (maximiza cache hit)
  2. Busca semântica - Usar blocos relevantes em vez de todos
  3. Histórico limitado - 20 mensagens evita estourar limite de contexto
  4. Monitorar tokens - Acompanhar custos via LlmUsage e dashboard
  5. Testar no playground - Validar prompts antes de produção
  6. Escolher modelo apropriado - Haiku/mini para casos simples, Sonnet/GPT-4 para complexos
  7. Prompt caching - Ordenar blocos do mais estável ao mais dinâmico para maximizar cache prefix