Pular para conteúdo

Integracao: LLM API

Visao Geral

O Ciba suporta multiplos providers de LLM (Large Language Models) para geracao de respostas de IA. Atualmente suportados:

  • Anthropic (Claude): claude-haiku-4-5-20251001, claude-sonnet-4-5-20250929, claude-opus-4-5-20251101
  • OpenAI (GPT): gpt-4.1, gpt-4.1-mini, gpt-4o-mini, gpt-5, gpt-5-mini

Configuracao

appsettings.json

Apenas as API keys sao configuraveis via appsettings ou variaveis de ambiente. Os demais valores (modelos, timeouts, etc.) sao hardcoded na classe LlmConfig.

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

Nota: Nao existe secao Llm:Providers no appsettings. Os providers e modelos sao definidos como static readonly na classe LlmConfig.

Variaveis de Ambiente (Producao)

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

Valores Hardcoded (LlmConfig.cs)

Propriedade Valor Default Descricao
DefaultModel claude-sonnet-4-5-20250929 Modelo padrao
MaxTokens 1024 Limite de tokens na resposta
TimeoutSeconds 30 Timeout da request
ImageDescriptionMaxTokens 256 Max tokens para descricao de imagem (Vision)
Providers anthropic, openai Lista de providers com modelos disponiveis
PromptCachingEnabled true Habilita prompt caching na Anthropic
PromptCacheTtl 1h TTL do cache de prompt (apenas 1 hora e utilizado)

Modelos Disponiveis

public static readonly IReadOnlyList<LlmProviderConfig> Providers =
[
    new() { Name = "anthropic", Models = ["claude-haiku-4-5-20251001", "claude-sonnet-4-5-20250929", "claude-opus-4-5-20251101"] },
    new() { Name = "openai", Models = ["gpt-4.1", "gpt-4.1-mini", "gpt-4o-mini", "gpt-5", "gpt-5-mini"] }
];

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);
}

Selecao de Provider

O sistema seleciona automaticamente o provider baseado no modelo:

  1. Se modelOverride especificado, usa o modelo solicitado
  2. Caso contrario, usa DefaultModel
  3. Provider e determinado pela lista de modelos de cada provider
// Exemplo: "claude-haiku-4-5-20251001" -> Anthropic provider
// Exemplo: "gpt-4.1" -> OpenAI provider

Selecao de Modelo via Intent Classifier

O modelo utilizado na resposta e determinado pelo intent classifier (classificador de intencao). O pipeline de processamento classifica a intencao da mensagem e pode definir um ClassifiedModelOverride:

// No pipeline de agregacao/playground/deferred:
context.ClassifiedModelOverride = IntentClassifierConfig.Model; // ex: claude-haiku-4-5-20251001

// Na chamada ao LLM:
var modelOverride = context.ClassifiedModelOverride; // null = usa DefaultModel

Se o intent classifier identificar FaqSimple, usa o modelo mais leve (Haiku). Caso contrario, usa o DefaultModel (Sonnet).

ClassifiedModelOverride ?? LlmConfig.DefaultModel

Anthropic (Claude)

Estrutura da Request (com Prompt Caching)

Quando PromptCachingEnabled = true, o system prompt e enviado como array de blocos com cache_control:

{
  "model": "claude-sonnet-4-5-20250929",
  "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 horario?" },
    { "role": "assistant", "content": "Funcionamos das 8h as 18h." },
    { "role": "user", "content": "E no sabado?" }
  ]
}

Nota sobre o Agent Prompt: O sistema usa agent.Prompt (nao agent.SystemPrompt ou OptimizedSystemPrompt).

Quando PromptCachingEnabled = false, o system prompt e enviado como string simples (concatenacao de todos os blocos).

Estrutura da Response

{
  "id": "msg_...",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "No sabado funcionamos das 9h as 14h."
    }
  ],
  "model": "claude-sonnet-4-5-20250929",
  "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

Nota: O OpenAI client usa max_completion_tokens (nao max_tokens), conforme a API mais recente da OpenAI:

{
  "model": "gpt-4.1",
  "max_completion_tokens": 1024,
  "messages": [
    {
      "role": "system",
      "content": "Voce e um assistente...\n\n## Base de Conhecimento\n\n..."
    },
    { "role": "user", "content": "Qual o horario?" },
    { "role": "assistant", "content": "Funcionamos das 8h as 18h." },
    { "role": "user", "content": "E no sabado?" }
  ]
}

Estrutura da Response

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "gpt-4.1",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "No sabado funcionamos das 9h as 14h."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 150,
    "completion_tokens": 25,
    "total_tokens": 175
  }
}

Nota: O OpenAiClient retorna LlmResponse com CacheCreationInputTokens = 0 e CacheReadInputTokens = 0 (defaults), pois a OpenAI nao suporta prompt caching nativo como a Anthropic.

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 e montado em blocos ordenados do mais estavel ao mais dinamico, otimizando o reuso de cache prefix:

┌─────────────────────────────────────────────────┐
│  BLOCO 1 -- ESTAVEL (cache breakpoint)           │
│  1. Regras do Sistema (ESTATICO)                │
│  2. Agent Prompt (semi-estatico)                │
│  3. Regras de Comportamento (semi-estatico)     │
│  4. Anexos Disponiveis (semi-estatico)          │
│  5. Response Format (ESTATICO)                  │
├─────────────────────────────────────────────────┤
│  BLOCO 2 -- SEMI-VOLATIL (cache breakpoint)      │
│  6. Etapas do Atendimento (muda em transicao)   │
├─────────────────────────────────────────────────┤
│  BLOCO 3 -- DINAMICO (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.";
}

Metricas e Custos

O tracking de tokens e feito via LlmUsage (entidade dedicada), incluindo tokens de cache.

Operacoes Rastreadas

LlmOperationType Provider/Modelo Descricao
Chat Determinado por ClassifiedModelOverride Resposta principal da IA
Vision Determinado por ClassifiedModelOverride Descricao de imagens
Transcription OpenAI (whisper-1) Transcricao de audio
Embedding OpenAI (text-embedding-3-small) Geracao de embeddings para busca vetorial
Chunking OpenAI (gpt-4.1) Divisao de blocos de conhecimento em chunks
QueryRewrite OpenAI (gpt-4o-mini) Reformulacao de mensagens anaforicas em queries standalone
Playground Determinado por ClassifiedModelOverride Teste de prompts no playground
AiAssistant Configuravel Assistente de IA para sugestoes ao atendente
IntentClassification Configuravel Classificacao de intencao da mensagem

Tracking via ILlmUsageTracker

public interface ILlmUsageTracker
{
    void Track(LlmOperationType type, Guid agentId, int inputTokens, int outputTokens = 0,
               int cacheCreationInputTokens = 0, int cacheReadInputTokens = 0,
               string? modelOverride = null, object? metadata = null);
}

Exemplo de uso:

_usageTracker.Track(
    LlmOperationType.Chat,
    agentId,
    response.InputTokens,
    response.OutputTokens,
    response.CacheCreationInputTokens,
    response.CacheReadInputTokens,
    modelOverride: context.ClassifiedModelOverride,
    metadata: new ChatUsageMetadata("aggregation"));

Metadata tipada por operacao:

Operacao Record de Metadata
Chat ChatUsageMetadata(Pipeline)
Vision VisionUsageMetadata(MimeType)
Transcription TranscriptionUsageMetadata(DurationSeconds, Source)
Embedding EmbeddingUsageMetadata(Source, KnowledgeBlockId?, FaqItemId?)
Chunking ChunkingUsageMetadata(KnowledgeBlockId)
AiAssistant AiAssistantUsageMetadata(SectionKey)
IntentClassification IntentClassificationUsageMetadata(ClassifiedIntent, Confidence)

Calculo de Custo com Cache

Tipo de Token Multiplicador
Input tokens (sem cache) 1x preco base
Output tokens 1x preco base
Cache write (1 hora TTL) 2x preco input
Cache read 0.1x preco input

Nota: Apenas o TTL de 1 hora (PromptCacheTtl = "1h") e utilizado.

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 (configuravel via TimeoutSeconds)
  • Retry: 2 retries com backoff exponencial (Polly)
  • Fallback: Usa ErrorMessage do agente

Configuracao de Retry

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

Boas Praticas

  1. System prompt estavel - Nao alterar frequentemente (maximiza cache hit)
  2. Busca semantica - Usar blocos relevantes em vez de todos
  3. Historico 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 producao
  6. Escolher modelo apropriado - Haiku/mini para casos simples, Sonnet/GPT-4 para complexos
  7. Prompt caching - Ordenar blocos do mais estavel ao mais dinamico para maximizar cache prefix