Integrar operações de exclusão consistentes em diferentes bancos de dados vetoriais é um desafio comum no desenvolvimento de aplicações baseadas em LLM. O framework Semantic Kernel, que atua como ponte entre LLMs e aplicações, atualmente lida com esta complexidade através de sua abordagem de interfaces, mas a fragmentação persiste.
A interface IMemoryStore original concentra muitas responsabilidades, gerando divergências nas implementações para bancos como Azure AI Search, Redis e Milvus. Os problemas incluem inconsistências no nome dos métodos de exclusão, lógicas de retorno variadas e falta de tratamento padronizado de erros. Isso obriga os desenvolvedores a reescrever código adaptativo sempre que mudam de provedor de banco de dados vetorial.
Princípios de Design para Padronização
A solução proposta envolve a separação de responsabilidades. Em vez de uma interface monolítica, definem-se interfaces específicas para gerenciamento de coleções e para operações de CRUD de registros. Para as operações de exclusão, a padronização se concentra em:
- Nomenclatura Unificada: Adotar o verbo
Deletede forma consistente (ex:DeleteAsync,DeleteBatchAsync). - Retornos Determinísticos: Operações de exclusão única devem retornar um booleano (
Task<bool>). Exclusões em lote devem retornar uma enumeração assíncrona (IAsyncEnumerable<DeleteResult>), detalhando o sucesso ou falha para cada chave. - Exceções Específicas: Unificar o lançamento de uma exceção do framework (ex:
VectorStoreException) que carregue cotnexto essencial como o nome da coleção e as chaves solicitadas.
Implementação Prática da Exclusão Padronizada
Abaixo, um exemplo em C# demonstra o fluxo recomendado, incorporando verificações de segurança e tratamento de falhas parciais.
// Exemplo de exclusão única com validação prévia
async Task DeleteSingleRecordAsync(
IVectorRecordStore<MeuObjetoVetorial> store,
string recordKey,
ILogger logger)
{
// Verificação de existência é uma boa prática
bool recordExists = await store.RecordExistsAsync(recordKey);
if (!recordExists)
{
logger.LogWarning("Tentativa de excluir registro inexistente: {Chave}", recordKey);
return;
}
try
{
bool success = await store.DeleteAsync(recordKey);
if (!success)
{
// A operação pode falhar silenciosamente em alguns provedores
logger.LogError("Falha não detalhada ao excluir registro: {Chave}", recordKey);
}
}
catch (VectorStoreException ex)
{
logger.LogError(ex, "Erro de armazenamento vetorial ao excluir registro.");
throw; // Propagar para tratamento de nível superior
}
}
// Exemplo de exclusão em lote com reprocessamento de falhas
async Task BulkDeleteWithRetryAsync(
IVectorRecordStore<MeuObjetoVetorial> store,
IEnumerable<string> keys,
ILogger logger)
{
var remainingKeys = new List<string>(keys);
int maxRetries = 2;
for (int attempt = 0; attempt <= maxRetries && remainingKeys.Any(); attempt++)
{
var failedKeys = new List<string>();
await foreach (var deleteResult in store.DeleteBatchAsync(remainingKeys))
{
if (!deleteResult.Success)
{
failedKeys.Add(deleteResult.Key);
logger.LogWarning(
"Exclusão falhou para {Chave}. Motivo: {Erro}",
deleteResult.Key,
deleteResult.ErrorMessage);
}
}
if (!failedKeys.Any()) break;
remainingKeys = failedKeys;
if (attempt < maxRetries)
{
logger.LogInformation("Reprocessando {Contagem} chaves falhas...", failedKeys.Count);
await Task.Delay(100 * (attempt + 1)); // Backoff simples
}
}
if (remainingKeys.Any())
{
logger.LogError("{Contagem} chaves puderam ser excluídas após todas as tentativas.", remainingKeys.Count);
}
}
Considerações de Compatibilidade entre Provedores
É crucial entender as limitações do banco de dados alvo. A tabela a seguir resume comportamentos típicos que influenciam a estratégia de implementação:
| Provedor | Exclusão em Lote Transacional | Detalhamenot de Falhas |
|---|---|---|
| Azure AI Search | Não spuortado | Parcial (pode não retornar por chave) |
| Redis (com módulo) | Não suportado | Simples (sucesso/falha global) |
| Milvus | Suportado | Completo (rollback em falha) |
| Pinecone | Não suportado | Simples (sucesso/falha global) |
Melhores Práticas
Para operações de alto risco, como a exclusão de uma coleção inteira, implementar salvaguardas é essencial:
// Procedimento seguro para excluir uma coleção
async Task SafeDeleteCollectionAsync(
IVectorCollectionManager collectionManager,
string collectionName,
IUserContext currentUser)
{
// 1. Validar pré-condições
if (!await collectionManager.CollectionExistsAsync(collectionName))
{
throw new InvalidOperationException($"Coleção '{collectionName}' não encontrada.");
}
// 2. Validação de negócio (ex: proteger coleções do sistema)
if (collectionName.StartsWith("sistema_"))
{
throw new UnauthorizedAccessException("Coleções de sistema não podem ser excluídas.");
}
// 3. Logging de auditoria OBRIGATÓRIO antes da ação destrutiva
logger.LogCritical(
"Solicitação de exclusão da coleção '{Colecao}' pelo usuário '{Usuario}'",
collectionName,
currentUser.Id);
// 4. Execução
try
{
await collectionManager.DeleteCollectionAsync(collectionName);
}
catch (Exception ex)
{
logger.LogError(ex, "Falha crítica ao excluir a coleção '{Colecao}'", collectionName);
throw;
}
}
Para cenários com falhas parciais em bancos não-transacionais, uma estratégia de re-tentativa com backoff exponencial, como mostrada no exemplo de lote, é recomendada para melhorar a resiliência.
Direções Futuras
A evolução da padronização deve incluir funcionalidades mais avançadas, como suporte a exclusão condicional baseada em metadados (DELETE WHERE metadata.filter = ...), mecanismos de exclusão suave (soft delete) para recuperação de dados, e eventualmente, suporte a transações que envolvam múltiplas coleções, garantindo atomicidade em operações complexas.