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:
- Cada bloco de conhecimento e dividido em chunks menores por um LLM (Claude Haiku)
- Gera um embedding (vetor de 1536 dimensoes) para cada chunk
- Quando uma mensagem chega, gera um embedding da pergunta do usuario
- Usa similaridade por cosseno para encontrar os chunks mais relevantes
- 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:
- Carrega prompt de um arquivo externo (
Prompts/chunking-prompt.md) - Substitui placeholder
{{MAX_SIZE}}com valor da configuracao - Envia o texto completo para o LLM com limite de tokens configuravel (
MaxOutputTokens) - LLM analisa, otimiza e divide o texto em chunks atomicos
- 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:
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.
Search¶
| 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¶
RewriteKnowledgeQueryStep(sub-step do pipeline de agregacao) extrai o conteudo consolidado e historico recente- Converte historico para
List<ChatMessage>usandoMessageContentFormatter(combina Content + ExtractedText) - Chama
QueryRewriteService.RewriteAsync()com mensagem atual + historico - O servico monta prompt com historico formatado e envia para o LLM
- LLM retorna query reformulada (uma linha, sem explicacoes)
- Query reescrita e armazenada em
context.RewrittenKnowledgeQuery InvokeAiServiceSteppassa a query paraAiResponseServiceque 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¶
- Admin cria bloco via CRUD
- Handler salva o bloco no banco
ChunkingService.CreateChunksAsync()usa LLM para dividir o textoIEmbeddingService.GenerateEmbeddingsAsync()gera embeddings em batch (1 chamada HTTP)- Chunks salvos com seus embeddings
- Se falhar, bloco e criado sem chunks (log warning)
Atualizacao de Bloco¶
- Admin atualiza titulo ou conteudo
- Handler detecta mudanca no conteudo (ou bloco sem chunks → backfill)
- Deleta todos os chunks antigos
- Cria novos chunks via LLM
- Gera embeddings em batch (1 chamada HTTP)
- Salva novos chunks
Delecao de Bloco¶
- Admin deleta bloco
- 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¶
- Verificar se chunks existem:
SELECT COUNT(*) FROM agent_knowledge_chunks WHERE knowledge_block_id = ... - Verificar se embeddings foram gerados:
SELECT embedding IS NOT NULL FROM agent_knowledge_chunks WHERE ... - Verificar threshold: Se muito alto (ex: 0.7), pode filtrar chunks validos
- Verificar se indice existe:
\d agent_knowledge_chunksno psql
Chunks nao estao sendo gerados¶
- Verificar se a chave OpenAI esta configurada nos providers LLM (
Llm:ProviderscomName: openai) - Verificar se a chave Anthropic esta configurada para chunking LLM
- Verificar logs por erros de API
- Verificar se o bloco foi criado (chunks sao criados apos o bloco)
Performance lenta¶
- Verificar se indice HNSW existe na tabela
agent_knowledge_chunks - Considerar ajustar
TopKpara valor menor - Verificar latencia com OpenAI API e Anthropic API
- Considerar aumentar
MaxSizese chunks forem muito pequenos
Migration¶
A migration AddKnowledgeChunks faz:
- Remove colunas
embeddingeembedding_updated_atda tabelaagent_knowledge - Remove indice HNSW da tabela
agent_knowledge - Cria tabela
agent_knowledge_chunkscom: - FK para
agent_knowledgecom CASCADE delete - Indice HNSW para busca semantica
- Indices para ordenacao e FK
Nota: Blocos existentes precisarao ter seus chunks gerados manualmente ou via backfill job.