Spring AI 스키마 검증과 자가 보정

Spring AI 스키마 검증과 자가 보정

기본 .entity(...) 호출은 모델에게 스키마에 맞는 JSON을 만들도록 요청하지만 강제할 수는 없습니다. 모델이 여분의 필드를 반환하거나, 필수 필드를 빠뜨리거나, JSON을 산문으로 감싸면 파서가 예외를 던져요.

잘못된 출력을 처리하는 가장 간단한 방법은 감지하고 재시도하는 것입니다. Spring AI는 EntityParamSpec 컨슈머의 스위치 하나로 이를 자동으로 처리해요:

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class, spec -> spec.validateSchema());

자가 보정 루프가 동작하는 방식

spec -> spec.validateSchema() 컨슈머는 자가 보정 재시도 루프를 켭니다:

  1. 모델이 응답합니다.
  2. Spring AI가 대상 타입의 JSON 스키마에 대해 응답을 검증합니다.
  3. 검증이 통과하면 타입 있는 record를 돌려받습니다.
  4. 실패하면 검증 오류(예: "필수 필드 actor 누락", "array를 기대했는데 string이 옴")를 사용자 프롬프트에 추가하고, 기본적으로 최대 3회까지 호출을 다시 발행합니다.

각 재시도에서 모델은 구체적인 오류를 봅니다. 그래서 두 번째 시도는 맹목적인 재시도가 아니에요 — 모델은 무엇이 잘못됐는지 알고 바로잡을 수 있습니다.

이 기능은 validateSchema() 를 호출할 때 자동 등록되는 재귀 advisorStructuredOutputValidationAdvisor 가 구동합니다. 직접 배선할 필요는 없어요. 스위치 하나가 전체 구성입니다.

validateSchema() 가 활성화되면 스트리밍은 지원되지 않습니다. advisor가 검증을 위해 완전한 응답이 필요하거든요.

advisor 커스터마이즈

StructuredOutputValidationAdvisor 는 기본적으로 3회 재시도이고 Spring AI 기본 JsonMapper를 사용합니다. 더 많은 시도, 사전 제공 스키마, 다른 매퍼 같은 커스터마이즈가 필요하면 직접 인스턴스를 만들어 ChatClient 에 등록하세요. 명시적으로 등록된 advisor는 자동 등록된 것을 대체합니다:

var validationAdvisor = StructuredOutputValidationAdvisor.builder()
    .outputType(ActorsFilms.class)
    .maxRepeatAttempts(5)
    .build();

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(validationAdvisor)
    .build();

advisor는 outputType(스키마 자동 파생) 또는 outputJsonSchema(사전 제공 스키마 문자열)으로 구성할 수 있으며, 두 옵션은 상호 배타적입니다.

var validationAdvisor = StructuredOutputValidationAdvisor.builder()
    .outputJsonSchema(myConverter.getJsonSchema())
    .build();

핵심 동작은 다음과 같습니다:

  • 예상 출력 타입에서 JSON 스키마를 파생하거나 사전 제공 스키마 문자열을 받습니다.
  • JSON Schema DRAFT_2020_12 로 LLM 응답을 스키마에 대해 검증합니다.
  • 검증 실패 시 호출을 재시도합니다 (기본 최대 3회).
  • 모델이 자가 보정하도록 재시도 시 검증 오류 메시지를 프롬프트에 추가합니다.
  • 모든 검증 시도의 토큰 사용량을 누적해, 반환된 ChatResponse 가 마지막 시도뿐 아니라 모든 재시도의 누적 사용량을 보고합니다 (누적 사용량 참고).
  • 선택적으로 커스텀 JsonMapper 를 지원합니다.

프로바이더 네이티브 출력과의 결합

validateSchema()응답 측 안전망입니다 — 잘못된 출력을 사후에 잡아 재시도해요. useProviderStructuredOutput() 은 보완적인 요청 측 제약입니다. 둘은 자연스럽게 결합됩니다:

ActorsFilms films = chatClient.prompt()
    .user("Generate the filmography for a random actor.")
    .call()
    .entity(ActorsFilms.class, spec -> spec
        .useProviderStructuredOutput()
        .validateSchema());

이 조합은 네이티브 강제가 부분적인 프로바이더 엣지 케이스에서 특히 유용합니다 — 예를 들어 JSON 대신 평문 추론 흔적을 내보낼 수 있는 Ollama 추론 모델 같은 경우요. 알려진 한계를 참고하세요.