Evita actualizaciones de IA obsoletas con un control de versión en SQLite
En esta página
Un asistente de IA lee un ticket de soporte y propone aumentar su prioridad. Antes de aprobar la propuesta, un compañero modifica el ticket. El valor propuesto puede ser válido, pero aplicarlo ahora sobrescribiría una decisión más reciente.
Añade un control de versión a la propia escritura. Este tutorial crea una prueba local con Python que rechaza una propuesta obsoleta, acepta otra revisada de nuevo y rechaza un intento repetido. Utiliza Python 3.10 o posterior y el módulo sqlite3 de la biblioteca estándar; no requiere un modelo, una cuenta ni una base de datos persistente.
1. Vincula la aprobación al estado que se leyó
Guarda un entero version junto a cada ticket. La aplicación captura el ID y la versión al leer el registro y los conserva con la propuesta. El modelo puede sugerir una prioridad; no debe elegir una versión más reciente para eludir un conflicto.
Todos los componentes que escriben, incluidas las herramientas para usuarios humanos, deben incrementar la versión cuando cambie el estado relevante. Mantén los ID estables y nunca reinicies las versiones ni reutilices una combinación antigua de ID y versión tras eliminar un registro.
Esta protección complementa una lista de campos permitidos para cambios de IA. No sustituye la autorización ni la validación estricta del JSON producido por el modelo.
2. Incluye la comparación dentro del UPDATE
Guarda lo siguiente como guard_stale_update.py. El SQL lo define la aplicación, no el modelo. Los parámetros enlazados transportan los valores. isolation_level=None deja SQLite en modo autocommit, por lo que cada actualización independiente termina sin un commit separado en este ejemplo. Consulta la documentación de Python sobre el control de transacciones.
El cambio humano se coloca deliberadamente entre la lectura inicial y la escritura de la IA. Para el intento actualizado, suponemos que se ha revisado el registro modificado y aprobado de nuevo la propuesta; consultar la última versión no constituye una aprobación.
import sqlite3
db = sqlite3.connect(":memory:", isolation_level=None)
db.execute(
"CREATE TABLE tickets (id INTEGER PRIMARY KEY, "
"priority TEXT NOT NULL CHECK(priority IN ('low','normal','high')), "
"version INTEGER NOT NULL CHECK(version > 0))"
)
db.execute("INSERT INTO tickets VALUES (7, 'normal', 1)")
def state():
return db.execute(
"SELECT priority, version FROM tickets WHERE id = 7"
).fetchone()
def apply_priority(ticket_id, expected_version, priority):
if type(expected_version) is not int or expected_version < 1:
raise ValueError("invalid version")
if type(priority) is not str or priority not in {"low", "normal", "high"}:
raise ValueError("invalid priority")
cursor = db.execute(
"UPDATE tickets SET priority = ?, version = version + 1 "
"WHERE id = ? AND version = ?",
(priority, ticket_id, expected_version),
)
return cursor.rowcount == 1
snapshot_version = state()[1]
db.execute(
"UPDATE tickets SET priority = 'low', version = version + 1 WHERE id = ?",
(7,),
)
stale = apply_priority(7, snapshot_version, "high")
assert stale is False and state() == ("low", 2)
print("stale:", "APPLY" if stale else "REJECT", state())
reviewed_version = state()[1]
fresh = apply_priority(7, reviewed_version, "high")
assert fresh is True and state() == ("high", 3)
print("fresh:", "APPLY" if fresh else "REJECT", state())
replay = apply_priority(7, reviewed_version, "high")
assert replay is False and state() == ("high", 3)
print("replay:", "APPLY" if replay else "REJECT", state())
missing = apply_priority(99, 1, "high")
assert missing is False and state() == ("high", 3)
print("missing:", "APPLY" if missing else "REJECT")
db.close()
La comparación y el incremento de la versión ocurren en una sola sentencia. Leer y comprobar por separado, para después escribir sin condiciones, dejaría un intervalo en el que otro proceso podría intervenir.
3. Ejecuta y valida los casos de prueba
Desde el directorio del script, ejecuta:
python3 guard_stale_update.py
Salida estándar esperada:
stale: REJECT ('low', 2)
fresh: APPLY ('high', 3)
replay: REJECT ('high', 3)
missing: REJECT
Este ejemplo exacto se comprobó localmente con Python 3.11.4: código de salida 0 y salida de error vacía. Las aserciones verifican que el intento obsoleto conserva la prioridad low del compañero en la versión 2. La actualización aprobada avanza a la versión 3; repetir la versión 2 no cambia nada. Un ticket inexistente tampoco produce cambios. Es una prueba de interferencia ordenada, no una prueba de carga concurrente ni una medición de la precisión del modelo.
SQLite considera normal que una actualización no coincida con ninguna fila; no es un error de SQL. La documentación de UPDATE de SQLite explica este comportamiento; Python expone el número de filas afectadas mediante Cursor.rowcount. Aquí, solo una fila afectada significa éxito.
4. Gestiona los conflictos sin quitar la protección
- No hay excepción, pero tampoco actualización: cero filas puede indicar una versión obsoleta o un registro inexistente. Devuelve un resultado de conflicto o cambio no aplicado; no anuncies éxito.
- Los registros modificados aceptan propuestas antiguas: revisa si algún componente omite incrementar la versión. La protección depende de esa regla compartida.
- Conflictos repetidos: vuelve a cargar el registro, muestra la diferencia y solicita una nueva decisión. No asignes silenciosamente la versión actual a una aprobación antigua.
- Error de bloqueo o de restricción de la base de datos: trátalo como un error de ejecución, no como una escritura correcta ni como un conflicto de versión normal. Infórmalo por separado.
Esta protección cubre una fila. No convierte un correo, un pago ni una operación sobre varios registros en una operación atómica. Una versión actual tampoco demuestra que la prioridad propuesta sea razonable. Mantén las comprobaciones de permisos y las reglas de revisión humana fuera del modelo.
En un servicio HTTP que admita actualizaciones condicionales, el mecanismo equivalente es un ETag fuerte con If-Match. RFC 9110 lo define para evitar actualizaciones perdidas; no supongas que cualquier API lo implementa.
Conclusión práctica
Aprueba un cambio para una versión concreta y exige esa versión donde se realiza la escritura. Combínalo con una vista previa de solo lectura que se pueda revisar. Si cambia el estado subyacente, detente y revisa otra vez, en lugar de permitir que una propuesta de IA aparentemente válida sobrescriba trabajo más reciente.
Artículos relacionados
Comprueba las dependencias de tareas de IA antes de programarlas con Python
Compara versiones de prompts de IA con una prueba de regresión en Python
Revisa planes de renombrado de IA sin cambiar archivos
Comprueba las cifras de resúmenes de IA con una tabla de referencia
Comprueba citas textuales de IA con fuentes locales y Python
Evalúa probabilidades de IA con la puntuación de Brier en Python
Detecta texto repetido entre entrenamiento y prueba antes de evaluar una IA
Comprueba las horas de calendario de IA antes de crear eventos
Rechaza JSON ambiguo de IA antes de automatizar con Python
Evalúa la clasificación de incidencias con una matriz de confusión
Comprueba los marcadores de traducciones de IA con Python
Prueba expresiones regulares de IA con casos pequeños en Python
Últimos artículos
Evalúa campos extraídos por IA con precisión y recall de coincidencia exacta
Comprueba las rutas propuestas por IA antes de escribir con Python
Comprueba las dependencias de tareas de IA antes de programarlas con Python
Revisa los cambios JSON de la IA con una lista de campos permitidos
Combina resultados RAG con fusión de rangos recíprocos
Comprueba las horas de calendario de IA antes de crear eventos
Comprueba citas textuales de IA con fuentes locales y Python
Comprueba las cifras de resúmenes de IA con una tabla de referencia
Elige umbrales de revisión de IA con cobertura y tasas de error
Comprueba los marcadores de traducciones de IA con Python
Compara versiones de prompts de IA con una prueba de regresión en Python