Pourquoi les agents IA échouent : limites des LLM en Python (Exemple complet)

Exemple runnable complet avec validation JSON, verification des sources et fallback vers un humain en cas de faible confiance.
Sur cette page
  1. Ce Que Cet Exemple Demontre
  2. Structure du Projet
  3. Comment Executer
  4. Ce Que Nous Construisons Dans Le Code
  5. Code
  6. knowledge.py — sources controlees
  7. validator.py — validation stricte de la sortie du modele
  8. llm.py — appel du modele avec un contrat clair
  9. main.py — boucle d'agent avec retry, confidence gate et handoff
  10. requirements.txt
  11. Exemple de Sortie
  12. Ce Que L'on Voit En Pratique
  13. Ou Aller Plus Loin
  14. Code Complet sur GitHub

Ceci est l'implementation complete de l'exemple de l'article Pourquoi l'agent se trompe : limites des LLM.

Si vous n'avez pas encore lu l'article, commencez par la. Ici, nous nous concentrons sur le code : comment rendre le comportement de l'agent plus stable malgre les limites naturelles du modele.


Ce Que Cet Exemple Demontre

  • LLM peut se tromper sur le format ou inventer des citations de sources
  • L'agent doit valider la reponse de maniere deterministe au lieu de faire confiance au texte "a vue d'oeil"
  • Une faible confiance du modele doit transferer la tache en handoff vers un humain
  • Une limite d'etapes et un contexte borne sont necessaires meme pour un cas simple

Structure du Projet

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 separation en modules est importante : le modele genere, et le validateur et la policy prennent les decisions.


Comment Executer

1. Clonez le depot et allez dans le dossier :

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

2. Installez les dependances :

BASH
pip install -r requirements.txt

3. Definissez la cle API :

BASH
export OPENAI_API_KEY="sk-..."

4. Lancez :

BASH
python main.py

Ce Que Nous Construisons Dans Le Code

Nous construisons une boucle gardee simple pour repondre a la question d'un client.

  • recuperer des extraits pertinents d'une base de connaissances controlee
  • donner au modele un contrat JSON de reponse strict
  • le validateur verifie le format, les citations et le niveau de confiance
  • si la reponse ne passe pas la validation, on fait un retry ; si la confiance est faible, on escalade vers un humain

C'est un exemple pedagogique ou l'essentiel n'est pas une "belle reponse", mais un comportement d'agent controle et verifiable.


Code

knowledge.py — sources controlees

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 repond uniquement sur la base de ce contexte. Cela reduit la marge pour les inventions.


validator.py — validation stricte de la sortie du modele

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)

Cela protege contre le "dechet confiant" : une reponse peut sembler bonne mais rester invalide.


llm.py — appel du modele avec un contrat clair

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()

Nous ne demandons pas de "simplement expliquer". Nous definissons un contrat strict et nous le verifions.


main.py — boucle d'agent avec retry, confidence gate et 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()

C'est l'idee cle : l'agent ne fait pas confiance au modele sans validation, meme si le texte semble convaincant.


requirements.txt

TEXT
openai>=1.0.0

Exemple de Sortie

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']

Note : le raw output exact du modele peut varier entre les executions.
Critere de correction de l'exemple : le validateur ecarte les reponses invalides/hallucinees, et la reponse finale passe la validation.


Ce Que L'on Voit En Pratique

Approche naiveApproche guardee
Accepte n'importe quel texte du modele
Valide le format et les sources
A un fallback en cas de faible confiance
A une limite d'etapes

Ou Aller Plus Loin

  • Ajoute un budget max_tokens_per_run separe et journalise l'utilisation reelle
  • Ajoute un deuxieme modele reviewer comme quality gate independant
  • Ajoute needs_human_reason pour que l'operateur voie la raison de l'escalade
  • Ajoute des tests de replay avec des raw outputs fixes du modele

Code Complet sur GitHub

Le depot contient la version complete de cette demo : contexte de retrieval, verifications de quality gate et escalation vers un humain.

Voir le code complet sur GitHub ↗
⏱️ 7 min de lectureMis à jour 3 mars 2026Difficulté: ★★☆
Intégré : contrôle en productionOnceOnly
Ajoutez des garde-fous aux agents tool-calling
Livrez ce pattern avec de la gouvernance :
  • Budgets (steps / plafonds de coût)
  • Permissions outils (allowlist / blocklist)
  • Kill switch & arrêt incident
  • Idempotence & déduplication
  • Audit logs & traçabilité
Mention intégrée : OnceOnly est une couche de contrôle pour des systèmes d’agents en prod.

Auteur

Nick — ingénieur qui construit une infrastructure pour des agents IA en production.

Focus : patterns d’agents, modes de défaillance, contrôle du runtime et fiabilité des systèmes.

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


Note éditoriale

Cette documentation est assistée par l’IA, avec une responsabilité éditoriale humaine pour l’exactitude, la clarté et la pertinence en production.

Le contenu s’appuie sur des défaillances réelles, des post-mortems et des incidents opérationnels dans des systèmes d’agents IA déployés.