Walidacja strukturyzowanych danych wyjściowych LLM w Pythonie, która się sprawdza

Przestań polegać na intuicji. Waliduj kontrakty.

Page content

Większość tutoriali dotyczących „strukturyzowanego wyjścia” (structured output) w LLM jest nieodpowiedzialna. Uczą, jak grzecznie poprosić o JSON, a potem liczą, że model zachowa się poprawnie. To nie jest walidacja. To optymizm z nawiasami klamrowymi.

Dokumentacja OpenAI wyraźnie rozróżnia te pojęcia. Tryb JSON gwarantuje poprawny składniowo JSON, podczas gdy Strukturyzowane Wyjścia (Structured Outputs) wymuszają zgodność ze schematem, a OpenAI zaleca używanie Strukturyzowanych Wyjść zamiast trybu JSON, o ile to możliwe.

infografika walidacji strukturyzowanego wyjścia

Nawet to nie czyni obciążenia (payload) godnym zaufania. JSON Schema definiuje strukturę i dozwalone wartości, Pydantic zapewnia typowaną walidację w Pythonie, a OpenAI wyraźnie zaznacza, że odpowiedź zgodna ze schematem może nadal zawierać nieprawidłowe wartości. Co więcej, odmowy i niekompletne wyjścia mogą ominąć oczekiwaną strukturę. W środowisku produkcyjnym walidacja strukturyzowanego wyjścia jest potokiem (pipeline), a nie przełącznikiem. Ta sama granica musi istnieć w szerszym kontekście przepustowości, ponownych prób i limitów planistycznych opisanych na hubie inżynierii wydajności LLM.

Walidacja strukturyzowanego wyjścia to umowa (kontrakt)

Walidacja strukturyzowanego wyjścia dla LLM oznacza, że z góry definiujesz kształt odpowiedzi, zmuszasz model do wygenerowania tego kształtu (o ile to możliwe), a następnie ponownie walidujesz wynik, zanim Twoja aplikacja mu ufa. W praktyce oznacza to sprawdzanie wymaganych pól, typów, enumów, zamkniętych kształtów obiektów i reguł domenowych, zanim obciążenie dotnie bazy danych, interfejsu użytkownika, kolejki lub usługi dół strumienia. JSON Schema istnieje dokładnie dla tego rodzaju walidacji strukturalnej, Pydantic jest zbudowany do walidacji niezaufanych danych względem podpowiedzi typów Pythona, a biblioteka jsonschema w Pythonie daje Ci bezpośredni sposób na walidację instancji względem schematu.

Istnieje również wyraźny podział między dwoma powszechnymi przypadkami użycia. Jeśli model ma odpowiadać użytkownikowi w strukturyzowanym formacie, użyj strukturyzowanego formatu odpowiedzi. Jeśli model ma wywoływać narzędzia lub funkcje Twojej aplikacji, użyj wywoływania funkcji (function calling). Dokumentacja OpenAI precyzuje to rozróżnienie i dla wywoływania funkcji zaleca włączenie opcji strict: true, aby argumenty niezawodnie przestrzegały schematu funkcji.

Moja silna opinia jest prosta. Traktuj każdą strukturyzowaną odpowiedź LLM jako granicę interfejsu API. Gdy zacznisz myśleć w kategoriach kontraktów, a nie promptów, architektura staje się czystsza, błędy tańsze, a problem „dlaczego model wymyślił nowe pole w produkcji” znika niemal całkowicie. To jest prawdziwa odpowiedź na pytanie „co to jest walidacja strukturyzowanego wyjścia dla LLM” i jest znacznie lepsza niż „uprzejmie poproś model o JSON”.

Tryb JSON nie jest walidacją

Jeśli zapamiętasz tylko jedną rzecz z tego artykułu, niech to będzie to. Tryb JSON nie jest walidacją schematu. Centrum Pomocy OpenAI mówi, że tryb JSON nie gwarantuje, że wyjście będzie zgodne z jakimkolwiek konkretnym schematem, tylko że jest to poprawny JSON i parsuje się bez błędów. Przewodnik po Strukturyzowanych Wyjściach mówi to samo w bardziej przejrzysty sposób. Zarówno tryb JSON, jak i Strukturyzowane Wyjścia mogą generować poprawny JSON, ale tylko Strukturyzowane Wyjścia wymuszają zgodność ze schematem.

Ta różnica ma większe znaczenie, niż ludzie przyznają. W swoim poście ogłoszeniowym dotyczącym Strukturyzowanych Wyjść, OpenAI doniosło, że gpt-4o-2024-08-06 ze Strukturyzowanymi Wyjściami uzyskał 100 procent w swoich złożonych testach schematu JSON, podczas gdy gpt-4-0613 uzyskał mniej niż 40 procent. Nie musisz traktować tych liczb jako uniwersalnej prawdy, aby zobaczyć szerszy punkt. Wymuszanie schematu zmienia powierzchnię błędu z „może się zdarzyć cokolwiek” na „kontrakt jest znacznie ściślejszy”.

Nadal istnieją przypadki brzegowe, a udawanie, że ich nie ma, to sposób, w jaki demo zabawki staje się obowiązkiem czuwania (pager duty). OpenAI dokumentuje, że model może odmówić niebezpiecznego żądania, a te odmowy są eksponowane poza Twoją normalną ścieżką schematu. Dokumentuje również niekompletne odpowiedzi, w tym przypadki takie jak osiągnięcie max_output_tokens lub przerwanie przez filtr treści. Więc pytanie FAQ „czy tryb JSON wystarczy dla niezawodnego wyjścia LLM” ma krótką i długą odpowiedź. Krótka odpowiedź brzmi: nie. Dłuższa odpowiedź brzmi: nawet ścisłe strukturyzowane wyjście wymaga wyraźnej obsługi błędów.

Gdzie strukturyzowane wyjście nadal się psuje

Wymuszanie schematu zmniejsza problem. Nie usuwa go. W rzeczywistym ruchu nadal widzisz uszkodzone lub zaskakujące obciążenia z powodów, które mają niewiele wspólnego z sformułowaniem promptu.

Kształty błędów, które warto zaprojektować

Modele i klienci nie zgadzają się co do szczegółów. Możesz otrzymać dodatkową prozę przed lub po JSONie, bloki ogrodzone Markdownem wokół obciążenia lub wywołanie narzędzia, którego nazwa jest poprawna, ale którego argumenty są JSONem niepasującym do Twojego modelu Pydantic. Streaming pogarsza sytuację, ponieważ możesz walidować niedokoń bufor. Kod defensywny powinien zakładać „string wchodzi, może być JSON w środku”, a nie „bity na linii już pasują do mojego modelu”.

Różnice między dostawcami i API

Nie każdy host eksponuje tę samą powierzchnię strukturyzowanego wyjścia. Jedno środowisko może dawać Ci pierwszoklasowe ukończenie powiązane ze schematem, inne może gwarantować tylko składnię JSON, a lokalne środowiska uruchomieniowe mogą opóźniać się względem hostowanych API. To jest jeden z powodów, dla których pytanie FAQ „jak walidować JSON LLM w Pythonie” zaczyna od wymuszania ze strony dostawcy, gdy ono istnieje, i kończy się walidacją po stronie Pythona. Dla szerszego widoku porównania dostawców zobacz porównanie strukturyzowanego wyjścia między popularnymi dostawcami LLM. Jeśli uruchamiasz modele lokalnie, ten sam potok walidacji obowiązuje po znormalizowaniu formatu linii, na przykład po ekstrakcji z Ollama, jak opisano w strukturyzowanym wyjściu LLM z Ollama w Pythonie i Go. Gdy środowisko uruchomieniowe nadal otacza JSON dziwnymi prefiksami lub śladami rozumowania, oczekuj tej samej klasy błędów parsera opisanej w problemach ze strukturyzowanym wyjściem Ollama GPT-OSS.

Stos Pythona, który naprawdę działa

Moja rekomendacja jest celowo nudna. Po pierwsze, pozwól dostawcy modelu wymusić kontrakt strukturalny, gdy to możliwe. Po drugie, waliduj zwrócone obciążenie w Pythonie za pomocą Pydantic. Po trzecie, użyj wyraźnej walidacji reguł biznesowych dla faktów, których sam schemat nie może udowodnić. Po czwarte, testuj kontrakt z użyciem fixture’ów i przykładów adversarialnych, zamiast machać ręką na zrzut ekranu z playgrounda i nazywać to gotowym. Dokumentacja OpenAI dotycząca Strukturyzowanych Wyjść, model walidacji Pydantic, narzędzia jsonschema w Pythonie i własne przykłady ewaluacji strukturyzowanego wyjścia OpenAI wskazują w tym kierunku.

Pydantic jest właściwym środkiem ciężkości dla Pythona. Pozwala modelować wyjście jako zwykłe typy Pythona, generować JSON Schema za pomocą model_json_schema() i walidować surowy JSON za pomocą model_validate_json(). Dokumentacja Pydantic również zaznacza, że model_validate_json() jest ogólnie lepszą ścieżką niż najpierw wykonanie json.loads(...) a następnie walidacja, ponieważ ta dwuetapowa metoda dodaje dodatkową pracę parsującą w Pythonie.

Jeśli przechowujesz samodzielne pliki schematów w repozytorium lub chcesz, aby CI walidowało obciążenia fixture’ów niezależnie od kodu modelu, pakiet jsonschema w Pythonie daje Ci najprostsze możliwe sprawdzenie kontraktu za pomocą jsonschema.validate(...). Jeśli chcesz tego w pre-commit, check-jsonschema istnieje specjalnie jako CLI i haczyk pre-commit zbudowany na jsonschema. To bardzo dobre rozwiązanie dla zespołów, które chcą, aby zmiany schematów były przeglądane jak zmiany kodu.

Frameworki mogą zmniejszyć plomby, ale nie usuwają potrzeby właściwej walidacji. LangChain teraz automatycznie wybiera natywne strukturyzowane wyjście dostawcy, gdy go wspiera, i w przeciwnym razie cofa się do strategii narzędziowej. Instructor nakłada modele odpowiedzi Pydantic, walidację, ponowne próby i wsparcie wielodostawcze na wywołania modelu. Guardrails koncentruje się na walidatorach i warstwach ochronnych wejścia-wyjścia. Przydatne narzędzia, wszystkie z nich. Ale schemat i reguły biznesowe nadal należą do Ciebie. Jeśli wybierasz między bibliotekami wyższego poziomu, porównanie BAML vs Instructor dla Pythona jest przydatnym uzupełnieniem tego artykułu.

Minimalny przykład OpenAI i Pydantic

Najmniejszy przykład wart produkcji ma kilka nieodłącznych elementów. Użyj zamkniętego zestawu wartości podobnych do enumów, gdzie to możliwe. Zabranij dodatkowych kluczy. Dodaj opisy pól, aby schemat był zrozumiały dla ludzi i czytelniejszy dla modelu. Trzymaj obiekt korzeniowy jawny i nudny. OpenAI zaleca jasne nazwy plus tytuły i opisy dla ważnych kluczy, JSON Schema używa enum do ograniczania wartości, a Pydantic może zamknąć kształt obiektu za pomocą extra="forbid".

from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field


class TicketClassification(BaseModel):
    model_config = ConfigDict(extra="forbid")

    category: Literal["billing", "bug", "how_to", "abuse"] = Field(
        description="Kategoria zgłoszenia wsparcia."
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="Pilność operacyjna."
    )
    needs_human: bool = Field(
        description="Czy człowiek powinien przejrzeć tę sprawę."
    )
    summary: str = Field(
        description="Jednowyrazowe podsumowanie problemu."
    )


client = OpenAI()

response = client.responses.parse(
    model="gpt-4o-2024-08-06",
    input=[
        {
            "role": "system",
            "content": "Klasyfikuj zgłoszenia wsparcia. Zwróć tylko strukturyzowany wynik.",
        },
        {
            "role": "user",
            "content": "Klient zgłasza podwójne obciążenia po odświeżeniu checkoutu.",
        },
    ],
    text_format=TicketClassification,
)

result = response.output_parsed
print(result.model_dump())

Dwa szczegóły w tym przykładzie są łatwe do przeoczenia i absolutnie warte uwagi. extra="forbid" po stronie Pydantic odzwierciedla ideę JSON Schema additionalProperties: false, która jest również wymaganiem dla ścisłych schematów narzędzi w dokumentacji OpenAI dotyczącej wywoływania funkcji. A enumy nie są tylko kosmetyką. Są jednym z najprostszych sposobów zapobieżenia tworzeniu przez modelu wartości, których Twój kod nie rozumie.

Python SDK OpenAI obsługuje client.responses.parse(...) z modelem Pydantic dostarczanym jako text_format, a sparsowany obiekt jest zwracany na response.output_parsed. To samo SDK obsługuje również client.chat.completions.parse(...), gdzie sparsowany obiekt znajduje się na message.parsed. Jeśli chcesz bezpośrednią ekstrakcję danych strukturyzowanych z minimalnym klejem, te pomocniki są najczystszym punktem startowym.

Parsuj, normalizuj, a następnie waliduj

Strukturyzowane Wyjścia i model_validate_json usuwają wiele bólu związanego z parsowaniem, gdy stos jest zsynchronizowany od początku do końca. W momencie, gdy obsługujesz dostawcę, który zwraca zwykły tekst czatu, model, który otacza JSON w ogrodzenia, lub ścieżkę logowania, która przechowuje surowy string ukończenia, potrzebujesz jednego wąskiego gardła, które przekształci tekst w słownik przed uruchomieniem Pydantic.

import json


def parse_json_from_llm_text(text: str) -> dict:
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1]
        cleaned = cleaned.rsplit("```", 1)[0].strip()

    # Typowy prefiks „Sure, here is the JSON:" przed obiektem.
    if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if end > start:
            cleaned = cleaned[start : end + 1]

    return json.loads(cleaned)


ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)

Ten pomocnik jest celowo nudny. Obsługuje ogrodzone bloki „json ... ” i wstępny preambułę języka naturalnego, gdy obciążenie jest nadal pojedynczym obiektem najwyższego poziomu. Nie jest to pełny ekstraktor JSON. Jeśli model zagnieżdża nawiasy klamrowe wewnątrz wartości stringowych, naiwne wycinanie może się zepsuć, a właściwą naprawą jest zwykle ściślejszy prompt, ukończenia powiązane ze schematem lub dedykowana biblioteka parsera.

Ukończenia strumieniowe (Streaming)

Jeśli strumieniujesz tokeny czatu, nie uruchamiaj json.loads ani model_validate_json na każdej delta. Buforuj, dopóki API nie zgłosi zakończonej wiadomości (sprawdź w kliencie zakończenie strumienia lub finish_reason), połącz tekst, a następnie parsuj raz. Ta sama zasada obowiązuje, gdy argumenty wywołań narzędzi przybywają partiami. Walidujesz dopiero po tym, jak string argumentów będzie kompletny.

chunks: list[str] = []
for chunk in completion_stream:
    delta = chunk.choices[0].delta.content or ""
    chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)

Nadal możesz przekazać raw_completion_text przez parse_json_from_llm_text najpierw, gdy spodziewasz się ogrodzeń lub gadania wokół JSONa.

Gdy już przejmujesz parsowanie zwykłych stringów, kolejnym ograniczeniem jest często nie Python, ale dialekt JSON Schema dostawcy i to, co zdalne API faktycznie akceptuje.

Limity schematu dostawcy (zanim staniesz się pomysłowy w Pythonie)

Nie wylewaj oślepnie dowolnego wyjścia generatora schematów do API i nie zakładaj, że każda funkcja JSON Schema jest obsługiwana. OpenAI obsługuje podzbiór JSON Schema, wymaga, aby wszystkie pola były wymagane dla Strukturyzowanych Wyjść, wymaga, aby korzeń był obiektem, a nie najwyższypoziomowym anyOf, i dokumentuje limity głębokości zagnieżdżenia i całkowitej liczby właściwości. Trzymaj schemat skierowany do dostawcy prosty. To nie jest kompromis. To dobra inżynieria.

Jeśli potrzebujesz ścieżki walidacji niezależnej od dostawcy lub chcesz walidować zapisane fixture’i i mocki, Pydantic plus jsonschema to nadal świetna kombinacja.

from jsonschema import validate as validate_json

schema = TicketClassification.model_json_schema()

payload = {
    "category": "bug",
    "priority": "high",
    "needs_human": True,
    "summary": "Checkout duplicates charges after refresh.",
}

validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)

Ten wzorzec jest szczególnie przydatny w testach, fixture’ach kontraktowych i integracjach, gdzie dostawca modelu nie oferuje natywnej wymuszonej strukturyzowanej walidacji. Pamiętaj tylko, że lokalnie wygenerowany schemat może być szerszy niż obsługiwany podzbiór danego dostawcy, więc „poprawny lokalnie” nie oznacza automatycznie „zaakceptowany przez każde API LLM". Zauważ również, że niektórzy dostawcy przetwarzają wstępnie i buforują artefakty schematu, więc pierwsze żądanie dla nowego schematu może być wolniejsze niż żądania „ciepłe".

Wywołania narzędzi to drugi kontrakt

Wywoływanie funkcji lub narzędzi to inny główny kształt strukturyzowanego wyjścia. Model wybiera nazwę i przekazuje argumenty, które powinny pasować do kontrolowanego przez Ciebie JSON Schema. OpenAI zaleca strict: true w definicjach narzędzi, aby argumenty pozostały zsynchronizowane ze schematem. W stosach opartych na agentach, złe próbkowanie szybko przekształca się w nieprawidłowy JSON narzędzi; trzymaj ustawienia sampler’a zsynchronizowane z pracą wieloetapową, używając referencji parametrów wnioskowania agencjonowego dla Qwen i Gemma.

Poniższe fragmenty zakładają, że już zmapowałeś obiekt wywołania narzędzia dostawcy na string name i słownik arguments, na przykład poprzez parsowanie tool_calls[].function w ukończeniach czatu (argumenty stringowe JSON stają się json.loads najpierw). dispatch_tool to krok po tej normalizacji.

Dwie praktyczne zasady pomagają w Pythonie. Po pierwsze, waliduj nazwę narzędzia względem wyraźnej listy dozwolonych przed routowaniem wykonania. Po drugie, waliduj słownik argumentów tym samym modelem Pydantic, którego używasz w testach, a nie z ad hoc dostępem do kluczy. Tryb awarii, którego unikasz, to „poprawne argumenty JSON, zły kształt dla narzędzia, które zostało uruchomione”, który przechodzi przez sprawdzenia stringowe.

from typing import Any, Callable

from pydantic import BaseModel

ToolHandler = Callable[[dict[str, Any]], str]


def dispatch_tool(
    *,
    name: str,
    arguments: dict[str, Any],
    handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
    if name not in handlers:
        raise ValueError(f"unsupported tool {name}")
    model_cls, handler = handlers[name]
    validated = model_cls.model_validate(arguments)
    return handler(validated.model_dump())


handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
    "classify_ticket": (
        TicketClassification,
        lambda data: f"queued as {data['category']}",
    ),
}

Ten wzorzec utrzymuje routowanie i walidację w jednym miejscu. Twoje prawdziwe handler’y będą bogatsze, ale podział powinien pozostać taki sam: dozwolone nazwy, typowane argumenty, a następnie efekty uboczne.

Walidacja schematu nadal wymaga reguł biznesowych

Poprawny obiekt nie jest tym samym, co poprawny obiekt. OpenAI mówi to bezpośrednio. Strukturyzowane Wyjścia nie zapobiegają błędom wewnątrz wartości obiektu JSON. Dlatego pytanie FAQ „dlaczego walidacja schematu i walidacja reguł biznesowych są ważne” ma prostą odpowiedź. Ponieważ odpowiedź może idealnie pasować do schematu, a nadal być błędna w sposób, który szkodzi biznesowi.

Oto realistyczny przykład. Struktura może być poprawna, ale logika cenowa może nadal być nonsensowna.

from decimal import Decimal
from typing import Literal
from typing_extensions import Self

from pydantic import BaseModel, ConfigDict, Field, model_validator


class Offer(BaseModel):
    model_config = ConfigDict(extra="forbid")

    currency: Literal["USD", "EUR", "GBP"]
    amount: Decimal = Field(gt=0)
    original_amount: Decimal | None
    discounted: bool

    @model_validator(mode="after")
    def check_discount_logic(self) -> Self:
        if self.discounted:
            if self.original_amount is None:
                raise ValueError(
                    "original_amount jest wymagane, gdy discounted jest true"
                )
            if self.original_amount <= self.amount:
                raise ValueError(
                    "original_amount musi być większe niż amount"
                )
        return self

Ten walidator robi coś, czego same schematy często robią źle w rzeczywistych systemach. Sprawdza semantykę między polami po sparsowaniu całego modelu. model_validator Pydantic istnieje dokładnie dla tego rodzaju walidacji całego obiektu. Zauważ pole Decimal | None bez domyślnej wartości. To utrzymuje pole obecne, jednocześnie pozwalając na null, co pasuje do udokumentowanego wzorca OpenAI dla wartości podobnych do opcjonalnych w ścisłych Strukturyzowanych Wyjściach.

Jeśli chcesz, aby błędy walidacji automatycznie wracały do modelu, Instructor to praktyczna warstwa nad Pydantic. Jego dokumentacja opisuje pętlę ponownych prób, gdzie błędy walidacji są przechwytywane, sformatowane jako feedback i użyte do poproszenia modelu o ponowną próbę.

import instructor

retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)

offer = retrying_client.create(
    response_model=Offer,
    messages=[
        {
            "role": "user",
            "content": (
                "Extract the offer from this text. "
                "Was 49.00 USD, now 19.00 USD."
            ),
        }
    ],
)

To jest jedna z nielicznych udogodnień, które chętnie polecam. Automatyczne ponowne próby połączone z rzeczywistymi błędami walidacji są przydatne. Cicha koercja nie jest. Warstwa modelu Instructor, dokumentacja ponownych prób i dokumentacja walidacji opierają się na tej samej idei i mają rację, robiąc tak.

Możesz wdrożyć tę samą ideę bez frameworka. Pętla jest mała. Poproś model, waliduj z Pydantic, a jeśli walidacja się nie powiedzie, wyślij szczegóły błędu z powrotem w wiadomości użytkownika i poproś o poprawiony tylko JSON. Ogranicz próby, zaloguj ostateczną awarię i eksponuj kontrolowany błąd do wywołujących. Gdy już polegasz na responses.parse lub innych pomocnikach powiązanych ze schematem, rzadko będziesz ćwiczyć tę ścieżkę. Nadal ma to znaczenie dla trybu JSON, starszych punktów końcowych czatu lub dowolnej bramki, która przekazuje Ci surowy string.

from openai import OpenAI
from pydantic import ValidationError

client = OpenAI()

messages = [
    {"role": "system", "content": "Return only JSON that matches the ticket schema."},
    {"role": "user", "content": "Customer reports duplicate charges after refreshing checkout."},
]

ticket: TicketClassification | None = None
for attempt in range(2):
    completion = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        response_format={"type": "json_object"},
    )
    raw_text = completion.choices[0].message.content or ""
    try:
        ticket = TicketClassification.model_validate_json(raw_text)
        break
    except ValidationError as exc:
        messages.append(
            {
                "role": "user",
                "content": f"Validation failed with {exc.errors()}. Return corrected JSON only.",
            }
        )
else:
    raise RuntimeError("exhausted structured output retries")

assert ticket is not None

W rzeczywistych usługach przyłączałbyś identyfikatory śledzenia (tracing IDs), redagował tekst klienta w logach i rozróżniał odzyskiwalne błędy walidacji od odmów lub niekompletnych odpowiedzi. Ważną częścią jest to, że ponowna próba jest napędzana przez rzeczywiste wyjście walidatora, a nie przez generyczną wiadomość „spróbuj ponownie".

Testuj, powtarzaj i kończ zamkniętą pętlę (fail closed)

Co powinno się stać, gdy walidacja LLM się nie powiedzie? Nie wzruszenie ramion. Odrzuć obciążenie, zaloguj awarię, powtórz próbę z ograniczonymi liczbami prób, jeśli zadanie jest warte ponownej próby, i zakończ zamkniętą pętlę (fail closed) zamiast normalizować śmieci w coś, co tylko wygląda na akceptowalne. To jest również miejsce, w którym wiele zespołów zapomina jawnie obsługi odmów i niekompletnych wyjść, mimo że dokumentacja dostawcy mówi im, że te ścieżki istnieją.

Dla API Responses OpenAI, obsługa awarii powinna być kodem pierwszoklasowym, a nie myślą po fakcie. Zmienną jest response z client.responses.create lub parse, a nie completion ze strumieniowania czatu indziej w tym artykule.

if response.status == "incomplete":
    raise RuntimeError(response.incomplete_details.reason)

content = response.output[0].content[0]

if content.type == "refusal":
    raise RuntimeError(content.refusal)

To nie jest defensywne nadmiernie inżynierowanie. Jest to bezpośrednio zgodne z udokumentowanymi trybami awarii. Jeśli model odmówi, nie trzymasz obciążenia zgodnego ze schematem. Jeśli odpowiedź jest niekompletna, nie trzymasz obciążenia zgodnego ze schematem. Traktuj oba jako jawne gałęzie w swoim przepływie sterowania.

Powinieneś również testować kontrakt poza samym wywołaniem modelu.

import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError


def test_ticket_fixture_matches_schema():
    payload = {
        "category": "bug",
        "priority": "high",
        "needs_human": True,
        "summary": "Checkout duplicates charges after refresh.",
    }
    validate_json(instance=payload, schema=TicketClassification.model_json_schema())


def test_discount_logic_rejects_broken_offer():
    with pytest.raises(ValidationError):
        Offer.model_validate(
            {
                "currency": "USD",
                "amount": "19.00",
                "original_amount": "10.00",
                "discounted": True,
            }
        )


def test_ticket_rejects_unknown_category_string():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "refund",
                "priority": "high",
                "needs_human": True,
                "summary": "Customer wants a refund.",
            }
        )


def test_ticket_rejects_extra_keys():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "bug",
                "priority": "high",
                "needs_human": True,
                "summary": "Broken flow.",
                "severity": "critical",
            }
        )

To jest właściwy kształt strategii testowej dla walidacji wyjścia LLM w Pythonie. Waliduj złote fixture’y z jsonschema, aby każde pole w kontrakcie było ćwiczone. Waliduj semantykę z Pydantic, a następnie dodaj przypadki adversarialne, takie jak nielegalne stringi enumów, zabronione dodatkowe klucze i sprzeczności między polami, które Cię dotyczą. Jeśli robisz snapshoty rzeczywistych wyjść modelu, wyczyść PII i traktuj je jako fixture’y regresyjne.

Jeśli Twój zespół żyje w ekosystemie OpenAI, API Evals zawiera również przepisy ewaluacji strukturyzowanego wyjścia specjalnie do testowania i iterowania zadań zależnych od formatów maszynowo-czytelnych. A jeśli przechowujesz surowe pliki schematów w repozytorium, podłącz check-jsonschema do CI lub pre-commit. Wysyłaj kontrakty, a nie vibe’y.

Sprawdzenia produkcyjne, które Cię uratują później

Gdy walidacja się nie powiedzie, odpowiedź FAQ jest prosta. Odrzuć obciążenie, zaloguj dlaczego, powtórz próbę z ukierunkowanym feedbackiem, gdy zadanie jest worth kolejnej próby, i zakończ zamkniętą pętlę zamiast koercjonować złe dane do kolejki.

Krótka lista kontrolna operacyjna pomaga zespołom unikać powtarzających się incydentów.

  • Loguj wersję schematu lub hash JSON Schema wysłanego do dostawcy, abyś mógł dokładnie odtworzyć awarie.
  • Redaguj wejścia i wyjścia modelu w logach. Strukturyzowane logi są bezużyteczne, jeśli wyciekają tekst klienta.
  • Emituj liczniki lub metryki dla wskaźnika odmów, wskaźnika niekompletnych odpowiedzi, wskaźnika awarii walidacji i wskaźnika sukcesu naprawy. Skoki tam pokonują zgadywanie, gdy wdrożono zmianę modelu lub promptu.

Szersze wytyczne dotyczące obserwowalności systemów LLM pomagają podłączyć te sygnały do dashboardów, śladów i przeglądów SLO, gdy liczniki istnieją.

Najlepsza praktyka nie jest skomplikowana. Używaj Strukturyzowanych Wyjść po stronie dostawcy lub ścisłych schematów narzędzi, gdy możesz. Normalizuj surowy tekst, gdy musisz. Odzwierciedl kontrakt w Pythonie z Pydantic. Dodaj walidację reguł biznesowych dla tego, czego schemat nie może udowodnić. Obsługuj odmowy i niekompletne odpowiedzi jako normalne gałęzie. Testuj kontrakt, dopóki nie przestanie być demo i nie zacznie być oprogramowaniem. Cokolwiek mniej, to tylko cosplay inżynierii promptów.

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.