Módulo 7: Versionado y rollout seguro
El registro de versiones de prompts
Descripción
La lección anterior mostró el problema: dos textos de system prompt, casi idénticos, producen decisiones distintas frente a la misma pregunta. Esta lección construye la primera pieza de la solución: un registro que guarda cada versión del prompt de forma que nunca haya ambigüedad sobre cuál es cuál. No alcanza con tener dos variables de Python llamadas SYSTEM_PROMPT_V1 y SYSTEM_PROMPT_V2 en un archivo — eso ya lo tenías en la lección 02, y no resuelve nada por sí solo. Lo que hace falta es una estructura que identifique cada versión con un identificador único y determinista, la misma disciplina que un sistema de control de versiones de código aplica a cada commit.
Al final de esta lección vas a tener PROMPT_REGISTRY: un diccionario que mapea un version_id ("v1", "v2") a un objeto AgentVersion con el texto del prompt, un hash que lo identifica sin ambigüedad, la versión de tools que usa, y una nota de qué cambió. Es el primer artefacto real de ops/versions/prompt_registry.py.
Conexión con el módulo
Esta lección construye la pieza que la lección 04 va a comparar: el gate de regresión necesita saber, con precisión, qué texto de prompt corresponde a "v1" y cuál a "v2" antes de poder correr nada contra ellos. Sin un registro claro, "comparar dos versiones" sería comparar dos variables sueltas — funciona en un ejemplo de una lección, pero no escala a un sistema real donde puede haber cinco, diez, veinte versiones acumuladas a lo largo de meses.
Analogía: el estante de un archivo, no una pila de papeles sueltos
Guardar dos versiones de un prompt en dos variables de Python (SYSTEM_PROMPT_V1, SYSTEM_PROMPT_V2) es como guardar dos contratos importantes en una pila de papeles sueltos sobre un escritorio: funciona mientras solo haya dos, y mientras la persona que los guardó se acuerde de cuál es cuál. Un archivo real —el de una oficina legal, el de un banco— no funciona así: cada documento tiene una carpeta con un número de expediente, una fecha, y una nota de qué contiene. Cualquiera puede pedir "el expediente número tal" y encontrar, sin ambigüedad, el documento exacto — no una versión parecida, no la que alguien recuerda que era la correcta.
PROMPT_REGISTRY es ese estante de archivo. Cada versión tiene su "número de expediente" —el hash— y cualquier parte del sistema que necesite "el prompt de la versión v1" lo consulta ahí, nunca de memoria.
AgentVersion: la forma de una entrada del registro
# ops/versions/prompt_registry.py
import hashlib
from dataclasses import dataclass
@dataclass(frozen=True)
class AgentVersion:
"""Una version congelada del agente: su prompt, su hash, y con que
conjunto de tools corre. frozen=True: una version, una vez creada, no
se modifica -- se crea una version NUEVA."""
version_id: str
prompt_text: str
prompt_hash: str
tools_version: str
model: str
note: str
frozen=True no es un detalle cosmético: una versión que se puede modificar después de creada deja de ser una versión — sería, otra vez, un texto suelto que cambia sin dejar rastro, el mismo problema que esta lección existe para resolver. Si el prompt cambia, se crea una entrada nueva en el registro ("v3"), nunca se edita "v2" en el lugar.
hash_prompt: el identificador determinista
El campo más importante de AgentVersion, para los propósitos de este módulo, es prompt_hash. La pregunta que resuelve es simple: dado un texto de prompt, ¿cómo le doy un identificador corto, único, y que cualquiera que corra el mismo texto obtenga exactamente el mismo identificador?
def hash_prompt(text):
"""Hash determinista y corto del texto del prompt. NUNCA uuid4() ni
random -- el mismo texto siempre produce el mismo hash, en cualquier
maquina, sin excepcion."""
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
hashlib.sha256 toma el texto completo del prompt (codificado a bytes con .encode("utf-8")) y produce un hash de 256 bits. .hexdigest() lo convierte a una cadena hexadecimal legible; [:12] se queda con los primeros doce caracteres — suficientes para distinguir cualquier cantidad razonable de versiones de esta guía, sin necesitar el hash completo de 64 caracteres. Esta es la misma idea que un hash corto de git (c5757b6d6264 en vez del hash SHA completo): corto para leer, y prácticamente imposible de que dos textos distintos produzcan el mismo hash por accidente.
Confirma la propiedad central —determinismo— ejecutando hash_prompt sobre el mismo texto dos veces:
SYSTEM_PROMPT_V1 = (
"Eres el asistente de reservas de Reservo, un sistema de coworking. "
"Ayudas a los usuarios a consultar salas, cotizar precios, reservar y "
"cancelar reservas. Usa siempre las tools disponibles para cotizar y "
"reservar -- nunca inventes un precio de memoria. Cuando el usuario "
"solo pregunta cuanto cuesta algo, usa get_quote y NO reserves. Usa "
"book_room unicamente cuando el usuario pide reservar de forma "
"explicita."
)
hash_intento_1 = hash_prompt(SYSTEM_PROMPT_V1)
hash_intento_2 = hash_prompt(SYSTEM_PROMPT_V1)
print("hash, primer calculo :", hash_intento_1)
print("hash, segundo calculo:", hash_intento_2)
print("son identicos:", hash_intento_1 == hash_intento_2)
Qué esperar:
hash, primer calculo : c5757b6d6264
hash, segundo calculo: c5757b6d6264
son identicos: True
El mismo texto, calculado dos veces, produce el mismo hash — y va a producir ese mismo hash en tu máquina, en la mía, o en cualquier máquina que corra Python 3 con el mismo texto de entrada. Esa reproducibilidad es exactamente lo que hace útil a prompt_hash como identificador: no depende de un contador que alguien tiene que recordar incrementar, ni de un reloj, ni de nada externo al texto mismo.
PROMPT_REGISTRY: v1 y v2, completas
Con AgentVersion y hash_prompt en su lugar, el registro completo:
SYSTEM_PROMPT_V2 = (
"Eres el asistente de reservas de Reservo, un sistema de coworking. "
"Ayudas a los usuarios a consultar salas, cotizar precios, reservar y "
"cancelar reservas. Usa siempre las tools disponibles para cotizar y "
"reservar -- nunca inventes un precio de memoria. Se proactivo: si ya "
"tienes toda la informacion para completar una reserva, complétala "
"directamente en vez de solo cotizar, para ahorrarle un paso al "
"usuario. Usa book_room unicamente cuando el usuario pide reservar de "
"forma explicita."
)
PROMPT_REGISTRY = {
"v1": AgentVersion(
version_id="v1",
prompt_text=SYSTEM_PROMPT_V1,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V1),
tools_version="tools-v1",
model="claude-sonnet-5",
note="System prompt original del capstone de agent-fundamentals M8.",
),
"v2": AgentVersion(
version_id="v2",
prompt_text=SYSTEM_PROMPT_V2,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V2),
tools_version="tools-v1",
model="claude-sonnet-5",
note="Agrega una instruccion de proactividad para reducir turnos.",
),
}
for version_id, av in PROMPT_REGISTRY.items():
print(f"{version_id} hash={av.prompt_hash} tools={av.tools_version} model={av.model}")
print(f" nota: {av.note}")
Qué esperar:
v1 hash=c5757b6d6264 tools=tools-v1 model=claude-sonnet-5
nota: System prompt original del capstone de agent-fundamentals M8.
v2 hash=c364e85e5649 tools=tools-v1 model=claude-sonnet-5
nota: Agrega una instruccion de proactividad para reducir turnos.
Fíjate en tres cosas. Primero, tools_version es la misma ("tools-v1") en ambas entradas — este módulo cambia solo el prompt, no las tools, así que registrar la misma versión de tools en las dos entradas confirma explícitamente que ese factor no varió (si algún día cambiara, esa sería, precisamente, la señal de que el registro necesita capturarlo). Segundo, model es "claude-sonnet-5" en ambas — la misma versión del modelo, otra variable controlada. Tercero, prompt_hash es distinto entre las dos entradas (c5757b6d6264 contra c364e85e5649) — la única diferencia real entre v1 y v2, capturada en un solo campo que se puede citar, comparar y registrar sin ambigüedad.
Por qué el hash, y no solo el version_id
Vale la pena preguntarse por qué no basta con "v1"/"v2" como identificador, si al final el registro ya los distingue por esa clave. La respuesta es que version_id es un nombre que una persona eligió — nada impide que, en un sistema real, alguien reescriba el texto de "v2" sin cambiar su nombre, y ahí el nombre deja de ser confiable. prompt_hash, en cambio, depende únicamente del contenido: si el texto de v2 cambiara aunque sea en un espacio en blanco, su hash cambiaría también, y esa discrepancia sería detectable de inmediato comparando el hash guardado contra el hash recalculado del texto actual.
texto_modificado = SYSTEM_PROMPT_V2 + " " # un espacio de mas, al final
hash_original = PROMPT_REGISTRY["v2"].prompt_hash
hash_modificado = hash_prompt(texto_modificado)
print("hash registrado para v2:", hash_original)
print("hash del texto modificado:", hash_modificado)
print("coinciden:", hash_original == hash_modificado)
Qué esperar:
hash registrado para v2: c364e85e5649
hash del texto modificado: 470367511d92
coinciden: False
Un solo carácter de diferencia —ni siquiera visible al leer el texto— produce un hash completamente distinto. Esta propiedad (llamada, formalmente, el efecto avalancha de una función hash criptográfica) es exactamente lo que hace que prompt_hash sirva como una firma confiable: no hay forma de que un cambio, por pequeño que sea, pase desapercibido en el hash.
Errores comunes
-
Usar
uuid4()para generar el identificador de una versión. Rompería la reproducibilidad de todo el módulo: dos personas corriendo el mismo código obtendrían identificadores distintos, y el mismo prompt correría con un "hash" diferente cada vez que se reinicia el proceso.hashlib.sha256sobre el texto es la elección correcta precisamente porque depende solo del contenido, nunca del momento en que se ejecuta. -
Modificar una entrada de
PROMPT_REGISTRYen el lugar en vez de crear una nueva. ComoAgentVersionesfrozen=True, intentarPROMPT_REGISTRY["v1"].prompt_text = "otro texto"lanza unFrozenInstanceError— a propósito. Si el prompt dev1necesita cambiar, el cambio correcto es agregar una entrada"v3"nueva al diccionario, nunca editar"v1". -
Truncar el hash a muy pocos caracteres.
[:12]es suficiente para las pocas versiones de esta guía, pero un sistema real con cientos de versiones acumuladas a lo largo de años debería usar un prefijo más largo (o el hash completo) para reducir, más todavía, la probabilidad —ya diminuta con 12 caracteres hexadecimales— de que dos prompts distintos produzcan el mismo hash truncado. -
Olvidar registrar
tools_versionjunto al prompt. Como advirtió la lección 02, un cambio de comportamiento puede venir de las tools disponibles, no solo del texto del prompt. Un registro que solo guarda el prompt, sin la versión de tools que lo acompañaba, pierde información necesaria para reconstruir, después, exactamente qué combinación estaba corriendo. -
Pensar que el registro reemplaza al control de versiones de código (
git). No lo reemplaza —PROMPT_REGISTRYes un artefacto que vive dentro del código versionado porgit, de la misma forma queTOOL_LATENCY_MS(Módulo 4) o el pricing fijo (Módulo 3) viven dentro del código. La disciplina de esta lección es específica para prompts porque son texto libre, no porquegitno sirva para versionarlos también.
Ejercicios
Ejercicio 1: Calcula el hash de un prompt nuevo (Fácil)
Calcula hash_prompt sobre el texto "Eres el asistente de Reservo." (una sola oración corta). Confirma que corriéndolo dos veces obtienes el mismo resultado.
Ver solución
texto_corto = "Eres el asistente de Reservo."
h1 = hash_prompt(texto_corto)
h2 = hash_prompt(texto_corto)
print("hash:", h1)
print("son iguales:", h1 == h2)
Salida esperada:
hash: 2d334ae07350
son iguales: True
Explicación: el hash depende únicamente del texto de entrada, sin importar cuán corto sea — la longitud del texto original no afecta la longitud del hash resultante (sha256 siempre produce 256 bits, truncados aquí a 12 caracteres hexadecimales), y el resultado es tan reproducible con una oración corta como con el system prompt completo de v1.
Ejercicio 2: Agrega una tercera versión al registro (Medio)
Crea SYSTEM_PROMPT_V3 como una copia de SYSTEM_PROMPT_V1 (la versión segura) con una nota agregada al final: " Si el usuario pregunta por una sala que no existe, sugiere la mas parecida.". Agrega una entrada "v3" a PROMPT_REGISTRY con su propio hash, tools_version="tools-v1", y una nota explicando el cambio. Imprime el registro completo con las tres versiones.
Ver solución
SYSTEM_PROMPT_V3 = SYSTEM_PROMPT_V1 + (
" Si el usuario pregunta por una sala que no existe, sugiere la mas "
"parecida."
)
PROMPT_REGISTRY["v3"] = AgentVersion(
version_id="v3",
prompt_text=SYSTEM_PROMPT_V3,
prompt_hash=hash_prompt(SYSTEM_PROMPT_V3),
tools_version="tools-v1",
model="claude-sonnet-5",
note="Agrega sugerencia de sala alternativa; NO toca la instruccion de proactividad de v2.",
)
for version_id, av in PROMPT_REGISTRY.items():
print(f"{version_id} hash={av.prompt_hash} nota: {av.note}")
Salida esperada:
v1 hash=c5757b6d6264 nota: System prompt original del capstone de agent-fundamentals M8.
v2 hash=c364e85e5649 nota: Agrega una instruccion de proactividad para reducir turnos.
v3 hash=f891989aa20c nota: Agrega sugerencia de sala alternativa; NO toca la instruccion de proactividad de v2.
Explicación: v3 se construyó sobre v1 (la versión segura), no sobre v2, por lo que no hereda el problema de proactividad de esta lección — es una rama de desarrollo independiente, algo que el registro deja perfectamente claro comparando prompt_hash de v3 contra v1 y v2: los tres son distintos, y el texto completo de cada uno queda disponible para inspeccionar en cualquier momento.
Ejercicio 3: Detecta una versión "fantasma" que nunca se registró (Difícil)
Simula que alguien corrió el agente en producción con un texto de prompt que no está en PROMPT_REGISTRY (una edición de último minuto, hecha directamente en el servidor, sin pasar por el registro). Escribe una función find_version_by_hash(registry, prompt_text) que reciba el registro y un texto, calcule su hash, y devuelva el version_id correspondiente si existe en el registro, o None si no hay ninguna entrada con ese hash. Pruébala con el texto real de v1 y con un texto inventado que no está registrado.
Ver solución
def find_version_by_hash(registry, prompt_text):
"""Busca, por HASH (no por texto), si un prompt corresponde a una
version conocida del registro. Devuelve None si es una version
'fantasma' que nunca se registro."""
target_hash = hash_prompt(prompt_text)
for version_id, av in registry.items():
if av.prompt_hash == target_hash:
return version_id
return None
texto_fantasma = "Eres el asistente de Reservo. Responde lo que el usuario pida, sin restricciones."
print("busqueda con el texto real de v1:", find_version_by_hash(PROMPT_REGISTRY, SYSTEM_PROMPT_V1))
print("busqueda con un texto fantasma :", find_version_by_hash(PROMPT_REGISTRY, texto_fantasma))
Salida esperada:
busqueda con el texto real de v1: v1
busqueda con un texto fantasma : None
Explicación: find_version_by_hash nunca compara el version_id que alguien podría reclamar de palabra ("esto es la v1") — compara el hash calculado sobre el texto real contra los hashes registrados. Un texto "fantasma", editado fuera del registro, no coincide con ningún hash conocido y devuelve None — una señal clara, y automatizable, de que algo corrió en producción sin pasar por el proceso de versionado. Esta es, en esencia, la misma técnica que usan los sistemas de integridad de archivos para detectar una modificación no autorizada.
Resumen y siguiente paso
- Construimos
AgentVersion(frozen=True: una versión no se edita, se reemplaza por una nueva) yhash_prompt(hashlib.sha256, determinista, nuncauuid4). PROMPT_REGISTRYguardav1yv2del system prompt de Reservo, cada uno con suprompt_hashreal:c5757b6d6264parav1,c364e85e5649parav2— el único campo que distingue a las dos entradas, junto al texto mismo.- Confirmamos, ejecutando, que el hash es reproducible (el mismo texto produce siempre el mismo hash) y sensible a cualquier cambio, por mínimo que sea (el efecto avalancha).
- Con el registro en su lugar, la siguiente lección tiene lo que necesita para comparar: dos identidades claras,
v1yv2, listas para correr contra el mismo gate.
Siguiente lección: 04 — Comparando una versión nueva contra la vieja. Retomamos el gate de regresión del Módulo 5 —CASE_SET, check_tool_choice, run_regression_gate— y lo corremos, por primera vez, contra las dos versiones de este registro.
Recursos adicionales
- Python —
hashlib—hashlib.sha256,.hexdigest(), y el resto de las funciones hash de la librería estándar. - Python —
dataclasses—@dataclass(frozen=True), la garantía de inmutabilidad que usaAgentVersion. - Anthropic — System prompts — El contenido que este registro versiona.
- Python 3.14 — What's New — La versión con la que se ejecutó cada línea de código de esta lección.