Чому агент помиляється: обмеження LLM — Python (повна реалізація)

Повний runnable приклад з JSON-валідацією, перевіркою джерел і fallback до людини при низькій впевненості.
На цій сторінці
  1. Що цей приклад демонструє
  2. Структура проєкту
  3. Як запустити
  4. Код
  5. knowledge.py — контрольовані джерела
  6. validator.py — сувора перевірка відповіді моделі
  7. llm.py — виклик моделі з чітким контрактом
  8. main.py — агентний цикл з retry, confidence-гейтом і handoff
  9. requirements.txt
  10. Приклад виводу
  11. Чому це працює проти LLM-обмежень
  12. Де далі копати
  13. 💻 Повний код на GitHub

Це повна реалізація прикладу зі статті Чому агент помиляється: обмеження LLM.

Якщо ти ще не читав статтю, почни з неї. Тут ми фокусуємось на коді: як зробити поведінку агента стабільнішою попри природні обмеження моделі.


Що цей приклад демонструє

  • LLM може помилятися у форматі або вигадувати посилання на джерела
  • Агент має перевіряти відповідь детерміновано, а не довіряти тексту "на вигляд"
  • Низька впевненість моделі має переводити задачу в handoff до людини
  • Ліміт кроків і обмежений контекст потрібні навіть для простого кейсу

Структура проєкту

TEXT
examples/
└── 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

Поділ на модулі важливий: модель генерує, а валідатор і policy приймають рішення.


Як запустити

1. Клонуй репозиторій і перейди в папку:

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

2. Встанови залежності:

BASH
pip install -r requirements.txt

3. Вкажи API-ключ:

BASH
export OPENAI_API_KEY="sk-..."

4. Запусти:

BASH
python main.py

Код

knowledge.py — контрольовані джерела

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 відповідає лише на основі цього контексту. Це зменшує простір для вигадок.


validator.py — сувора перевірка відповіді моделі

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)

Це захист від "впевненого сміття": відповідь може бути красивою, але невалідною.


llm.py — виклик моделі з чітким контрактом

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

Ми не просимо "просто пояснити". Ми задаємо строгий контракт і перевіряємо його.


main.py — агентний цикл з retry, confidence-гейтом і 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:", 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()

Це ключова ідея: агент не довіряє моделі без перевірки, навіть коли текст виглядає переконливо.


requirements.txt

TEXT
openai>=1.0.0

Приклад виводу

TEXT
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, a refund is possible.","citations":["KB-404"],"confidence":0.92,"needs_human":false}
Validation failed: ["unknown citations: ['KB-404']"]

=== STEP 2 ===
Model raw output: {"answer":"Yes, refunds are available within 14 days after payment.","citations":["KB-101"],"confidence":0.78,"needs_human":false}

Final answer:
Yes, refunds are available within 14 days after payment.
Citations: ['KB-101']

Чому це працює проти LLM-обмежень

Наївний підхідGuarded підхід
Приймає будь-який текст моделі
Валідує формат і джерела
Має fallback при низькій впевненості
Має межу кроків

Де далі копати

  • Додай окремий max_tokens_per_run бюджет і логуй фактичне використання
  • Додай другу модель-ревʼювер як незалежний quality gate
  • Додай needs_human_reason, щоб оператор бачив причину ескалації
  • Додай replay-тести з фіксованими raw-відповідями моделі

💻 Повний код на GitHub

Дивись на GitHub

⏱️ 6 хв читанняОновлено Бер, 2026Складність: ★★☆
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.