Por qué fallan los agentes de IA: límites de LLM en Python (Ejemplo completo)

Ejemplo runnable completo con validacion JSON, verificacion de fuentes y fallback a humano con baja confianza.
En esta página
  1. Que Demuestra Este Ejemplo
  2. Estructura del Proyecto
  3. Como Ejecutarlo
  4. Que Construimos en el Codigo
  5. Codigo
  6. knowledge.py — fuentes controladas
  7. validator.py — validacion estricta de la salida del modelo
  8. llm.py — llamada al modelo con contrato claro
  9. main.py — ciclo de agente con retry, confidence gate y handoff
  10. requirements.txt
  11. Ejemplo de Salida
  12. Lo Que Se Ve en la Practica
  13. Donde Seguir Profundizando
  14. Codigo Completo en GitHub

Esta es la implementacion completa del ejemplo del articulo Por que falla el agente: limites de LLM.

Si aun no leiste el articulo, empieza por ahi. Aqui nos enfocamos en el codigo: como hacer que el comportamiento del agente sea mas estable pese a las limitaciones naturales del modelo.


Que Demuestra Este Ejemplo

  • LLM puede fallar en el formato o inventar citas de fuentes
  • El agente debe validar la respuesta de forma determinista y no confiar en un texto "a simple vista"
  • La baja confianza del modelo debe llevar la tarea a handoff con una persona
  • El limite de pasos y el contexto acotado son necesarios incluso para un caso simple

Estructura del Proyecto

TEXT
foundations/
└── llm-limits-agents/
    └── python/
        ├── main.py           # agent loop with retry + handoff
        ├── llm.py            # model call
        ├── validator.py      # strict output validation
        ├── knowledge.py      # local KB for grounding
        └── requirements.txt

La separacion por modulos importa: el modelo genera y el validador y la policy toman decisiones.


Como Ejecutarlo

1. Clona el repositorio y entra en la carpeta:

BASH
git clone https://github.com/AgentPatterns-tech/agentpatterns.git
cd foundations/llm-limits-agents/python

2. Instala las dependencias:

BASH
pip install -r requirements.txt

3. Define la API key:

BASH
export OPENAI_API_KEY="sk-..."

4. Ejecuta:

BASH
python main.py

Que Construimos en el Codigo

Construimos un guarded loop simple para responder la pregunta de un cliente.

  • obtenemos fragmentos relevantes de una base de conocimiento controlada
  • damos al modelo un contrato JSON de respuesta estricto
  • el validador verifica formato, citas y nivel de confianza
  • si la respuesta no pasa la validacion, hacemos retry; si la confianza es baja, escalamos a una persona

Este es un ejemplo didactico donde lo principal no es una "respuesta bonita", sino un comportamiento del agente controlado y verificable.


Codigo

knowledge.py — fuentes controladas

PYTHON
from typing import Any

KB = [
    {
        "id": "KB-101",
        "title": "Refund Policy",
        "text": "Refunds are available within 14 days after payment. "
                "After 14 days, refunds are not available.",
    },
    {
        "id": "KB-102",
        "title": "Pro Plan",
        "text": "Pro customers have priority support and a 4-hour SLA.",
    },
    {
        "id": "KB-103",
        "title": "Free Plan",
        "text": "Free customers get business-hours support with no SLA.",
    },
]


def search_kb(question: str, limit: int = 2) -> list[dict[str, Any]]:
    q = question.lower()
    scored: list[tuple[int, dict[str, Any]]] = []
    for item in KB:
        score = 0
        text = f"{item['title']} {item['text']}".lower()
        for token in ("refund", "pro", "free", "sla", "support"):
            if token in q and token in text:
                score += 1
        scored.append((score, item))

    scored.sort(key=lambda x: x[0], reverse=True)
    return [item for _, item in scored[:limit]]


def build_context(snippets: list[dict[str, Any]], max_chars: int = 700) -> str:
    parts: list[str] = []
    total = 0
    for s in snippets:
        line = f"[{s['id']}] {s['title']}: {s['text']}\n"
        if total + len(line) > max_chars:
            break
        parts.append(line)
        total += len(line)
    return "".join(parts).strip()

LLM responde solo con base en este contexto. Esto reduce el espacio para inventos.


validator.py — validacion estricta de la salida del modelo

PYTHON
import json
from dataclasses import dataclass
from typing import Any


@dataclass
class ValidationResult:
    ok: bool
    data: dict[str, Any] | None
    errors: list[str]


def validate_model_output(raw: str, allowed_sources: set[str]) -> ValidationResult:
    errors: list[str] = []

    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return ValidationResult(False, None, ["invalid JSON"])

    if not isinstance(data, dict):
        return ValidationResult(False, None, ["output must be a JSON object"])

    answer = data.get("answer")
    citations = data.get("citations")
    confidence = data.get("confidence")
    needs_human = data.get("needs_human")

    if not isinstance(answer, str) or not answer.strip():
        errors.append("answer must be non-empty string")

    if not isinstance(citations, list) or not all(isinstance(x, str) for x in citations):
        errors.append("citations must be a list of strings")
    else:
        unknown = [x for x in citations if x not in allowed_sources]
        if unknown:
            errors.append(f"unknown citations: {unknown}")

    if not isinstance(confidence, (int, float)) or not (0 <= float(confidence) <= 1):
        errors.append("confidence must be a number in [0, 1]")

    if not isinstance(needs_human, bool):
        errors.append("needs_human must be boolean")

    return ValidationResult(len(errors) == 0, data if len(errors) == 0 else None, errors)

Esto protege contra "basura confiada": una respuesta puede verse bien y aun ser invalida.


llm.py — llamada al modelo con contrato claro

PYTHON
import os
from openai import OpenAI

api_key = os.environ.get("OPENAI_API_KEY")

if not api_key:
    raise EnvironmentError(
        "OPENAI_API_KEY is not set.\n"
        "Run: export OPENAI_API_KEY='sk-...'"
    )

client = OpenAI(api_key=api_key)

SYSTEM_PROMPT = """
You are a support agent.
Reply with VALID JSON only in this format:
{
  "answer": "short answer",
  "citations": ["KB-101"],
  "confidence": 0.0,
  "needs_human": false
}
Use only sources that exist in the provided context.
If data is insufficient, set needs_human=true.
""".strip()


def ask_model(question: str, context: str, feedback: str | None = None) -> str:
    user_prompt = (
        f"Customer question:\n{question}\n\n"
        f"Context:\n{context}\n\n"
        "Return JSON only."
    )

    if feedback:
        user_prompt += f"\n\nFix your previous response using this error feedback: {feedback}"

    completion = client.chat.completions.create(
        model="gpt-4.1-mini",
        temperature=0,
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt},
        ],
    )

    content = completion.choices[0].message.content
    return (content or "").strip()

No pedimos "solo explicar". Definimos un contrato estricto y lo verificamos.


main.py — ciclo de agente con retry, confidence gate y handoff

PYTHON
from knowledge import build_context, search_kb
from llm import ask_model
from validator import validate_model_output

MAX_STEPS = 4
MIN_CONFIDENCE = 0.65

QUESTION = "Can I get a refund for my subscription if 10 days have passed since payment?"


def run():
    snippets = search_kb(QUESTION, limit=2)
    allowed_sources = {s["id"] for s in snippets}
    context = build_context(snippets, max_chars=700)

    print("Allowed sources:", sorted(allowed_sources))
    print("Context:")
    print(context)

    feedback: str | None = None

    for step in range(1, MAX_STEPS + 1):
        print(f"\n=== STEP {step} ===")
        raw = ask_model(QUESTION, context, feedback=feedback)
        print("Model raw output:", raw)

        validation = validate_model_output(raw, allowed_sources)
        if not validation.ok:
            print("Validation failed:", validation.errors)
            feedback = "; ".join(validation.errors)
            continue

        data = validation.data
        assert data is not None

        if data["needs_human"] or data["confidence"] < MIN_CONFIDENCE:
            print("\nHandoff required:")
            print(
                "Model confidence is too low. Escalate this case to a human "
                f"(confidence={data['confidence']})."
            )
            return

        print("\nFinal answer:")
        print(data["answer"])
        print("Citations:", data["citations"])
        return

    print("\nStop: MAX_STEPS reached without a valid answer. Escalate to human.")


if __name__ == "__main__":
    run()

Esta es la idea clave: el agente no confia en el modelo sin validacion, incluso cuando el texto parece convincente.


requirements.txt

TEXT
openai>=1.0.0

Ejemplo de Salida

TEXT
python main.py
Allowed sources: ['KB-101', 'KB-102']
Context:
[KB-101] Refund Policy: Refunds are available within 14 days after payment. After 14 days, refunds are not available.
[KB-102] Pro Plan: Pro customers have priority support and a 4-hour SLA.

=== STEP 1 ===
Model raw output: {
  "answer": "Yes, you can get a refund for your subscription if 10 days have passed since payment, as refunds are available within 14 days after payment.",
  "citations": ["KB-101"],
  "confidence": 1.0,
  "needs_human": false
}

Final answer:
Yes, you can get a refund for your subscription if 10 days have passed since payment, as refunds are available within 14 days after payment.
Citations: ['KB-101']

Nota: el raw output exacto del modelo puede variar entre ejecuciones.
Criterio de correccion del ejemplo: el validador filtra respuestas invalidas/alucinadas y la respuesta final pasa la validacion.


Lo Que Se Ve en la Practica

Enfoque ingenuoEnfoque guarded
Acepta cualquier texto del modelo
Valida formato y fuentes
Tiene fallback con baja confianza
Tiene limite de pasos

Donde Seguir Profundizando

  • Agrega un presupuesto separado de max_tokens_per_run y registra el uso real
  • Agrega un segundo modelo revisor como quality gate independiente
  • Agrega needs_human_reason para que el operador vea la razon de escalacion
  • Agrega pruebas de replay con raw outputs fijos del modelo

Codigo Completo en GitHub

El repositorio contiene la version completa de esta demo: contexto de retrieval, verificaciones de quality gate y escalacion a humano.

Ver el codigo completo en GitHub ↗
⏱️ 7 min de lecturaActualizado 3 de marzo de 2026Dificultad: ★★☆
Integrado: control en producciónOnceOnly
Guardrails para agentes con tool-calling
Lleva este patrón a producción con gobernanza:
  • Presupuestos (pasos / topes de gasto)
  • Permisos de herramientas (allowlist / blocklist)
  • Kill switch y parada por incidente
  • Idempotencia y dedupe
  • Audit logs y trazabilidad
Mención integrada: OnceOnly es una capa de control para sistemas de agentes en producción.

Autor

Nick — ingeniero que construye infraestructura para agentes de IA en producción.

Enfoque: patrones de agentes, modos de fallo, control del runtime y fiabilidad del sistema.

🔗 GitHub: https://github.com/mykolademyanov


Nota editorial

Esta documentación está asistida por IA, con responsabilidad editorial humana sobre la exactitud, la claridad y la relevancia en producción.

El contenido se basa en fallos reales, post-mortems e incidentes operativos en sistemas de agentes de IA desplegados.