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.
Nota: Nao existe secao Llm:Providers no appsettings. Os providers e modelos sao definidos como static readonly na classe LlmConfig.
Variaveis de Ambiente (Producao)¶
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:
- Se
modelOverrideespecificado, usa o modelo solicitado - Caso contrario, usa
DefaultModel - 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).
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
ErrorMessagedo agente
Configuracao de Retry¶
// Retry policy com Polly
.WaitAndRetryAsync(
retryCount: 2,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
Boas Praticas¶
- System prompt estavel - Nao alterar frequentemente (maximiza cache hit)
- Busca semantica - Usar blocos relevantes em vez de todos
- Historico limitado - 20 mensagens evita estourar limite de contexto
- Monitorar tokens - Acompanhar custos via
LlmUsagee dashboard - Testar no playground - Validar prompts antes de producao
- Escolher modelo apropriado - Haiku/mini para casos simples, Sonnet/GPT-4 para complexos
- Prompt caching - Ordenar blocos do mais estavel ao mais dinamico para maximizar cache prefix