$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогРуководство по контрактам для AI: делаем входные/выходные схемы и тестируем ответы модели

Руководство по контрактам для AI: делаем входные/выходные схемы и тестируем ответы модели

$ sudo teach IT
·4 августа 2026 г.·9 мин·58
Руководство по контрактам для AI: делаем входные/выходные схемы и тестируем ответы модели

Как превратить LLM-интеграцию в предсказуемый сервис: задаём схемы входных данных и результата, валидируем ответы, ловим деградацию качества и строим минимальный набор регрессионных тестов. Без иллюзий о “гарантиях” модели.

Содержание
Контракт для AI: что именно вы “гарантируете” системойВыбор формы контрактов: текст vs структурированные данныеКонтракт входа: фиксируем семантику и ограниченияЧто обычно уходит в контракт входаПочему это снижает деградациюТиповые ошибки в контракте входаКонтракт выхода: проектируем схему, а не “просим ответить”Минимально жизнеспособная схемаПример схемы на Python (Pydantic)Стратегии при невалидном выходеИнструментируем промпт под контракт: как формулировать требования моделиПример системной инструкции (идея, не привязана к конкретному SDK)Валидируем ответы: автоматическая проверка контрактовВалидация по типам и полямПример валидатора инвариантовНе ограничивайтесь JSON-валидациейТестирование LLM: почему нужны регрессионные тесты, а не “ручная проверка”Уровни тестовМинимальный набор регрессионных тестов: как не утонутьПрактика: тестируйте на фиксированных входах и фиксированных версиях промптаПример регрессионного теста (property-based) на Python

Интеграция LLM (large language model) в продукт часто начинается как прототип: “сформируем промпт — получим текст”. Но как только модель становится частью сервиса, появляются инженерные вопросы: насколько предсказуемы результаты, как обнаруживать деградацию качества, как валидировать ответы и как удерживать стабильность при изменениях модели, промптов и контекста.

Ключевая идея — относиться к LLM как к компоненту, который обменивается данными по контракту. Контракт задаёт формат входа, формат выхода, семантические инварианты и правила валидации. В отличие от “гарантий качества” самой модели, контракт даёт вам контроль на стороне системы: вы можете проверять структуру, снижать вариативность, отлавливать сбои и вести регрессионные тесты.

Ниже — практическое руководство: как превратить LLM-интеграцию в предсказуемый сервис за счёт схем входа/выхода, валидаторов, мониторинга и минимального набора регрессионных тестов. Без иллюзий: модель может ошибаться, но вы можете системно уменьшить хаос.


Контракт для AI: что именно вы “гарантируете” системой

Начнём с определения. Контракт для AI — это набор требований, которые ваша система гарантирует до и после вызова модели:

  1. Контракт входа

    • модель получает строго определённый формат данных;
    • поля не “плавают” по типам;
    • присутствуют обязательные атрибуты;
    • есть ограничения (длины, допустимые значения, язык, домен и т. п.).
  2. Контракт выхода

    • модель возвращает данные в ожидаемой структуре;
    • формат устойчив к шуму (а значит, вы можете парсить и валидировать);
    • есть явные поля и коды ошибок/отказов.
  3. Инварианты и правила

    • если ответ “должен” содержать определённые поля, он обязан их содержать;
    • если не может выполнить задачу — возвращает отказ в структурированном виде;
    • текстовые поля ограничены по длине или по допустимым символам/языку (где возможно).
  4. Валидация и обработка несоответствий

    • при невалидном ответе вы не “молчаливо принимаете текст”;
    • есть стратегия: повторный запрос, fallback, отказ пользователю, логирование.
  5. Тестирование и регрессия

    • на уровне контрактов — валидность структуры и инвариантов;
    • на уровне качества — заранее выбранные сценарии с ожидаемыми свойствами (а не “точным текстом”).

Важно: контракт не гарантирует правильность содержания так, как гарантирует строгая формальная система. Но он гарантирует, что ваш сервис ведёт себя предсказуемо: ошибки обнаруживаются, формат не ломается, деградация не прячется.


Выбор формы контрактов: текст vs структурированные данные

Слабое место большинства LLM-подходов — “попросили текстом — получили текстом”. Для контракта лучше перейти к структурированным ответам, которые можно парсить.

Практические варианты:

  • JSON-ответ: модель возвращает объект со строго фиксированной схемой.
  • Схема “tool/function calling”: модель заполняет аргументы функции (часто это ближе к JSON).
  • Семантическая разметка (например, XML/пронумерованные секции) — хуже с точки зрения строгой валидации, но иногда удобнее.

На практике чаще всего выигрывает JSON + строгий валидатор схемы (например, JSON Schema или Pydantic/TypedDict в Python). Модель при этом просим “вернуть только JSON”.

Однако важно понимать: модель может вернуть некорректный JSON. Поэтому контракт включает не только “просим JSON”, но и механизмы восстановления: повтор с “исправь формат”, fallback на другой режим, или отказ.


Контракт входа: фиксируем семантику и ограничения

Что обычно уходит в контракт входа

Для большинства сценариев LLM-интеграции имеет смысл фиксировать:

  • task_type — тип задачи (классификация, извлечение, генерация ответа, резюме и т. п.)
  • language — язык ответа
  • input — основной текст/данные
  • context — дополнительные контекстные факты (если нужны)
  • constraints — ограничения (тон, длина, запреты, формат)
  • request_id — идентификатор запроса (для логирования и трассировки)
  • client_version / prompt_version — версия промпта/схемы (важно для регрессии)

Почему это снижает деградацию

Модель чувствительна к тому, как мы формулируем условия. Если вы не фиксируете поля и их значения, любые изменения в промпте могут незаметно изменить поведение. Контракт входа позволяет:

  • сохранять единообразие структуры;
  • сравнивать результаты по одним и тем же сценариям;
  • проще воспроизводить баги (“эта регрессия случилась на входе X при версии промпта Y”).

Типовые ошибки в контракте входа

  1. Переизбыток текста без явных полей
    Когда всё “в одном абзаце”, вы не валидируете ничего. Лучше вынести параметры в поля.

  2. Отсутствие ограничений
    Если не ограничить длину/язык/домен, LLM начнёт “компенсировать” неопределённость текстом.

  3. Неявные зависимости
    Например, “если user спрашивает про возврат — значит можно упоминать политику”. Это должно быть либо в правилах, либо в параметрах/контексте.


Контракт выхода: проектируем схему, а не “просим ответить”

Минимально жизнеспособная схема

Любая схема выхода должна решать три задачи:

  1. Определить статус (успех/отказ/частичный успех).
  2. Дать структуру результата (что именно вернуло).
  3. Добавить диагностические поля для инженерной эксплуатации.

Пример: контракт для задачи “ответ на вопрос с цитатами из контекста” или “ассистент”. Он может выглядеть так:

  • status: "ok" | "refusal" | "error"
  • answer: строка (или структурированный объект)
  • citations: массив ссылок/фрагментов (опционально)
  • safety_notes: краткие пояснения почему отказ
  • confidence: оценка (осторожно: не как “истина”, а как внутренний сигнал модели)
  • validation: метаданные вашей валидации (например, какие поля были проверены)

Пример схемы на Python (Pydantic)

code
from pydantic import BaseModel, Field
from typing import List, Literal, Optional

class AIResponse(BaseModel):
    status: Literal["ok", "refusal", "error"]
    answer: Optional[str] = Field(default=None, max_length=4000)
    citations: List[str] = Field(default_factory=list)
    safety_notes: Optional[str] = Field(default=None, max_length=1000)
    confidence: Optional[float] = Field(default=None, ge=0.0, le=1.0)

    # Поля для отладки и контроля контрактов
    model: Optional[str] = None
    prompt_version: Optional[str] = None
    error_code: Optional[str] = None

Важно: answer может быть None при refusal/error. Это тоже контракт — вы не допускаете “ответ в неправильном статусе”.

Стратегии при невалидном выходе

Если модель вернула невалидный JSON или несоответствующее поле:

  1. Лёгкая реконсиляция: второй запрос — “исправь формат, верни только JSON по схеме”.
  2. Жёсткий отказ: возвращаем status="error" и логируем.
  3. Fallback на другой тип ответа: если у вас есть альтернативный генератор (например, более строгий режим извлечения).

Ключевой принцип: контракт выхода и обработчик ошибок должны быть частью системы, а не “надеяться, что модель справится”.


Инструментируем промпт под контракт: как формулировать требования модели

Контракт — это не только схемы в коде, но и инструкция модели. Практический подход:

  • “Верни ТОЛЬКО JSON”
  • “Используй строго эти поля”
  • “Не добавляй дополнительных ключей”
  • “Статус refusal, если …”
  • “Длина answer не более … символов”
  • “Если данных недостаточно — верни refusal или error_code”

Но будьте осторожны: чрезмерная строгость промпта может увеличить вероятность “продуцирования кода”, т. е. модель начнёт имитировать структуру, но при этом ошибётся в содержании. Поэтому лучше сочетать:

  • чёткие правила,
  • валидацию,
  • повторную фиксацию формата только по необходимости.

Пример системной инструкции (идея, не привязана к конкретному SDK)

code
Ты — модуль ответов по контракту.
Верни результат строго в JSON согласно схеме AIResponse.
- Используй только поля: status, answer, citations, safety_notes, confidence, model, prompt_version, error_code.
- Не добавляй других полей и не выводи текст вне JSON.
- status:
  - "ok" — если можно сформировать ответ;
  - "refusal" — если нельзя отвечать по политике/безопасности;
  - "error" — если недостаточно данных или произошла ошибка.
- answer: строка <= 4000 символов или null, если status != "ok".

Валидируем ответы: автоматическая проверка контрактов

Валидация по типам и полям

После вызова модели у вас должен быть этап:

  1. Парсинг результата (JSON).
  2. Проверка схемы (types, длины, диапазоны, обязательность).
  3. Проверка инвариантов (статус ↔ поля).

Например, инвариант: если status="refusal", то answer должен быть None (или пустым) и safety_notes заполнен.

Пример валидатора инвариантов

code
def validate_invariants(resp: AIResponse) -> None:
    if resp.status == "ok":
        if not resp.answer:
            raise ValueError("status=ok требует answer")
    elif resp.status == "refusal":
        if resp.answer is not None and resp.answer.strip() != "":
            raise ValueError("status=refusal не должен содержать answer")
        if not resp.safety_notes:
            raise ValueError("status=refusal требует safety_notes")
    elif resp.status == "error":
        # Обычно answer отсутствует
        if resp.answer is not None and resp.answer.strip() != "":
            # допускайте ваш вариант, но лучше фиксировать строго
            raise ValueError("status=error не должен содержать answer")
        if not resp.error_code:
            raise ValueError("status=error требует error_code")

Не ограничивайтесь JSON-валидацией

JSON-схема проверяет структуру, но не содержание. Для инженеринга качества добавляют семантические проверки:

  • Ответ на классификацию содержит ровно одно значение из набора.
  • В ответе отсутствуют запрещённые фразы.
  • Если ожидаются “цитаты”, то citations не пустой.
  • Если ответ должен быть на русском — проверьте язык (хотя бы грубо: доля кириллицы).

Эти проверки превращают LLM из “генератора текста” в “компонент с измеримыми критериями”.


Тестирование LLM: почему нужны регрессионные тесты, а не “ручная проверка”

Ручной контроль в какой-то момент перестаёт масштабироваться. LLM-системы деградируют не только из-за “плохой модели”. Типичные причины регрессии:

  • обновление модели в провайдере;
  • изменение параметров генерации (temperature, top_p, max_tokens);
  • изменение промпта/шаблонов;
  • изменение системы контекста (retrieval, ранжирование фрагментов);
  • обновление токенизации/лимитов;
  • случайные изменения в подготовке входа (например, вырезание важного поля).

Поэтому стратегия: минимальный набор регрессионных тестов должен быть внедрён в пайплайн так же, как тесты для обычного кода.

Уровни тестов

  1. Контрактные тесты (обязательные)

    • валиден JSON/схема;
    • инварианты статуса соблюдены;
    • поля не выходят за лимиты;
    • отсутствие “лишнего текста”.
  2. Семантические свойства (желательные)

    • классификация в пределах допустимых значений;
    • ответ содержит ключевые элементы (например, заголовок/список/поля);
    • соблюдение политик (не выдаём запрещённое).
  3. Качественные тесты (аккуратные)

    • вместо точного совпадения текста используем измеримые метрики:
      • наличие фактов/чисел,
      • соответствие требованиям (“должно быть кратко”),
      • “не противоречит” контексту (через эвристику),
      • формат ответа.

Важно: точное совпадение ответа почти всегда хрупкое. LLM — стохастическая система (даже при низкой температуре). Поэтому правильнее тестировать свойства, а не строки.


Минимальный набор регрессионных тестов: как не утонуть

Хорошая цель — 20–50 тестовых сценариев, которые покрывают риск. Не “все запросы пользователя”, а ключевые классы:

  1. Нормальные кейсы

    • типичный вопрос/задача;
    • ожидаемый формат и структура.
  2. Пограничные кейсы ввода

    • пустой/короткий input,
    • слишком длинный input,
    • нестандартные символы,
    • отсутствующий контекст.
  3. Кейсы отказа (safety/refusal)

    • запросы, где по политике нужно отказаться;
    • запросы, где недостаточно данных (должен вернуться error или refusal в зависимости от вашей модели поведения).
  4. Проверка устойчивости формата

    • тестирует “всегда ли возвращается JSON” при разных входах;
    • ловит случаи, когда модель внезапно начала возвращать markdown/текст.

Практика: тестируйте на фиксированных входах и фиксированных версиях промпта

Пример: вы хотите сравнивать результат до/после. Для этого:

  • фиксируйте prompt_version;
  • фиксируйте параметры генерации (temperature и т. п.);
  • сохраняйте “золотые” входы и ожидания.

Если вы не можете фиксировать ответы (из-за API стохастики), тесты должны быть property-based.


Пример регрессионного теста (property-based) на Python

Ниже — упрощённый пример. Он не привязан к конкретному SDK, но демонстрирует идею: вызываем модель, валидируем контракт и проверяем свойства ответа.

code
import json
from typing import Any, Dict

# Допустим, у вас есть функция call_llm(...) которая возвращает строку
# с JSON и вы уже знаете prompt_version и используемую модель.
def call_llm(prompt_version: str, user_input: str) -> str:
    # Заглушка: здесь ваш реальный вызов провайдера
    raise NotImplementedError

def run_test_case(test: Dict[str, Any]) -> None:
    prompt_version = test["prompt_version"]
    user_input = test["input"]

    raw = call_llm(prompt_version=prompt_version, user_input=user_input)

    # 1) Парсинг JSON
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as e:
        raise AssertionError(f"LLM returned invalid JSON: {e}. Raw={raw[:200]}...")

    # 2) Валидация схемы (пример через Pydantic)
    resp = AIResponse(**data)
    validate_invariants(resp)

    # 3) Property checks
    # Например, для ok-ответа должны быть citations (если требуются по вашему ТЗ)
    if test.get("require_citations", False):
        if resp.status == "ok" and len(resp.citations) == 0:
            raise AssertionError("Expected non-empty citations for ok response")

    if test.get("answer_must_contain", None):
        must = test["answer_must_contain"]
        if resp.status == "ok":
            if must.lower() not in (resp.answer or "").lower():
                raise AssertionError(f"Answer must contain '{must}'")

    # Проверка длины (контракт уже проверил, но это дополнительная защита)
    if resp.answer and len(resp.answer) > 4000:
        raise AssertionError("Answer exceeds max length")

def main():
    tests = [
        {
            "name": "normal_question",
            "prompt_version": "v1.3",
            "input": "Кратко объясни, что такое TF-IDF.",
            "require_citations": False,
            "answer_must_contain": "TF-IDF",
        },
        {
            "name": "missing_context_should_error",
            "prompt_version": "v1.3",
            "input": "Сформируй ответ по документу, которого нет в контексте.",
            # Ожидаем status=error или refusal — зависит от вашей политики
            # Добавьте проверки, которые отражают ваш контракт поведения
        },
    ]

    for t in tests:
        run_test_case(t)
        print(f"OK: {t['name']}")

if __name__ == "__main__":

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Solidity DLC - поймай, если сможешь

Solidity DLC - поймай, если сможешь

Аудит смарт-контрактов на реальных взломах. The DAO, Parity, Wormhole, Ronin, Euler и ещё 7 разборов: что пошло не так, сколько потеряли и что бы спасло. Атаки и защиты на Foundry, план входа в профессию аудитора.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Solidity pro - всё или ничего

Solidity pro - всё или ничего

Как пишут контракты, которые работают в основной сети. Наследование и OpenZeppelin, паттерны продакшена, оптимизация газа, ABI, delegatecall и подписи. 7 модулей, 33 урока, а в финале собственное dApp: мини-DAO с токеном, голосованием и казной.

Перейти к курсу

Другие статьи

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
1960
Как начать с HTML и сделать первую страницу: пошагово и без сложных слов
html

Как начать с HTML и сделать первую страницу: пошагово и без сложных слов

Разберём, что нужно новичку, чтобы за 1–2 вечера собрать свою первую веб-страницу: структура HTML, базовые теги, ссылки и картинки, плюс простой чек-лист, как понять, что страница работает правильно.

2 ноября 2026 г.
20
Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей
cargo

Cargo для продакшена: workspaces, фичи и стратегия версий зависимостей

Разберём, как организовать Rust workspace, управлять optional features, фиксировать версии библиотек и уменьшать “дрейф” зависимостей при командной разработке и CI.

18 сентября 2026 г.
1960
Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах
такое

Что такое HTML и чем он отличается от CSS — объясню простыми словами и на примерах

Разберём, из чего состоит веб-страница: что делает HTML (смысл и структура) и что делает CSS (оформление). В конце соберёте мини-страницу и поймёте, что нужно учить в каком порядке.

30 октября 2026 г.
130
Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck
деплой

Деплой без сюрпризов: как подготовить Docker-образ, переменные окружения и healthcheck

Разберём практический чек-лист подготовки контейнера к продакшену: как собрать воспроизводимый образ, как правильно прокидывать конфиги/секреты через окружение и как настроить healthcheck, чтобы деплой не “успевал” раньше готовности сервиса.

18 сентября 2026 г.
1930
Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами
такое

Что такое SQL и зачем он нужен новичку: как думать о данных простыми словами

Разберём, что такое базы данных и SQL без сложной теории: как выбирать данные, фильтровать их и собирать таблицы в отчёт. В конце — короткий план, как потренироваться и собрать первые запросы для портфолио.

28 октября 2026 г.
230

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Контракт для AI: что именно вы “гарантируете” системойВыбор формы контрактов: текст vs структурированные данныеКонтракт входа: фиксируем семантику и ограниченияЧто обычно уходит в контракт входаПочему это снижает деградациюТиповые ошибки в контракте входаКонтракт выхода: проектируем схему, а не “просим ответить”Минимально жизнеспособная схемаПример схемы на Python (Pydantic)Стратегии при невалидном выходеИнструментируем промпт под контракт: как формулировать требования моделиПример системной инструкции (идея, не привязана к конкретному SDK)Валидируем ответы: автоматическая проверка контрактовВалидация по типам и полямПример валидатора инвариантовНе ограничивайтесь JSON-валидациейТестирование LLM: почему нужны регрессионные тесты, а не “ручная проверка”Уровни тестовМинимальный набор регрессионных тестов: как не утонутьПрактика: тестируйте на фиксированных входах и фиксированных версиях промптаПример регрессионного теста (property-based) на Python