Construcción de Conversaciones Multi-turno con el Array de Mensajes
Una conversación real con Claude no es una sola llamada a la API, sino una secuencia de llamadas, cada una llevando consigo todo el historial hasta el momento.
Esta página cubre la mecánica práctica de construir y mantener ese historial en el código de la aplicación.
Resumen
La API de Mensajes no tiene memoria del lado del servidor de solicitudes pasadas.
Cada llamada se evalúa de forma independiente, utilizando únicamente el array messages incluido en esa solicitud específica.
Para construir una conversación que se sienta continua, tu aplicación posee una lista creciente de turnos y reenvía todo cada vez.
Este es un diseño deliberado: mantiene la API simple y sin estado, y te da control total sobre exactamente qué contexto ve Claude en cada turno.
La contrapartida es que la gestión del estado —añadir turnos correctamente, en el orden adecuado, con el contenido correcto— se convierte en el trabajo de tu aplicación.
Receta
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
import anthropic
client = anthropic.Anthropic()
messages = []
def send(user_text: str) -> str:
messages.append({"role": "user", "content": user_text})
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
system="Eres un asistente útil.",
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
return response.content[0].textCuándo usar esto:
- Cualquier interfaz de estilo chat donde el usuario envía más de un mensaje por sesión.
- Bucles de agente donde las respuestas de Claude (incluidas las llamadas a herramientas) necesitan informar su siguiente turno.
- Servicios backend que mantienen una conversación por usuario, sesión o ticket.
- Cualquier lugar donde necesites que Claude se refiera a algo dicho anteriormente en la misma interacción.
Ejemplo de Trabajo
import anthropic
client = anthropic.Anthropic()
class Conversation:
"""Posee el array de mensajes para una conversación y lo añade en cada turno."""
def __init__(self, system_prompt: str, model: str = "claude-opus-4-8"):
self.system_prompt = system_prompt
self.model = model
self.messages: list[dict] = []
def send(self, user_text: str, max_tokens: int = 1024) -> str:
self.messages.append({"role": "user", "content": user_text})
response = client.messages.create(
model=self.model,
max_tokens=max_tokens,
system=self.system_prompt,
messages=self.messages,
)
# Añade la lista completa de contenido, no solo el texto extraído, para que
# los bloques de uso de herramientas u otro contenido estructurado se conserven para turnos futuros.
self.messages.append({"role": "assistant", "content": response.content})
# Concatena solo los bloques de texto para el valor de retorno visible para el llamador.
return "".join(block.text for block in response.content if block.type == "text")
if __name__ == "__main__":
convo = Conversation(system_prompt="Eres un asistente útil para planificar viajes.")
print(convo.send("Estoy planeando un viaje a Japón en abril."))
print(convo.send("¿Qué debería empacar, dado lo que te acabo de decir?"))
print(convo.send("¿Cuántos mensajes hemos intercambiado hasta ahora?"))Lo que esto demuestra:
- Una pequeña clase
Conversationposeeself.messagespara que el estado no se filtre a variables globales del módulo. send()añade el turno deluserantes de la llamada a la API y el turno delassistantdespués, manteniendo una estricta alternancia de roles intacta.- Se almacena la lista completa de
response.content, no una cadena de texto plano, para que los bloques de uso de herramientas u otro contenido estructurado sobrevivan a turnos posteriores. system_promptse establece una vez en la construcción y se reutiliza sin cambios en cada llamada de la conversación.
Análisis Profundo
Cómo Funciona
- Cada llamada a
client.messages.create()es completamente autocontenida: la API no tiene concepto de sesión, ni ID de conversación, ni memoria entre solicitudes. - La lista
messagesque envías se recorre en orden; la API valida que comience conusery alterne estrictamente los roles a partir de ahí. - La respuesta de Claude llega como un objeto
Messagecuyo.contentes una lista de bloques de contenido —típicamente un bloque detextpara una respuesta simple, pero potencialmente más para uso de herramientas o contenido mixto. - Añadir
response.content(el objeto, no la cadena derivada) como el siguiente turno delassistantes lo que permite a Claude "ver" su propio razonamiento y llamadas a herramientas anteriores en la siguiente solicitud. - Dado que se reenvía todo el array, tanto el tamaño de la carga útil de la solicitud como el número de tokens de entrada facturados crecen a medida que la conversación se alarga —cubierto más a fondo en la página de gestión de la ventana de contexto.
Crecimiento de Turnos por Intercambio
| Después | longitud de messages | Roles presentes |
|---|---|---|
| Primera pregunta del usuario | 1 | user |
| Primera respuesta de Claude añadida | 2 | user, assistant |
| Segunda pregunta del usuario | 3 | user, assistant, user |
| Segunda respuesta de Claude añadida | 4 | user, assistant, user, assistant |
Cada intercambio añade exactamente dos entradas: un user y un assistant.
Notas de Python
# Prefiere una estructura tipada sobre diccionarios sueltos si estás gestionando muchas conversaciones.
from dataclasses import dataclass, field
@dataclass
class Turn:
role: str
content: str | list
@dataclass
class Session:
messages: list[Turn] = field(default_factory=list)
def as_api_payload(self) -> list[dict]:
return [{"role": t.role, "content": t.content} for t in self.messages]Usar un pequeño tipo contenedor (o una dataclass, como la de arriba) en lugar de diccionarios crudos dispersos por tu código base hace que sea mucho más fácil serializar una conversación para su almacenamiento y recargarla más tarde sin adivinar la forma.
Trampas Comunes
- Añadir solo
response.content[0].texten lugar de la lista completa de contenido. Esto descarta los bloquestool_usey cualquier contenido estructurado, rompiendo los flujos de uso de herramientas en el siguiente turno. Solución: siempre añaderesponse.contenttal cual para el turno del asistente; extrae el texto por separado para mostrarlo. - Olvidar añadir el turno del usuario antes de llamar a la API. El array enviado a
messages.create()ya debe incluir la última pregunta. Solución: añade primero el turno del usuario, luego llama a la API, y después añade la respuesta. - Mutar la misma cadena
systema mitad de la conversación. Editar el prompt del sistema entre llamadas funciona, pero invalida silenciosamente cualquier caché de prompt construida sobre el prefijo anterior y puede cambiar el comportamiento de Claude a mitad de la conversación de maneras confusas. Solución: mantén el prompt del sistema fijo durante la vida de una conversación a menos que tengas una razón explícita para cambiarlo. - Permitir que el array de mensajes crezca sin límites. Cada solicitud reenvía todo el historial, por lo que una conversación muy larga eventualmente corre el riesgo de exceder la ventana de contexto del modelo y aumenta el costo en cada llamada. Solución: planifica recortar o resumir turnos más antiguos antes de que eso suceda.
- Compartir una lista
messagesmutable entre usuarios concurrentes. Una sola lista global utilizada por múltiples conversaciones simultáneas intercalará turnos no relacionados y romperá la alternancia. Solución: delimita una listamessagespor conversación/sesión, nunca una lista compartida a nivel de módulo. - Asumir que la API elimina duplicados o ignora contexto repetido. Enviar la misma información dos veces en el array (por ejemplo, duplicar un documento grande en dos turnos
userdiferentes) se factura como tokens de entrada separados ambas veces. Solución: incluye el contexto compartido una vez y confía en que permanecerá en el historial de la conversación en lugar de reenviarlo.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Almacenamiento de sesión del lado del servidor (base de datos/caché) con clave por ID de conversación | Múltiples usuarios, múltiples dispositivos o conversaciones que deben sobrevivir a un reinicio del proceso | Un script de corta duración o una herramienta CLI de sesión única donde el estado en memoria es suficiente |
| Compresión o resumen de turnos más antiguos | La conversación es de larga duración y se acerca a la ventana de contexto | La conversación es corta y es poco probable que crezca mucho |
Una única solicitud de un solo disparo con todo el contexto en un turno user | La tarea es una sola pregunta sin necesidad de ida y vuelta | Realmente necesitas que Claude se refiera a sus propias respuestas anteriores a través de múltiples turnos |
Preguntas Frecuentes
¿La API de Mensajes almacena mi historial de conversación por mí?
No. La API es sin estado — no tiene concepto de ID de conversación o sesión. Tu aplicación es responsable de mantener el array messages y reenviarlo en cada llamada.
¿Qué debo añadir exactamente después de que Claude responda?
Añade response.content — la lista completa de bloques de contenido — como el content de una nueva entrada {"role": "assistant", ...}. No almacenes solo la cadena de texto extraída.
¿Necesito reenviar el prompt del sistema en cada solicitud?
Sí. A diferencia de messages, system no es acumulativo — es un parámetro de nivel superior nuevo en cada llamada, así que pasas el mismo valor cada vez que deseas un comportamiento consistente.
¿Puedo editar un turno anterior en el array antes de reenviarlo?
Sí, nada te impide modificar el historial del lado del cliente — la API solo valida la alternancia de roles y la estructura, no que el contenido coincida textualmente con una respuesta anterior. Editar el historial es ocasionalmente útil para corregir la propia salida pasada de Claude, pero hazlo deliberadamente ya que cambia lo que Claude "cree" que sucedió.
¿Qué sucede si omito añadir la respuesta del asistente y solo añado mi siguiente pregunta?
Violarás la regla de alternancia — dos turnos user seguidos — y la siguiente solicitud fallará con un error 400. Cada respuesta del asistente debe ser añadida antes de que se añada el siguiente turno del usuario.
¿Hay un límite en cuántos turnos puedo tener en una conversación?
No hay un límite fijo en el número de turnos, pero el recuento total de tokens de todos los turnos más el prompt del sistema debe caber dentro de la ventana de contexto del modelo. Las conversaciones largas eventualmente necesitan ser recortadas o resumidas.
¿Debería almacenar el array de mensajes como JSON en una base de datos?
Ese es un enfoque común y razonable para cualquier cosa más allá de un simple script en memoria — almacena el array (o una representación estructurada equivalente) con clave por ID de conversación o sesión, y recárgalo antes de la siguiente llamada.
¿Reenviar el array completo cuesta más a medida que la conversación crece?
Sí. Cada solicitud se factura por todos los tokens de entrada en esa solicitud, incluido el historial reenviado, por lo que el uso de tokens y el tamaño de la solicitud aumentan a medida que una conversación se alarga.
¿Pueden dos conversaciones diferentes compartir la misma lista de mensajes de forma segura?
No. Una lista mutable compartida utilizada por conversaciones concurrentes intercalará turnos de diferentes usuarios y romperá la alternancia requerida. Delimita una lista por conversación.
¿Cuál es el array de mensajes válido mínimo?
Una única entrada con rol user y contenido no vacío. El array debe comenzar con user, y una solicitud de un solo turno sin más es una llamada completamente válida.
Si Claude llama a una herramienta, ¿eso cambia cómo construyo el array?
Se aplica el mismo patrón de añadir — el turno del asistente solicitando la herramienta se añade como de costumbre, y el resultado de la herramienta regresa como un bloque de contenido tool_result dentro del siguiente turno user, manteniendo la alternancia intacta.
¿Es seguro truncar o eliminar los primeros turnos del array yo mismo?
Sí, y es una técnica normal para conversaciones largas — solo asegúrate de que el array que envías todavía comience con un turno user y siga alternando correctamente después del truncamiento.
Relacionado
- Entendiendo los Roles y Turnos en la API de Mensajes - el modelo conceptual en el que se basa esta página.
- Conceptos Básicos de la API de Mensajes - introducción más corta y basada en ejemplos de la misma mecánica.
- Gestión del Historial de Conversaciones y Recorte para la Ventana de Contexto - qué hacer una vez que el array se vuelve demasiado grande.
- Referencia de Roles y Tipos de Bloques de Contenido de la API de Mensajes - referencia para cada tipo de bloque de contenido que puedas añadir.
Versiones de Stack: Escrito contra la línea de modelos de Claude actual a partir de ~junio de 2026 — Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado), y Claude Haiku 4.5 — y el SDK oficial de Python
anthropic(última versión 0.x). Los nombres de modelos, versiones de SDK y precios cambian rápidamente — verifica los detalles actuales en platform.claude.com/docs antes de confiar en ellos.