Início Tecnologia Por que sua conta de LLM está explodindo – e como o...

Por que sua conta de LLM está explodindo – e como o cache semântico pode reduzi-la em 73%

14
0

Nossa fatura de API LLM estava crescendo 30% mês a mês. O tráfego estava aumentando, mas não tão rápido. Quando analisei nossos logs de consulta, descobri o verdadeiro problema: os usuários fazem as mesmas perguntas de maneiras diferentes.

“Qual é a sua política de devolução?”, “Como faço para devolver algo?” e “Posso obter um reembolso?” todos acessamos nosso LLM separadamente, gerando respostas quase idênticas, cada uma incorrendo em custos totais de API.

O cache de correspondência exata, a primeira solução óbvia, capturou apenas 18% dessas chamadas redundantes. A mesma questão semântica, formulada de forma diferente, contornou totalmente o cache.

Então, implementei o cache semântico com base no significado das consultas, não em como elas são redigidas. Após implementá-lo, nossa taxa de acertos de cache aumentou para 67%, reduzindo os custos da API LLM em 73%. Mas chegar lá requer resolver problemas que as implementações ingênuas deixam passar.

Por que o cache de correspondência exata é insuficiente

O cache tradicional usa texto de consulta como chave de cache. Isso funciona quando as consultas são idênticas:

# Cache de correspondência exata

cache_key = hash(query_text)

se cache_key estiver no cache:

cache de retorno[cache_key]

Mas os usuários não formulam perguntas de forma idêntica. Minha análise de 100.000 consultas de produção encontrou:

  • Apenas 18% eram duplicatas exatas de consultas anteriores

  • 47% eram semanticamente semelhantes às consultas anteriores (mesma intenção, redação diferente)

  • 35% eram consultas genuinamente novas

Esses 47% representavam enormes economias de custos que estávamos perdendo. Cada consulta semanticamente semelhante acionou uma chamada LLM completa, gerando uma resposta quase idêntica àquela que já havíamos computado.

Arquitetura de cache semântico

O cache semântico substitui chaves baseadas em texto por pesquisa de similaridade baseada em incorporação:

classe SemanticCache:

def __init__(self, embedding_model, similarity_threshold=0,92):

self.embedding_model = embedding_model

self.threshold = similaridade_limite

self.vector_store = VectorStore() # FAISS, Pinha, and so forth.

self.response_store = ResponseStore() # Redis, DynamoDB, and so forth.

def get(self, question: str) -> Opcional[str]:

“””Retorna resposta em cache se existir uma consulta semanticamente semelhante.”””

query_embedding = self.embedding_model.encode(consulta)

# Encontre a consulta em cache mais semelhante

correspondências = self.vector_store.search(query_embedding, top_k=1)

se correspondências e correspondências[0].similaridade >= self.threshold:

cache_id = corresponde[0].eu ia

retornar self.response_store.get(cache_id)

retornar Nenhum

def set(self, consulta: str, resposta: str):

“””Par consulta-resposta do cache.”””

query_embedding = self.embedding_model.encode(consulta)

cache_id = gerar_id()

self.vector_store.add(cache_id, query_embedding)

self.response_store.set(cache_id, {

‘consulta’: consulta,

‘resposta’: resposta,

‘carimbo de information e hora’: datetime.utcnow()

})

O principal perception: em vez de fazer hash do texto da consulta, incorporo consultas no espaço vetorial e encontro consultas em cache dentro de um limite de similaridade.

O problema do limite

O limite de similaridade é o parâmetro crítico. Defina um valor muito alto e você perderá acessos de cache válidos. Defina um valor muito baixo e você retornará respostas erradas.

Nosso limite inicial de 0,85 parecia razoável; 85% semelhante deveria ser “a mesma pergunta”, certo?

Errado. Em 0,85, obtivemos ocorrências de cache como:

São perguntas diferentes com respostas diferentes. Retornar a resposta em cache seria incorreto.

Descobri que os limites ideais variam de acordo com o tipo de consulta:

Tipo de consulta

Limite excellent

Justificativa

Perguntas do tipo FAQ

0,94

É necessária alta precisão; respostas erradas prejudicam a confiança

Pesquisas de produtos

0,88

Mais tolerância para quase correspondências

Consultas de suporte

0,92

Equilíbrio entre cobertura e precisão

Consultas transacionais

0,97

Tolerância muito baixa a erros

Implementei limites específicos do tipo de consulta:

classe AdaptiveSemanticCache:

def __init__(self):

self.thresholds = {

‘perguntas frequentes’: 0,94,

‘pesquisa’: 0,88,

‘suporte’: 0,92,

‘transacional’: 0,97,

‘padrão’: 0,92

}

self.query_classifier = QueryClassifier()

def get_threshold(self, consulta: str) -> float:

query_type = self.query_classifier.classify(consulta)

retornar self.thresholds.get(query_type, self.thresholds[‘default’])

def get(self, question: str) -> Opcional[str]:

limite = self.get_threshold (consulta)

query_embedding = self.embedding_model.encode(consulta)

correspondências = self.vector_store.search(query_embedding, top_k=1)

se correspondências e correspondências[0].similaridade >= limite:

retornar self.response_store.get(corresponde[0].eu ia)

retornar Nenhum

Metodologia de ajuste de limite

Não consegui ajustar os limites cegamente. Eu precisava de informações básicas sobre quais pares de consultas eram realmente “iguais”.

Nossa metodologia:

Etapa 1: Exemplos de pares de consultas. Amostramos 5.000 pares de consultas em vários níveis de similaridade (0,80-0,99).

Etapa 2: Rotulagem humana. Os anotadores rotularam cada par como “mesma intenção” ou “intenção diferente.” Usei três anotadores por par e obtive uma votação majoritária.

Etapa 3: Calcule curvas de precisão/recall. Para cada limite, calculamos:

  • Precisão: Dos acessos ao cache, qual fração teve a mesma intenção?

  • Lembre-se: de pares com a mesma intenção, que fração atingimos em cache?

def computar_precision_recall(pares, rótulos, limite):

“””Calcular precisão e recall em determinado limite de similaridade.”””

previsões = [1 if pair.similarity >= threshold else 0 for pair in pairs]

true_positivos = soma (1 para p, l em zip (previsões, rótulos) se p == 1 e l == 1)

falsos_positivos = soma (1 para p, l em zip (previsões, rótulos) se p == 1 e l == 0)

falsos_negativos = soma (1 para p, l em zip (previsões, rótulos) se p == 0 e l == 1)

precisão = verdadeiros_positivos / (verdadeiros_positivos + falsos_positivos) if (verdadeiros_positivos + falsos_positivos) > 0 senão 0

recordar = verdadeiros_positivos / (verdadeiros_positivos + falsos_negativos) if (verdadeiros_positivos + falsos_negativos) > 0 senão 0

precisão de retorno, recall

Etapa 4: Selecione o limite com base no custo dos erros. Para consultas de FAQ em que respostas erradas prejudicam a confiança, otimizei a precisão (o limite de 0,94 deu 98% de precisão). Para consultas de pesquisa em que perder um acerto de cache custa apenas dinheiro, otimizei para recall (limite de 0,88).

Sobrecarga de latência

O cache semântico adiciona latência: você deve incorporar a consulta e pesquisar o armazenamento de vetores antes de saber se deve chamar o LLM.

Nossas medidas:

Operação

Latência (p50)

Latência (p99)

Incorporação de consulta

12ms

28ms

Pesquisa vetorial

8ms

19ms

Pesquisa whole de cache

20ms

47ms

Chamada de API LLM

850ms

2.400ms

A sobrecarga de 20 ms é insignificante em comparação com a chamada LLM de 850 ms que evitamos em ocorrências de cache. Mesmo no p99, a sobrecarga de 47ms é aceitável.

No entanto, as perdas de cache agora demoram 20 ms a mais do que antes (incorporação + pesquisa + chamada LLM). Com nossa taxa de acerto de 67%, a matemática funciona favoravelmente:

Melhoria da latência líquida de 65% juntamente com redução de custos.

Invalidação de cache

As respostas armazenadas em cache ficam obsoletas. Mudanças nas informações do produto, atualizações de políticas e a resposta correta de ontem torna-se a resposta errada de hoje.

Implementei três estratégias de invalidação:

  1. TTL baseado em tempo

Expiração simples com base no tipo de conteúdo:

TTL_BY_CONTENT_TYPE = {

‘preço’: timedelta(horas=4), #Muda frequentemente

‘política’: timedelta(dias=7), #Muda raramente

‘informações_do_produto’: timedelta(dias=1), # Atualização diária

‘general_faq’: timedelta(dias=14), # Muito estável

}

  1. Invalidação baseada em eventos

Quando os dados subjacentes forem alterados, invalide as entradas de cache relacionadas:

classe CacheInvalidator:

def on_content_update(self, content_id: str, content_type: str):

“””Invalidar entradas de cache relacionadas ao conteúdo atualizado.”””

# Encontre consultas em cache que referenciaram este conteúdo

consultas_afetadas = self.find_queries_reference(content_id)

para query_id em consultas_afetadas:

self.cache.invalidate(query_id)

self.log_invalidation(content_id, len(affected_queries))

  1. Detecção de obsolescência

Para respostas que podem ficar obsoletas sem eventos explícitos, implementei verificações periódicas de atualização:

def check_freshness(self, cached_response: dict) -> bool:

“””Verifique se a resposta em cache ainda é válida.”””

# Execute novamente a consulta nos dados atuais

resposta_fresca = self.generate_response(cached_response[‘query’])

# Examine a semelhança semântica das respostas

cached_embedding = self.embed(cached_response[‘response’])

fresco_embedding = self.embed(fresh_response)

similaridade = cosseno_similaridade(cached_embedding, Fresh_embedding)

# Se as respostas divergirem significativamente, invalide

se similaridade < 0,90:

self.cache.invalidate(cached_response[‘id’])

retornar falso

retornar verdadeiro

Executamos verificações de atualização em uma amostra de entradas em cache diariamente, detectando a desatualização que o TTL e a invalidação baseada em eventos deixam passar.

Resultados de produção

Após três meses de produção:

Métrica

Antes

Depois

Mudar

Taxa de acerto do cache

18%

67%

+272%

Custos da API LLM

US$ 47 mil/mês

US$ 12,7 mil/mês

-73%

Latência média

850ms

300ms

-65%

Taxa de falso-positivo

N / D

0,8%

Reclamações de clientes (respostas erradas)

Linha de base

+0,3%

Aumento mínimo

A taxa de falsos positivos de 0,8% (consultas nas quais retornamos uma resposta em cache que period semanticamente incorreta) estava dentro dos limites aceitáveis. Esses casos ocorreram principalmente nos limites do nosso limite, onde a similaridade estava brand acima do ponto de corte, mas a intenção diferia ligeiramente.

Armadilhas a evitar

Não use um único limite world. Diferentes tipos de consulta têm diferentes tolerâncias a erros. Ajuste os limites por categoria.

Não pule a etapa de incorporação em ocorrências de cache. Você pode ficar tentado a ignorar a sobrecarga de incorporação ao retornar respostas em cache, mas precisa da incorporação para geração de chave de cache. A sobrecarga é inevitável.

Não se esqueça da invalidação. O cache semântico sem estratégia de invalidação leva a respostas obsoletas que prejudicam a confiança do usuário. Crie invalidação desde o primeiro dia.

Não armazene tudo em cache. Algumas consultas não devem ser armazenadas em cache: respostas personalizadas, informações urgentes, confirmações transacionais. Crie regras de exclusão.

def deveria_cache(self, consulta: str, resposta: str) -> bool:

“””Determinar se a resposta deve ser armazenada em cache.””

# Não armazene em cache respostas personalizadas

se self.contains_personal_info(resposta):

retornar falso

# Não armazene em cache informações urgentes

se self.is_time_sensitive(consulta):

retornar falso

# Não armazene em cache as confirmações transacionais

se self.is_transactional(consulta):

retornar falso

retornar verdadeiro

Principais conclusões

O cache semântico é um padrão prático para controle de custos LLM que captura falhas de cache de correspondência exata de redundância. Os principais desafios são o ajuste de limites (usar limites específicos do tipo de consulta com base na análise de precisão/recall) e invalidação de cache (combinar TTL, detecção baseada em eventos e detecção de inatividade).

Com uma redução de custos de 73%, esta foi a nossa otimização de maior ROI para sistemas LLM de produção. A complexidade da implementação é moderada, mas o ajuste do limite requer atenção cuidadosa para evitar degradação da qualidade.

Sreenivasa Reddy Hulebeedu Reddy é engenheiro de software program líder.

Bem-vindo à comunidade VentureBeat!

Nosso programa de visitor posts é onde especialistas técnicos compartilham insights e fornecem análises profundas, neutras e não adquiridas, sobre IA, infraestrutura de dados, segurança cibernética e outras tecnologias de ponta que moldam o futuro das empresas.

Leia mais do nosso programa de visitor publish – e confira nosso diretrizes se você estiver interessado em contribuir com um artigo de sua autoria!

avots