Skip to content
This page has been auto-translated and may contain errors.View in English

구조화된 출력

docs.scrimba.com

먼저 문제의 형태부터 살펴봅시다. 구조화된 출력은 코드가 직접 읽을 수 있는 데이터를 모델에서 반환하도록 하는 관행으로, 손으로 파싱해야 하는 문단 대신 분기할 수 있는 필드입니다. 이 장은 그것을 신뢰할 수 있게 만드는 방법에 관한 것입니다.

지금까지 모델은 텍스트를 전달해주는데, 이는 사람이 읽을 때는 괜찮습니다. 하지만 소프트웨어는 문단으로 많은 일을 할 수 없습니다. 모델의 답변에 따라 코드에서 조치를 취하려면 데이터 형태여야 합니다: 읽을 수 있는 필드, 분기할 수 있는 값, 저장할 수 있는 레코드입니다.

변화는 작지만 그 이후로는 모든 것이 바뀝니다. 모델이 고객이 불행해 보인다는 문장 대신 { "sentiment": "negative" }를 반환하면, 코드는 티켓을 라우팅하고, 대시보드를 업데이트하거나, 경고를 보낼 수 있습니다. 흥미로운 부분은 텍스트만 예측하는 모델이 깨끗한 데이터를 생성하도록 만드는 방법인데, 그 답변은 당신이 몰랐던 레버를 드러냅니다.

모델은 텍스트를 반환합니다. 코드는 데이터가 필요합니다: 읽을 필드, 분기할 값, 저장할 레코드입니다. 이 장은 그 둘 사이의 다리이며, 움직임은 모델이 깔끔하게 포맷되기를 바라는 것을 멈추고 출력할 수 있는 것을 제한하는 것입니다.

실제 레버가 있습니다, 프롬프팅 트릭이 아닙니다. LLM이 작동하는 방식의 동일한 샘플링 루프를 조종하여 출력이 원하는 형태로 강제될 수 있으므로, 흐릿한 텍스트 생성기를 나머지 시스템이 의존할 수 있는 컴포넌트로 바꿉니다.

산문을 반환하는 모델은 구성할 수 없는 컴포넌트입니다. 그 출력이 다른 시스템에 제공되는 순간, 알려진 형태의 데이터가 필요하며, 그 데이터를 얻기 위한 실패 모드는 당신이 볼 수 있고 처리할 수 있는 것이어야 합니다.

이 장은 생성을 스키마로 제한하는 것과 그것이 지연 시간에 비용을 얼마나 드는지, 그것이 당신에게 무엇을 사주지 못하는지(올바른 값), 그리고 가장자리에서 어떻게 실패하는지에 관한 것입니다. 밑에 있는 기계는 도구 사용을 구동하는 것과 같습니다: 도구 호출은 함수 시그니처가 스키마를 대신하는 구조화된 출력입니다.

JSON 요청

첫 번째 본능은 프롬프트에서 요청하는 것입니다: "**JSON**으로 답장하세요", 소프트웨어가 중괄호 안의 필드와 값으로 작성된 데이터를 전달하는 데 사용하는 일반 텍스트 형식입니다. 시도하면 대부분 작동하는데, 이것이 함정입니다. 모델은 여전히 확률 높은 텍스트를 예측하는 것뿐이며, 때로 확률 높은 텍스트는 코드 펜스이거나, 데이터 앞의 친절한 "물론이죠!"이거나, 그 뒤의 후행 주석입니다. 그 중 어느 것이든 JSON을 읽는 코드를 깨뜨립니다. 친절하게 요청하면 예측이 JSON으로 편향되지만, 강제하지는 않습니다.

따라서 제공업체는 실제 레버를 제공합니다. response_format으로 JSON 모드를 켜면 요청할 뿐만 아니라 모델을 제한하여 구문이 유효한 JSON으로 돌아옵니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '도시와 온도를 추출합니다. JSON으로 답장하세요: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "지금 오슬로는 약 18도입니다."},
    ],
    response_format={"type": "json_object"},  # 출력을 유효한 JSON 구문으로 제한합니다
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "오슬로" 18

response_format={"type": "json_object"}는 출력을 유효한 JSON 구문으로 만들므로 json.loads가 산문 조각에 질식하지 않습니다. 원하는 필드를 여전히 프롬프트에서 설명해야 하는데, JSON 모드는 유효한 JSON을 약속하지만, 당신이 염두에 두고 있는 특정 필드를 약속하지는 않습니다. "전혀 JSON이 아님"을 제외하지, "잘못된 형태의 JSON"은 제외하지 않습니다. 여전히 약속할 수 없는 한 가지: 응답이 중간에 끊기면, 절반의 JSON 객체를 받을 수 있으므로, 신뢰할 수 있는 구문이지만 모든 경우에 어려운 보장은 아닙니다.

JunoJSON 요청 프롬프트에서만 JSON을 요청하는 것은 불안정합니다. 모델은 확률 높은 텍스트만 예측하며 펜스나 채팅으로 감싸서 파싱을 깨뜨릴 수 있기 때문입니다. response_format을 JSON 모드로 설정하면 구문이 유효한 JSON으로 돌아오도록 제한합니다. 필드가 요청한 항목과 일치한다는 것을 약속하지 않으며, 중간에 끊긴 응답은 여전히 절반의 객체로 도착할 수 있으므로, 프롬프트에서 형태를 설명하고 도착한 것을 확인하세요.

프롬프트에 "JSON으로 답장하세요"라고 쓰는 것이 본능인데, **JSON**은 소프트웨어가 전달하는 필드와 값의 데이터 형식입니다. 대부분 작동하는데, 이것이 정확히 문제입니다: 대부분은 계약이 아닙니다. 모델은 확률 높은 텍스트를 예측하며, 확률 높은 텍스트는 때때로 코드 펜스, "물론이죠!" 전문, 또는 후행 주석을 포함하는데, 이들 중 어느 것이든 json.loads를 깨뜨립니다.

JSON 모드가 첫 번째 실제 레버입니다. response_format을 JSON 객체 유형으로 설정하면 생성이 제한되어 구문이 JSON이지, 마치 JSON처럼 보이는 산문이 아닙니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '도시와 온도를 추출합니다. JSON으로 답장하세요: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "지금 오슬로는 약 18도입니다."},
    ],
    response_format={"type": "json_object"},  # 출력을 유효한 JSON 구문으로 제한합니다
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "오슬로" 18

여기서 response_format의 형태는 OpenAI의 것입니다; 다른 제공업체는 자신의 필드 아래에 같은 생각을 노출하므로, 개념을 이식하지만 리터럴 키는 아닙니다. JSON 모드는 구문을 제한하지만, 필드 형태는 절대 아닙니다. 프롬프트에서 필드를 설명하면, 모델은 유효한 JSON을 계속 내보내면서 필드의 이름을 바꾸거나, 삭제하거나, 다르게 중첩할 수 있습니다.

한 가지 더 갈라진 부분: JSON 모드는 응답이 끝나면 올바른 형태의 JSON만 약속합니다. max_tokens 상한에 객체 중간에 도달하면 잘린 파싱할 수 없는 출력을 얻으므로, 파싱도 여전히 보호가 필요합니다.

JunoJSON 요청 JSON 모드는 구문이 펜스와 채팅으로 감싸지지 않도록 제한하지만, 필드를 고정시키지 않으므로, 모델은 여전히 이름을 바꾸거나 삭제하거나 다르게 중첩할 수 있습니다. response_format 키는 OpenAI의 형태입니다; 다른 제공업체는 다르게 표기합니다. 그리고 약속은 완료된 응답에 대해서만 유지되므로, max_tokens에 의해 잘린 응답은 파싱할 수 없게 도착합니다. 어쨌든 파싱을 보호하세요.

프롬프트의 "JSON으로 답장하세요"는 제안이고, 제안은 부하 아래에서 실패합니다: 여기 코드 펜스, 거기 전문, 모델이 수다스러울 때 후행 주석, 각각은 프로덕션의 json.loads 예외입니다. JSON 모드(여기 response_format={"type": "json_object"})는 제안을 생성된 구문에 대한 제약으로 바꾸므로, 돌아오는 바이트는 JSON으로 파싱됩니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": '도시와 온도를 추출합니다. JSON으로 답장하세요: { "city": string, "tempC": number }.'},
        {"role": "user", "content": "지금 오슬로는 약 18도입니다."},
    ],
    response_format={"type": "json_object"},  # 출력을 유효한 JSON 구문으로 제한합니다
)

data = json.loads(response.choices[0].message.content)
print(data["city"], data["tempC"])  # "오슬로" 18

이것이 제한하는 것을 정확히 하세요. JSON 모드는 구문을 고정시키고, 스키마는 아니며, 완료된 응답에만 해당됩니다. 모델은 여전히 필드 이름을 선택하고, 필수 필드를 생략할 수 있으며, 어떻게든 중첩할 수 있습니다.

보장은 생성이 종료되는 조건부입니다: 모델이 max_tokens에 객체 중간에 도달하면, 유효한 JSON이 아닌 잘린 문자열을 얻습니다. 이는 JSON 모드가 제거하지 않는 유일한 실패 모드가 당신의 재시도 경로가 가장 필요로 하는 것임을 의미합니다. 표시된 response_format 키는 OpenAI의 것입니다; 다른 제공업체의 동등물은 다른 이름 아래에 있으므로, 개념을 이식 가능하다고 취급하고 리터럴 필드는 아닙니다. 형태 보장을 위해 스키마가 필요하며, 이것이 다음 섹션입니다.

JunoJSON 요청 JSON 모드는 완료된 응답의 구문을 제한합니다, 그 이상: 모델은 여전히 필드의 이름을 짓고, 생략하고, 중첩합니다. `max_tokens`에 의해 죽은 생성은 잘린 파싱할 수 없는 출력을 제공합니다. 그 자르기 경우는 모드가 아니라 오류 경로가 소유해야 하는 것입니다. response_format 형태는 OpenAI의 것이므로, 생각을 이식하되 키는 아닙니다. 형태를 원하면 스키마가 필요합니다.

스키마로 형태 고정

JSON 모드는 구문을 유지하지만, 모델은 여전히 필드의 이름을 바꾸거나, 삭제하거나, 다르게 중첩할 수 있습니다. 그 여유를 제거하려면 **스키마**를 제공하세요: 출력이 가져야 하는 필드와 유형의 정확한 설명으로, 모델이 채워야 하는 형식입니다. strict: true를 사용하면 출력이 그 형태와 일치하도록 보장됩니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "메시지에서 연락처 정보를 추출합니다."},
        {"role": "user", "content": "안녕하세요, 저는 마라 임입니다. [email protected]으로 연락하세요."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # 스키마를 정확히 강제합니다
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "마라 임", "email": "[email protected]"}

모델이 토큰만 예측할 때 스키마가 어떻게 어려운 보장일 수 있습니까? 제공업체는 예측할 수 있는 토큰을 제한하기 때문입니다. 각 단계에서 모델은 여전히 가능한 모든 다음 토큰의 순위를 매기지만, 시스템은 스키마를 깨뜨릴 수 있는 것들을 제거하고, 모델은 남은 것에서 선택합니다.

스키마가 다음 필드가 email이어야 한다고 하면, 다른 필드를 시작할 수 있는 토큰은 모델이 선택하기 전에 테이블에서 제거됩니다. 형태를 벗어난 토큰이 제거되므로, 모델은 형태를 벗어날 수 없습니다.

알 가치가 있는 더 부드러운 효과가 있습니다. 선택한 필드 이름은 값을 예측할 때 모델이 읽는 부분이 되므로, 명확한 이름이 더 나은 답변을 안내합니다. tempC를 채우라고 요청하면, 모델은 섭씨 숫자로 기울어집니다; value를 채우라고 요청하면, 훨씬 덜 갈 곳이 있습니다. 명확하게 필드의 이름을 짓는 것은 부분 명령, 부분 스키마입니다.

Juno스키마로 형태 고정 스키마는 모델이 채워야 하는 형식이고, strict: true를 사용하면 정확한 필드와 유형을 얻습니다. 시스템이 형태를 깨뜨릴 수 있는 토큰을 제거하기 때문입니다. 모델이 선택하기 전에 그것은 강제이지, 정중한 요청이 아닙니다. 필드의 이름을 명확히 짓기도 하세요. 필드 이름은 모델이 값을 예측할 때 읽는 문맥이기 때문입니다. tempC와 같은 이름이 실제 일을 한다는 것을 신뢰하는 데 시간이 걸렸습니다.

JSON 모드는 구문을 고정시키지만 모델이 필드의 이름을 바꾸고, 삭제하거나, 다시 중첩할 수 있도록 남겨둡니다. 스키마는 그 간격을 닫습니다: 필드와 유형의 정확한 설명이고, strict: true를 사용하면 출력이 일치하도록 보장됩니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "메시지에서 연락처 정보를 추출합니다."},
        {"role": "user", "content": "안녕하세요, 저는 마라 임입니다. [email protected]으로 연락하세요."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,  # 스키마를 정확히 강제합니다
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)
# {"name": "마라 임", "email": "[email protected]"}

메커니즘은 제약된 디코딩: 샘플링 단계에 적용된 토큰 가지치기입니다. 각 위치에서 모델은 모든 토큰에 대한 일반적인 순위를 생성하고, 시스템은 스키마와 일치하는 것을 멈추게 할 수 있는 토큰을 마스크하고, 모델은 남은 것에서 샘플합니다. 문법이 다음 토큰이 email 필드를 열어야 한다고 하면, 다른 것을 시작할 수 있는 토큰은 뽑기 전에 0으로 설정됩니다. 모델은 형태를 벗어날 수 없습니다. 형태를 벗어난 움직임이 보드에서 제거되었기 때문입니다, 프롬프트에서 낙담하지 않았습니다.

두 가지 실제 핸들이 있습니다. 필드 이름은 명령이므로, 원하는 값에 대해 이름을 짓기 바랍니다. tempC는 모델을 섭씨 숫자로 조종하고 value는 추측하도록 남겨두고, 필드의 설명은 더 힘있게 조종합니다. 그리고 같은 제약은 enum(허용된 값의 고정 목록)을 강제하거나 숫자 범위를 강제하는데, 이것이 분류를 신뢰할 수 있게 만드는 것입니다: 라벨 위치에서, 나열된 라벨만 마스크를 살아남으므로, 모델이 목록에 없는 범주를 만들 수 없습니다.

Juno스키마로 형태 고정strict: true를 사용하면, 제약된 디코딩은 모델이 샘플하기 전에 스키마를 깨뜨릴 수 있는 모든 토큰을 마스크하므로, 형태가 강제됩니다, 요청이 아닙니다. 그것에 기대세요: 필드 이름과 설명은 명령이므로, tempCvalue가 아닌 것으로 이름 짓고 모델이 더 잘 채웁니다. 같은 마스킹은 enum을 강제하는데, 이것이 분류를 유지하는 이유입니다. 모델이 목록에 없는 라벨을 만들 수 없습니다.

JSON 모드는 파싱 가능한 바이트를 제공합니다; 스키마는 알려진 형태를 제공합니다. strict: true를 사용하면 출력이 선언한 필드 집합과 유형과 일치하도록 보장되는데, 이것이 맹목적으로 색인할 수 있는 데이터와 방어적으로 조사해야 하는 데이터 간의 차이입니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "메시지에서 연락처 정보를 추출합니다."},
        {"role": "user", "content": "안녕하세요, 저는 마라 임입니다. [email protected]으로 연락하세요."},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "contact",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string"},
                },
                "required": ["name", "email"],
                "additionalProperties": False,
            },
        },
    },
)

contact = json.loads(response.choices[0].message.content)

메커니즘은 **제약된 디코딩**이고, 당신은 처음부터 그것을 이해하고 싶습니다. 왜냐하면 그것의 비용은 무료가 아니기 때문입니다. 모델은 각 단계에서 어휘의 모든 토큰에 대한 확률을 발생시킵니다. 제약된 디코딩은 스키마를 문법(어떤 토큰 시퀀스가 합법인지에 대한 규칙 집합)으로 컴파일한 다음 각 단계에서 모든 불법 토큰의 확률을 0으로 설정하는 마스크를 구축하므로, 샘플링은 지금까지 출력을 유효하게 유지하는 토큰에만 착지할 수 있습니다. 형태는 요청으로 아니라 구성으로 강제됩니다.

그 컴파일은 무료가 아니고, 지연 시간으로 표시됩니다. 새 스키마에 대한 첫 호출은 문법을 구축하기 위한 일회성 비용을 지불하므로, 신선한 스키마에 대한 콜드 스타트 지연은 정상 상태보다 높습니다. 스키마를 요청당 생성되지 않고 안정적이고 재사용되도록 유지하면, 계속해서 그 컴파일에 비용을 지불할 수 있습니다.

additionalProperties: false와 전체 required 목록은 장식이 아닙니다: 시스템이 추가 또는 누락된 필드를 완전히 거부할 수 있게 하는 것이고, 스키마 기반 거부를 지원하는 제공업체에서는 모델이 잘못된 객체 대신 구조화된 거부를 반환할 수 있는 깨끗한 방법입니다. 그것을 삭제하면 유효한 것을 확장합니다. 이것은 엄격한 모드에 도달한 이유와 반대입니다.

몇 가지 프로덕션 현실이 있습니다. 엄격한 지원은 불균형합니다: 일부 제공업체는 실제 문법을 강제하고, 다른 것은 근사하거나 JSON 스키마의 하위 집합만 지원합니다(pattern 없음, 제한된 중첩), 그래서 명세서를 가정하기보다는 당신의 제공업체가 실제로 명예하는 것을 테스트하세요. 부분 및 스트리밍 출력이 어색한 경우입니다: 토큰이 스트리밍되는 동안, 당신은 구문상 불완전한 객체를 보유하고 있으므로, 완료까지 버퍼하거나 절반 구성된 구조를 견디는 증분 파서를 사용하세요. 그리고 이것은 도구 사용과 동일한 기계입니다: 함수의 인수 스키마는 동일한 방식으로 제한되므로, 여기 있는 모든 것은 도구 호출을 신뢰할 수 있게 만드는 것으로 직접 이전됩니다.

Juno스키마로 형태 고정 제약된 디코딩은 스키마를 문법으로 컴파일하고 샘플링 전에 모든 불법 토큰을 0으로 설정하므로, 형태는 구성으로 강제됩니다. 그 컴파일은 새 스키마에 대한 첫 호출에서 지연 시간을 낸다. 그래서 요청당 스키마를 민트하는 대신 스키마를 재사용하세요. additionalProperties: false와 전체 required 목록을 유지하세요, 그들은 거부를 강제하고, 일부 제공업체에서, 깨끗한 구조화된 거부입니다. 엄격한 지원은 다양합니다, 스트리밍은 절반의 객체를 제공합니다, 그리고 같은 마스킹이 도구 호출 인수를 구동합니다. 그래서 당신이 여기서 배운 것은 두 배로 갚습니다.

어쨌든 검증하세요

스키마는 형태를 제한하지만, 어쨌든 외부 세계에서 온 데이터로 출력을 취급하세요. 형태는 유효하지만 콘텐츠는 잘못될 수 있습니다: 실제로 오타인 추출된 이메일, 모델이 추측한 숫자, 입력에 포함되지 않았기 때문에 비워진 필드입니다. 그리고 호출은 max_tokens에 의해 잘려서 잘린 JSON으로 도착하는 것처럼 일반적인 방식으로 실패할 수 있습니다. 방어적으로 파싱하세요: 유효한 형태는 올바른 값이 아닙니다.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
        if not data.get("name") or not data.get("email"):
            return None  # 있지만 비어있음
        return data
    except json.JSONDecodeError:
        return None  # 유효한 JSON이 아님 (예: 잘린 응답)

contact = parse_contact(response.choices[0].message.content)
if not contact:
    pass  # 실패 처리: 재시도, 다시 요청, 또는 친절한 오류 표시

이것은 LLM이 작동하는 방식환각 교훈을 새로운 옷입니다: 형태가 맞는 것이 값이 맞다는 것을 만들지 않습니다. 스키마는 name 필드를 얻는다는 것을 보장합니다; 그것은 이름이 맞다는 것을 보장하지 않습니다. 파싱 주위에 try/except와 값의 검사는 잘 형성되고 여전히 잘못된 응답에 대한 저렴한 보험입니다.

Juno어쨌든 검증하세요 스키마는 형태를 보장합니다, 값의 진실은 절대 아닙니다. 따라서 유효한 레코드는 여전히 잘못되거나 빈 값을 보유할 수 있고, 호출은 여전히 도착할 수 있습니다. try/except로 파싱을 감싸고 신뢰하기 전에 값을 확인하세요. 검증이 실패하면 어떤 일이 발생하는지 미리 결정합니다, 재시도 또는 친절한 오류. 그래서 나중에 스크램블이 아닙니다.

엄격한 모드는 형태를 보장하지만, 콘텐츠는 보장하지 않습니다. 출력은 스키마와 일치할 수 있으면서도 잘못될 수 있습니다: 오타인 그럴듯한 이메일, 필수 필드를 채우기 위해 발명된 숫자, 입력이 아무것도 보유하지 않은 곳에 빈 문자열입니다. 그리고 응답이 max_tokens에 잘리면 파싱이 완전히 실패할 수 있습니다. 그래서 당신은 검증합니다, 매번.

python
import json

def parse_contact(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "unparseable"  # 잘리거나 잘못 형성됨

    if not data.get("name") or not data.get("email"):
        return None, "empty_field"  # 있지만 비어있음
    if "@" not in data["email"]:
        return None, "bad_email"  # 형태 확인, 값 아님
    return data, None

contact, error = parse_contact(response.choices[0].message.content)
if error == "unparseable":
    pass  # 한 번 재시도하세요, 가능하면 잘림: max_tokens를 올리거나 입력을 줄이고 재시도하세요
elif error:
    pass  # 값 수준 실패를 기록하고 폴백하세요

패턴은 **방어적 파싱**인데, 실패 모드를 구분하기 때문입니다. 이들은 다른 처리를 원합니다. JSONDecodeError는 보통 잘림을 의미하므로, 수정은 max_tokens를 올리거나 입력을 축소하고 재시도하는 것입니다. 빈 또는 범위를 벗어난 값은 콘텐츠 실패이지, 포맷 실패가 아니므로, 같은 호출을 재시도하는 것은 드문일입니다; 로그하고 폴백하세요.

"파싱할 수 없음"을 "파싱했지만 잘못됨"에서 분리하세요, 수정이 다르기 때문입니다. 이것은 환각으로 다시 연결됩니다: 제약된 디코딩이 잘못된 형성 출력 실패 모드를 제거하고 잘못된 값 모드는 완전히 남겨두었습니다.

Juno어쨌든 검증하세요 엄격한 모드는 형태를 고정시킵니다, 콘텐츠는 고정하지 않으므로, 매번 검증하세요: 레코드는 잘 형성되고 오타, 추측, 또는 공백을 보유할 수 있습니다. 실패를 분리하세요, JSONDecodeError는 보통 잘림(max_tokens을 올리거나 입력을 축소하고 재시도)을 의미합니다. 잘못된 값은 콘텐츠 미스입니다. 같은 호출을 재시도하는 것은 고쳐지지 않습니다. 제약된 디코딩이 잘못된 형성 문제를 삭제했고 잘못된 값 문제를 남겼습니다.

제약된 디코딩은 잘못된 형성 출력 실패 모드를 닫고 다른 것은 닫지 않습니다. 형태는 보장됩니다; 콘텐츠는 보장되지 않습니다. 입력이 절대 지원하지 않은 필수 필드는 자신있는 추측으로 채워집니다, 숫자는 건전한 범위를 벗어나 착지합니다, 이메일은 문자열로 파싱하지만 오타입니다.

형태 보장 자체는 잘림에서 실패합니다. 그래서 검증은 선택적 위생이 아니라, 스키마가 구조적으로 할 수 없는 것을 잡는 계층입니다.

python
import json

def parse_contact(raw, retry):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return None, "truncated"  # 불완전한 생성, 콘텐츠 문제 아님

    if not data.get("name") or "@" not in data.get("email", ""):
        return None, "invalid_value"  # 형태가 유지됨, 값은 유지되지 않음
    return data, None

contact, failure = parse_contact(raw, retry=False)
if failure == "truncated":
    pass  # max_tokens을 올리거나 입력을 자르고, 호출을 재시도하세요
elif failure == "invalid_value":
    pass  # 복구 경로: 잘못된 출력을 다시 피드하고 고쳐달라고 요청하거나, 폴백으로 라우팅하세요

검증이 잘 유지되는 것과 모든 것을 삼키는 try/except를 분리하는 세 가지 사항이 있습니다:

  • 실패를 분류합니다: 잘림은 더 많은 max_tokens으로 재시도해서 수정한 예산 문제입니다. 잘못된 값은 콘텐츠 문제입니다. 맹목적 재시도는 재현할 것입니다.
  • 값 수준 경우에 대한 폴백뿐 아니라 **복구 경로**를 구축하세요. 잘못된 출력을 고쳐달라는 명령으로 다시 피드하는 것은 종종 한 번의 추가 호출에서 복구합니다. 이것은 요청을 실패하는 것보다 저렴하지만, 재시도 예산을 설정하여 지속적으로 잘못된 입력이 루프할 수 없습니다.
  • 비용을 제한하세요: 모든 재시도는 또 다른 청구된 호출과 지연 시간의 또 다른 왕복입니다. 그래서 재시도 예산은 설계의 일부입니다, 애프터쏘우트가 아닙니다.

파싱이 아니라 값을 검증하세요. 이것은 구조화된 데이터에 적용된 환각 격리 이야기입니다: 제약된 디코딩은 깨끗한 봉투를 제공하고 그 안에 있는 것에 대해 아무것도 알려주지 않으므로, 봉투가 정확히 팀이 검증을 멈추고 불타는 곳입니다. 스키마 검증, 범위 및 의미 검사, 그리고 바운드된 복구 루프는 세 계층이고, 다른 것들을 잡습니다: 스키마는 형태를 잡습니다, 검사는 말도 안 되는 값을 잡습니다, 복구 루프는 구원할 수 있는 것들을 복구합니다.

Juno어쨌든 검증하세요 엄격한 모드는 잘못된 형성 출력을 삭제하고 잘못된 값은 완전히 남겨두므로, 파싱만이 아니라 콘텐츠를 검증하세요. 실패를 분류합니다: 잘림은 예산 수정입니다(더 많은 `max_tokens`, 재시도). 잘못된 값은 콘텐츠 미스입니다. 맹목적 재시도는 오직 재현합니다. 복구 경로를 구축하세요. 잘못된 출력을 다시 피드하고 고쳐달라고 요청하고, 재시도를 제한하여 나쁜 입력이 당신의 청구서를 루프할 수 없도록 하세요. 깨끗한 봉투가 정확히 사람들이 검사를 멈추고 불타는 곳입니다.

두 가지 패턴: 분류 및 추출

대부분의 구조화된 출력 작업은 두 가지 형태 중 하나입니다.

**분류**는 입력을 고정된 라벨 집합 중 하나로 정렬합니다. 스키마의 enum은 동일한 토큰 가지치기 트릭을 사용하여 출력을 정확히 그 라벨로 제한합니다: 라벨 위치에서, 허용된 값만 예측될 수 있으므로, 모델이 목록에 없는 범주를 만들 수 없습니다.

python
# 분류를 위한 스키마 조각
{"type": "string", "enum": ["billing", "technical", "general"]}

추출은 무료 텍스트에서 특정 필드를 꺼냅니다, 예를 들어 이전의 연락처 예제처럼: 이름, 날짜, 금액, 제품 이름 목록입니다. 함께 분류와 추출은 실제 AI 기능의 큰 공유를 다룹니다: 지원 티켓 라우팅, 콘텐츠 태깅, 이메일을 레코드로 바꾸기, 영수증 읽기입니다. 둘 다 흐릿한 텍스트 생성기를 당신의 코드가 구축할 수 있는 신뢰할 수 있는 컴포넌트로 변환합니다.

Juno두 가지 패턴: 분류 및 추출 분류는 입력을 고정된 라벨 집합 중 하나로 정렬합니다, `enum`으로 잠겨서 모델이 목록 밖의 범주를 만들 수 없습니다. 추출은 무료 텍스트에서 명명된 필드를 레코드로 꺼냅니다. 둘 다 흐릿한 텍스트 생성기를 신뢰할 수 있는 컴포넌트로 변환합니다, 그리고 그들은 실제 AI 기능의 큰 공유를 다룹니다.

거의 모든 것이 구조화된 출력으로 구축하는 것은 두 가지 형태 중 하나이고, 그들의 이름은 올바른 스키마에 도달하는 것을 도웁니다.

**분류**는 입력을 고정된 집합에서 하나의 라벨로 매핑합니다. enum은 전체 작업을 수행합니다: 제약된 디코딩은 라벨 위치에서 나열된 라벨을 제외한 모든 토큰을 마스크하므로, 모델은 그들 중 하나 또는 다른 것을 반환합니다.

python
# 분류: 닫힌 집합에서 하나의 라벨
{"type": "string", "enum": ["billing", "technical", "general"]}

추출은 무료 텍스트에서 명명된 필드를 레코드로 꺼냅니다, 연락처 및 티켓 예제입니다. 둘은 구성합니다: 하나의 스키마는 동시에 분류하고 추출할 수 있으며, 이것은 대부분의 실제 기능입니다. 닫힌 집합은 enum을 얻습니다; 열린 값은 타입화된 필드를 얻습니다.

신식으로 결정할 사항: 값이 정말 고정된 집합(상태, 범주, 우선 순위)이면, enum을 사용하여 모델이 표류할 수 없습니다, 그리고 그것이 열려있으면(이름, 요약), 일반 타입화된 필드를 사용하고, 검증할 충분한 값을 수락합니다. 섞기가 바뀝니다, enum이 실제 세계가 더 경우를 가진 곳이거나, 폐쇄 집합이 오류를 잡을 곳에서 자유 필드는 조용히 이 기능들이 잘못되는 곳입니다.

Juno두 가지 패턴: 분류 및 추출 두 가지 형태는 대부분을 다룹니다: 분류는 닫힌 집합에서 하나의 라벨로 매핑합니다(`enum`이 강제합니다) 그리고 추출은 명명된 필드를 레코드로 꺼냅니다, 그리고 하나의 스키마는 한 번에 둘을 할 수 있습니다. 신식으로 선택합니다: enum 고정된 집합을 위해 모델이 표류할 수 없도록, 일반 타입화된 필드를 검증할 열린 값을 위해. 실제 세계 경우를 놓친 enum은 잘못된 라벨의 조용한 원천입니다.

구조화된 출력은 두 패턴으로 붕괴되고, 구분은 단순히 어휘가 아니라 설계 레버입니다. 분류는 입력을 닫힌 집합에서 하나의 라벨로 매핑합니다, 제약된 디코딩이 값들의 어려운 선택으로 줄이는 enum으로 강제됩니다. 추출은 자유 텍스트에서 타입화된 필드를 꺼냅니다. 대부분의 프로덕션 스키마는 그들을 결합합니다.

python
# 분류: 제약된 디코딩은 라벨 위치에서만 이 토큰을 허용합니다
{"type": "string", "enum": ["billing", "technical", "general"]}

레버는 닫힌 집합 경계를 그리는 곳입니다, 왜냐하면 enum은 날카로운 가장자리를 가진 정확성 보장이기 때문입니다. 실제로 바운드된 값(상태, 우선 순위)은 enum에 속합니다. 모델이 목록을 벗어날 아무것도 내보낼 수 없으므로, 이것이 열려있는 생성을 당신의 다운스트림 코드가 의존할 수 있는 확인으로 변환합니다.

그러나 enum은 아무 라벨도 맞지 않을 때도 선택을 강제합니다: 실제 입력이 라벨과 일치하지 않으면, 모델은 당신에게 맞지 않는다는 것을 알려주기보다는 가장 가까운 잘못된 것을 선택하도록 제약됩니다. 탈출구 없는 enum은 "맞지 않음"을 자신있는 잘못된 라벨로 변환합니다. 그래서 지저분한 실제 입력에 직면한 모든 분류기를 위해, 명시적 **탈출구**를 추가합니다, other 또는 unknown 멤버, 그리고 그것을 다른 곳으로 라우팅하는 신호로 읽습니다.

같은 주의는 신뢰에 적용됩니다: 결정이 라벨에 탄다면, 모델에게 신뢰 또는 짧은 이유 필드도 내보내도록 합니다, 그래서 경계선 호출은 깨끗하게 보이는 enum 값으로 세탁되지 않고 보입니다. 이것들은 도구 사용과 같은 본능입니다, 모델이 닫힌 집합에서 하나의 도구를 선택하고 당신이 동일한 "맞지 않으면 어떻게 되나요" 문제에 직면합니다.

Juno두 가지 패턴: 분류 및 추출 분류는 enum으로 강제된 닫힌 집합입니다, 추출은 자유 텍스트에서 타입화된 필드입니다, enum은 날카로운 가장자리를 가진 보장입니다: 아무것도 맞지 않을 때도 선택을 강제합니다, "맞지 않음"을 자신있는 잘못된 라벨로 세탁합니다. other 또는 unknown 멤버를 추가하고 라우팅하세요. 라벨에 결정이 탄다면 신뢰 필드도 내보내세요. 도구 사용에서 닫힌 집합 중 하나의 도구를 선택하는 것처럼.

실제로

지저분한 지원 이메일을 하나의 스키마에서 분류와 추출을 결합하여 구조화된 티켓으로 변환합니다:

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "지원 이메일을 티켓으로 변환합니다."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "지난달 구독에서 이중 청구되었습니다."}

하나의 호출이 이메일을 두 가지 방식으로 분류하고 요약을 추출하여, 당신의 코드가 라우팅하고 저장할 수 있는 레코드를 반환합니다, 형태가 보장되고 값은 여전히 확인할 가치가 있습니다. 지금까지 모든 것이 텍스트로 흘러들어가고 텍스트로 나갔습니다. 다음으로 당신은 모델에게 완전히 다른 의미를 제공합니다: 임베딩에서, 텍스트는 의미로 비교할 수 있는 숫자가 되는데, 이것은 검색과 당신의 문서로 작업하기 위한 기초입니다.

Juno실제로 하나의 호출은 엄격한 스키마를 가지고 이메일을 두 가지 방식으로 분류하고 요약을 한 번에 추출할 수 있으며, 당신의 코드가 라우팅하고 저장할 수 있는 레코드를 반환합니다. 형태가 보장됩니다; 값은 여전히 확인할 가치가 있습니다. 도구 사용은 나중에 이 같은 트릭을 기울입니다: 모델이 도구에 보내는 인수는 구조화된 출력이기도 합니다.

실제 기능은 보통 하나의 스키마에서 두 패턴을 결합합니다. 여기서 지원 이메일은 티켓이 됩니다: 두 분류와 하나의 추출은 하나의 호출에서입니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "지원 이메일을 티켓으로 변환합니다."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "지난달 구독에서 이중 청구되었습니다."}

두 enum은 제약되어 있으므로 categoryurgency는 항상 라우팅 가능합니다. summary는 열린 필드이므로, 신뢰하기 전에 검증할 것입니다. 그 분할, 강제된 라벨과 확인된 무료 텍스트 필드는 대부분의 추출 기능의 형태입니다. 다음으로, 임베딩은 모델에게 다른 의미를 제공합니다: 의미로 비교할 수 있는 숫자로 변환된 텍스트는 검색의 기초이고 당신의 문서로 작업합니다.

Juno실제로 하나의 엄격한 스키마는 두 가지 방식으로 분류하고 한 번에 요약을 추출할 수 있으며, 당신이 라우팅하고 저장할 수 있는 레코드를 반환합니다. enum은 강제되므로 라벨은 조치를 취하기에 안전합니다; 열린 summary는 검증할 필드입니다. 잠긴 라벨과 확인된 무료 텍스트의 그 혼합은 추출 기능의 일상적인 형태입니다.

일상적인 프로덕션 형태는 복합: 몇 가지 축에서 분류하고 하나의 스키마와 하나의 호출에서 무료 텍스트 필드를 추출합니다.

python
import json

response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {"role": "system", "content": "지원 이메일을 티켓으로 변환합니다."},
        {"role": "user", "content": email_text},
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "ticket",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "enum": ["billing", "technical", "general"]},
                    "urgency": {"type": "string", "enum": ["low", "medium", "high"]},
                    "summary": {"type": "string"},
                },
                "required": ["category", "urgency", "summary"],
                "additionalProperties": False,
            },
        },
    },
)

ticket = json.loads(response.choices[0].message.content)
# {"category": "billing", "urgency": "high", "summary": "지난달 구독에서 이중 청구되었습니다."}

강제는 필드 전체에 불균등하고, 이것이 설계 점입니다. enum은 어려운 보장이므로, categoryurgency는 추가 검사 없이 라우팅하기에 안전합니다. summary는 제약 없는 문자열이므로, 그것이 정확히 환각되거나 기반을 벗어난 값이 깨끗한 스키마 뒤에 숨을 수 있는 것이고, 검증 통과를 얻는 필드입니다. 두 개의 강제된 라벨과 열린 summary 필드도 잘림에 가장 많이 노출된 스키마입니다: 길게 생성된 요약은 max_tokens에 들어가 전체 객체를 파싱할 수 없게 합니다, 그래서 예산을 라벨이 아니라 무료 텍스트 필드에 맞춰서 설정하세요.

이것은 도구 사용과 동일한 기계입니다: 도구 호출은 모델이 함수 스키마에 대해 구조화된 인수를 발생시킵니다, 동일한 방식으로 제약됩니다, 그래서 검증, 복구, enum 탈출구 본능은 여기서 에이전트를 신뢰할 수 있게 만드는 것으로 도매로 옮깁니다. 여기서 임베딩은 모델을 다른 모드로 이동시킵니다: 의미로 비교할 수 있는 벡터인 텍스트는 검색의 기초이고 당신의 문서에 답변을 기반으로 합니다.

Juno실제로 하나의 엄격한 스키마, 두 개의 강제된 enum과 열린 `summary`: 라벨은 라우팅하기에 안전합니다, 무료 텍스트 필드는 환각된 값이 숨을 곳이고 긴 생성이 `max_tokens`에 잘림으로 넘어지는 곳입니다. 그래서 그것을 구체적으로 예산과 검증하세요. 이것은 도구 사용과 같은 기계입니다, 함수 스키마에 대해 제약된 인수, 그래서 모든 습관이 여기서 에이전트를 신뢰할 수 있게 만드는 것으로 직접 옮깁니다.