Saltar al contenido

Capítulo 18 de 18

Miniproyecto · Caídas, pruebas y evolución

Comprueba recuperación con procesos que terminan de golpe, interpreta cada resultado y planifica la evolución según límites observados.

#Comprobar la garantía que acabas de construir

Las dos páginas anteriores producen un programa completo. Ahora vamos a intentar romperlo en lugares donde un sistema ingenuo perdería trabajo o duplicaría un informe. La pregunta no es si el proceso vuelve a arrancar, sino si el estado recuperado representa lo que realmente ocurrió.

El laboratorio usa bases de datos distintas para cada caso. No borres tu base habitual ni ejecutes las pruebas sobre datos importantes. La variable AGENT_DB solo selecciona el archivo de este programa; no cambia ninguna configuración global del sistema.

#Caída 1 · Hay decisión, todavía no hay efecto

Crea una ejecución nueva y termina el proceso justo después de aceptar la decisión report:

AGENT_DB=caida-plan.db python3 agente.py new caso-caida "api db 5433"
AGENT_DB=caida-plan.db CRASH_AT=after_plan python3 agente.py work caso-caida
AGENT_DB=caida-plan.db python3 agente.py show caso-caida

El segundo comando termina deliberadamente con código 92. La ejecución debe seguir en running, con tres operaciones confirmadas y una decisión report en pending. No existe informe todavía. La decisión pendiente contiene exactamente el texto y las fuentes que se pretendían guardar.

Reanuda sin la variable de caída:

AGENT_DB=caida-plan.db python3 agente.py work caso-caida

El worker no vuelve a pedir al modelo que redacte otra cosa. Ejecuta la propuesta guardada, confirma el informe y pasa a completed. Esta es la razón para persistir la decisión antes del efecto.

#Caída 2 · El efecto ocurrió dentro de una transacción no confirmada

Repite con otra base y CRASH_AT=inside_report:

AGENT_DB=caida-transaccion.db python3 agente.py new caso-caida "api db 5433"
AGENT_DB=caida-transaccion.db CRASH_AT=inside_report python3 agente.py work caso-caida
AGENT_DB=caida-transaccion.db python3 agente.py show caso-caida
AGENT_DB=caida-transaccion.db python3 agente.py work caso-caida

La caída ocurre después del INSERT del informe, antes de guardar operación y avance. Al reabrir, SQLite recupera la transacción sin confirmar: no aparece un informe a medias. La decisión pendiente sigue existiendo porque se confirmó en una transacción anterior. Al reanudar se ejecuta una vez y se confirma todo junto.

No generalices este resultado a una API externa. Si execute hubiera enviado un correo antes de caer, SQLite no podría retirarlo. Ese efecto necesitaría una operación idempotente o reconciliación en el receptor.

#Caída 3 · Todo se confirmó, pero el proceso no llegó a contarlo

Con CRASH_AT=after_commit, el informe, observación y avance ya son durables cuando termina el proceso:

AGENT_DB=caida-confirmada.db python3 agente.py new caso-caida "api db 5433"
AGENT_DB=caida-confirmada.db CRASH_AT=after_commit python3 agente.py work caso-caida
AGENT_DB=caida-confirmada.db python3 agente.py work caso-caida
AGENT_DB=caida-confirmada.db python3 agente.py work caso-caida

Ambas reanudaciones encuentran completed y no repiten acciones. Debe seguir habiendo cuatro operaciones y un informe. Que la terminal no mostrara éxito antes de la caída no implica que faltara el efecto: la base es la evidencia de esta implementación.

Fronteras de recuperación y qué puede decidir un worker tras reiniciar.Ampliar diagrama ↗

Las tres pruebas recorren fronteras diferentes. Cubrir solo la caída anterior a la escritura no demostraría que se evita duplicar un efecto ya confirmado.

#Una comprobación automática de las tres fronteras

Este bloque es un programa de prueba separado; no lo añadas a agente.py. Puedes copiarlo en comprobar.py, situado al lado, y ejecutarlo con python3 comprobar.py. Usa directorios temporales y llama al programa real en procesos distintos.

import os
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path

script = Path("agente.py").resolve()
with tempfile.TemporaryDirectory() as directory:
    for mode in ["normal", "after_plan", "inside_report", "after_commit"]:
        path = str(Path(directory) / f"{mode}.db")
        env = {**os.environ, "AGENT_DB": path}
        env.pop("OLLAMA_MODEL", None)
        env.pop("CRASH_AT", None)
        subprocess.run([sys.executable, str(script), "new", "r-1", "api db 5433"],
                       env=env, check=True, capture_output=True)
        attempt = subprocess.run([sys.executable, str(script), "work", "r-1"],
                                 env={**env, "CRASH_AT": mode}, capture_output=True)
        assert attempt.returncode == (0 if mode == "normal" else 92)
        db = sqlite3.connect(path)
        count = db.execute("SELECT COUNT(*) FROM reports").fetchone()[0]
        assert count == (1 if mode in {"normal", "after_commit"} else 0)
        db.close()
        for _ in range(2):
            subprocess.run([sys.executable, str(script), "work", "r-1"],
                           env=env, check=True, capture_output=True)
        db = sqlite3.connect(path)
        assert db.execute("SELECT status FROM runs").fetchone()[0] == "completed"
        assert db.execute("SELECT COUNT(*) FROM reports").fetchone()[0] == 1
        assert db.execute("SELECT COUNT(*) FROM operations").fetchone()[0] == 4
        assert db.execute("SELECT pending FROM runs").fetchone()[0] is None
        db.close()
        print(mode, "→ 4 operaciones, 1 informe, sin decisión pendiente")

La salida debe contener cuatro líneas con esos invariantes. Este test se ha ejecutado durante la preparación del curso. Prueba reinicios de proceso y transacciones locales; no simula pérdida física del disco ni verifica un modelo real.

#Cancelación, límites y entradas inválidas

Para comprobar que cancelar impide empezar nuevas acciones:

AGENT_DB=cancelacion.db python3 agente.py new caso-cancelado "api db"
AGENT_DB=cancelacion.db python3 agente.py cancel caso-cancelado
AGENT_DB=cancelacion.db python3 agente.py work caso-cancelado

Debe quedar cancelled y no tener operaciones ni informe. Para estudiar cancelación durante inferencia, un test puede bloquear propose, cancelar desde otra conexión y liberar la respuesta. La actualización condicional debe rechazarla por revisión o estado. No se necesita un modelo lento real para provocar esa carrera.

El límite de ocho acciones evita que un modelo repita búsquedas indefinidamente. El límite de doce solicitudes cuenta inferencias admitidas aunque fallen. En una aplicación real añade deadline, cuota de tokens y reservas compartidas entre hijos. Aquí el presupuesto limita vueltas; no afirma un límite monetario.

Prueba asimismo una fuente nunca leída, un identificador inexistente y un objeto con claves extra. La fuente no leída produce una observación para corregir; una forma inválida aborta antes de ejecutar. El run conserva lo confirmado. Después de un error debes inspeccionar la causa antes de reintentar, no repetir indefinidamente el mismo fallo.

#Diagnóstico por síntomas

Síntoma Qué mirar Razón
database is locked Otro escritor o transacción demasiado larga SQLite permite un escritor simultáneo
Se queda con pending Error al ejecutar esa decisión La propuesta está guardada para poder diagnosticar
budget_exhausted Historial y número de solicitudes Puede haber repetición o intentos fallidos
No aparece un informe Fuentes leídas, errores de herramienta y estado Guardar una cita requiere lectura previa
Funciona simulado y falla real Respuesta del adaptador, formato y capacidad El simulador no prueba comportamiento del modelo
El informe cita bien pero concluye mal Contenido de fuentes y afirmaciones La validación estructural no demuestra soporte semántico

No «repares» un estado cambiándolo a completed a mano. Corrige la causa, conserva la evidencia y define si se puede reanudar la intención o hace falta una nueva ejecución.

#Evolucionar sin reescribir la idea central

El siguiente cambio debe responder a un límite que puedas observar:

Límite encontrado Evolución Contrato que debes conservar
Documentos reales grandes Ingesta, fragmentos, versiones y contexto acotado Toda cita apunta a evidencia recuperable
Varios usuarios Autenticación y permisos en cada consulta El modelo no elige su identidad
Herramientas lentas o remotas Operaciones pendientes, idempotencia y reconciliación Un timeout no se interpreta como ausencia de efecto
Varios workers o máquinas Cola, leases y fencing, base de servidor Solo la autoridad vigente confirma avance
Interfaz de progreso Endpoint de consulta y SSE con cursor Reconectar no vuelve a ejecutar herramientas
Investigación paralela Hijos con briefing, inbox y admisión atómica Una entrega repetida se incorpora una vez
Trayectorias largas Constructor de contexto y compaction versionada El resumen no sustituye estado ni artefactos

Estas ampliaciones ya tienen su mecanismo desarrollado en los capítulos anteriores. Por ejemplo, el siguiente paso hacia varios workers no es poner work detrás de cinco procesos y esperar: debes mover la ejecución lenta fuera de la transacción y aplicar el protocolo de propiedad de la ejecución. Para efectos externos, usa el receptor idempotente como referencia de la garantía que debe ofrecer el destino.

#Decisiones que ahora deberías poder justificar

Debes poder explicar por qué se guarda la propuesta antes del efecto, por qué el modelo se llama fuera de la transacción, qué significa cada caída, por qué un hash no prueba verdad y qué parte de la recuperación dejaría de funcionar si el informe se enviara a un servicio remoto.

También deberías poder distinguir el objetivo del producto de la calidad del modelo. Este miniproyecto produce un borrador documentado y recuperable. Convertirlo en una herramienta profesional requiere evaluar respuestas reales, aplicar permisos de tu dominio y operar las garantías descritas; no se consigue únicamente cambiando el nombre del modelo.

#Referencias

    ↑ ↓ para recorrer · Intro para abrir · Ctrl K para buscar