Pular para conteúdo

Knowledge Retrieval com pgvector

Visao Geral

O Ciba utiliza pgvector para busca semantica na base de conhecimento dos agentes. O sistema implementa chunking inteligente via LLM para reduzir consumo de tokens:

  1. Cada bloco de conhecimento e dividido em chunks menores por um LLM (Claude Haiku)
  2. Gera um embedding (vetor de 1536 dimensoes) para cada chunk
  3. Quando uma mensagem chega, gera um embedding da pergunta do usuario
  4. Usa similaridade por cosseno para encontrar os chunks mais relevantes
  5. Inclui apenas esses chunks no contexto do Claude

Resultado: Reducao de ~60-80% no consumo de tokens mantendo (ou melhorando) a qualidade das respostas.

Arquitetura

┌─────────────────────────────────────────────────────────────────┐
│                     Knowledge Block (pai)                        │
│                                                                  │
│  ┌──────────┐    ┌─────────────────────────────────────────────┐│
│  │  Title   │    │              Content (texto completo)       ││
│  └──────────┘    └─────────────────────────────────────────────┘│
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
                              ↓ ChunkingService (LLM)
┌─────────────────────────────────────────────────────────────────┐
│                     Knowledge Chunks (filhos)                    │
│                                                                  │
│  ┌────────────────────┐  ┌────────────────────┐  ┌───────────┐ │
│  │ Chunk 0            │  │ Chunk 1            │  │ Chunk N   │ │
│  │ content + embedding│  │ content + embedding│  │ ...       │ │
│  └────────────────────┘  └────────────────────┘  └───────────┘ │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                     PostgreSQL + pgvector                        │
│                                                                  │
│  • Tabela: agent_knowledge_chunks                               │
│  • Coluna: embedding vector(1536)                               │
│  • Indice: HNSW (Hierarchical Navigable Small World)            │
│  • Operador: vector_cosine_ops (similaridade por cosseno)       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                    Busca Semantica                               │
│                                                                  │
│  1. Usuario envia mensagem                                       │
│  2. QueryRewriteService reformula em query standalone (LLM)      │
│     Ex: "E o primeiro" → "Qual e o primeiro melhor refrigerante?"│
│  3. Gera embedding da query reescrita                            │
│  4. SELECT title, content FROM chunks                            │
│     JOIN knowledge ON chunks.knowledge_block_id = knowledge.id   │
│     WHERE knowledge.agent_id = $1 AND knowledge.is_active = true │
│     ORDER BY embedding <=> $query_embedding                      │
│     LIMIT 5                                                      │
│  5. Retorna chunks mais similares formatados como "## Title\n..."│
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Modelo de Dados

AgentKnowledge (bloco pai)

public class AgentKnowledge
{
    public Guid Id { get; set; }
    public Guid AgentId { get; set; }
    public required string Title { get; set; }
    public required string Content { get; set; }  // Texto completo editavel
    public int DisplayOrder { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }

    // Navigation
    public Agent Agent { get; set; } = null!;
    public List<AgentKnowledgeChunk> Chunks { get; set; } = [];
}

AgentKnowledgeChunk (chunk filho)

public class AgentKnowledgeChunk
{
    public Guid Id { get; set; }
    public Guid KnowledgeBlockId { get; set; }
    public int ChunkIndex { get; set; }           // Ordem do chunk
    public required string Content { get; set; }  // Fragmento do texto
    public Vector? Embedding { get; set; }        // Vetor 1536 dimensoes
    public DateTime? EmbeddingUpdatedAt { get; set; }
    public DateTime CreatedAt { get; set; }

    // Navigation
    public AgentKnowledge KnowledgeBlock { get; set; } = null!;
}

Componentes

1. Modelo de Embedding

Modelo: text-embedding-3-small (OpenAI) - Dimensoes: 1536 - Limite de tokens: 8.191 (~25.000 caracteres) - Custo: $0.02 / 1M tokens

2. Servicos

Servico Responsabilidade
IEmbeddingService Gera embeddings via OpenAI API
IChunkingService Divide blocos em chunks usando LLM
IKnowledgeRetrievalService Busca chunks relevantes por similaridade
IQueryRewriteService Reescreve mensagens anaforicas em queries standalone via LLM

3. ChunkingService (LLM)

O ChunkingService usa um LLM (Claude Haiku) para dividir e otimizar o texto:

  1. Carrega prompt de um arquivo externo (Prompts/chunking-prompt.md)
  2. Substitui placeholder {{MAX_SIZE}} com valor da configuracao
  3. Envia o texto completo para o LLM com limite de tokens configuravel (MaxOutputTokens)
  4. LLM analisa, otimiza e divide o texto em chunks atomicos
  5. Retorna chunks compactos otimizados para busca semantica

O que o LLM faz: - Remove ruido: redundancias, introducoes genericas, transicoes desnecessarias - Compacta: reescreve de forma concisa mantendo 100% do significado - Atomiza: cada chunk responde UMA pergunta especifica - Normaliza: padroniza termos e formatos (datas, valores, etc.) - Lista: converte paragrafos densos em listas quando possivel

Vantagens: - Chunks menores e mais precisos para busca semantica - Melhor match entre pergunta do usuario e chunk relevante - Reducao de tokens consumidos no contexto do Claude - Fallback automatico para chunk unico em caso de falha

Custo: ~$0.25/1M tokens de entrada + ~$1.25/1M tokens de saida (Claude Haiku)

public interface IChunkingService
{
    Task<List<AgentKnowledgeChunk>> CreateChunksAsync(
        AgentKnowledge block,
        ILlmClient llmClient,
        CancellationToken ct = default);
}

4. Prompt Externo

O prompt de chunking e armazenado como embedded resource em:

src/Ciba.Infrastructure/Prompts/chunking-prompt.md

Isso permite facil manutencao do prompt sem recompilar o codigo.

Placeholder disponivel: - {{MAX_SIZE}} - Limite maximo de caracteres por chunk

Regras principais do prompt:

Regra Descricao
Remover ruido Redundancias, introducoes, transicoes
Compactar Reescrever de forma concisa
Atomizar Um chunk = uma pergunta
Normalizar Padronizar termos e formatos
Listar Converter paragrafos em listas

Exemplo de transformacao:

Entrada:
"Bem-vindo! Aceitamos Pix com 10% de desconto, cartao em ate 3x..."

Saida:
["Pagamento:\n- Pix: 10% desconto\n- Cartao: ate 3x sem juros", ...]

Configuracao

Estrutura Unificada

A configuracao de Knowledge Retrieval foi unificada em uma unica secao com sub-objetos:

{
  "KnowledgeRetrieval": {
    "Embedding": {
      "Provider": "openai",
      "Model": "text-embedding-3-small",
      "Dimensions": 1536,
      "MaxTokens": 8191
    },

    "Chunking": {
      "Provider": "anthropic",
      "Model": "claude-haiku-4-5-20251001",
      "MaxSize": 4000,
      "MaxOutputTokens": 8192
    },

    "Search": {
      "TopK": 5,
      "SimilarityThreshold": 0.3
    },

    "QueryRewrite": {
      "Enabled": true,
      "Provider": "openai",
      "Model": "gpt-4o-mini",
      "MaxOutputTokens": 256,
      "MaxHistoryMessages": 6
    }
  }
}

Nota: As API keys sao obtidas da secao Llm:Providers: - Embedding: Provider com Name igual a Embedding.Provider (ex: openai) - Chunking: Provider com Name igual a Chunking.Provider (ex: anthropic)

Opcoes de Configuracao

Embedding

Opcao Descricao Default
Provider Nome do provider em Llm.Providers openai
Model Modelo de embedding text-embedding-3-small
Dimensions Dimensoes do vetor 1536
MaxTokens Limite de tokens por request 8191

Chunking

Opcao Descricao Default
Provider Nome do provider em Llm.Providers anthropic
Model Modelo LLM para chunking claude-haiku-4-5-20251001
MaxSize Limite maximo de caracteres por chunk (safety net) 4000
MaxOutputTokens Limite de tokens na resposta do LLM (evita truncamento) 8192

Nota: O LLM decide semanticamente o tamanho ideal de cada chunk. O MaxSize e apenas um limite de seguranca para evitar chunks gigantes. O MaxOutputTokens deve ser aumentado para blocos de conhecimento muito grandes.

Opcao Descricao Default
TopK Numero de chunks a retornar 5
SimilarityThreshold Threshold minimo de similaridade (0-1) 0.3

QueryRewrite

Opcao Descricao Default
Enabled Habilita/desabilita query rewriting true
Provider Nome do provider em Llm.Providers openai
Model Modelo LLM para rewriting gpt-4o-mini
MaxOutputTokens Limite de tokens na resposta 256
MaxHistoryMessages Numero maximo de mensagens recentes para contexto 6

Query Rewriting

Problema

Quando o usuario envia mensagens anaforicas como "E o primeiro", "E no domingo?", "Quanto custa esse?", a busca vetorial nao encontra resultados relevantes porque a query nao contem contexto suficiente para gerar um embedding significativo.

Solucao

O QueryRewriteService usa um LLM leve (gpt-4o-mini) para reformular a mensagem do usuario em uma query standalone antes da busca vetorial:

Usuario: "Segundo melhor refrigerante do mundo"
IA: "Coca-Cola"
Usuario: "E o primeiro"
                ↓ QueryRewriteService
Query reescrita: "Qual e o primeiro melhor refrigerante do mundo?"
                ↓ EmbeddingService + pgvector
Chunks relevantes encontrados

Fluxo

  1. RewriteKnowledgeQueryStep (sub-step do pipeline de agregacao) extrai o conteudo consolidado e historico recente
  2. Converte historico para List<ChatMessage> usando MessageContentFormatter (combina Content + ExtractedText)
  3. Chama QueryRewriteService.RewriteAsync() com mensagem atual + historico
  4. O servico monta prompt com historico formatado e envia para o LLM
  5. LLM retorna query reformulada (uma linha, sem explicacoes)
  6. Query reescrita e armazenada em context.RewrittenKnowledgeQuery
  7. InvokeAiServiceStep passa a query para AiResponseService que a usa na busca vetorial

Prompt

O prompt de query rewriting esta em src/Ciba.Infrastructure/Prompts/query-rewrite-prompt.md (embedded resource).

Regras do prompt: - Resolver referencias anaforicas usando historico - Output deve ser standalone (compreensivel sem contexto) - Preservar idioma original - Nao inventar informacao - Se ja for standalone, retornar como esta - Retornar apenas a query (uma linha, sem explicacoes)

Fallback

Situacao Comportamento
Query rewrite desabilitado Usa mensagem original
Mensagem vazia Usa mensagem original
LLM retorna resposta vazia Usa mensagem original
Erro na chamada LLM Usa mensagem original (log error)

Tracking

Tokens consumidos sao rastreados via LlmUsageTracker com tipo LlmOperationType.QueryRewrite.

Fluxo de Criacao/Atualizacao

Criacao de Bloco

  1. Admin cria bloco via CRUD
  2. Handler salva o bloco no banco
  3. ChunkingService.CreateChunksAsync() usa LLM para dividir o texto
  4. IEmbeddingService.GenerateEmbeddingsAsync() gera embeddings em batch (1 chamada HTTP)
  5. Chunks salvos com seus embeddings
  6. Se falhar, bloco e criado sem chunks (log warning)

Atualizacao de Bloco

  1. Admin atualiza titulo ou conteudo
  2. Handler detecta mudanca no conteudo (ou bloco sem chunks → backfill)
  3. Deleta todos os chunks antigos
  4. Cria novos chunks via LLM
  5. Gera embeddings em batch (1 chamada HTTP)
  6. Salva novos chunks

Delecao de Bloco

  1. Admin deleta bloco
  2. Chunks sao deletados automaticamente via CASCADE

Indice HNSW

Por que HNSW? - 15.5x mais rapido que IVFFlat em queries - Resiliente a atualizacoes frequentes - Pode ser criado em tabela vazia - Recall de 0.998

Configuracao na Migration:

builder.HasIndex(x => x.Embedding)
    .HasDatabaseName("idx_chunks_embedding")
    .HasMethod("hnsw")
    .HasOperators("vector_cosine_ops");

Fallback

O sistema nunca faz fallback para toda a base de conhecimento quando a busca semantica nao encontra resultados. Isso garante que o agente so responda com informacoes relevantes.

Situacao Comportamento
Query vazia Retorna vazio (sem conhecimento)
Nenhum chunk com embedding Retorna vazio (sem conhecimento)
Nenhum chunk passa do threshold Retorna vazio (sem conhecimento)
Erro na busca semantica Retorna vazio (sem conhecimento)
Erro na geracao de embedding Log warning, bloco criado sem chunks
Erro no LLM de chunking Cria chunk unico com conteudo completo

Validacoes

Para garantir que embeddings funcionem corretamente:

  • MaximumLength(25000) nos validators de titulo e conteudo
  • Texto truncado silenciosamente se ultrapassar 8k tokens
  • Warning logado quando truncamento ocorre

Monitoramento

Logs Importantes

// Busca semantica bem-sucedida
Semantic search for agent {AgentId}: found {ChunkCount} relevant chunks (threshold: {Threshold})

// Nenhum resultado encontrado (sem fallback)
No results matched similarity threshold for agent {AgentId}. Returning empty knowledge (no fallback).

// Chunks criados
Knowledge block '{Title}' created with {ChunkCount} chunks for Agent {AgentId}

// Chunks atualizados
Knowledge block '{Title}' updated with {ChunkCount} chunks for Agent {AgentId}

// LLM chunking tokens
LLM chunking used {InputTokens} input tokens and {OutputTokens} output tokens

Metricas Recomendadas

  • Tempo de geracao de embedding (P50, P95, P99)
  • Taxa de hit do threshold de similaridade
  • Chunks retornados por busca (media)
  • Taxa de buscas sem resultado (zero matches)
  • Numero medio de chunks por bloco
  • Tokens consumidos no chunking LLM

Troubleshooting

Busca nao retorna chunks relevantes

  1. Verificar se chunks existem: SELECT COUNT(*) FROM agent_knowledge_chunks WHERE knowledge_block_id = ...
  2. Verificar se embeddings foram gerados: SELECT embedding IS NOT NULL FROM agent_knowledge_chunks WHERE ...
  3. Verificar threshold: Se muito alto (ex: 0.7), pode filtrar chunks validos
  4. Verificar se indice existe: \d agent_knowledge_chunks no psql

Chunks nao estao sendo gerados

  1. Verificar se a chave OpenAI esta configurada nos providers LLM (Llm:Providers com Name: openai)
  2. Verificar se a chave Anthropic esta configurada para chunking LLM
  3. Verificar logs por erros de API
  4. Verificar se o bloco foi criado (chunks sao criados apos o bloco)

Performance lenta

  1. Verificar se indice HNSW existe na tabela agent_knowledge_chunks
  2. Considerar ajustar TopK para valor menor
  3. Verificar latencia com OpenAI API e Anthropic API
  4. Considerar aumentar MaxSize se chunks forem muito pequenos

Migration

A migration AddKnowledgeChunks faz:

  1. Remove colunas embedding e embedding_updated_at da tabela agent_knowledge
  2. Remove indice HNSW da tabela agent_knowledge
  3. Cria tabela agent_knowledge_chunks com:
  4. FK para agent_knowledge com CASCADE delete
  5. Indice HNSW para busca semantica
  6. Indices para ordenacao e FK

Nota: Blocos existentes precisarao ter seus chunks gerados manualmente ou via backfill job.