Pular para conteúdo

LLM Input Safety

Limites de segurança para controlar o tamanho e custo dos inputs enviados ao LLM, aplicados em múltiplas camadas.

Visão Geral

O sistema implementa limites de segurança em três camadas:

  1. AppConfig -- configuração centralizada dos limites globais
  2. ChatHistoryTrimmer -- truncamento do histórico antes de enviar ao LLM
  3. PlaygroundChatValidator -- validação de input do usuário via FluentValidation

Esses limites protegem contra: - Tokens excessivos (custo elevado) - Context window overflow - Abuse/spam via mensagens longas - Timeouts em transcrição de áudio


Arquivos Principais

Arquivo Descrição
src/Ciba.Infrastructure/Configuration/AppConfig.cs Limites configuráveis (Options pattern)
src/Ciba.Infrastructure/Services/Llm/ChatHistoryTrimmer.cs Truncamento de histórico para LLM
src/Ciba.Shared/Features/Agents/Playground/PlaygroundChatValidator.cs Validação de input do Playground

Limites Configuráveis (AppConfig)

Seção "App" no appsettings.json, registrada via Options pattern.

Histórico de Mensagens

Propriedade Default Descrição
MaxHistoryMessages 20 Número máximo de mensagens no contexto do LLM
MaxCharsPerMessage 5.000 Caracteres máximos por mensagem individual
MaxHistoryTotalChars 30.000 Caracteres máximos do histórico inteiro
DefaultTemperature 0.6 Temperatura padrão para geração de respostas

Extração de Mídia

Propriedade Default Descrição
MaxMediaExtractionsPerBatch 5 Itens de mídia com extração AI (OCR/transcrição) por batch de agregação
MaxAudioBytesForTranscription 2 MB (2.097.152 bytes) Tamanho máximo de áudio para transcrição Whisper

Nota: Mídias além do limite de extração são salvas no storage, mas sem OCR/transcrição. Áudios maiores que 2 MB são salvos mas não transcritos. A prioridade é dada aos itens mais recentes.

Storage

Propriedade Default Descrição
MaxAttachmentFileSizeBytes 16 MB Tamanho máximo por arquivo de anexo
MaxAttachmentStorageBytesPerAgent 500 MB Quota de storage por agente

ChatHistoryTrimmer

Classe estática que trunca o histórico de mensagens em duas etapas antes do envio ao LLM.

Algoritmo

Entrada: lista de ChatMessage + maxCharsPerMessage + maxTotalChars
    ├── Step 1: Truncar cada mensagem individualmente
    │   └── message.Content[..maxCharsPerMessage] se exceder limite
    └── Step 2: Aplicar budget total
        └── Percorre do mais recente ao mais antigo
            └── Mantém mensagens enquanto cabem no budget
            └── Remove as mais antigas primeiro (break ao estourar)

Código

public static List<ChatMessage> Trim(
    List<ChatMessage> messages,
    int maxCharsPerMessage,
    int maxTotalChars)
{
    // Step 1: Truncar cada mensagem individualmente
    var trimmed = messages
        .Select(m => TrimMessage(m, maxCharsPerMessage))
        .ToList();

    // Step 2: Aplicar budget total (remove mais antigas primeiro)
    return ApplyTotalBudget(trimmed, maxTotalChars);
}

Comportamento

Cenário Resultado
Mensagem com 8.000 chars Truncada para 5.000 chars
Histórico com 50.000 chars total Mensagens mais antigas removidas até caber em 30.000
25 mensagens no histórico Limitado a 20 mensagens (via MaxHistoryMessages no caller)
Mensagem vazia/null Preservada sem modificação

Importante: O ChatHistoryTrimmer não aplica o limite de MaxHistoryMessages -- este é aplicado pelo caller (ex: AiResponseService) ao montar a lista de mensagens. O trimmer recebe a lista já limitada em quantidade e aplica os limites de caracteres.


Playground Validator

Validação de input do usuário no Playground via FluentValidation, aplicada antes do handler.

public class PlaygroundChatValidator : AbstractValidator<PlaygroundChatRequest>
{
    private const int MaxMessages = 20;
    private const int MaxContentChars = 5_000;

    public PlaygroundChatValidator()
    {
        RuleFor(x => x.Messages)
            .NotEmpty().WithMessage("Mensagens são obrigatórias")
            .Must(m => m.Count <= MaxMessages)
                .WithMessage($"Máximo de {MaxMessages} mensagens permitidas");

        RuleForEach(x => x.Messages).ChildRules(msg =>
        {
            msg.RuleFor(m => m.Content)
                .MaximumLength(MaxContentChars)
                .WithMessage($"Conteúdo da mensagem deve ter no máximo {MaxContentChars} caracteres");
        });
    }
}
Regra Limite Mensagem de Erro
Mensagens obrigatórias > 0 "Mensagens são obrigatórias"
Máximo de mensagens 20 "Máximo de 20 mensagens permitidas"
Tamanho por mensagem 5.000 chars "Conteúdo da mensagem deve ter no máximo 5000 caracteres"

Camadas de Proteção

Requisição do Usuário
    ├── [1] PlaygroundChatValidator (FluentValidation)
    │       └── Rejeita antes de executar qualquer lógica
    ├── [2] MaxHistoryMessages (aplicado no handler)
    │       └── Limita quantidade de mensagens carregadas do banco
    ├── [3] ChatHistoryTrimmer (preparação para LLM)
    │       ├── Trunca cada mensagem a 5k chars
    │       └── Remove mensagens antigas se total > 30k chars
    ├── [4] MaxMediaExtractionsPerBatch (pipeline de agregação)
    │       └── Limita extrações AI por batch (prioriza mais recentes)
    └── [5] MaxAudioBytesForTranscription (pipeline de mídia)
            └── Áudios > 2MB salvos mas não transcritos

Configuração via appsettings.json

{
  "App": {
    "MaxHistoryMessages": 20,
    "MaxCharsPerMessage": 5000,
    "MaxHistoryTotalChars": 30000,
    "DefaultTemperature": 0.6,
    "MaxMediaExtractionsPerBatch": 5,
    "MaxAudioBytesForTranscription": 2097152,
    "MaxAttachmentFileSizeBytes": 16777216,
    "MaxAttachmentStorageBytesPerAgent": 524288000
  }
}

Regras

  1. Limites são aplicados apenas na camada LLM -- mensagens são salvas integralmente no banco de dados
  2. Budget total tem prioridade sobre mensagens individuais -- mesmo truncadas, mensagens antigas podem ser removidas
  3. Mensagens mais recentes são preservadas -- o trimmer remove do mais antigo para o mais recente
  4. Playground valida antes do handler -- rejeição rápida via FluentValidation (HTTP 400)
  5. Valores default são conservadores -- 30k chars total equivale a aproximadamente 7.500 tokens
  6. Mídias além do limite de extração são preservadas -- salvas no storage, apenas sem OCR/transcrição