🎁 Novo Cadastre-se grátis, 10 chamadas por nossa conta. Até US$ 1, sem cartão.
Prompt Caching em LLM #5: Configurações de LangChain que Realmente Funcionam

Prompt Caching em LLM #5: Configurações de LangChain que Realmente Funcionam

Conteúdo
  1. Primeiro: qual “caching” você está procurando?
  2. A solução: content blocks, não strings
  3. Onde ficam suas variáveis de template define sua hit rate
  4. As definições de tools também são cacheadas
  5. Multi-turn: mova o marcador para a última mensagem
  6. Leia os medidores — e saiba o nome de cada um
  7. Caches implícitos: a ordem errada falha em silêncio, então redobre a atenção aqui
  8. Checklist
  9. Disclaimer
  10. Sources

Este é um system prompt de LangChain que parece totalmente correto e não faz cache de nada:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", BIG_STABLE_SYSTEM_PROMPT),   # the syntax every tutorial uses
    ("human", "{question}"),
])

Rodamos isso contra o claude-sonnet-5 duas vezes, com um system prompt idêntico de 1.800 tokens, e lemos os campos de usage. Nas duas chamadas: cache writes 0, cache reads 0. Não foi um hit parcial, não foi um cache fragmentado. Nada. O motivo: a Anthropic só faz cache do que você marca com cache_control, e uma string simples numa tupla ("system", ...) não tem onde colocar o marcador. A sintaxe mais conveniente do LangChain é justamente a que deixa o desconto inteiro na mesa, e nenhum erro te avisa disso.

TL;DR

  • A tupla ("system", "string") do LangChain não consegue carregar cache_control, então o Claude não faz cache de nada: um prompt de sistema idêntico de 1.800 tokens registrou 0 gravações de cache e 0 leituras no claude-sonnet-5.
  • A correção é um SystemMessage com cache_control em bloco de conteúdo; um marcador no bloco de sistema também cobre ferramentas vinculadas com bind_tools.
  • Com langchain-anthropic 1.4.8, input_token_details.cache_creation permanece 0 mesmo em gravações reais; a contagem verdadeira está em ephemeral_5m_input_tokens.
  • Um prompt RAG com ordem incorreta (contexto variável antes de regras estáveis) paga o prêmio de gravação de cache, cerca de 1.25x, em cada chamada: mais caro do que não fazer cache algum.

Esta é a parte 5 da série sobre caching. A parte 1 explica como o prefix caching funciona, a parte 3 traz o tutorial com o SDK puro. Esta parte trata do que muda quando é o LangChain que monta seus prompts. Tudo abaixo foi medido em 2026-07-04 através do gateway da Synthorai, com langchain-core 1.4.8, langchain-anthropic 1.4.8 e langchain-openai 1.3.3.

Primeiro: qual “caching” você está procurando?

Duas features sem relação entre si compartilham a mesma palavra, e a página da doc do LangChain que aparece quando você pesquisa costuma ser a errada.

Response caching (InMemoryCache do LangChain)Prompt caching (esta série)
O que guardaA completion inteira, no seu appO estado KV do prefixo do prompt, no provider
Quando economizaQuando a mesma requisição se repete exatamenteQuando requisições diferentes compartilham um prefixo
Ondeset_llm_cache(InMemoryCache()), SQLite, RedisMarcadores cache_control ou prefix matching automático
Loops de agente, RAG, chatQuase inútil (toda requisição é diferente)A alavanca principal, já que system + tools se repetem a cada turno

E “a mesma requisição exatamente” significa exatamente isso: os built-ins usam como chave o par (prompt serializado, string de config do modelo). Medido: uma repetição idêntica retornou em 0 ms sem chamada de API; adicionar um espaço ao prompt gerou miss; o mesmo prompt com max_tokens alterado em uma unidade também gerou miss. (O replay do cache também retorna os números de usage da chamada original, então uma contagem ingênua de tokens conta em dobro.) Caches semânticos existem como integrações de terceiros; os built-ins são só de match exato.

Ou seja, o set_llm_cache serve bem para deduplicar chamadas idênticas em testes; já o system prompt de 2.000 tokens que você reenvia a cada turno do agente é trabalho do prompt caching, e ele precisa do prompt montado da forma certa.

A solução: content blocks, não strings

O cache_control vai dentro de um content block, então a mensagem de sistema precisa ser um SystemMessage com conteúdo em bloco, 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

Mesmo system prompt de 1.800 tokens, medido pelo mesmo gateway:

ChamadaSintaxe string-tupleSintaxe content-block
1ª (cold)write 0 / read 0write 1,875 / read 0
2ª, pergunta diferentewrite 0 / read 0write 0 / read 1,875

Um read quente custa cerca de 10% do preço de input, então no Claude essa única mudança estrutural é a diferença entre pagar preço cheio para sempre e ter 90% de desconto na metade estável de cada chamada. Os números estão na parte 1; a mecânica do marker é a mesma do uso do SDK bruto na documentação da integração LangChain Anthropic e no guia de prompt caching da Anthropic.

Onde ficam suas variáveis de template define sua hit rate

Os templates da LangChain deixam trivial interpolar variáveis em qualquer lugar, e é justamente aí que mora o perigo. A cache key é o prefixo byte a byte. Colocamos uma data dentro do bloco cacheado e medimos:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
ChamadaResultado
dia A, pergunta 1write 1,865 (cold para este valor)
dia A, pergunta 2read 1,865 (mesmo valor, hit)
dia B, pergunta 1write 1,865 (novo valor, cold de novo)

O cache não quebrou. Ele passou a ser chaveado pela variável. Um valor que se repete, como uma data, custa um cache write por valor e dá hit depois disso. Um valor único por chamada, como um timestamp ou um request ID, faz de toda chamada um cold write e zera a hit rate.

A versão cara desse erro no mundo real é o RAG. O template que muitas chains adotam coloca o contexto recuperado no topo do system prompt, antes das instruções estáticas. Medimos as duas ordens com um contexto recuperado de 800 tokens que muda a cada query e um bloco de sistema marcado:

Ordem dentro do promptChamada 1Chamada 2 (nova query, novo contexto)
Contexto primeiro, depois regraswrite 3,133write 3,133 de novo, read 0
Regras primeiro (marcadas), contexto no turno humanwrite 1,852read 1,852

A linha errada não é só “sem desconto”: toda chamada paga o prêmio de cache write, cerca de 1,25× o preço normal de input, sobre os 3.133 tokens inteiros, e nada nunca é lido de volta. Um prompt RAG com a ordem errada e caching habilitado custa mais do que não cachear. O conteúdo fixo fica depois do conteúdo variável, então é como se ele não existisse.

A regra que sai das medições:

  • Texto estático primeiro, dentro do bloco marcado. Regras de sistema, definições de tools, exemplos few-shot.
  • Tudo que varia vem depois do marker, de preferência no turno human: contexto recuperado, datas, perguntas do usuário.
  • Uma variável dentro do bloco só é aceitável se ela se repete com frequência suficiente para amortizar o próprio cache write.

As definições de tools também são cacheadas

Um agente reenvia os schemas das suas tools a cada chamada e, no layout de requisição da Anthropic, as tools ficam antes do system prompt. Como um marcador significa “cacheie tudo desde o início da requisição até aqui”, surgem duas questões práticas. Um marcador no bloco de system também cobre as tools que estão na frente dele? E o bind_tools do LangChain transforma suas tools nos mesmos bytes exatos a cada chamada? Porque, se a serialização variar, o prefixo muda e toda chamada dá miss.

As duas respostas foram medidas. Com o mesmo system prompt marcado, a leitura de cache quente foi de 1.861 tokens sem tools e 2.389 tokens com duas tools vinculadas: os 528 tokens extras são os schemas das tools saindo do cache. E esse 2.389 se repetiu exatamente em três chamadas consecutivas, o que significa que o bind_tools serializa da mesma forma toda vez; o framework não injeta ruído no prefixo. Para deixar explícito: enquanto o bloco de system carregar o marcador, as tools em si não precisam de cache_control; aquele único marcador atrás delas faz todo o trabalho.

O arranjo oposto existe para um formato específico: as tools são a maior coisa estável que você envia e o system prompt é curto ou inexistente. Nesse caso, a requisição ainda precisa de um marcador em algum lugar, e ele pode ficar numa tool. Isso só funciona com o dict no formato bruto da Anthropic, porque uma função decorada com @tool não tem campo para carregá-lo; 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
}])

Medido: write 3.002 a frio, read 3.002 a quente, sem nenhuma mensagem de system marcada na requisição.

Multi-turn: mova o marcador para a última mensagem

Uma conversa parece mais um problema de ordenação, mas é o caso oposto: a ordem já está perfeita, porque o histórico só faz append, então a transcrição inteira é prefixo estável. O problema aqui é cobertura. Um marcador no bloco de system cacheia o bloco de system e nada depois dele: conforme o histórico cresce, a leitura quente fica travada no tamanho do system enquanto cada turno acumulado é cobrado como input comum.

O padrão que resolve isso é o mesmo que o SDK bruto usa: coloque o marcador na última mensagem, para que o breakpoint deslize para frente e toda a conversa até ali vire o prefixo cacheado:

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)])

Medido ao longo de dois turnos: o turno 1 escreveu 1.864; o turno 2 leu 1.864 e escreveu apenas o delta de 15 tokens (a resposta anterior mais a nova pergunta), com o prefixo anterior sendo cobrado à taxa de leitura de ≈10%. É esse o formato que um loop de agente quer, e o LangChain expressa isso com uma lista de mensagens comum. A Anthropic permite até quatro marcadores por requisição, então o marcador deslizante se combina com um marcador fixo no bloco de system ou nas tools.

Leia os medidores — e saiba o nome de cada um

O LangChain padroniza o uso em usage_metadata, e aqui está a pegadinha em que esbarramos: com o langchain-anthropic 1.4.8, em toda resposta das nossas execuções, o campo padrão input_token_details.cache_creation ficou em 0 mesmo quando houve escrita de cache. A contagem real da escrita cai numa chave fora do padrão:

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 provider reportou a escrita corretamente (cache_creation_input_tokens: 1875 na resposta bruta, visível via r.response_metadata["usage"]); o mapeamento padronizado só arquiva o valor sob a chave do bucket de TTL. Um dashboard de custo que observa cache_creation vai te dizer que o cache é grátis enquanto o prêmio de escrita se acumula em silêncio. Confie no objeto de uso bruto ou saiba as chaves dos buckets. É o mesmo tipo de problema de gateways que reportam campos de cache errados, que auditamos em O Seu LLM Gateway Mente Sobre o Cache?.

Caches implícitos: a ordem errada falha em silêncio, então redobre a atenção aqui

O cache do Claude é explícito. O GPT e a maioria dos providers open-weight fazem cache automaticamente por match de prefixo, sem marcadores, e via LangChain a mesma chain funciona depois de uma única mudança no construtor:

llm = ChatOpenAI(model="glm-5.2", base_url="https://synthorai.io/v1")

System prompt como string simples, sem marcadores: a segunda chamada do GLM 5.2 leu 1.088 tokens do prefixo de cerca de 1.850 tokens. (Não todos: caches automáticos fazem match em blocos de granularidade grossa, e não byte a byte até o fim; a OpenAI, por exemplo, documenta granularidade de 128 tokens.) Até aqui, dinheiro de graça. Mas o risco da ordem errada da tabela de RAG acima se aplica com força total aqui, e com um modo de falha mais desagradável. Rodamos de novo o mesmo experimento de ordem no caminho automático, com contexto recuperado novo a cada chamada:

Ordem (sem marcadores, cache automático)Chamada 1Chamada 2 (query nova, contexto novo)
Contexto primeiro, depois as regrasleu 0leu 0
Regras primeiro, contexto no turno humanleu 0leu 1.088

A ordem errada é um zero incondicional: o contexto que muda fica na frente, nenhuma das duas chamadas compartilha prefixo, e o desconto nunca chega. No caminho explícito, o mesmo erro pelo menos aparece na conta como um prêmio de escrita de cache em toda chamada; no caminho implícito não há prêmio, não há erro, não há sinal nenhum. O prompt simplesmente nunca se qualifica, em silêncio, enquanto você presume que “automático” significa “funcionando”. E como não há marcador para posicionar, a ordem do prompt é o único botão que o caminho implícito te oferece.

Então verifique pelos medidores, em produção e não uma vez só num teste: input_token_details.cache_read (LangChain) ou prompt_tokens_details.cached_tokens (bruto). O cache automático da OpenAI documenta ainda um prefixo mínimo de 1.024 tokens, e o TTL e os critérios de elegibilidade variam por provider — território da parte 2.

Checklist

  • No Claude, uma tupla de string ("system", "...") não tem onde carregar o cache_control: nada é cacheado e nenhum aviso aparece. System prompts cacheáveis vão numa SystemMessage com blocos de conteúdo e o marcador.
  • A chave de cache é o prefixo byte a byte: conteúdo estático primeiro, variáveis depois do marcador ou no turno do humano. Colocar o contexto RAG antes das regras não só perde o cache; ainda paga o prêmio de escrita a cada chamada.
  • Uma variável dentro do bloco cacheado cria uma entrada de cache por valor: valores que se repetem amortizam, valores únicos por chamada (timestamps, request IDs) nunca dão hit.
  • As tools ficam antes do system prompt no prefixo, então o marcador do system cacheia também as tools vinculadas (bind_tools serializa de forma determinística). Se as tools forem o seu maior bloco estável, o marcador pode ir num dict de tool no formato Anthropic.
  • Em conversas, um marcador fixo no bloco de system deixa o histórico crescente a preço cheio; ponha-o na última mensagem para que cada turno leia o prefixo anterior e escreva só o delta.
  • Não monitore input_token_details.cache_creation: ele fica em 0 mesmo nas escritas, então um dashboard conclui que o cache é grátis enquanto os prêmios de escrita se acumulam. A contagem real está em ephemeral_5m_input_tokens, ou leia o response_metadata["usage"] bruto.
  • Em modelos de cache automático (GPT, GLM, DeepSeek), a ordem do prompt é o único ajuste e ordená-la errado falha em silêncio: sem prêmio, sem erro, só um desconto que nunca chega. Verifique os hits pelos campos de usage.
  • set_llm_cache guarda respostas inteiras indexadas pelo prompt exato e pela config do modelo; só compensa quando requisições idênticas se repetem, nunca num loop de agente.

Os hábitos são pequenos: um bloco de conteúdo em vez de uma string, o estático antes da variável, um marcador que desliza junto com a conversa, um campo de usage lido corretamente. A diferença medida foi um desconto de 90% em cada token estável contra nada, e no caso do RAG mal ordenado, contra pagar a mais. O LangChain não atrapalha o prompt caching; ele só torna a forma errada de prompt tão fácil de escrever quanto a certa.


Disclaimer

Medido em 2026-07-04 contra https://synthorai.io/ com langchain-core 1.4.8, langchain-anthropic 1.4.8, langchain-openai 1.3.3, modelos claude-sonnet-5 e glm-5.2, um prefixo de system em inglês de cerca de 1.800 tokens, amostras pequenas e um intervalo de 1 a 2 segundos entre chamadas consecutivas para dar tempo das escritas de cache serem gravadas. Cada experimento usou um prefixo aleatório novo para garantir cache frio, e é por isso que as contagens de tokens da baseline variam um pouco entre as tabelas (1.852 a 1.875). Os mapeamentos de campos das bibliotecas e o comportamento de cache dos provedores mudam entre versões; refaça a medição contra a sua própria stack antes de depender dos números.

Sources

← Voltar ao blog