🎁 Nuevo Regístrate gratis, 10 llamadas de regalo. Hasta 1 $, sin tarjeta.
Caché de prompts en LangChain: configuraciones que sí aciertan

Caché de prompts en LangChain: configuraciones que sí aciertan

Contenido
  1. Primero: ¿qué tipo de «caché» buscas?
  2. La solución: bloques de contenido, no cadenas
  3. La ubicación de las variables de plantilla determina la tasa de aciertos
  4. Las definiciones de tools también se almacenan en caché
  5. Conversaciones multTurno: mueve el marcador al último mensaje
  6. Revisa los contadores y conoce sus nombres
  7. Cachés implícitas: un orden incorrecto falla sin avisar, así que vigílalo aún más
  8. Lista de comprobación
  9. Aviso
  10. Fuentes

Este system prompt de LangChain parece perfectamente válido, pero no almacena nada en caché:

from langchain_core.prompts import ChatPromptTemplate

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

Lo ejecutamos dos veces contra claude-sonnet-5, con un system prompt idéntico de 1,800 tokens, y revisamos los campos de uso. En ambas llamadas: 0 escrituras y 0 lecturas de caché. No fue un acierto parcial ni una caché fragmentada. No se almacenó nada. Anthropic solo guarda en caché lo que se marca con cache_control, y una cadena simple en una tupla ("system", ...) no tiene dónde incluir el marcador. La sintaxis más cómoda de LangChain también es la que hace perder todo el descuento, sin mostrar ningún error.

TL;DR

  • La tupla ("system", "string") de LangChain no admite cache_control, por lo que Claude no almacena nada en caché: con un system prompt idéntico de 1,800 tokens, medimos 0 escrituras y 0 lecturas en claude-sonnet-5.
  • La solución es usar un SystemMessage con cache_control en un bloque de contenido. Un solo marcador en el bloque de sistema también cubre las tools enlazadas mediante bind_tools.
  • Con langchain-anthropic 1.4.8, input_token_details.cache_creation permanece en 0 incluso cuando sí hay una escritura; el valor real está en ephemeral_5m_input_tokens.
  • Un prompt RAG mal ordenado, con el contexto variable antes de las reglas estables, paga en cada llamada el recargo de escritura en caché, aproximadamente 1.25x. Sale más caro que no usar caché.

Serie: Parte 5 de 5 · Anteriores: Parte 1 — Principios de la caché · Parte 2 — Comparativa y evaluación de proveedores · Parte 3 — Tutorial con código funcional · Parte 4 — Mejor LLM según el caso de uso

Esta es la parte 5 de la serie sobre caché. La parte 1 explica cómo funciona la caché por prefijo, la parte 3 incluye el tutorial con los SDK sin abstracciones y la guía completa sobre caché de prompts reúne la información de distintos proveedores. Esta parte se centra en lo que cambia cuando LangChain construye los prompts. Todas las mediciones siguientes se realizaron el 2026-07-04 mediante el gateway de Synthorai, con langchain-core 1.4.8, langchain-anthropic 1.4.8 y langchain-openai 1.3.3.

Primero: ¿qué tipo de «caché» buscas?

Hay dos funciones distintas con el mismo nombre, y la página de documentación de LangChain que suele aparecer en las búsquedas normalmente corresponde a la que no necesitas.

Caché de respuestas (InMemoryCache de LangChain)Caché de prompts (esta serie)
Qué almacenaLa respuesta completa, en tu aplicaciónEl estado KV del prefijo del prompt, en el proveedor
Cuándo ahorra dineroCuando se repite exactamente la misma solicitudCuando varias solicitudes distintas comparten un prefijo
Dóndeset_llm_cache(InMemoryCache()), SQLite, RedisMarcadores cache_control o coincidencia automática de prefijos
Bucles de agentes, RAG y chatCasi inútil, porque cada solicitud es distintaEl principal mecanismo de ahorro, porque system + tools se repiten en cada turno

Y «exactamente la misma solicitud» significa exactamente eso: las implementaciones incluidas usan como clave el par formado por el prompt serializado y la cadena de configuración del modelo. En nuestras mediciones, una repetición idéntica se devolvió en 0 ms sin ninguna llamada a la API. Añadir un espacio al prompt produjo un fallo de caché; usar el mismo prompt cambiando max_tokens en una unidad también. La respuesta recuperada de la caché conserva además las cifras de uso de la llamada original, por lo que una contabilidad de tokens ingenua las cuenta dos veces. Existen cachés semánticas como integraciones de terceros; las incluidas solo funcionan con coincidencias exactas.

Por tanto, set_llm_cache sirve para deduplicar llamadas idénticas en pruebas. En cambio, almacenar el system prompt de 2,000 tokens que se reenvía en cada turno de un agente corresponde a la caché de prompts, y exige construir el prompt correctamente.

La solución: bloques de contenido, no cadenas

cache_control se incluye dentro de un bloque de contenido. Por eso, el mensaje de sistema debe ser un SystemMessage con contenido estructurado, no una cadena simple:

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

Con el mismo system prompt de 1,800 tokens y a través del mismo gateway, medimos lo siguiente:

LlamadaSintaxis con tupla y cadenaSintaxis con bloque de contenido
1.ª (en frío)escritura 0 / lectura 0escritura 1,875 / lectura 0
2.ª, con otra preguntaescritura 0 / lectura 0escritura 0 / lectura 1,875

Una lectura en caliente se factura aproximadamente al 10% del precio de input. En Claude, este único cambio estructural marca la diferencia entre pagar siempre el precio completo y obtener un descuento del 90% sobre la parte estable de cada llamada. La parte 1 analiza los costes; el funcionamiento de los marcadores coincide con el uso directo del SDK descrito en la documentación de la integración de Anthropic con LangChain y la guía de Anthropic sobre caché de prompts.

La ubicación de las variables de plantilla determina la tasa de aciertos

Las plantillas de LangChain permiten interpolar variables en cualquier lugar con mucha facilidad, y ese es precisamente el riesgo. La clave de caché es el prefijo exacto, byte por byte. Colocamos una fecha dentro del bloque almacenado en caché y medimos el resultado:

SystemMessage(content=[{
    "type": "text",
    "text": f"Today is {today}. " + BIG_STABLE_SYSTEM_PROMPT,   # variable INSIDE the block
    "cache_control": {"type": "ephemeral"},
}])
LlamadaResultado
día A, pregunta 1escritura 1,865 (en frío para este valor)
día A, pregunta 2lectura 1,865 (mismo valor, acierto)
día B, pregunta 1escritura 1,865 (nuevo valor, otra vez en frío)

La caché no dejó de funcionar. La variable pasó a formar parte de la clave. Un valor que se repite, como una fecha, requiere una escritura por cada valor y produce aciertos a partir de ahí. Un valor único por llamada, como un timestamp o un request ID, convierte cada llamada en una escritura en frío y deja la tasa de aciertos exactamente en cero.

La versión costosa de este error en producción aparece con RAG. Muchas cadenas colocan el contexto recuperado al principio del system prompt, antes de las instrucciones estáticas. Medimos ambos órdenes con un contexto recuperado de 800 tokens que cambia en cada consulta y un bloque de sistema marcado:

Orden dentro del promptLlamada 1Llamada 2 (otra consulta y otro contexto)
Primero el contexto, después las reglasescritura 3,133otra escritura de 3,133, lectura 0
Primero las reglas (marcadas), contexto en el turno humanoescritura 1,852lectura 1,852

La primera fila no implica simplemente «ningún descuento»: cada llamada paga el recargo de escritura en caché, aproximadamente 1.25× el precio normal del input, sobre los 3,133 tokens completos, sin recuperar nunca nada. Un prompt RAG mal ordenado con la caché activada cuesta más que no usar caché. El contenido fijo queda después del contenido variable, por lo que a efectos de caché es como si no existiera.

Las mediciones dejan estas reglas:

  • Primero el texto estático, dentro del bloque marcado. Reglas del sistema, definiciones de tools y ejemplos few-shot.
  • Todo lo variable debe ir después del marcador, preferiblemente en el turno humano: contexto recuperado, fechas y preguntas del usuario.
  • Una variable dentro del bloque solo es aceptable si se repite lo suficiente como para amortizar su propia escritura en caché.

Las definiciones de tools también se almacenan en caché

Un agente reenvía los esquemas de sus tools en cada llamada. En la estructura de solicitud de Anthropic, las tools aparecen antes del system prompt. Como un marcador significa «almacenar en caché todo lo que haya desde el principio de la solicitud hasta aquí», surgen dos preguntas prácticas. ¿El marcador del bloque de sistema también cubre las tools que lo preceden? ¿Y bind_tools de LangChain serializa las tools en exactamente los mismos bytes en cada llamada? Si la serialización varía, cambia el prefijo y todas las llamadas fallan.

Medimos ambas cosas. Con el mismo system prompt marcado, la lectura en caliente fue de 1,861 tokens sin tools y de 2,389 tokens con dos tools enlazadas. Los 528 tokens adicionales corresponden a los esquemas de las tools recuperados de la caché. Además, el valor 2,389 se repitió exactamente en tres llamadas consecutivas, lo que demuestra que bind_tools serializa siempre del mismo modo; el framework no introduce variaciones en el prefijo. Por tanto, si el bloque de sistema contiene el marcador, las propias tools no necesitan cache_control. Ese único marcador posterior se encarga de todo.

Hay una disposición alternativa para un caso concreto: las tools son el bloque estable más grande de la solicitud y el system prompt es muy pequeño o no existe. La solicitud sigue necesitando un marcador, que puede colocarse en una tool. Esto solo funciona con un diccionario en formato Anthropic, porque una función decorada con @tool no tiene un campo donde incluirlo. bind_tools reenvía el diccionario sin modificarlo:

# 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: escritura en frío de 3,002 y lectura en caliente de 3,002, sin ningún mensaje de sistema marcado en la solicitud.

Conversaciones multTurno: mueve el marcador al último mensaje

Una conversación puede parecer otro problema de orden, pero es el caso contrario: el orden ya es el adecuado, porque el historial solo crece añadiendo mensajes al final y toda la conversación anterior forma un prefijo estable. El problema es la cobertura. Un marcador en el bloque de sistema solo almacena ese bloque, no lo que viene después. A medida que crece el historial, la lectura en caliente permanece fija en el tamaño del sistema, mientras que todos los turnos acumulados se facturan como input normal.

La solución es la misma que se usa con el SDK sin abstracciones: colocar el marcador en el mensaje más reciente. Así, el punto de corte avanza y toda la conversación acumulada pasa a ser el prefijo almacenado:

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

Medimos dos turnos: el primero escribió 1,864 tokens; el segundo leyó 1,864 y escribió únicamente el delta de 15 tokens, formado por la respuesta anterior y la nueva pregunta. El prefijo previo se facturó a la tarifa de lectura de ≈10%. Esta es la estructura adecuada para un bucle de agente, y LangChain puede expresarla con una lista normal de mensajes. Anthropic permite hasta cuatro marcadores por solicitud, por lo que el marcador deslizante puede combinarse con uno fijo en el bloque de sistema o en las tools.

Revisa los contadores y conoce sus nombres

LangChain normaliza el uso en usage_metadata. Aquí encontramos una trampa: con langchain-anthropic 1.4.8, el campo estándar input_token_details.cache_creation permaneció en 0 en todas nuestras respuestas, incluso cuando hubo una escritura en caché. El recuento real aparece en una clave no estándar:

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)

El proveedor informó correctamente de la escritura: cache_creation_input_tokens: 1875 en la respuesta original, disponible mediante r.response_metadata["usage"]. El mapeo normalizado simplemente la coloca bajo la clave correspondiente al TTL. Un panel de costes que supervise cache_creation indicará que la caché no genera costes, mientras los recargos de escritura siguen acumulándose. Usa el objeto de uso original o ten en cuenta las claves de los distintos intervalos TTL. Es el mismo tipo de problema que aparece cuando los gateways informan incorrectamente de los campos de caché, algo que auditamos en ¿Miente tu gateway de LLM sobre la caché?.

Cachés implícitas: un orden incorrecto falla sin avisar, así que vigílalo aún más

La caché de Claude es explícita. GPT y la mayoría de los proveedores de modelos open-weight almacenan automáticamente cuando coincide el prefijo, sin marcadores. En LangChain, basta con cambiar el constructor para usar la misma cadena:

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

Con un system prompt como cadena simple y sin marcadores, la segunda llamada de GLM 5.2 leyó 1,088 tokens de un prefijo de aproximadamente 1,850 tokens. No leyó el prefijo completo: las cachés automáticas comparan bloques de cierto tamaño, no todos los bytes hasta el final. OpenAI, por ejemplo, documenta una granularidad de 128 tokens. Hasta aquí, ahorro gratuito. Sin embargo, el riesgo de orden incorrecto de la tabla RAG anterior se aplica por completo, con un fallo todavía más difícil de detectar. Repetimos el experimento con la ruta automática y un contexto recuperado distinto en cada llamada:

Orden (sin marcadores, caché automática)Llamada 1Llamada 2 (otra consulta y otro contexto)
Primero el contexto, después las reglaslectura 0lectura 0
Primero las reglas, contexto en el turno humanolectura 0lectura 1,088

Con el orden incorrecto, el resultado es siempre cero: el contexto variable está al principio, ninguna llamada comparte prefijo con otra y nunca se aplica el descuento. En la ruta explícita, el mismo error al menos aparece en la factura como un recargo de escritura en cada llamada. En la ruta implícita no hay recargo, error ni señal alguna. El prompt simplemente nunca cumple los requisitos, mientras das por hecho que «automático» significa «funcionando». Y como no hay ningún marcador que colocar, el orden del prompt es el único control disponible en la ruta implícita.

Compruébalo mediante los contadores en producción, no una sola vez durante una prueba: input_token_details.cache_read en LangChain o prompt_tokens_details.cached_tokens en la respuesta original. La documentación sobre caché automática de OpenAI también establece un prefijo mínimo de 1,024 tokens. El TTL y los requisitos varían entre proveedores; eso se trata en la parte 2.

Lista de comprobación

  • En Claude, una tupla de cadena ("system", "...") no tiene dónde incluir cache_control: no se almacena nada y no aparece ninguna advertencia. Los system prompts que deban almacenarse en caché tienen que ir en un SystemMessage con bloques de contenido y el marcador.
  • La clave de caché es el prefijo exacto byte por byte: primero el contenido estático; las variables, después del marcador o en el turno humano. Colocar el contexto RAG antes de las reglas no solo impide los aciertos, sino que obliga a pagar el recargo de escritura en cada llamada.
  • Una variable dentro del bloque almacenado crea una entrada de caché por valor: los valores repetidos se amortizan; los valores únicos por llamada, como timestamps y request IDs, nunca producen aciertos.
  • Las tools aparecen antes del system prompt dentro del prefijo, por lo que el marcador de sistema también almacena las tools enlazadas. bind_tools las serializa de forma determinista. Si las tools son el bloque estable más grande, el marcador puede colocarse en un diccionario de tool con formato Anthropic.
  • En una conversación, un marcador fijo en el bloque de sistema deja todo el historial creciente a precio completo. Colócalo en el último mensaje para que cada turno lea el prefijo anterior y escriba solo el delta.
  • No supervises input_token_details.cache_creation: permanece en 0 incluso cuando hay escrituras, por lo que un panel puede concluir que la caché no tiene coste mientras se acumulan recargos. El valor real está en ephemeral_5m_input_tokens; también puedes leer response_metadata["usage"] directamente.
  • En los modelos con caché automática, como GPT, GLM y DeepSeek, el orden del prompt es el único control disponible. Un orden incorrecto falla sin avisar: no hay recargo ni error, solo un descuento que nunca llega. Comprueba los aciertos en los campos de uso.
  • set_llm_cache almacena respuestas completas usando como clave el prompt exacto y la configuración del modelo. Solo resulta útil cuando se repiten solicitudes idénticas, nunca en un bucle de agente.

Los cambios son pequeños: usar un bloque de contenido en vez de una cadena, colocar lo estático antes que lo variable, desplazar el marcador con la conversación y leer correctamente un campo de uso. La diferencia medida fue un descuento del 90% sobre cada token estable frente a ningún ahorro; en el caso del RAG mal ordenado, además se evitó pagar de más. LangChain no impide usar la caché de prompts. Simplemente hace que construir el prompt de forma incorrecta sea tan fácil como hacerlo bien.


Aviso

Mediciones realizadas el 2026-07-04 contra https://synthorai.io/, con langchain-core 1.4.8, langchain-anthropic 1.4.8, langchain-openai 1.3.3, los modelos claude-sonnet-5 y glm-5.2, un prefijo de sistema en inglés de aproximadamente 1,800 tokens, muestras pequeñas y una espera de 1–2 segundos entre llamadas consecutivas para dar tiempo a que las escrituras llegaran a la caché. Cada experimento usó un prefijo aleatorio nuevo para garantizar una caché en frío; por eso, los recuentos base varían ligeramente entre tablas, de 1,852 a 1,875. El mapeo de campos de las librerías y el comportamiento de la caché de los proveedores cambian entre versiones. Repite las mediciones en tu propio stack antes de depender de estas cifras.

Fuentes

← Volver al blog