Loop Engineering: Cuatro bucles anidados para agentes de IA automejorables
Ingeniería de bucles para agentes de IA automejorables: cuatro bucles anidados (ReAct, reintentos, revisor de colas, escalada de colinas) además de un juez, un medidor de presupuesto y una verificación de alteraciones.
Fuentes: Loop Engineering: Building Self-Improving AI Agents with Four Nested Loops
Durante aproximadamente dos años, trabajar con un modelo significó turnarse. Un ingeniero escribe una instrucción, lee lo que recibe como respuesta y proporciona manualmente el siguiente fragmento de contexto. Esa rutina conlleva un costo oculto, una especie de impuesto por la coordinación, ya que la velocidad de todo el proceso está limitada por la rapidez con la que una persona puede revisar las respuestas. Superar esa limitación implica un cambio de enfoque: dejar de utilizar la herramienta y comenzar a diseñar el sistema que la contenga.
Tres ideas definen ese cambio.
- La influencia asciende un nivel. El papel pasa de redactar instrucciones exactas a diseñar bucles que se repiten, y eventualmente a algo similar a la posición de un director. El ser humano proporciona el objetivo y la condición de parada, mientras que el agente se encarga de cada paso del camino entre ellos.
- Los bucles autónomos tienen una anatomía reconocible. Ellos encuentran el trabajo, asignan tareas a subagentes, verifican los resultados con pruebas independientes y registran su estado, lo que permite que la persona se aleje del proceso de triaje, corrección y verificación.
- La ingeniería de objetivos ocupa la posición más importante. En este nivel, los mecanismos internos del bucle pasan a un segundo plano. AlphaEvolve, que superó los récords de multiplicación matricial vigentes desde 1969, y Kiro de Amazon, que convierte objetivos de alto nivel en requisitos y especificaciones, ilustran este patrón: si se define correctamente la función de puntuación, la máquina encuentra la respuesta.
Para diseñar un bucle alrededor de un agente, se necesita un modelo mental claro del agente que se encuentra dentro de él. Por lo tanto, el orden de construcción lógico comienza con un trabajador que simplemente llama a herramientas, se agrega un segundo modelo que evalúa su salida (de modo que el sistema deje de calificar su propio trabajo), se conectan los dos en un bucle y se guardan las mejoras en disco, ya que los modelos olvidan mientras que un repositorio no. Después vienen las defensas contra que un agente manipule las métricas, ese tipo de trampas que se detectaron cuando se probó o3 de OpenAI con METR, luego un medidor que registra el costo en un entorno que puede ser rico o pobre en tokens, y finalmente el paso de un script de 70 líneas a algo que pueda funcionar en producción.
Lo que sigue es una implementación funcional de toda esa idea, utilizando un proceso de aprobación de seguros como entorno de pruebas.
Por qué esto es un problema de arquitectura
Pasar de un modelo que responde preguntas a un agente que toma acciones de manera fiable no es un ejercicio de redacción de prompts. Es un problema de diseño. La mayoría de los tutoriales muestran un agente que utiliza una sola herramienta. Los sistemas reales necesitan tolerancia a fallos, autocorrección, límites de gasto, detección de manipulaciones y mejora constante, todo al mismo tiempo.
El enfoque aquí se denomina ingeniería de bucles: se anidan varios bucles de control, cada uno con una tarea específica, de modo que todo el sistema resista mejor que cualquier bucle individual.
La suscripción de seguros es un dominio conveniente. Las decisiones provienen de un conjunto pequeño y fijo (aceptar, modificar, rechazar, remitir), las reglas son explícitas y los datos históricos proporcionan respuestas correctas e incorrectas. Los mismos patrones se aplican a la revisión de código, el análisis de documentos, la triage de soporte y el procesamiento de historiales médicos.
Cuatro bucles, uno dentro del otro
El software ordinario controla la complejidad mediante capas de abstracción. La ingeniería de bucles controla la falta de fiabilidad de un agente a través de capas de retroalimentación:
┌──────────────────────────────────────────────────────┐
│ Loop 4 — Hill Climbing (self-improvement over time) │
│ ┌────────────────────────────────────────────────┐ │
│ │ Loop 3 — Event Queue Poller (orchestration) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Loop 2 — Verification & Retry │ │ │
│ │ │ ┌────────────────────────────────────┐ │ │ │
│ │ │ │ Loop 1 — ReAct Agent │ │ │ │
│ │ │ │ (think → act → observe → think) │ │ │ │
│ │ │ └────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
Cada bucle tiene una responsabilidad, un fallo contra el cual se protege y una señal de retroalimentación a la que reacciona. La disciplina importante es evitar que las preocupaciones se filtren entre los bucles.
El dominio
Antes de la arquitectura, aquí están los datos. Una solicitud de seguro es un registro plano:
{
"id": "APP-001",
"applicant_age": 34,
"state": "FL",
"property_type": "residential",
"coverage_amount": 450000,
"claim_history_5yr": 1,
"credit_score_tier": "good",
"flood_zone": true,
"has_flood_rider": false,
"business_use": false
}
Cada una de las 20 aplicaciones de ejemplo también cuenta con una etiqueta de referencia veraz:
"APP-001": {
"split": "train",
"expected_decision": "modify",
"expected_flags": ["flood_rider_required", "windstorm_exclusion"]
}
Una decisión es una de cuatro opciones:
- aceptar: riesgo estándar, aprobarlo tal como está
- modificar: aprobar con condiciones o adiciones
- rechazar: el riesgo supera el umbral aceptable
- referir: un suscriptor senior debe analizarlo
Existe una pequeña base de conocimientos que contiene cinco reglas de suscripción ocultas relacionadas con zonas inundables, historial de reclamaciones, puntaje de crédito, límites de cobertura y restricciones específicas por estado. Al agente no se le informan estas reglas en sus instrucciones iniciales; debe encontrarlas y aplicarlas mediante llamadas a herramientas. Dieciséis solicitudes forman el conjunto de entrenamiento y cuatro se reservan para pruebas.
Bucle 1: un agente ReAct
Protege contra: un modelo que toma decisiones sin evidencia. Señal de retroalimentación: el grafo de llamadas a herramientas, que indica si el agente consultó todo lo relevante antes de decidir.
ReAct, abreviatura de reason plus act, es el patrón básico. El agente piensa en lo que necesita, llama a una herramienta, lee el resultado, vuelve a pensar y repite el proceso hasta tomar una decisión final:
System Prompt
│
▼
[Think] → What information do I need?
│
▼
[Act] → Call tool (lookup_application, retrieve_rules, ...)
│
▼
[Observe] → Tool returns result
│
▼
[Think] → What does this mean? What next?
│
... (repeat until decision is made)
│
▼
[Act] → draft_decision (terminal tool call)
En LangGraph esto se convierte en un grafo de estados con dos nodos y un arco condicional. El estado contiene el ID de la aplicación, la lista de mensajes, los campos de decisión, las lecciones, el gasto en curso y un contador de pasos:
class UnderwritingState(TypedDict):
application_id: str
messages: Annotated[list[AnyMessage], add_messages] # reducer accumulates
decision: str
rationale: str
flags: list[str]
recommended_premium_adj: float
lessons: list[str]
spend: float
_steps: int
Un detalle merece atención. La anotación add_messages hace que LangGraph añada elementos a la lista de mensajes en cada paso en lugar de reemplazarla, de modo que el agente conserva toda la conversación a lo largo de cada llamada a herramientas para una misma aplicación.
El generador toma el prompt del sistema y el medidor de costos compartido, y vincula las herramientas al modelo:
def build_loop1(system_prompt: str, meter: CostMeter):
model = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(TOOLS)
Dentro de él se encuentran el nodo de agente, el nodo de herramientas, la función de enrutamiento y el ensamblaje del grafo:
def agent_node(state: UnderwritingState) -> dict:
prompt = system_prompt
if state.get("lessons"):
prompt += "\n\nUnderwriting lessons:\n" + "\n".join(
f"- {l}" for l in state["lessons"]
)
msgs = [SystemMessage(content=prompt)] + list(state["messages"])
response = model.invoke(msgs)
meter.record_from_message(response) # track spend per call
return {
"messages": [response],
"_steps": state.get("_steps", 0) + 1,
"spend": meter.spent,
}
def tools_node(state: UnderwritingState) -> dict:
last = state["messages"][-1]
tool_results, updates = _dispatch_tools(last.tool_calls)
return {"messages": tool_results, **updates} # updates captures draft_decision output
def should_continue(state) -> Literal["tools", "__end__"]:
if state.get("_steps", 0) >= MAX_STEPS: # hard cap at 8 steps
return END
if getattr(state["messages"][-1], "tool_calls", None):
return "tools"
return END
g = StateGraph(UnderwritingState)
g.add_node("agent", agent_node)
g.add_node("tools", tools_node)
g.set_entry_point("agent")
g.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
g.add_edge("tools", "agent")
return g.compile()
Las cuatro herramientas
El agente cuenta con exactamente cuatro herramientas, suficientes para cubrir todo lo que necesita:
@tool
def lookup_application(app_id: str) -> dict:
"""Return the full application record for the given application ID."""
# reads from applications.json — deterministic, no LLM call
...
@tool
def retrieve_rules(risk_factor: str) -> list[str]:
"""Return underwriting rules matching the given risk factor keyword.
Use terms like: flood, claims, credit, coverage, state, business."""
# keyword match against rules_kb.json - forces explicit rule lookup
...
@tool
def fetch_risk_signal(signal_type: str) -> dict:
# simulated external data pull (credit bureaus, flood maps, etc.)
...
@tool
def draft_decision(
decision: Literal["accept", "modify", "decline", "refer"],
rationale: str,
flags: list[str],
recommended_premium_adj: float,
) -> dict:
"""Finalize the underwriting decision with structured output."""
# terminal action - structured schema forces the agent to commit explicitly
...
lookup_application solo lee datos. retrieve_rules realiza una búsqueda por palabras clave en un conjunto de reglas fijo, lo que obliga a una búsqueda explícita. fetch_risk_signal simula la obtención de datos externos como los de las agencias de crédito o los mapas de inundaciones. draft_decision es la acción final y es la más importante; hace que el agente adopte una decisión escrita con una justificación y flags específicos, de modo que el resultado estructurado proviene directamente de la llamada a la herramienta y nadie necesita analizar texto libre.
Por qué es importante el límite de pasos
MAX_STEPS = 8 evita cadenas interminables de herramientas. Un modelo confundido podría llamar a la función de búsqueda de reglas relacionadas con “inundaciones” veinte veces, buscando una respuesta que no existe, y así desperdiciar tokens y tiempo. Cuando el recuento de pasos alcanza el límite, el estado recibe la decisión ran_out_of_steps, una señal que Loop 2 puede reconocer para intentarlo de nuevo.
Bucle 2: verificar y volver a intentar
Protege contra: una sola respuesta incorrecta debido a un razonamiento erróneo.
Señal de retroalimentación: el juez determinista, is_correct(), sin intervención de ningún modelo.
El Bucle 1 puede fallar. El agente podría interpretar mal una marca, omitir una regla o alcanzar el límite de pasos. El Bucle 2 detecta esas fallas y le da al agente otra oportunidad con retroalimentación específica:
def run_loop2(
app_id: str,
system_prompt: str,
meter: CostMeter,
max_retries: int = 3,
) -> tuple[dict, bool]:
state = run_loop1(app_id, system_prompt, meter)
for attempt in range(max_retries):
decision = state.get("decision", "ran_out_of_steps")
if decision == "refer":
_write_human_queue(app_id, state) # human-in-the-loop path
return state, True
if is_correct(decision, app_id): # deterministic check
return state, True
if attempt >= max_retries - 1:
return state, False # exhausted retries
# construct targeted feedback for next attempt
expected = load_historical_decisions()[app_id]["expected_decision"]
feedback = HumanMessage(content=(
f"Your decision was '{decision}' but this application requires '{expected}'. "
f"Review the risk factors carefully and call draft_decision again."
))
extra_messages = list(state.get("messages", [])) + [feedback]
state = run_loop1(app_id, system_prompt, meter, extra_messages=extra_messages)
return state, False
Algunas decisiones de diseño merecen ser explicadas con detalle.
El juez es determinista. is_correct() lee el archivo con las decisiones históricas y compara cadenas de texto. Nunca llama a un modelo. Utilizar un segundo modelo como juez añadiría una segunda fuente de falta de fiabilidad, ya que aquí la verdad objetiva simplemente es la verdad objetiva.
La remisión es un resultado ya gestionado. Cuando el agente indica refer, el Bucle 2 escribe el caso en human_queue.json e informa de éxito. Escalar la tarea a un humano se considera un estado final válido y no algo que deba intentarse de nuevo.
La retroalimentación indica el objetivo. En lugar de decir “intenta de nuevo”, el mensaje indica al agente qué categoría de decisión es la correcta y le pide que vuelva a analizar los factores de riesgo. Esto orienta la siguiente ejecución hacia la respuesta correcta, mientras que el muestreo aleatorio ciego podría llevarlo por caminos erróneos.
La historia se transmite. Los mensajes adicionales incluyen toda la conversación anterior más los comentarios, de modo que en su siguiente ejecución el agente ve todo lo que ya intentó y es menos probable que repita el mismo camino incorrecto.
Bucle 3: el revisor de la cola de eventos
Protege contra: una fuerte vinculación entre quien genera el trabajo y quien lo consume.
Señal de retroalimentación: el contenido de pending_applications.json, un archivo sencillo que funciona como cola.
Este bucle es el orquestador. Extrae elementos de la cola pendiente, hace que una aplicación pase por los Bucles 1 y 2, actualiza el estado de ejecución y, de vez en cuando, activa el Bucle 4:
def run_event_loop(
system_prompt: str,
meter: CostMeter,
pending_path: Path = DEFAULT_PENDING,
state_path: Path = DEFAULT_STATE,
once: bool = False,
) -> None:
run_state = load_run_state(state_path)
verify_judge_checksum(run_state.get("judge_checksum", "")) # tamper check
total = len(json.loads(pending_path.read_text(encoding="utf-8-sig"))
if pending_path.exists() else [])
processed = 0
while True:
pending = (json.loads(pending_path.read_text(encoding="utf-8-sig"))
if pending_path.exists() else [])
if not pending:
if once:
print("[loop3] queue empty - done")
break
print("[loop3] queue empty - sleeping...")
time.sleep(POLL_INTERVAL)
continue
app_id = pending[0]
processed += 1
print(f"[loop3] {processed}/{total} {app_id} ...", flush=True)
try:
result_state, passed = run_loop2(app_id, prompt_with_lessons, meter)
except BudgetExhaustedError:
raise # budget exhaustion is fatal
except Exception as exc:
print(f"[loop3] {app_id} ERROR: {exc!r} - skipping")
# remove from queue and continue - one bad app cannot block the rest
remaining = [x for x in pending if x != app_id]
pending_path.write_text(json.dumps(remaining), encoding="utf-8")
continue
# ... update state, distill lesson, trigger hill-climbing
Por qué un archivo para la cola
La cola es un archivo JSON y no un flujo de Redis ni una cola de SQS, a propósito, porque para el desarrollo local es fácil de inspeccionar (abrir el archivo y ver qué está pendiente), fácil de reanudar (reiniciar después de una caída y la cola permanece intacta) y desacoplada (lo que la llena no necesita conocer al consumidor). Un sistema de producción utilizaría un broker de mensajes adecuado, y el contrato de interfaz, que es una lista de IDs, no cambiaría.
Detección de manipulaciones
Antes de procesar cualquier cosa, Loop 3 verifica la información fiable:
verify_judge_checksum(run_state.get("judge_checksum", ""))
La verificación funciona de la siguiente manera:
def compute_judge_checksum() -> str:
content = _HIST.read_bytes() # historical_decisions.json
return hashlib.sha256(content).hexdigest()
def verify_judge_checksum(expected: str) -> None:
actual = compute_judge_checksum()
if actual != expected:
raise RuntimeError(
f"Judge checksum mismatch - historical_decisions.json was tampered with."
)
Un hash SHA-256 del archivo con las decisiones históricas se almacena en el estado de ejecución durante la inicialización y se verifica nuevamente cada vez que se inicia el bucle de eventos. Si alguien edita los datos de referencia para favorecer al agente, el sistema se niega a ejecutarse. Esto cierra una laguna discreta en los sistemas de auto-mejora: la forma más barata de elevar una puntuación es cambiar al evaluador.
Salto versus fallo
El uso de try/except en el Bucle 3 traza una línea divisoria deliberada. Cuando se agota el presupuesto, se genera nuevamente ese error, lo que detiene todo. Cualquier otra excepción se registra, la aplicación problemática se elimina de la cola y se procesa la siguiente. Por lo tanto, una sola aplicación mal formada no puede congelar el proceso, mientras que agotar los fondos es fatal por diseño, ya que el sistema no debería seguir funcionando sin nada con qué gastar.
Bucle 4: ascenso por colinas
Protege contra: un prompt fijo que nunca aprende de la experiencia. Señal de retroalimentación: la precisión del entrenamiento junto con el veredicto de auditoría.
Cada octava decisión (HILL_CLIMB_EVERY = 8), el Bucle 3 pasa el control al Bucle 4, el cual ejecuta un ciclo de reflexión, mutación, evaluación y control para mejorar automáticamente el prompt del sistema:
seed prompt
│
▼
evaluate on training set → score, failures
│
▼
┌────────────────────────────────┐
│ for each round: │
│ │
│ reflect(current, failures) │ ← LLM analyzes failure patterns
│ │ │
│ ▼ │
│ candidate_prompt │
│ │ │
│ ▼ │
│ evaluate(candidate, train) │ ← full eval run
│ │ │
│ ▼ │
│ audit_prompt(candidate) │ ← anti-cheating check
│ │ │
│ ▼ │
│ if score > best AND clean: │
│ best = candidate │ ← keep
│ else: │
│ discard │ ← revert
└────────────────────────────────┘
│
▼
write best_prompt.txt
Reflexionar
REFLECT_TEMPLATE = """You are improving the system prompt of an insurance underwriting agent.
CURRENT PROMPT:
{prompt}
The agent got these decisions WRONG on training applications:
{failures}
Look for PATTERNS in the failures. Infer GENERAL underwriting rules that would fix them.
Do NOT memorize specific application IDs or applicant details.
Write an improved system prompt. Reply with ONLY the new prompt text."""
def reflect(current_prompt: str, failures: list[dict]) -> str:
client = anthropic.Anthropic()
failure_text = "\n".join(
f"- APP {f['app_id']}: agent said '{f['got']}', expected '{f['expected']}'. "
f"Application data: {f['application']}"
for f in failures
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1500,
messages=[{"role": "user", "content": REFLECT_TEMPLATE.format(
prompt=current_prompt,
failures=failure_text,
)}],
)
return response.content[0].text.strip()
La instrucción de no memorizar IDs específicos de aplicaciones ni detalles de los solicitantes tiene una gran importancia. Sin ella, el modelo podría generar una instrucción como “para APP-003, siempre rechazar”, la cual obtendría una puntuación perfecta en los datos de entrenamiento pero fallaría completamente en casos que no haya visto. La nueva instrucción debe generalizar a partir de los patrones de los fallos, y no de casos individuales.
Evaluar
def evaluate_prompt(system_prompt: str, split: str = "train", meter=None):
tasks = load_tasks(split)
failures = []
for task in tasks:
state = run_loop1(task["id"], system_prompt, meter)
decision = state.get("decision", "ran_out_of_steps")
if not is_correct(decision, task["id"]):
failures.append({
"app_id": task["id"],
"got": decision,
"expected": task["expected_decision"],
"application": task["application"],
})
score = (len(tasks) - len(failures)) / len(tasks)
return score, failures
Cada prompt de candidato pasa por el Bucle 1 en las 16 aplicaciones de entrenamiento. Eso es costoso: 16 ejecuciones del modelo por ronda y al menos 80 en cinco rondas, pero la señal es clara: un prompt debe funcionar en toda la distribución de entrenamiento y no solo en una muestra.
Gate
def improve(seed_prompt: str, lessons: list[str], meter: CostMeter, rounds: int = 5) -> str:
best = with_lessons(seed_prompt, lessons)
best_score, failures = evaluate_prompt(best, split="train", meter=meter)
for r in range(1, rounds + 1):
candidate = reflect(best, failures)
score, cand_failures = evaluate_prompt(candidate, split="train", meter=meter)
verdict = audit_prompt(candidate) # anti-cheat check
if score > best_score and verdict == "VERDICT: CLEAN":
best, best_score, failures = candidate, score, cand_failures
BEST_PROMPT_PATH.write_text(best, encoding="utf-8")
print(f"round {r}: {score:.0%} KEPT ({len(cand_failures)} failures)")
else:
reason = "cheating" if verdict != "VERDICT: CLEAN" else f"{score:.0%} <= {best_score:.0%}"
print(f"round {r}: {score:.0%} REVERTED ({reason})")
return best
Deben cumplirse ambas condiciones antes de aceptar a un candidato. Primero, score > best_score; es decir, debe superar claramente al mejor resultado actual. Se rechazan los empates, lo que evita la desviación en las instrucciones. Segundo, el auditor debe devolver VERDICT: CLEAN, confirmando que el candidato no contiene respuestas codificadas de antemano.
El auditor de instrucciones
AUDIT_SYSTEM = (
"You are a security auditor for AI systems. "
"Check whether the prompt contains any hardcoded application IDs (e.g. APP-001) "
"or hardcoded decisions for specific applications. "
"These would constitute cheating — memorizing answers instead of learning rules. "
"Reply with exactly one of:\n"
"VERDICT: CLEAN\n"
"VERDICT: CHEATING\n"
"Followed by a one-sentence explanation."
)
def audit_prompt(evolved_prompt: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
system=AUDIT_SYSTEM,
messages=[{"role": "user", "content": f"PROMPT TO AUDIT:\n{evolved_prompt}"}],
)
return response.content[0].text.strip().split("\n")[0] # first line only
Se trata de un escenario en el que un LLM actúa como juez, pero limitado a una única pregunta estructural binaria: ¿el prompt contiene IDs específicos o respuestas memorizadas? Ese es el tipo de clasificación en la que los jueces basados en modelos son fiables. El auditor nunca evalúa la calidad, el estilo ni la corrección; solo busca un patrón específico.
Memoria de lecciones
Además del método de ascenso por colinas, el sistema mantiene una lista lessons que persiste entre ejecuciones. Cuando una aplicación sigue fallando tras cada intento, llama a distill_lesson():
DISTILL_PROMPT = """An insurance underwriting agent made a wrong decision.
Application details: {application}
Agent decided: {got}
Correct decision: {expected}
Write ONE short, general underwriting rule that would prevent this mistake.
Do NOT reference this specific application ID or any applicant names/details.
Reply with only the rule, as a single sentence."""
def distill_lesson(application: dict, got: str, expected: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=150,
messages=[{"role": "user", "content": DISTILL_PROMPT.format(...)}],
)
return response.content[0].text.strip()
Cada lección se agrega luego a cada ejecución posterior de Loop 1 a través de with_lessons():
def with_lessons(base_prompt: str, lessons: list[str]) -> str:
if not lessons:
return base_prompt
lessons_text = "\n".join(f"- {l}" for l in lessons)
return base_prompt + f"\n\nUnderwriting lessons learned:\n{lessons_text}"
Se trata de un aprendizaje de pocos ejemplos sin descenso de gradiente. El sistema aprende de los errores en tiempo de ejecución al recopilar conocimiento comprimido en forma de reglas en lenguaje natural. Una función al estilo del protocolo CL-BENCH verifica si esas lecciones realmente son útiles:
def measure_gain(base_prompt: str, lessons: list[str], split: str = "test", meter=None) -> float:
stateless_score, _ = evaluate_prompt(base_prompt, split=split, meter=meter)
augmented = with_lessons(base_prompt, lessons)
stateful_score, _ = evaluate_prompt(augmented, split=split, meter=meter)
return stateful_score - stateless_score # positive = lessons help
Control de presupuesto con un medidor de costos
Cada llamada al modelo pasa por un CostMeter compartido, que se transmite desde Loop 4 a Loop 3, luego a Loop 2 y finalmente a Loop 1, de modo que todos los gastos se acumulan en un único lugar:
BUDGET_USD = 2.00
INPUT_PRICE_PER_TOKEN = 3.00 / 1_000_000
OUTPUT_PRICE_PER_TOKEN = 15.00 / 1_000_000
class CostMeter:
def record(self, input_tokens: int, output_tokens: int) -> None:
if self.spent >= self._budget: # check BEFORE adding
raise BudgetExhaustedError(
f"Budget ${self._budget:.2f} exhausted at ${self.spent:.4f}"
)
self.spent += input_tokens * INPUT_PRICE_PER_TOKEN \
+ output_tokens * OUTPUT_PRICE_PER_TOKEN
Lo importante es el orden: verifica el presupuesto antes de añadir el costo. De esa manera, el error se detecta en el momento en que se alcanza el límite, en lugar de después de que ya se haya incurrido en un gasto excesivo. BudgetExhaustedError se transmite hacia arriba a través de la jerarquía de bucles, y el Bucle 3 lo vuelve a lanzar en lugar de ignorarlo, por lo que un presupuesto agotado siempre detiene toda la ejecución.
Estado de ejecución con configuración idempotente
run_state.json registra todo lo que ha ocurrido: aplicaciones procesadas, listas de éxitos y fracasos, lecciones acumuladas, decisiones desde la última fase crítica y el checksum del evaluador. Su inicialización es defensiva:
def _init_run_state() -> dict:
path = _STATE_DIR / "run_state.json"
state = {}
if path.exists():
try:
content = path.read_text(encoding="utf-8-sig").strip() # handles Windows BOM
if content:
state = json.loads(content)
except (json.JSONDecodeError, UnicodeDecodeError):
pass # corrupt file → start fresh
if not state:
state = dict(_DEFAULT_RUN_STATE)
state["judge_checksum"] = compute_judge_checksum() # always refresh
path.write_text(json.dumps(state, indent=2), encoding="utf-8")
return state
Aquí se presentan tres hábitos de resiliencia. Al leer con utf-8-sig se eliminan silenciosamente las marcas de orden de bytes de Windows. Un archivo vacío o dañado vuelve al estado predeterminado en lugar de causar una caída. Además, el checksum del juez se vuelve a calcular en cada inicio, sin confiar nunca en el de una ejecución anterior.
Tres formas de ejecutarlo
Existen tres maneras de hacerlo, y cada una utiliza solo una parte del stack.
El modo individual ejecuta solo el Bucle 1, lo cual es útil para depurar una aplicación y observar qué herramientas llama el agente y por qué:
python -m src.main --mode single --app-id APP-001
El modo de bucle de eventos ejecuta los bucles 1, 2 y 3 en cada aplicación pendiente y puede activar el bucle 4 después de cada ocho decisiones. La bandera --once vacía la cola y finaliza el proceso en lugar de realizar consultas indefinidamente:
python -m src.main --mode event-loop --once
El modo de mejora ejecuta directamente el Bucle 4, con cinco rondas de subida de colinas en la división de entrenamiento. Luego evalúa el mejor prompt encontrado frente a la división de prueba reservada e imprime la mejora obtenida:
python -m src.main --mode improve --rounds 5
Una sola aplicación, de principio a fin
Este es el flujo de datos completo de una única aplicación al moverse por el sistema:
pending_applications.json
│
│ [Loop 3 reads APP-009]
▼
run_loop2("APP-009", prompt, meter)
│
│ [Loop 2 calls Loop 1]
▼
run_loop1("APP-009", prompt, meter)
│
│ [LangGraph StateGraph]
▼
agent_node → ChatAnthropic.invoke([SystemMsg, HumanMsg])
│
│ response: tool_call(lookup_application, {"app_id": "APP-009"})
▼
tools_node → lookup_application.invoke({"app_id": "APP-009"})
│
│ returns: {id: APP-009, state: TX, coverage: 1200000, ...}
▼
agent_node → ChatAnthropic.invoke([..., ToolMessage])
│
│ response: tool_call(retrieve_rules, {"risk_factor": "coverage"})
▼
tools_node → retrieve_rules.invoke({"risk_factor": "coverage"})
│
│ returns: ["Coverage > $1M requires mandatory refer. Flag: high_coverage"]
▼
agent_node → ChatAnthropic.invoke([..., ToolMessage])
│
│ response: tool_call(draft_decision, {decision: "refer", ...})
▼
tools_node → draft_decision.invoke({...})
│
│ updates state: {decision: "refer", flags: ["high_coverage"], ...}
▼
should_continue → END (no more tool calls)
│
▼
Loop 2: decision == "refer" → write human_queue.json → return (state, True)
│
▼
Loop 3: passed=True → update run_state.json → remove APP-009 from queue
│
│ [decisions_since_last_improvement becomes 9 >= 8]
▼
Loop 4: improve(seed_prompt, lessons, meter, rounds=5)
│
│ reflect → evaluate → audit → gate → write best_prompt.txt
▼
Loop 3: continue with APP-010 using new best_prompt
Lecciones de su desarrollo
1. Mantenga al oráculo separado del agente. El juez (is_correct, evaluate_prompt) no comparte código alguno con el agente (run_loop1). El juez lee archivos estáticos mientras que el agente llama a un modelo. Deje que el modelo evalúe su propio resultado y obtendrá un asentidor que nunca falla.
2. Haga que la evaluación sea determinista. La corrección por aplicación, la precisión en el entrenamiento y la precisión en las pruebas son todas deterministas para los mismos datos de entrada. La única variable aleatoria proviene de las respuestas del modelo, por lo que cualquier puntuación puede reproducirse a partir del prompt y la semilla.
3. Utilice una herramienta de respuesta final para obtener un resultado estructurado. Una herramienta draft_decision es más eficaz que el análisis de texto narrativo. Su esquema establece el vocabulario (accept | modify | decline | refer), los campos obligatorios (flags, rationale, recommended_premium_adj) y sus tipos, por lo que el agente debe ser explícito.
4. La lucha contra el fraude no es opcional en los sistemas de auto-mejora. Una vez que un sistema puede editar sus propias instrucciones, buscará atajos, y el más obvio es memorizar las respuestas de entrenamiento. audit_prompt() detecta esto automáticamente. Sin ella, el método de ascenso por colinas genera de forma fiable prompts con una precisión de entrenamiento casi perfecta y una precisión en pruebas casi aleatoria.
5. Las marcas de orden de bytes y las codificaciones son errores de producción. Si desarrollas en Windows o recibes archivos de usuarios de Windows, es obligatorio utilizar encoding="utf-8-sig" en cada lectura. La BOM generada por el Bloc de notas de Windows provoca json.JSONDecodeError: Unexpected UTF-8 BOM, lo cual es difícil de diagnosticar porque el archivo parece perfectamente normal en un editor.
6. Revisa el presupuesto antes de gastar, no después.
# WRONG: allows one overspend before raising
self.spent += cost
if self.spent > self._budget:
raise BudgetExhaustedError(...)
# CORRECT: raises before the overspend registers
if self.spent >= self._budget:
raise BudgetExhaustedError(...)
self.spent += cost
El orden es importante cuando varias llamadas al modelo se realizan una tras otra con rapidez. Si se agrega primero y se verifica más tarde, el presupuesto puede superarse con el costo de más de una llamada antes de que aparezca el error.
7. Limpie las líneas de progreso en bucles largos.
print(f"[loop3] {processed}/{total} {app_id} ...", flush=True)
En Windows, la salida estándar se almacena en búfer a menos que se indique lo contrario. Si se omite flush=True, la línea “processing APP-009” aparece después de que la llamada a la API finalice, lo cual anula la utilidad de un indicador de progreso. Siempre se debe vaciar el búfer de las líneas que aparecen antes de las operaciones de E/S bloqueantes.
8. Mantenga el presupuesto para el método de ascenso gradual separado del del bucle de eventos. El agotamiento del presupuesto durante este proceso debe detectarse en el lugar donde se realiza la llamada y no permitir que esto provoque la interrupción del bucle de eventos. Después de que ocho evaluaciones de aplicaciones hayan consumido casi todo un presupuesto de 2 dólares, queda poco margen para seguir utilizando este método. El bucle de eventos detecta específicamente el BudgetExhaustedError generado por este proceso y continúa procesando las aplicaciones restantes.
try:
system_prompt = run_hill_climbing(system_prompt, lessons, meter)
run_state["decisions_since_last_improvement"] = 0
except BudgetExhaustedError:
print(f"[loop3] hill-climbing skipped — budget exhausted at ${meter.spent:.4f}")
run_state["decisions_since_last_improvement"] = 0
Aplicar el patrón en otros lugares
El diseño de cuatro bucles no está vinculado al sector de seguros. Para adaptarlo, cambie estas partes:
- Herramientas del Bucle 1. Aquí: búsqueda, reglas, señal, decisión. Las suyas: herramientas de recuperación y acción para su área de trabajo.
- Verdad de referencia. Aquí: el archivo con las decisiones históricas. Las suyas: su propio conjunto de datos etiquetado.
- Juez. Aquí: comparación de cadenas de texto. Las suyas: una prueba de corrección específica para su tarea.
- Lecciones aprendidas. Aquí: reglas de aprobación de pólizas. Las suyas: patrones de errores de su área de trabajo.
- Indicación para el método de ascenso por colina. Aquí: la indicación del sistema de aprobación de pólizas. Las suyas: la indicación del sistema de su agente.
- Presupuesto. Aquí: $2.00. Las suyas: lo que deba costar cada ejecución.
Hay una restricción fundamental: el juez debe ser determinista e independiente del agente. Si no se puede definir una prueba de corrección determinista para su tarea, el sistema no puede mejorar de manera fiable por sí mismo.
Probándolo
Configuración y los cuatro comandos:
# 1. Install
cd underwriting_loop
pip install -e ".[dev]"
export ANTHROPIC_API_KEY=sk-ant-...
# 2. Run a single application (debug mode - cheapest)
python -m src.main --mode single --app-id APP-001
# 3. Drain the pending queue with event loop
python -m src.main --mode event-loop --once
# 4. Run standalone hill-climbing (5 rounds)
python -m src.main --mode improve --rounds 5
# 5. Run the full offline test suite (no API key needed)
pytest tests/ -v
El resultado del método de ascenso por colinas se ve así:
seed score: 44% (9 failures)
round 1: 56% KEPT (7 failures)
round 2: 56% REVERTED (score 56% <= best 56%)
round 3: 69% KEPT (5 failures)
round 4: 75% KEPT (4 failures)
round 5: 69% REVERTED (score 69% <= best 75%)
Final test evaluation...
spend: $1.8342
test score: 75%
gain (vs baseline): +25%
El prompt inicial alcanza un 44% en el entrenamiento. Cinco rondas después, el prompt ganador obtiene un 75% en el conjunto de prueba no utilizado, lo que representa un aumento de 25 puntos respecto al perfeccionamiento automático del prompt sin ingeniería manual del mismo.
Qué aportan los bucles
La ingeniería de bucles no es un marco de trabajo. Es una forma de dividir la fiabilidad del agente en capas, cada una con una única responsabilidad y un único mecanismo de retroalimentación. Los cuatro bucles abordan cuatro modos de fallo distintos:
- Bucle 1 se encarga de la fundamentación: el agente debe consultar pruebas antes de tomar una decisión.
- Bucle 2 se ocupa de los fallos en una sola ejecución: un camino de razonamiento incorrecto no debe hacer que la aplicación falle de forma permanente.
- Bucle 3 gestiona el rendimiento y la resiliencia: una aplicación defectuosa no debe bloquear el proceso completo.
- Bucle 4 soluciona los errores recurrentes reescribiendo el propio prompt a partir de los fallos acumulados.
Al acumularlos, agregar un evaluador determinista, proteger todo con control de presupuesto y detección de manipulaciones, el sistema mejora de manera medible con el tiempo sin necesidad de intervención humana.
Qué se obtiene
Adopte este diseño con ojos abiertos. La mayor precaución que se menciona en la literatura sobre bucles de agentes es la “rendición cognitiva” de Addy Osmani: dejar que el bucle tenga control total y dejar de leer sus resultados. Las ventajas siguen siendo reales y medibles.
- Mejora automática sin intervención humana. El método de ascenso por colinas elevó la precisión del entrenamiento del 44% al 75% sin necesidad de trabajar manualmente en los prompts, y el sistema extrajo por sí mismo las reglas adecuadas de aprobación a partir de los patrones de fallos.
- Aislamiento de fallos entre capas. Una aplicación con errores no interrumpe todo el proceso. El bucle 3 detecta los errores de cada aplicación, los registra y continúa.
BudgetExhaustedErrores la única excepción que llega hasta la capa superior, y eso es intencional. - Progreso que se puede medir y auditar.
measure_gain()genera una cifra concreta, mostrando las diferencias entre los sistemas con estado y los sin estado en el conjunto de datos de prueba. La mejora puede comunicarse a los interesados como un porcentaje y un costo en dólares. - Evaluación a prueba de manipulaciones. El checksum SHA-256 de los valores de referencia evita el fallo más común en los sistemas automejorables: que alguien modifique las respuestas para que las calificaciones parezcan mejores. Si los checksums no coinciden, nada se ejecuta.
- Conocimiento que se acumula. Las lecciones extraídas de los fracasos persisten a lo largo de las ejecuciones, por lo que cada nueva llamada a Loop 1 comienza de manera más inteligente que la anterior y no tiene que reaprender desde cero.
- El productor y el consumidor permanecen desacoplados. Con una cola de archivos, lo que genera la lista pendiente está separado del pipeline de evaluación. La cola puede pausarse, reanudarse, examinarse o reproducirse sin que el código del agente se vea afectado.
- La escalada humana es un estado de primera categoría. Las decisiones
referse guardan enhuman_queue.jsony se consideran aprobadas. El bucle no vuelve a intentar algo que debería decidir un humano. - Las reintentos son dirigidos. La retroalimentación después de un error indica qué salió mal, algo como “dijiste modificar pero la respuesta correcta es consultar, revisa nuevamente los factores de riesgo”. Esto conduce a una solución más rápido que el muestreo aleatorio.
Qué cuesta
- Los tokens se multiplican rápidamente. Cinco rondas de ascenso por colina en 16 aplicaciones de entrenamiento requieren al menos 80 ejecuciones de Loop 1, además de llamadas de reflexión y auditoría. Una ejecución con
--mode improveconsumió 1,83 dólares de un presupuesto de 2,00 dólares. Superar el presupuesto no es un caso excepcional; es la forma habitual en que termina una ejecución. - La latencia es real. Una aplicación que atraviesa toda la capa de lógica puede tardar de 30 a 60 segundos, y 16 aplicaciones en modo bucle de eventos toman minutos. Eso está bien para trabajos en lotes, pero no es adecuado para uso interactivo.
- El no determinismo oculta los fallos. El bucle 1 utiliza un modelo en tiempo real, por lo que el mismo prompt en la misma aplicación puede generar secuencias de herramientas diferentes. Un fallo podría no reproducirse, y para obtener cifras de precisión significativas se necesitan varias ejecuciones de evaluación.
- La configuración es compleja. Antes de la primera ejecución se requiere el estado de ejecución, la cola de tareas pendientes, las decisiones históricas con checksums, un archivo con los mejores prompts y la lista de lecciones. Esto implica una cantidad de infraestructura mucho mayor que una sola llamada a
agent.invoke(). - El sobreajuste nunca desaparece. Sin la protección de auditoría, el método de ascenso por colinas acaba generando un prompt que memoriza las respuestas de entrenamiento, obteniendo una puntuación perfecta en los datos de entrenamiento y resultados casi aleatorios en las pruebas. La auditoría es lo único que impide que el sistema se mejore a sí mismo o engañe a sí mismo.
- La rendición cognitiva es un verdadero modo de fallo. Cuanto más fiable se vuelve el bucle, menos personas leen sus resultados, y cuando dejas de leer, dejas de darte cuenta de que el bucle ha comenzado a fallar de una nueva manera. Los bucles sin supervisión requieren monitoreo externo, ya que un bucle no puede auditar por sí mismo su corrección semántica.
- La depuración abarca toda la estructura. Una decisión errónea puede provenir de una reflexión incorrecta en el Bucle 4, de una regla de rúbrica omitida en el Bucle 2, de una herramienta que no devolvió nada en el Bucle 1 o de un estado de ejecución corrupto en el Bucle 3. Rastrear un fallo a través de cuatro bucles anidados es más difícil que hacerlo con uno en una sola función.
¿Deberías crear un bucle en absoluto?
Un análisis de las configuraciones de bucle público ofrece consejos claros: realice bucles con tareas repetitivas, revisables y de alto valor, que incluyan una herramienta de verificación integrada; para todo lo demás, basta con hacer una solicitud una sola vez. Todo se reduce a dos preguntas.
- ¿La tarea se repite o requiere muchos pasos impredecibles? Si una sola solicitud es suficiente, el bucle añade sobrecarga sin aportar ninguna ventaja arquitectónica.
- ¿Puede el agente determinar por sí mismo si ha terminado? Sin pruebas, un formato esperado o respuestas de referencia, nada detiene el bucle, que continúa indefinidamente.
Las respuestas a ambas preguntas deben ser sí. Si alguna es no, omita el bucle.
Casos adecuados
Tareas repetitivas y de gran volumen. Dieciséis solicitudes de seguro, 500 archivos de código, 1,000 tickets de soporte. El bucle recupera su costo de configuración cuando la tarea se ejecuta muchas veces y no solo una.
Existe una verificación determinista del resultado. Etiquetas de referencia, un conjunto de pruebas, un validador de esquemas o una lista de reglas binarias de aprobación/rechazo. El evaluador nunca debe recurrir a un modelo, ya que un evaluador basado en modelos introduce su propia falta de fiabilidad en el proceso.
El agente puede verificar su propio trabajo. El Agent SDK de Anthropic considera esto el paso más subestimado. Aquí is_correct() se encarga de ello, y en un agente de programación lo hace el conjunto de pruebas. Si el agente no puede verificar su salida, no se cierra el ciclo.
Datos etiquetados o criterios de éxito reutilizables. El método de ascenso por colinas requiere una división entre entrenamiento y pruebas con respuestas conocidas. Sin ella, se pueden analizar los fallos, pero no se puede saber si ese análisis fue útil.
Nadie supervisa cada ejecución. Piense en lotes de aprobación nocturnos, auditorías programadas o flujos de documentos, donde no hay nadie dentro del bucle interno y este diseño está pensado para funcionar así. Si alguien puede revisar cada resultado de inmediato, la instrucción directa es más rápida.
El costo por ejecución de evaluación es aceptable. Una ronda de búsqueda de óptimo tiene un precio similar al de un pase completo para cada aplicación de entrenamiento. Si eso es inasequible, Loop 4 también lo es, así que úselo con menos frecuencia o con un conjunto de entrenamiento más pequeño.
Necesita números. Existe measure_gain() porque “el sistema mejoró” debe cuantificarse. Si los interesados quieren cifras, la ingeniería de bucles se las proporciona. Si “parece mejor” es suficiente, es un exceso.
Ajustes deficientes
Un único prompt es suficiente para ello. Reformatear un CSV, extraer una fecha de un documento, resumir un párrafo. Estas tareas tienen un número predecible de pasos, por lo que basta con escribir un script o un prompt único. Utilizar bucles en estos casos representa una complejidad innecesaria para un problema sencillo.
“Listo” es cuestión de gusto. La solicitud “Hacer que este correo sea más profesional” no cuenta con un sistema de verificación binaria, y un segundo modelo que evalúe la estética solo añade variabilidad sin resolver nada. Primero defina claramente el objetivo, por ejemplo, menos de 100 palabras, tono profesional, sin signos de exclamación, y luego sí se puede verificar.
Las acciones son irreversibles si no se revisan primero. Enviar correos, fusionar solicitudes de integración, eliminar registros, transferir dinero. Defina con precisión los permisos de acceso y exija una pausa antes de eliminar, enviar o pagar cualquier cosa. Los bucles que permiten realizar acciones irreversibles sin controles son fuentes potenciales de errores.
El presupuesto no puede cubrir la evaluación. Si 2,00 dólares ya son insuficientes, los procesos adicionales se consumen antes de que se procesen todas las solicitudes de entrenamiento. El sistema manejó esto con BudgetExhaustedError, pero en un entorno con presupuesto limitado la solución real es utilizar un conjunto de entrenamiento más pequeño, menos rondas, o evitar el Loop 4 hasta que se pueda pagar por él.
No hay persistencia de estado. Si no se puede guardar el estado del proceso, las lecciones y la cola en disco o en una base de datos, no será posible reanudar después de una interrupción, acumular lecciones a lo largo de sesiones diferentes ni protegerse de agotar el presupuesto durante el entrenamiento. Los entornos sin estado requieren otra arquitectura.
Todavía estás explorando. Llamar al agente una vez con herramientas adecuadas te muestra dónde comete errores, mientras que los bucles anidados solo indican dónde falla todo el sistema. Examina primero el agente individual y añade capas una vez que conozcas bien sus propios modos de fallo.
Una última regla práctica proviene de Rhys Sullivan, quien advierte contra dos extremos: los bucles de "tokenmaxxing" que se ejecutan únicamente para inflar los costos, y las frases simples ingenuas de tipo "promptchud". Un bucle se justifica cuando reproduce fallos reales del producto con una distribución de pruebas realista, y pierde su validez cuando solo parece impresionante.
Ocho propiedades de un bucle que funciona
Los bucles suelen romperse de maneras poco atractivas. Nunca se detienen, agotan el presupuesto o devuelven resultados inútiles etiquetados como terminados. Un análisis de las configuraciones de bucles públicos reveló que en cada bucle funcional de forma fiable había ocho características, y cada una de ellas correspondía a una decisión de diseño en esta base de código.
1. Un objetivo verificable
Defina qué significa “terminado” de modo que una máquina pueda verificarlo, no para que una persona se sienta satisfecha. “Producir una buena decisión de aprobación” es algo imposible de verificar. is_correct(decision, app_id) permite compararla con las decisiones históricas: o coincide o no. Sin un objetivo verificable, el bucle nunca sabe cuándo detenerse y entregará como trabajo final resultados erróneos, pero aparentemente correctos y pulidos. El objetivo debe ser un predicado binario y no una descripción de calidad. La división entre datos de entrenamiento y de prueba asegura que el modelo generalice más allá de lo memorizado hasta entonces.
2. Una parada definitiva
Max tries, a budget or a time limit, always, with no exceptions. Never assume the model will stop on its own. Grassroots setups like a shell while :; do ... done loop contain nothing on the model side that ends them, so they run until somebody kills the process, and OpenAI's SDK ships max_turns because experience showed models don't reliably end themselves. This codebase has three stops at three levels: MAX_STEPS = 8 in Loop 1 prevents endless tool chains, max_retries = 3 in Loop 2 prevents endless retries, and BUDGET_USD = 2.00 through CostMeter is the ultimate backstop. At the step cap Loop 1 returns ran_out_of_steps, when retries run out Loop 2 returns passed=False, and when the budget is gone the error propagates up and halts everything.
3. Good tools
Las acciones disponibles para el agente deben ser fiables, estar claramente descritas y tener un alcance bien definido. El bucle en sí es sencillo, y el trabajo se realiza mediante las herramientas. La guía de Anthropic indica que los conjuntos de herramientas y su documentación deben diseñarse con cuidado, ya que una descripción vaga genera solicitudes vagas, mientras que una herramienta demasiado potente provoca errores irreversibles. Aquí hay cuatro herramientas, cada una con una función específica: recuperación pura sin efectos secundarios, búsqueda por palabras clave en una base de conocimientos estática, extracción simulada de datos externos, y una acción en terminal estructurada que exige el uso de un esquema tipado (Literal["accept", "modify", "decline", "refer"]), junto con una justificación y banderas explícitas. Sin esta última, el agente devuelve texto en prosa que requiere un análisis delicado.
4. Memoria
Mantenga el historial pero síntetícelo para que el contexto no se vuelva excesivo, y para cualquier conversación más larga que una sola sesión, guarde el estado en disco. El mecanismo de agente de larga duración de Anthropic permite al agente leer el estado anterior desde git y los archivos de progreso, avanzar de forma incremental y dejar el entorno limpio para la siguiente iteración. Una ventana de contexto sin procesar no sobrevive a reinicios, caídas o límites de tamaño. Aquí funcionan dos mecanismos a diferentes escalas de tiempo: dentro de una misma ejecución, el reductor add_messages de LangGraph acumula toda la conversación del herramienta para una aplicación; entre ejecuciones, la lista lessons en run_state.json acumula reglas extraídas de todas las aplicaciones y todos los reinicios. distill_lesson() es el paso de compresión: almacena una única regla generalizable en lugar de una transcripción completa del fallo, lo que mantiene la lista corta y el contexto insertado manejable.
5. Un verificador independiente
Generar, evaluar, corregir, repetir: el creador nunca califica su propio trabajo. El SDK de Anthropic considera que verificar el trabajo es el paso más subestimado, pero dicha verificación debe ser independiente de la generación. Un agente que se califica a sí mismo se convierte en un adulador, y asignarle únicamente la tarea de evaluación a otro agente evita eso. Esta base de código cuenta con tres niveles independientes de verificación:
- Verificación por rúbrica (estructura).
RubricMiddlewarecomprueba nueve reglas en el resultado procesado mediante predicados booleanos puros, sin utilizar ningún modelo. - Verificación por evaluador (corrección).
is_correct()compara la respuesta con la etiqueta almacenada mediante una comparación de cadenas exactas. - Verificación de auditoría (anti-trampa).
audit_prompt()busca respuestas memorizadas mediante una clasificación binaria del modelo, sin realizar una evaluación de calidad.
La rúbrica y el juez son completamente deterministas. La auditoría utiliza un modelo, pero solo para una pregunta estructural binaria (“¿contiene esto IDs de aplicación?”), la categoría en la que los jueces basados en modelos son los más fiables.
6. Un plan para tareas complejas
Para un trabajo grande y de múltiples pasos, haga que el agente escriba un plan antes de actuar; para uno pequeño, pásese de ello. La investigación de Anthropic sobre agentes de larga duración reveló que es crucial trabajar en una sola característica a la vez, ya que un bucle que recibe la instrucción de arreglar todo de inmediato funciona de forma ineficiente, mientras que realizar un cambio por paso conduce a resultados óptimos. En esta base de código, la instrucción establece cuatro pasos con herramientas en orden, lo que funciona como un plan integrado: obtener los datos, obtener las reglas, obtener señales externas y tomar una decisión. Este orden impide que el agente pase directamente a una decisión sin consultar pruebas, y el límite de ocho pasos está diseñado para permitir toda la secuencia más una repetición.
7. Registro de actividades
Registre cada pensamiento, acción y resultado para que tenga algo de qué leer durante una sesión de depuración a las 3 de la mañana. Si un paso no se registra,, desde el punto de vista de la depuración, no ocurrió. Las fallas en bucles anidados son casi imposibles de diagnosticar sin registros: ¿qué bucle, qué aplicación, qué intento, qué herramienta devolvió un resultado incorrecto? Tres niveles de registro cubren esto. Las líneas de progreso como [loop3] 3/16 APP-009 ... se actualizan constantemente, lo cual es indispensable en Windows donde la salida está en búfer. Las líneas de decisión incluyen la decisión tomada, el estado y el tiempo de ejecución en una sola línea por aplicación. Las líneas de lección confirman que la extracción se realizó.
8. Conciencia del costo
Los bucles consumen tokens rápidamente, así que comience con cantidades pequeñas y limitadas antes de escalar, y observe las primeras ejecuciones antes de dejar un bucle sin supervisión. Una ronda de ascenso de pendiente a través de 16 aplicaciones de entrenamiento, con cinco rondas más sesiones de reflexión y auditoría, cuesta casi 2,00 dólares. Solo después de conocer el costo por iteración podrá establecer un presupuesto y retirarse. CostMeter registra los gastos con cuatro decimales y genera el error BudgetExhaustedError antes de que se produzca un gasto excesivo:
# The ordering matters:
if self.spent >= self._budget: # check BEFORE adding
raise BudgetExhaustedError(...)
self.spent += cost # add AFTER the check clears
El bucle de eventos también detecta el agotamiento del presupuesto durante la fase de mejora por separado del agotamiento al procesar, por lo que quedarse sin recursos en medio de una ronda de auto-mejora no hace que se descarte el resto de la cola. El bucle funciona de manera gradual: se omite el paso de mejora costoso y se continúa con el procesamiento.
Una lista de verificación antes de dejarlo funcionar solo
Antes de confiar en que cualquier bucle funcione sin supervisión, confirme lo siguiente:
- El objetivo es verificable por la máquina (en este caso,
is_correct()frente a los datos reales). - Existe un límite estricto en cada nivel (
MAX_STEPS,max_retries,BUDGET_USD). - Las herramientas están delimitadas y documentadas (cuatro herramientas, una tarea cada una).
- El estado se mantiene tras los reinicios (
run_state.jsonjunto con la lista de lecciones). - El verificador está separado del generador (middleware de rúbrica más un juez determinista).
- Las tareas complejas cuentan con un plan implícito o explícito (la secuencia de cuatro pasos mencionada).
- Los pasos clave se registran y actualizan (las líneas de progreso
[loop3]). - Se establece un presupuesto que se monitorea desde la primera llamada (un
CostMetercompartido entre todos los bucles).
Si se omite alguno de estos elementos, el bucle fallará en entornos de producción; no siempre de inmediato, pero sí de forma fiable al final. La mayoría de los fallos en producción se deben a la falta de un paso de verificación, de una condición de parada o de un estado persistente. Corrija primero estos aspectos y luego ajuste todo lo demás.
Leer más
- Agentes paralelos de LangGraph que revisan solicitudes de pull request mediante webhooks de FastAPI — Cómo un webhook, una cola de Redis y cuatro agentes de LangGraph convierten una solicitud de pull request en una revisión estructurada y puntuada por IA en aproximadamente 30 segundos, con ngrok para pruebas locales.
- Construcción de un agente de soporte LEPA con LangGraph — Configuración de nodos de SupportState, intake y classify, bordes condicionales y marcadores START/END para que una solicitud de soporte escolar pueda aclarar dudas o redirigirse a la documentación correspondiente.