본문 바로가기
Study

Gemini 구조화 출력 — JSON 형식이 맞아도 리뷰 라벨을 검증해야 하는 이유

by 이게뭐시당 2026. 10. 11.

밥;도에서는 Gemini API와 LangChain으로 음식점 리뷰의 특징을 라벨링했다. 맛, 양, 분위기, 대기 같은 정보를 추천에 쓰기 위한 작업이었다. 같은 작업에 새 모델을 쓴다면 무엇을 확인해야 할까. 출력 형식과 검증 기준을 정리했다.

당시 구현한 내용은 밥;도 프로젝트 글에 정리해 두었다. 모델을 실제로 교체해 비교한 글은 아니다. 2026.10.11에 확인한 공식 문서와 이 글을 위해 만든 작은 검증 예제를 바탕으로 썼다.

1. 최근 Gemini 변경에서 먼저 확인한 것

2026.10.08 릴리스 노트에는 gemini-3.7-flash 요청이 gemini-3.8-flash로 자동 연결된다는 공지가 올라왔다. gemini-3.5-flash 요청도 gemini-3.6-flash로 연결된다. 코드에 적힌 모델 ID를 그대로 두더라도 실제로 응답하는 모델은 바뀔 수 있다는 얘기다. Gemini API 릴리스 노트

Google은 2026.09.18 릴리스 노트에서 Gemini 2.5 모델의 접근을 과거에 해당 모델을 활발히 사용한 사용자로 제한한다고 안내했다. 이 공지는 2.5 모델의 지원 종료를 뜻하지 않는다. 같은 안내에서 신규 프로젝트에는 3.5 Flash-Lite 또는 3.8 Flash를 권장했다. Gemini API 릴리스 노트

기존 노트북이 있어도 같은 조건으로 API를 호출할 수 있는지는 따로 확인해야 한다. 어떤 모델 ID를 사용했는지, 그 모델에 접근할 수 있는지, 현재 라이브러리에서 같은 호출 방식을 지원하는지부터 보는 편이 낫다. 밥;도에서 2.5 모델을 썼다는 뜻은 아니다.

리뷰 라벨링에서는 호출에 성공한 다음이 더 중요하다. 모델을 바꾸고도 결과 JSON이 잘 읽힌다고 해서, 대기나 맛을 분류하는 기준까지 유지됐다고 볼 수는 없다. 그래서 예전 결과와 새 결과는 같은 입력으로 비교해야 한다.

2. 라벨보다 먼저 출력 형식을 정하기

가상의 리뷰 한 문장을 놓고 보자.

점심에 갔는데 기다리지 않고 바로 들어갔다.

대기 여부를 뽑는다면 결과는 단순한 설명문보다 정해진 값으로 받는 편이 다루기 쉽다. 이 예제에서는 waited, no_wait, unknown 세 값으로 구분했다. 실제 밥;도에서 사용한 라벨 사전을 옮긴 것은 아니다.

필드 허용 값 의미
label waited, no_wait, unknown 기다림 / 기다리지 않음 / 판단할 언급 없음
evidence 원문 문자열 또는 null 판단에 사용한 리뷰 구절

여기서 unknown은 빠뜨리기 쉬운 값이다. 국물 맛만 적힌 리뷰에 대기가 없었다고 채워 넣으면 정보가 없는 경우까지 대기가 없었다고 집계한다. 언급이 없는 상태를 별도로 남겨야 이후 음식점별 집계에서도 구분할 수 있다.

Gemini의 Structured Outputs는 JSON Schema로 응답 구조를 지정하는 기능이다. 필드의 자료형과 필수 여부, 허용할 문자열 등을 정해 둘 수 있다. 다만 JSON Schema의 모든 기능을 지원하는 것은 아니므로 사용하는 키워드가 지원 범위에 들어가는지 확인해야 한다. Structured Outputs 문서

LangChain의 현재 ChatGoogleGenerativeAI 문서에서는 with_structured_output(..., method="json_schema")로 Gemini의 네이티브 구조화 출력을 사용하는 방법을 안내한다. 단순히 프롬프트 끝에 JSON으로 답하라고 적는 것과는 호출 설정이 다르다. 예전 프로젝트의 패키지를 그대로 둔 채 최신 예제를 붙이기보다는 설치 버전과 API 문서를 함께 확인하는 편이 안전하다. LangChain 연동 문서

형식을 고정하면 후속 코드가 결과를 읽기 쉬워진다. 다만 label 값이 허용 목록에 있다는 사실만으로 그 판단이 맞는지는 알 수 없다. Google도 구조에 맞는 출력의 값은 애플리케이션에서 검증하라고 안내한다.

3. 원문 근거를 검사해도 남는 오류

아래는 JSON을 읽은 뒤 결과 객체에 적용할 작은 검사 함수다. 실제 리뷰나 API 응답을 가져오지 않고, 가상의 입력으로 규칙이 어떻게 작동하는지 확인했다. Python 3.12.10에서 실행했으며 외부 패키지는 쓰지 않았다.

def valid_record(review, result):
    if not isinstance(review, str) or not isinstance(result, dict):
        return False
    if set(result) != {"label", "evidence"}:
        return False

    label = result["label"]
    evidence = result["evidence"]
    if not isinstance(label, str):
        return False
    if label not in {"waited", "no_wait", "unknown"}:
        return False
    if label == "unknown":
        return evidence is None

    return (
        isinstance(evidence, str)
        and bool(evidence.strip())
        and evidence in review
    )

이 함수는 필드 이름과 라벨 범위를 검사한다. unknown일 때는 근거를 null로 두고, 판단한 라벨에는 원문에 실제로 들어 있는 구절을 요구한다. 근거 구절은 원문 그대로 가져온다. 띄어쓰기를 고친 문장이나 의역한 문장은 이 규칙에서 통과하지 않는다.

검사 범위를 알기 위해 정상 예시와 잘못된 예시를 함께 넣었다. 아래 네 행은 테스트를 위해 만든 입력이다. 모델이 생성한 결과는 아니다.

리뷰 검사할 결과 반환값
기다리지 않고 바로 들어갔다. no_wait + 원문 전체 True
기다리지 않고 바로 들어갔다. waited + “20분 기다렸다.” False
기다리지 않고 바로 들어갔다. waited + 원문 전체 True
국물이 담백했다. unknown + null True

세 번째 행이 이 검사의 한계다. 라벨은 틀렸지만 근거 구절은 원문에 있으므로 통과한다. 부정 표현을 제대로 읽었는지는 검사하지 않기 때문이다. 네 가지 입력은 예상한 반환값과 모두 일치했지만, 이것을 라벨링 정확도로 계산할 수는 없다.

실제 처리에서는 JSON 파싱 실패, API 오류, 응답 누락도 별도로 다뤄야 한다. 이런 실패를 unknown으로 저장하면 리뷰에 대기 언급이 없는 경우와 시스템이 결과를 만들지 못한 경우가 섞인다. 실패 상태를 따로 기록해야 재처리할 대상을 찾을 수 있다.

이 예제의 검사 범위는 형식과 원문 포함 여부까지다. 라벨 판단의 오류는 사람의 판단과 대조해야 드러난다.

4. 모델 교체 전후를 같은 리뷰로 비교하기

밥;도의 라벨링을 새 모델로 다시 구성할 때 이전 결과를 그대로 정답으로 삼으면 비교 기준에도 오류가 남을 수 있다. 이전 모델도 부정 표현이나 여러 방문 시점이 섞인 리뷰를 잘못 읽었을 수 있기 때문이다. 작은 평가 자료부터 만들고 사람이 정한 기준으로 두 결과를 비교하는 편이 낫다. 아래는 아직 실험하지 않은 적용 방법이다.

평가 자료에는 대기가 있었다는 문장뿐 아니라 기다리지 않았다는 문장, 대기를 아예 언급하지 않은 문장, 평소에는 기다리지만 이번에는 바로 들어갔다는 문장도 넣는다. 마지막 문장처럼 시점이 섞인 경우에는 이번 방문을 분류할지, 음식점의 일반적인 특성을 분류할지부터 정해야 한다. 그 기준 없이 모델의 답만 비교하면 서로 다른 문제를 풀게 된다.

결과에는 모델 ID, 프롬프트, 스키마, 패키지 버전, 실행 날짜도 함께 남겨야 한다. 모델 효과를 비교하는 실험에서는 나머지 조건을 고정하고, 라이브러리까지 교체해야 한다면 전체 실행 환경이 바뀌었다는 사실을 기록한다. 이전 모델을 더 이상 호출할 수 없을 때는 저장된 결과와 새 결과의 비교라는 한계도 남는다.

결과를 볼 때는 JSON을 읽지 못한 비율과 사람이 정한 라벨을 틀린 비율을 나눈다. unknown이 얼마나 늘었는지, 근거 구절이 실제 판단을 뒷받침하는지도 함께 봐야 한다. 어느 한 값만 좋아졌다는 이유로 교체를 결정하기는 어렵다.

처리 시간과 비용도 같은 리뷰 묶음으로 비교해야 의미가 있다. 입력 길이와 동시 요청 수가 다르면 응답 시간의 차이를 모델 성능으로 설명하기 어렵고, 재시도가 많으면 한 번 호출할 때의 비용만으로 전체 비용을 계산할 수 없다. 이번 글에서는 API를 호출하지 않았으므로 모델 간 정확도·속도·비용 수치를 제시하지 않았다.

밥;도에서는 리뷰 라벨을 음식점 특징과 추천 이유를 정리하는 데 썼다. 모델을 바꾼다면 응답 형식을 고정하고 원문 근거를 남긴 뒤, 같은 리뷰의 판단이 어떻게 달라졌는지 비교해야 한다. 모델 이름이 바뀌었다는 사실만으로는 라벨이 어떻게 달라졌는지 알 수 없다.

관련 자료