Cache de prompts no LangChain: o que realmente acerta o cache
Conteúdo
- Primeiro: qual tipo de “cache” você procura?
- A correção: blocos de conteúdo, não strings
- A posição das variáveis do template determina a taxa de hits
- As definições de ferramentas também entram no cache
- Vários turnos: mova a marcação para a última mensagem
- Consulte os medidores e saiba o nome de cada campo
- Caches implícitos: erros de ordem passam despercebidos, então monitore-os ainda mais
- Checklist
- Aviso
- Fontes
Este system prompt do LangChain parece perfeitamente válido, mas não armazena nada em cache:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", BIG_STABLE_SYSTEM_PROMPT), # the syntax every tutorial uses
("human", "{question}"),
])
Executamos esse código duas vezes com claude-sonnet-5, usando o mesmo system prompt de 1.800 tokens, e analisamos os campos de uso. Nas duas chamadas, houve 0 gravações e 0 leituras do cache. Não foi um hit parcial nem um cache fragmentado. Nada foi armazenado. O motivo é que a Anthropic só armazena o conteúdo marcado com cache_control, e uma string simples na tupla ("system", ...) não tem onde incluir essa marcação. A sintaxe mais conveniente do LangChain também é a que abre mão de todo o desconto, sem emitir erro algum.
TL;DR
- A tupla
("system", "string")do LangChain não comportacache_control, então o Claude não armazena nada: nos testes, um system prompt idêntico de 1.800 tokens teve 0 gravações e 0 leituras noclaude-sonnet-5. - A correção é usar um
SystemMessagecomcache_controlno bloco de conteúdo. Uma única marcação no bloco de sistema também cobre as ferramentas vinculadas combind_tools. - Com
langchain-anthropic1.4.8,input_token_details.cache_creationpermanece em 0 mesmo quando há uma gravação real. A contagem correta fica emephemeral_5m_input_tokens. - Um prompt de RAG na ordem errada, com contexto variável antes das regras estáveis, paga em todas as chamadas o adicional de gravação em cache, cerca de 1.25x. Sai mais caro do que não usar cache.
Série: Parte 5 de 5 · Anteriormente: Parte 1 — Princípios de cache · Parte 2 — Comparação e avaliação de provedores · Parte 3 — Tutorial com código funcional · Parte 4 — Melhor LLM por caso de uso
Esta é a parte 5 da série sobre cache. A parte 1 explica como funciona o cache por prefixo, a parte 3 traz um tutorial com o SDK nativo, e o guia completo sobre cache de prompts compara os provedores. Aqui, o foco é o que muda quando o LangChain monta os prompts por você. Todas as medições abaixo foram feitas em 2026-07-04 pelo gateway da Synthorai, com langchain-core 1.4.8, langchain-anthropic 1.4.8 e langchain-openai 1.3.3.
Primeiro: qual tipo de “cache” você procura?
Duas funcionalidades sem relação entre si usam a mesma palavra. Em geral, a página da documentação do LangChain encontrada nas buscas trata da funcionalidade errada.
Cache de respostas (InMemoryCache do LangChain) | Cache de prompts (esta série) | |
|---|---|---|
| O que armazena | A resposta completa, na aplicação | O estado KV do prefixo do prompt, no provedor |
| Quando reduz custos | Quando a mesma requisição se repete exatamente | Quando requisições diferentes compartilham um prefixo |
| Onde | set_llm_cache(InMemoryCache()), SQLite, Redis | Marcações cache_control ou correspondência automática de prefixos |
| Loops de agentes, RAG, chat | Quase inútil, pois cada requisição é diferente | Principal mecanismo de economia, pois sistema e ferramentas se repetem a cada turno |
E “a mesma requisição” significa exatamente igual: as implementações nativas usam como chave o par formado pelo prompt serializado e pela string de configuração do modelo. Nas medições, uma repetição idêntica retornou em 0 ms, sem chamada à API. Adicionar um espaço ao prompt gerou miss. Usar o mesmo prompt e alterar max_tokens em uma unidade também gerou miss. A resposta reaproveitada ainda retorna os números de uso da chamada original, portanto uma contabilização ingênua duplica os tokens. Há caches semânticos em integrações de terceiros, mas as implementações nativas exigem correspondência exata.
Portanto, set_llm_cache serve para eliminar chamadas idênticas em testes. Já o system prompt de 2.000 tokens reenviado a cada turno de um agente precisa do cache de prompts, e o prompt deve ser montado da forma correta.
A correção: blocos de conteúdo, não strings
O cache_control fica dentro de um bloco de conteúdo. Por isso, a mensagem de sistema precisa ser um SystemMessage com conteúdo em blocos, e não uma string simples:
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langchain_core.prompts import ChatPromptTemplate
llm = ChatAnthropic(
model="claude-sonnet-5",
base_url="https://synthorai.io", # any Anthropic-compatible endpoint
)
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content=[{
"type": "text",
"text": BIG_STABLE_SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}, # a bare string has nowhere to put this
}]),
("human", "{question}"),
])
chain = prompt | llm
Usando o mesmo system prompt de 1.800 tokens e o mesmo gateway, medimos:
| Chamada | Sintaxe com tupla e string | Sintaxe com bloco de conteúdo |
|---|---|---|
| 1ª (cache frio) | gravação 0 / leitura 0 | gravação 1.875 / leitura 0 |
| 2ª, pergunta diferente | gravação 0 / leitura 0 | gravação 0 / leitura 1.875 |
Uma leitura com cache quente custa aproximadamente 10% do preço normal de input. No Claude, essa única mudança estrutural separa dois cenários: pagar o preço integral para sempre ou obter 90% de desconto sobre a parte estável de cada chamada. A parte econômica está na parte 1. A mecânica das marcações segue o uso do SDK nativo descrito na documentação da integração Anthropic com LangChain e no guia de cache de prompts da Anthropic.
A posição das variáveis do template determina a taxa de hits
Os templates do LangChain permitem interpolar variáveis em qualquer lugar sem esforço, e aí está o risco. A chave do cache é o prefixo byte a byte. Colocamos uma data dentro do bloco armazenado e medimos:
SystemMessage(content=[{
"type": "text",
"text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT, # variable INSIDE the block
"cache_control": {"type": "ephemeral"},
}])
| Chamada | Resultado |
|---|---|
| dia A, pergunta 1 | gravação 1.865 (cache frio para esse valor) |
| dia A, pergunta 2 | leitura 1.865 (mesmo valor, hit) |
| dia B, pergunta 1 | gravação 1.865 (novo valor, cache frio novamente) |
O cache não falhou. A variável passou a fazer parte da chave. Um valor repetido, como uma data, exige uma gravação por valor e gera hits nas chamadas seguintes. Um valor exclusivo de cada chamada, como timestamp ou request ID, faz com que todas as chamadas sejam gravações a frio e mantém a taxa de hits exatamente em zero.
A versão mais cara desse erro no mundo real ocorre em RAG. Muitos chains usam um template que coloca o contexto recuperado no início do system prompt, antes das instruções estáticas. Medimos as duas ordens com um contexto recuperado de 800 tokens, diferente para cada consulta, e um bloco de sistema marcado:
| Ordem dentro do prompt | Chamada 1 | Chamada 2 (nova consulta e novo contexto) |
|---|---|---|
| Primeiro o contexto, depois as regras | gravação 3.133 | nova gravação de 3.133, leitura 0 |
| Primeiro as regras (marcadas), contexto no turno do usuário | gravação 1.852 | leitura 1.852 |
A primeira linha não representa apenas “nenhum desconto”. Cada chamada paga o adicional de gravação em cache, cerca de 1.25× o preço normal de input, sobre todos os 3.133 tokens, sem que nada seja reaproveitado depois. Um prompt de RAG na ordem errada e com cache habilitado custa mais do que não usar cache. Como o conteúdo fixo vem depois do conteúdo variável, ele acaba sendo inútil para o cache.
As medições levam a estas regras:
- Texto estático primeiro, dentro do bloco marcado. Regras de sistema, definições de ferramentas e exemplos few-shot.
- Tudo o que varia deve vir depois da marcação, de preferência no turno do usuário: contexto recuperado, datas e perguntas.
- Uma variável dentro do bloco só é aceitável quando se repete o suficiente para amortizar sua própria gravação em cache.
As definições de ferramentas também entram no cache
Um agente reenvia os schemas das ferramentas em todas as chamadas. No formato de requisição da Anthropic, as ferramentas ficam antes do system prompt. Como uma marcação significa “armazene tudo desde o início da requisição até este ponto”, surgem duas questões práticas. Uma marcação no bloco de sistema também cobre as ferramentas anteriores? E o bind_tools do LangChain serializa as ferramentas exatamente da mesma forma em todas as chamadas? Se a serialização variar, o prefixo muda e todas as chamadas geram miss.
Medimos as duas respostas. Com o mesmo system prompt marcado, a leitura com cache quente foi de 1.861 tokens sem ferramentas e de 2.389 tokens com duas ferramentas vinculadas. Os 528 tokens adicionais correspondem aos schemas das ferramentas recuperados do cache. A leitura de 2.389 tokens se repetiu exatamente em três chamadas consecutivas. Portanto, bind_tools produz uma serialização estável, sem introduzir ruído no prefixo. Resumindo: se o bloco de sistema tiver a marcação, as ferramentas não precisam de cache_control. A única marcação posterior a elas cobre tudo.
Há um caso específico em que a configuração oposta faz sentido: as ferramentas são o maior bloco estável da requisição e o system prompt é pequeno ou inexistente. A requisição ainda precisa de uma marcação em algum ponto, e ela pode ficar em uma ferramenta. Isso só funciona com um dict no formato nativo da Anthropic, pois uma função decorada com @tool não tem um campo para receber a marcação. O bind_tools repassa o dict sem alterações:
# variant: NO marked system block anywhere; the tool carries the request's only marker
llm.bind_tools([{
"name": "get_weather",
"description": LONG_TOOL_DESCRIPTION,
"input_schema": {...},
"cache_control": {"type": "ephemeral"}, # passes through bind_tools verbatim
}])
Resultado medido: gravação a frio de 3.002 e leitura a quente de 3.002, sem nenhuma mensagem de sistema marcada na requisição.
Vários turnos: mova a marcação para a última mensagem
Uma conversa também parece ser um problema de ordem, mas aqui ocorre o oposto: a ordem já é ideal. O histórico só cresce no final, portanto a transcrição inteira é um prefixo estável. O problema é a cobertura. Uma marcação no bloco de sistema armazena esse bloco e nada depois dele. À medida que o histórico cresce, a leitura do cache permanece limitada ao tamanho do sistema, enquanto todos os turnos acumulados são cobrados como input normal.
A solução é a mesma usada pelo SDK nativo: colocar a marcação na mensagem mais recente. Assim, o ponto de corte avança e todo o histórico da conversa passa a formar o prefixo armazenado:
def marked(text):
return HumanMessage(content=[{
"type": "text", "text": text,
"cache_control": {"type": "ephemeral"},
}])
# each turn: history stays plain, only the newest human message carries the marker
llm.invoke([system, *history, marked(new_question)])
Em dois turnos, medimos o seguinte: o turno 1 gravou 1.864; o turno 2 leu 1.864 e gravou apenas o delta de 15 tokens, formado pela resposta anterior e pela nova pergunta. O prefixo anterior foi cobrado pela tarifa de leitura de ≈10%. Esse é o comportamento ideal para um loop de agente, e o LangChain permite implementá-lo com uma lista comum de mensagens. A Anthropic aceita até quatro marcações por requisição, portanto a marcação móvel pode coexistir com uma marcação fixa no bloco de sistema ou nas ferramentas.
Consulte os medidores e saiba o nome de cada campo
O LangChain padroniza o uso em usage_metadata, mas há uma armadilha: com langchain-anthropic 1.4.8, em todas as respostas dos nossos testes, o campo padrão input_token_details.cache_creation permaneceu em 0 mesmo quando houve gravação em cache. A contagem real aparece em uma chave não padronizada:
r = chain.invoke({"question": "..."})
det = r.usage_metadata["input_token_details"]
det["cache_read"] # correct on hits (1875 above)
det["cache_creation"] # 0 even on a cold write; do not alert on this
det["ephemeral_5m_input_tokens"] # the actual write count (1875)
O provedor informou a gravação corretamente, com cache_creation_input_tokens: 1875 na resposta bruta, disponível em r.response_metadata["usage"]. O mapeamento padronizado apenas a registrou na chave correspondente ao bucket de TTL. Um dashboard de custos que monitore cache_creation indicará que o cache não tem custo, enquanto o adicional de gravação continua se acumulando. Use o objeto de uso bruto ou conheça as chaves dos buckets. É o mesmo tipo de problema encontrado em gateways que informam campos de cache incorretamente, tema que auditamos em Seu gateway de LLM mente sobre o cache?.
Caches implícitos: erros de ordem passam despercebidos, então monitore-os ainda mais
O cache do Claude é explícito. GPT e a maioria dos provedores de modelos open-weight armazenam prefixos automaticamente quando há correspondência, sem marcações. No LangChain, o mesmo chain funciona alterando apenas o construtor:
llm = ChatOpenAI(model="glm-5.2", base_url="https://synthorai.io/v1")
Com um system prompt em string simples e sem marcações, a segunda chamada ao GLM 5.2 leu 1.088 tokens de um prefixo com aproximadamente 1.850 tokens. Nem todo o prefixo foi lido: caches automáticos fazem a correspondência em blocos, não byte a byte até o final. A OpenAI, por exemplo, documenta granularidade de 128 tokens. Até aqui, é economia sem esforço. Porém, o risco de ordem incorreta mostrado na tabela de RAG continua valendo e produz uma falha ainda mais difícil de detectar. Repetimos o experimento no caminho automático, usando um novo contexto recuperado em cada chamada:
| Ordem (sem marcações, cache automático) | Chamada 1 | Chamada 2 (nova consulta e novo contexto) |
|---|---|---|
| Primeiro o contexto, depois as regras | leitura 0 | leitura 0 |
| Primeiro as regras, contexto no turno do usuário | leitura 0 | leitura 1.088 |
A ordem errada sempre resulta em zero. O contexto variável fica no início, nenhuma chamada compartilha o mesmo prefixo e o desconto nunca aparece. No caminho explícito, o mesmo erro ao menos fica visível na fatura como um adicional de gravação em todas as chamadas. No caminho implícito, não há adicional, erro nem sinal. O prompt simplesmente nunca se qualifica, enquanto você presume que “automático” significa “funcionando”. Como não há uma marcação para posicionar, a ordem do prompt é o único controle disponível no caminho implícito.
Confira os medidores em produção, não apenas uma vez em um teste: input_token_details.cache_read no LangChain ou prompt_tokens_details.cached_tokens na resposta bruta. A documentação do cache automático da OpenAI também especifica um prefixo mínimo de 1.024 tokens. O TTL e os critérios de elegibilidade variam entre provedores, assunto tratado na parte 2.
Checklist
- No Claude, uma tupla de string
("system", "...")não tem onde incluircache_control: nada é armazenado e nenhum aviso é emitido. System prompts armazenáveis devem usar umSystemMessagecom blocos de conteúdo e a marcação. - A chave do cache é o prefixo byte a byte: conteúdo estático primeiro, variáveis depois da marcação ou no turno do usuário. Colocar o contexto de RAG antes das regras não causa apenas um miss; também cobra o adicional de gravação em todas as chamadas.
- Uma variável dentro do bloco armazenado cria uma entrada de cache por valor. Valores repetidos amortizam o custo; valores únicos por chamada, como timestamps e request IDs, nunca geram hits.
- As ferramentas ficam antes do system prompt no prefixo, portanto a marcação do sistema também armazena as ferramentas vinculadas. O
bind_toolsproduz uma serialização determinística. Se as ferramentas forem seu maior bloco estável, a marcação pode ficar em um dict de ferramenta no formato da Anthropic. - Em conversas, uma marcação fixa no bloco de sistema deixa o histórico crescente sujeito ao preço integral. Coloque-a na mensagem mais recente para que cada turno leia o prefixo anterior e grave apenas o delta.
- Não monitore
input_token_details.cache_creation: ele permanece em 0 mesmo quando há gravações. Um dashboard concluirá que o cache é gratuito enquanto os adicionais de gravação se acumulam. A contagem real fica emephemeral_5m_input_tokens; outra opção é consultarresponse_metadata["usage"]diretamente. - Em modelos com cache automático, como GPT, GLM e DeepSeek, a ordem do prompt é o único controle. Uma ordem incorreta falha silenciosamente: sem adicional, sem erro, apenas um desconto que nunca aparece. Confirme os hits pelos campos de uso.
set_llm_cachearmazena respostas completas e usa como chave o prompt exato e a configuração do modelo. Ele só compensa quando requisições idênticas se repetem, nunca em um loop de agente.
Os ajustes são pequenos: usar um bloco de conteúdo em vez de uma string, colocar conteúdo estático antes do variável, mover a marcação junto com a conversa e ler corretamente um campo de uso. Nas medições, isso representou 90% de desconto em cada token estável, em vez de economia zero. No caso do RAG com ordem incorreta, evitou inclusive pagar mais caro. O LangChain não impede o cache de prompts; ele apenas torna o formato errado tão fácil de escrever quanto o correto.
Aviso
Medições realizadas em 2026-07-04 usando https://synthorai.io/, com langchain-core 1.4.8, langchain-anthropic 1.4.8, langchain-openai 1.3.3, os modelos claude-sonnet-5 e glm-5.2, um prefixo de sistema em inglês com aproximadamente 1.800 tokens, amostras pequenas e um intervalo de 1 a 2 segundos entre chamadas consecutivas para permitir a conclusão das gravações em cache. Cada experimento usou um prefixo aleatório inédito para garantir um cache frio. Por isso, as contagens de tokens de referência variam ligeiramente entre as tabelas, de 1.852 a 1.875. O mapeamento de campos das bibliotecas e o comportamento do cache dos provedores mudam entre versões. Repita as medições na sua própria stack antes de depender desses números.