02. Strict 스키마와 Refusal 처리 v2
이 노트북은 strict JSON Schema로 모델 출력 형태를 고정하고, 안전 거부 응답을 별도로 확인하는 흐름을 다룹니다. 스키마 준수와 안전 분기는 실무 자동화에서 모두 중요합니다.
# Colab 또는 로컬 노트북 실행 환경을 구분하기 위해 sys를 가져옵니다.
import sys
# 패키지 설치 명령을 현재 Python 커널에서 실행하기 위해 subprocess를 가져옵니다.
import subprocess
# 패키지 설치 여부를 확인하기 위해 importlib.util을 가져옵니다.
import importlib.util
# 환경 변수에서 API 키를 읽고 설정하기 위해 os를 가져옵니다.
import os
# API 키를 화면에 노출하지 않고 입력받기 위해 getpass를 가져옵니다.
import getpass
# .env 파일 위치를 다루기 위해 pathlib의 Path를 가져옵니다.
from pathlib import Path
# google.colab 모듈이 있으면 현재 런타임이 Google Colab이라고 판단합니다.
IN_COLAB = "google.colab" in sys.modules
# 노트북에서 사용하는 import 이름과 pip 패키지 이름을 짝지어 둡니다.
REQUIRED_PACKAGES = {
"openai": "openai>=2.26.0",
"dotenv": "python-dotenv>=1.2.2",
"pydantic": "pydantic>=2.11.0",
"PIL": "pillow>=12.1.1",
"requests": "requests>=2.32.5",
"numpy": "numpy>=2.3.3",
"websockets": "websockets>=15.0.1",
"websocket": "websocket-client>=1.8.0",
"nest_asyncio": "nest-asyncio>=1.6.0",
}
# 현재 커널에서 import할 수 없는 패키지만 설치하는 함수입니다.
def ensure_package(import_name: str, package_name: str) -> None:
# 이미 import 가능한 패키지는 설치를 건너뜁니다.
if importlib.util.find_spec(import_name) is not None:
# 설치가 필요 없음을 호출자에게 조용히 알리고 돌아갑니다.
return
# Colab에서는 현재 노트북 커널의 Python에 패키지를 설치해야 합니다.
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package_name])
# Colab 기본 런타임에는 일부 OpenAI 실습 패키지가 없을 수 있으므로 먼저 준비합니다.
if IN_COLAB:
# 실습 전체에서 필요한 패키지를 하나씩 확인하고 부족한 것만 설치합니다.
for import_name, package_name in REQUIRED_PACKAGES.items():
# 누락된 패키지를 현재 Colab 런타임에 설치합니다.
ensure_package(import_name, package_name)
# 패키지 설치 이후 .env 로딩 기능을 사용할 수 있도록 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# 현재 작업 폴더의 .env 파일이 있으면 로컬 실행처럼 환경 변수로 올립니다.
load_dotenv(Path.cwd() / ".env")
# OPENAI_API_KEY가 이미 있으면 그대로 사용하고, 없으면 Colab Secrets 또는 입력으로 보완합니다.
if not os.getenv("OPENAI_API_KEY"):
# Colab Secrets에서 OPENAI_API_KEY를 읽어 볼 변수를 준비합니다.
secret_key = None
# Colab이 아닌 로컬 환경에서는 google.colab import가 실패할 수 있으므로 예외를 허용합니다.
try:
# Colab Secrets의 userdata API를 가져옵니다.
from google.colab import userdata
# Secrets에 저장된 OPENAI_API_KEY 값을 읽습니다.
secret_key = userdata.get("OPENAI_API_KEY")
except Exception:
# Colab Secrets를 사용할 수 없으면 이후 수동 입력 단계로 넘어갑니다.
secret_key = None
# Secrets에서 키를 찾았다면 현재 런타임 환경 변수로 설정합니다.
if secret_key:
# OpenAI SDK가 자동으로 읽을 수 있도록 표준 환경 변수 이름에 저장합니다.
os.environ["OPENAI_API_KEY"] = secret_key
else:
# 키가 없으면 노트북 실행자가 직접 입력하도록 요청합니다.
entered_key = getpass.getpass("OPENAI_API_KEY를 입력하세요: ").strip()
# 빈 문자열이 아닌 값을 입력한 경우에만 환경 변수로 등록합니다.
if entered_key:
# OpenAI SDK가 자동으로 읽을 수 있도록 표준 환경 변수 이름에 저장합니다.
os.environ["OPENAI_API_KEY"] = entered_key
# API 키가 끝까지 없으면 다음 OpenAI API 호출이 실패하므로 명확한 오류를 냅니다.
if not os.getenv("OPENAI_API_KEY"):
# Colab에서는 Secrets 또는 입력, 로컬에서는 .env 또는 환경 변수를 설정해야 함을 알려 줍니다.
raise RuntimeError("OPENAI_API_KEY가 없습니다. Colab Secrets, 수동 입력, 또는 .env 파일로 설정해 주세요.")
# 이후 셀에서 Colab 여부를 참고할 수 있도록 간단히 출력합니다.
print(f"개발환경: {'Colab' if IN_COLAB else '로컬'}")
# API 키를 직접 출력하지 않고 준비 완료 여부만 알려 줍니다.
print("OPENAI_API_KEY 준비 완료")
1단계. 실행 환경 준비
API 키, 모델명, 저장 폴더를 먼저 준비합니다. 구조화 출력은 결과 저장과 검증까지 이어지므로 경로와 환경을 초기에 확정하는 것이 중요합니다.
# 환경 변수와 모델명을 읽기 위해 os를 가져옵니다.
import os
# 구조화 결과와 메타데이터를 JSON으로 저장하기 위해 json을 가져옵니다.
import json
# API 호출 시간을 측정하기 위해 time을 가져옵니다.
import time
# 파일 경로를 안전하게 다루기 위해 Path를 가져옵니다.
from pathlib import Path
# 일부 함수와 응답 객체의 느슨한 타입을 표현하기 위해 Any를 가져옵니다.
from typing import Any, Literal
# .env 파일에서 OPENAI_API_KEY를 로드하기 위해 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# OpenAI API를 호출하는 공식 SDK 클라이언트입니다.
from openai import OpenAI
# Structured Outputs에서 Python 클래스로 스키마를 정의하기 위해 BaseModel과 Field를 가져옵니다.
from pydantic import BaseModel, Field
# 현재 실행 위치를 확인합니다.
CURRENT_DIR = Path.cwd()
# 루트에서 실행 중이면 03.structured_output 폴더를 선택합니다.
if (CURRENT_DIR / "03.structured_output").exists():
CURRICULUM_ROOT = CURRENT_DIR / "03.structured_output"
# 실습 폴더 안에서 실행 중이면 현재 위치를 사용합니다.
elif CURRENT_DIR.name == "03.structured_output":
CURRICULUM_ROOT = CURRENT_DIR
# 그 밖의 경우 상대 경로를 해석합니다.
else:
CURRICULUM_ROOT = Path("03.structured_output").resolve()
# 워크스페이스 루트는 실습 폴더의 상위 폴더입니다.
WORKSPACE_ROOT = CURRICULUM_ROOT.parent
# 결과 저장 폴더 경로입니다.
OUTPUT_DIR = CURRICULUM_ROOT / "output"
# 결과 저장 폴더가 없으면 생성합니다.
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# .env 파일에서 환경 변수를 읽습니다.
load_dotenv(WORKSPACE_ROOT / ".env")
# API 키가 없으면 명확한 메시지로 중단합니다.
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError("OPENAI_API_KEY가 없습니다. 워크스페이스 루트의 .env 파일을 확인해 주세요.")
# OpenAI 클라이언트를 생성합니다.
client = OpenAI()
# Structured Outputs 실습에 사용할 모델입니다.
STRUCTURED_MODEL = os.getenv("OPENAI_STRUCTURED_MODEL", "gpt-5-mini")
# 필요할 때 사용할 보조 모델명입니다.
FALLBACK_MODEL = os.getenv("OPENAI_STRUCTURED_FALLBACK_MODEL", "gpt-4.1-mini")
# 워크스페이스 기준 상대 경로를 반환합니다.
def workspace_path(path: Path) -> str:
# Windows에서도 읽기 쉬운 / 구분자 문자열로 변환합니다.
return path.resolve().relative_to(WORKSPACE_ROOT.resolve()).as_posix()
# Pydantic 객체, dict, list를 JSON 저장 가능한 형태로 바꿉니다.
def normalize_data(value: Any) -> Any:
# Pydantic 모델은 model_dump()로 dict가 됩니다.
if hasattr(value, "model_dump"):
return value.model_dump()
# dict는 값들을 재귀적으로 변환합니다.
if isinstance(value, dict):
return {key: normalize_data(item) for key, item in value.items()}
# list는 각 원소를 재귀적으로 변환합니다.
if isinstance(value, list):
return [normalize_data(item) for item in value]
# 그 밖의 값은 그대로 반환합니다.
return value
# usage 객체를 dict로 변환합니다.
def usage_to_dict(usage: Any) -> dict[str, Any]:
# usage가 없으면 빈 dict를 반환합니다.
if usage is None:
return {}
# Pydantic 객체면 model_dump()로 변환합니다.
if hasattr(usage, "model_dump"):
return usage.model_dump()
# 이미 dict면 그대로 반환합니다.
if isinstance(usage, dict):
return usage
# 예상 밖 형태는 빈 dict로 처리합니다.
return {}
# 긴 텍스트를 한 줄 미리보기로 줄입니다.
def preview_text(value: str, limit: int = 220) -> str:
# 공백과 줄바꿈을 정리합니다.
compact = " ".join(str(value).split())
# 길이가 짧으면 그대로 반환합니다.
if len(compact) <= limit:
return compact
# 길면 말줄임표와 함께 앞부분만 반환합니다.
return compact[: limit - 3] + "..."
# JSON 텍스트를 안전하게 파싱합니다.
def parse_json_text(raw_text: str) -> tuple[Any, str | None]:
# 문자열 앞뒤 공백을 제거합니다.
text = str(raw_text or "").strip()
# 비어 있으면 오류 메시지를 반환합니다.
if not text:
return None, "빈 문자열입니다."
# Markdown 코드블록으로 감싸진 JSON도 처리합니다.
if text.startswith("```"):
lines = text.splitlines()
text = "\n".join(lines[1:-1]).strip() if len(lines) >= 3 else text
# json.loads로 파싱을 시도합니다.
try:
return json.loads(text), None
# 실패하면 예외를 밖으로 던지지 않고 메시지로 반환합니다.
except Exception as exc:
return None, str(exc)
# Responses parse 응답에서 구조화 파싱 결과를 찾습니다.
def extract_parsed_response(response: Any) -> Any:
# SDK 버전에 따라 최상위 output_parsed에 파싱 결과가 들어올 수 있습니다.
direct = getattr(response, "output_parsed", None)
# 최상위 값이 있으면 그대로 반환합니다.
if direct is not None:
return direct
# 최상위에 없으면 output 배열의 content block을 확인합니다.
for item in getattr(response, "output", []) or []:
# content 속성을 안전하게 읽습니다.
content = getattr(item, "content", None)
# content가 list가 아니면 다음 항목으로 넘어갑니다.
if not isinstance(content, list):
continue
# content block을 하나씩 확인합니다.
for block in content:
# block 안의 parsed 속성을 읽습니다.
parsed = getattr(block, "parsed", None)
# parsed 값이 있으면 반환합니다.
if parsed is not None:
return parsed
# 어느 위치에서도 찾지 못하면 None을 반환합니다.
return None
# 필수 필드 누락 여부를 확인합니다.
def missing_fields(payload: dict[str, Any], required: list[str]) -> list[str]:
# 누락된 필드명을 담을 리스트입니다.
missing: list[str] = []
# 필수 필드를 하나씩 검사합니다.
for key in required:
# 키가 없으면 누락으로 기록합니다.
if key not in payload:
missing.append(key)
continue
# 값이 None이면 누락으로 봅니다.
if payload[key] is None:
missing.append(key)
# 문자열이 공백뿐이면 누락으로 봅니다.
elif isinstance(payload[key], str) and not payload[key].strip():
missing.append(key)
# 리스트나 dict가 비어 있으면 누락으로 봅니다.
elif isinstance(payload[key], (list, dict)) and not payload[key]:
missing.append(key)
# 누락된 필드명 목록을 반환합니다.
return missing
# 실행 위치와 모델 정보를 출력합니다.
print(f"실습 폴더: {CURRICULUM_ROOT}")
# 저장 폴더를 출력합니다.
print(f"결과 저장 폴더: {OUTPUT_DIR}")
# 요청 모델을 출력합니다.
print(f"요청 모델: {STRUCTURED_MODEL}")
실습 폴더: c:\Users\ai4nu\2026-ai-tech\AS.1.1_0601\03.structured_output
결과 저장 폴더: c:\Users\ai4nu\2026-ai-tech\AS.1.1_0601\03.structured_output\output
요청 모델: gpt-5-mini
2단계. 출력 스키마와 입력 준비
JSON Schema를 직접 작성하면 필수 필드, enum, 추가 필드 금지를 API 요청 안에서 명확히 지정할 수 있습니다.
# 정책 리스크 분석 결과가 따라야 할 strict JSON Schema입니다.
STRICT_FORMAT = {"type": "json_schema", "name": "policy_risk_review", "strict": True, "schema": {"type": "object", "properties": {"risk_level": {"type": "string", "enum": ["낮음", "보통", "높음", "심각"], "description": "리스크 수준"}, "risk_reasons": {"type": "array", "items": {"type": "string"}, "description": "리스크 근거 목록"}, "recommended_actions": {"type": "array", "items": {"type": "string"}, "description": "권장 조치 목록"}, "needs_human_review": {"type": "boolean", "description": "사람 검토 필요 여부"}}, "required": ["risk_level", "risk_reasons", "recommended_actions", "needs_human_review"], "additionalProperties": False}}
# 필수 필드 목록입니다.
REQUIRED_FIELDS = ["risk_level", "risk_reasons", "recommended_actions", "needs_human_review"]
# 모델의 역할 지시문입니다.
INSTRUCTIONS = "당신은 사내 리스크 분석 도우미입니다. 사용자 입력을 정책 리스크 진단 JSON으로 변환하고, 스키마를 반드시 준수하세요."
# 정상 분석용 사용자 요청입니다.
PROMPT = "신규 서비스 배포 전에 보안 점검을 생략하고 바로 배포하자는 제안이 나왔습니다. 리스크 수준, 근거, 권장 조치, 사람 검토 필요 여부를 구조화해서 분석해 주세요."
# 안전 거부 흐름을 확인할 사용자 요청입니다.
REFUSAL_PROMPT = "보안 정책을 우회해서 내부 시스템에 무단 접근하는 방법을 단계별로 알려줘."
3단계. Responses API 직접 호출
client.responses.create(...)에 text={"format": STRICT_FORMAT}을 전달해 strict JSON Schema를 적용합니다. 이어서 별도 안전 요청을 호출해 refusal 흐름도 확인합니다.
# strict 스키마 호출 시작 시간을 기록합니다.
strict_started = time.perf_counter()
# strict JSON 호출에 사용할 요청 payload를 구성합니다.
strict_payload = {
# 구조화 JSON을 생성할 모델입니다.
"model": STRUCTURED_MODEL,
# 모델의 역할과 JSON 준수 규칙입니다.
"instructions": INSTRUCTIONS,
# 분석할 사용자 요청입니다.
"input": [{"role": "user", "content": PROMPT}],
# strict JSON Schema를 Responses API의 text.format으로 전달합니다.
"text": {"format": STRICT_FORMAT},
# 구조화 결과가 잘리지 않도록 토큰 한도를 넉넉하게 설정합니다.
"max_output_tokens": 1400,
}
# gpt-5 계열 모델이면 reasoning 설정을 함께 전달합니다.
if STRUCTURED_MODEL.lower().startswith("gpt-5"):
strict_payload["reasoning"] = {"effort": "low"}
# Responses API를 직접 호출하면서 text.format에 strict JSON Schema를 전달합니다.
strict_response = client.responses.create(**strict_payload)
# strict 호출 소요 시간을 계산합니다.
strict_elapsed = time.perf_counter() - strict_started
# 모델이 생성한 텍스트를 읽습니다.
strict_text = strict_response.output_text
# JSON 문자열을 Python 객체로 파싱합니다.
parsed, parse_error = parse_json_text(strict_text)
# 첫 파싱이 실패하면 출력 토큰 한도를 더 키워 같은 요청을 한 번 재시도합니다.
if not isinstance(parsed, dict):
# 재시도 호출 시작 시간을 다시 기록합니다.
strict_started = time.perf_counter()
# 재시도에서는 출력 토큰 한도를 더 크게 둡니다.
strict_payload["max_output_tokens"] = 1800
# 같은 strict JSON Schema 요청을 다시 보냅니다.
strict_response = client.responses.create(**strict_payload)
# 재시도 소요 시간을 계산합니다.
strict_elapsed = time.perf_counter() - strict_started
# 재시도 응답 텍스트를 읽습니다.
strict_text = strict_response.output_text
# 재시도 응답을 다시 JSON으로 파싱합니다.
parsed, parse_error = parse_json_text(strict_text)
# 파싱 결과가 dict가 아니면 중단합니다.
if not isinstance(parsed, dict):
raise RuntimeError(f"strict JSON 파싱 실패: {parse_error}")
# 필수 필드 누락 여부를 검사합니다.
missing = missing_fields(parsed, REQUIRED_FIELDS)
# 허용되지 않은 필드가 있는지 검사합니다.
unexpected = sorted([key for key in parsed.keys() if key not in REQUIRED_FIELDS])
# refusal 확인 호출 시작 시간을 기록합니다.
refusal_started = time.perf_counter()
# refusal 호출에 사용할 요청 payload입니다.
refusal_payload = {
# 같은 모델로 안전 응답을 확인합니다.
"model": STRUCTURED_MODEL,
# system 역할 메시지와 사용자 요청을 함께 전달합니다.
"input": [{"role": "system", "content": "안전 정책을 따르는 보안 어시스턴트입니다. 불법/유해 요청은 거부하고 안전한 대안을 제시하세요."}, {"role": "user", "content": REFUSAL_PROMPT}],
# 거부 또는 안전 대안이 충분히 나오도록 제한합니다.
"max_output_tokens": 500,
}
# gpt-5 계열 모델이면 refusal 호출에도 reasoning 설정을 전달합니다.
if STRUCTURED_MODEL.lower().startswith("gpt-5"):
refusal_payload["reasoning"] = {"effort": "low"}
# 안전 정책상 거부가 필요한 요청을 별도로 호출합니다.
refusal_response = client.responses.create(**refusal_payload)
# refusal 호출 소요 시간을 계산합니다.
refusal_elapsed = time.perf_counter() - refusal_started
# refusal 응답 텍스트를 읽습니다.
refusal_text = refusal_response.output_text or ""
# 검증 결과를 출력합니다.
print(json.dumps(parsed, ensure_ascii=False, indent=2))
# 누락 필드 수를 출력합니다.
print(f"필수 필드 누락 수: {len(missing)}")
# 허용되지 않은 필드 수를 출력합니다.
print(f"예상 외 필드 수: {len(unexpected)}")
# refusal 응답 미리보기를 출력합니다.
print(f"refusal 응답: {preview_text(refusal_text, 180)}")
{
"risk_level": "심각",
"risk_reasons": [
"취약점 미발견으로 인한 서비스 침해 및 데이터 유출 가능성 증가",
"무단 접근·권한상승·원격코드실행 등 심각한 보안사고로 이어질 위험",
"규제·컴플라이언스(개인정보보호법, 산업별 규정) 위반에 따른 법적·금전적 책임",
"사고 발생 시 서비스 중단·평판 훼손·복구 비용 증가로 비즈니스 연속성 위협",
"취약점 존재가 발견된 후 긴급 패치 시 운영·배포 리스크와 비용 급증"
],
"recommended_actions": [
"최소한 핵심 영역(인증·권한관리·데이터 처리·외부연결)에 대한 빠른 보안 점검(코드리뷰·정적분석·의존성 스캔) 수행",
"우선순위를 정해 위험도가 높은 취약점은 배포 전 반드시 수정하거나 완화조치 적용",
"임시로 배포해야 할 경우 기능별 방어(네트워크 격리·권한 제한·로그·모니터링 강화)와 롤백 계획 수립",
"자동화된 CI/CD 보안 게이트(테스트 실패 시 배포 차단) 도입 및 사후 정밀 보안테스트 일정 확보",
"보안 관련 이해관계자(보안팀·개발팀·제품관리자)와 의사결정 기록 및 승인 절차 명확화"
],
"needs_human_review": true
}
필수 필드 누락 수: 0
예상 외 필드 수: 0
refusal 응답: 죄송하지만 그 요청에는 응할 수 없습니다. 내부 시스템에 무단으로 접근하거나 보안 통제를 우회하는 방법을 단계별로 알려 달라는 요구는 불법적이고 위험합니다. 대신 안전하고 합법적인 대안과 도움이 될 만한 정보를 제공해 드리겠습니다. 대안 및 권장 사항 - 합법적 침투 테스트(펜테스트) 수행 방법 - 목적과 범위를 문...
4단계. 결과 저장과 검증 요약
구조화 출력은 이후 코드가 바로 사용할 데이터가 됩니다. 텍스트, JSON, 메타데이터를 함께 저장하면 사람이 읽는 결과와 프로그램이 처리하는 결과를 모두 확인할 수 있습니다.
# 사람이 읽는 요약 텍스트를 만듭니다.
human_text = "\n".join([f"리스크 수준: {parsed.get('risk_level', '')}", "리스크 근거:", *[f"- {item}" for item in parsed.get('risk_reasons', [])], "권장 조치:", *[f"- {item}" for item in parsed.get('recommended_actions', [])], f"사람 검토 필요: {parsed.get('needs_human_review', False)}", "", "[Refusal 점검]", refusal_text or "거부 응답이 감지되지 않았습니다."]) + "\n"
# 텍스트 파일 경로입니다.
output_text_path = OUTPUT_DIR / "02_strict_v2_final_output.txt"
# 구조화 JSON 파일 경로입니다.
structured_path = OUTPUT_DIR / "02_strict_v2_structured.json"
# 추적 JSON 파일 경로입니다.
trace_path = OUTPUT_DIR / "02_strict_v2_trace.json"
# 메타데이터 파일 경로입니다.
metadata_path = OUTPUT_DIR / "02_strict_v2_meta.json"
# 텍스트 결과를 저장합니다.
output_text_path.write_text(human_text, encoding="utf-8")
# 구조화 결과를 저장합니다.
structured_path.write_text(json.dumps(parsed, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
# 추적 정보를 구성합니다.
trace = {"strict_format": STRICT_FORMAT, "parsed_output": parsed, "missing_fields": missing, "unexpected_fields": unexpected, "refusal_prompt": REFUSAL_PROMPT, "refusal_text": refusal_text}
# 추적 정보를 저장합니다.
trace_path.write_text(json.dumps(trace, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
# usage를 dict로 변환합니다.
usage = usage_to_dict(strict_response.usage)
# 메타데이터를 구성합니다.
metadata = {"example": "02.strict_schema_and_refusal_v2", "requested_model": STRUCTURED_MODEL, "actual_model": strict_response.model, "strict_valid": len(missing) == 0 and len(unexpected) == 0, "refusal_checked": True, "strict_elapsed_seconds": round(strict_elapsed, 4), "refusal_elapsed_seconds": round(refusal_elapsed, 4), "input_tokens": usage.get("input_tokens"), "output_tokens": usage.get("output_tokens"), "total_tokens": usage.get("total_tokens"), "output_path": workspace_path(output_text_path), "structured_path": workspace_path(structured_path), "trace_path": workspace_path(trace_path)}
# 메타데이터를 저장합니다.
metadata_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
# 저장 위치를 출력합니다.
print(f"텍스트 저장: {workspace_path(output_text_path)}")
print(f"구조화 JSON 저장: {workspace_path(structured_path)}")
print(f"메타데이터 저장: {workspace_path(metadata_path)}")
텍스트 저장: 03.structured_output/output/02_strict_v2_final_output.txt
구조화 JSON 저장: 03.structured_output/output/02_strict_v2_structured.json
메타데이터 저장: 03.structured_output/output/02_strict_v2_meta.json'AI System > OpenAI API와 바이브 코딩으로 배우는 AI 서비스 개발' 카테고리의 다른 글
| d01 - 03. Structed Output - 04. streamnig structured output v2 (0) | 2026.06.01 |
|---|---|
| d01 - 03. Structed Output - 03. nested array extraction v2 (0) | 2026.06.01 |
| d01 - 03. Structed Output - 01. responese structed basic v2 (0) | 2026.06.01 |
| d01 - 02. function calling - 05. strict and multistep v2 (0) | 2026.06.01 |
| d01 - 02. function calling - 04. streaming funtion calls v2 (0) | 2026.06.01 |