Warum KI-Agenten scheitern: LLM-Limits in Python (Vollständiges Beispiel)

Vollstandiges runnable Beispiel mit JSON-Validierung, Quellenprufung und Fallback zum Menschen bei niedriger Konfidenz.
Auf dieser Seite
  1. Was Dieses Beispiel Zeigt
  2. Projektstruktur
  3. Ausfuhren
  4. Was Wir Im Code Bauen
  5. Code
  6. knowledge.py — kontrollierte Quellen
  7. validator.py — strikte Validierung der Modellausgabe
  8. llm.py — Modellaufruf mit klarem Vertrag
  9. main.py — Agentenzyklus mit Retry, Confidence-Gate und Handoff
  10. requirements.txt
  11. Beispielausgabe
  12. Was Man In Der Praxis Sieht
  13. Wo Du Als Nachstes Vertiefen Kannst
  14. Vollstandiger Code auf GitHub

Dies ist die vollstandige Implementierung des Beispiels aus dem Artikel Warum Agenten scheitern: LLM-Grenzen.

Wenn du den Artikel noch nicht gelesen hast, beginne dort. Hier konzentrieren wir uns auf den Code: wie sich das Verhalten des Agents trotz der naturlichen Grenzen des Modells stabiler machen lasst.


Was Dieses Beispiel Zeigt

  • LLM kann beim Format Fehler machen oder Quellenverweise erfinden
  • Der Agent muss die Antwort deterministisch prufen und darf dem Text nicht nur "auf den ersten Blick" vertrauen
  • Niedrige Modellkonfidenz soll die Aufgabe in einen Handoff an einen Menschen uberfuhren
  • Schritt-Limit und begrenzter Kontext sind auch fur einen einfachen Fall erforderlich

Projektstruktur

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

Die Aufteilung in Module ist wichtig: Das Modell generiert, wahrend Validator und Policy Entscheidungen treffen.


Ausfuhren

1. Repository klonen und in den Ordner wechseln:

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

2. Abhangigkeiten installieren:

BASH
pip install -r requirements.txt

3. API-Key setzen:

BASH
export OPENAI_API_KEY="sk-..."

4. Starten:

BASH
python main.py

Was Wir Im Code Bauen

Wir bauen einen einfachen Guarded-Loop, um eine Kundenfrage zu beantworten.

  • wir holen relevante Ausschnitte aus einer kontrollierten Wissensbasis
  • wir geben dem Modell einen klaren JSON-Antwortvertrag
  • der Validator pruft Format, Zitate und Konfidenzniveau
  • wenn die Antwort die Prufung nicht besteht, machen wir Retry; ist die Konfidenz niedrig, eskalieren wir an einen Menschen

Dies ist ein Lernbeispiel, bei dem nicht die "schone Antwort" zahlt, sondern kontrollierbares und uberprufbares Agentenverhalten.


Code

knowledge.py — kontrollierte Quellen

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 antwortet nur auf Basis dieses Kontexts. Das verringert den Spielraum fur Erfindungen.


validator.py — strikte Validierung der Modellausgabe

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)

Das schutzt vor "selbstsicherem Mull": Eine Antwort kann gut aussehen und trotzdem ungueltig sein.


llm.py — Modellaufruf mit klarem Vertrag

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

Wir bitten nicht darum, "einfach zu erklaren". Wir definieren einen strikten Vertrag und prufen ihn.


main.py — Agentenzyklus mit Retry, Confidence-Gate und 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()

Das ist die Kernidee: Der Agent vertraut dem Modell ohne Prufung nicht, selbst wenn der Text uberzeugend wirkt.


requirements.txt

TEXT
openai>=1.0.0

Beispielausgabe

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

Hinweis: Der genaue raw output des Modells kann zwischen Ausfuhrungen variieren.
Korrektheitskriterium des Beispiels: Der Validator filtert ungueltige/halluzinierte Antworten heraus, und die finale Antwort besteht die Prufung.


Was Man In Der Praxis Sieht

Naiver AnsatzGuarded-Ansatz
Akzeptiert jeden Modelltext
Validiert Format und Quellen
Hat Fallback bei niedriger Konfidenz
Hat ein Schritt-Limit

Wo Du Als Nachstes Vertiefen Kannst

  • Fuge ein separates max_tokens_per_run-Budget hinzu und logge die tatsachliche Nutzung
  • Fuge ein zweites Reviewer-Modell als unabhangiges Quality Gate hinzu
  • Fuge needs_human_reason hinzu, damit der Operator den Eskalationsgrund sieht
  • Fuge Replay-Tests mit festen raw outputs des Modells hinzu

Vollstandiger Code auf GitHub

Im Repository liegt die vollstandige Version dieser Demo: Retrieval-Kontext, Quality-Gate-Prufungen und Eskalation an einen Menschen.

Vollstandigen Code auf GitHub ansehen ↗
⏱️ 7 Min. LesezeitAktualisiert 3. März 2026Schwierigkeit: ★★☆
Integriert: Production ControlOnceOnly
Guardrails für Tool-Calling-Agents
Shippe dieses Pattern mit Governance:
  • Budgets (Steps / Spend Caps)
  • Tool-Permissions (Allowlist / Blocklist)
  • Kill switch & Incident Stop
  • Idempotenz & Dedupe
  • Audit logs & Nachvollziehbarkeit
Integrierter Hinweis: OnceOnly ist eine Control-Layer für Production-Agent-Systeme.

Autor

Nick — Engineer, der Infrastruktur für KI-Agenten in Produktion aufbaut.

Fokus: Agent-Patterns, Failure-Modes, Runtime-Steuerung und Systemzuverlässigkeit.

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


Redaktioneller Hinweis

Diese Dokumentation ist KI-gestützt, mit menschlicher redaktioneller Verantwortung für Genauigkeit, Klarheit und Produktionsrelevanz.

Der Inhalt basiert auf realen Ausfällen, Post-Mortems und operativen Vorfällen in produktiv eingesetzten KI-Agenten-Systemen.