Code › tail-villain
구조화된 응답이 끝까지 오지 않았을 때
GPT-5.6 로드맵 생성이 스키마 검증을 통과한 뒤에도 실패했던 원인과 실제 요청으로 검증 범위를 넓힌 과정
GPT-5.6으로 로드맵 토픽을 생성하는 경로에서 같은 오류가 세 번 반복됐다.
처음에는 응답 스키마가 문제였고, 그다음에는 모델이 배열 개수 제한을 지키지 않았다. 두 문제를 고친 뒤 작은 테스트도 통과했지만, 사용자가 실제 로드맵에서 다시 실행하자 생성은 또 실패했다.
세 번째 실패는 앞의 두 번과 원인이 달랐다. 스키마는 맞았지만 응답이 끝까지 오지 않았다.
루트 배열부터 닫힌 객체로 바꿨다
로드맵은 사용자의 Goal과 Background를 분석한 뒤 여러 토픽을 만들고, 각 토픽에 질문 관점과 기대 신호, 후속 질문, 평가 기준을 붙인다. 백엔드는 이 결과를 구조화된 데이터로 받아 검증하고 저장한다.
GPT-5.6 경로로 전환한 뒤 첫 번째로 확인한 문제는 응답의 최상위 구조였다. 기존 스키마는 토픽 배열을 바로 반환하도록 되어 있었는데, OpenAI의 엄격한 구조화 출력(Structured Outputs)에 사용하기에는 닫힌 최상위 객체가 더 안정적인 계약이었다.
그래서 루트 배열을 topics 필드를 가진 객체로 감쌌다. 토픽과 인터뷰 모듈의 중첩 객체에도 정의하지 않은 필드를 허용하지 않도록 설정했고, provider에 요청별 strict 옵션을 전달할 수 있게 만들었다.
이 수정으로 JSON의 큰 형태는 맞았지만 애플리케이션 검증은 여전히 실패했다. 실제 응답을 확인해 보니 최대 다섯 개만 허용한 배열에 여섯 개나 일곱 개의 항목이 들어오고 있었다. JSON 문법은 맞아도 제품이 정한 계약에는 맞지 않는 데이터였다.
원인은 strict 옵션이 실제 OpenAI 요청까지 전달되지 않았던 데 있었다. 스키마를 함께 보냈다는 사실만으로 모델이 모든 제한을 강제해서 지키는 것은 아니었다. 토픽 생성과 로드맵 구체화, 현지화 경로에 엄격한 스키마 적용을 연결하자 이 문제는 사라졌다.
엄격한 출력도 중간에서 끊길 수 있다
수정 후 작은 서비스 테스트를 실행했고 정상적인 구조의 응답을 받았다. 여기서 작업이 끝났다고 판단했지만, 실제 사용자가 실패했던 로드맵을 다시 실행하자 같은 화면에서 또 오류가 났다.
이번에는 응답 길이가 문제였다.
실제 요청에는 더 긴 Goal과 Background, 로드맵 분석 결과가 들어갔고 모델도 더 많은 내용을 생성했다. 응답은 설정된 출력 한도인 4,096토큰을 정확히 채운 뒤 JSON 중간에서 끝났다. 약 87초를 기다린 결과가 닫히지 않은 JSON이었으니, 파서는 이를 정상적인 구조화 응답으로 처리할 수 없었다.
엄격한 스키마는 모델이 어떤 구조로 답해야 하는지는 제한하지만, 출력 예산이 소진된 뒤에도 나머지 내용을 만들어 주지는 않는다. 정해진 구조를 따르며 생성하다가 토큰 한도에 도달하면 그 구조 자체가 완성되기 전에 응답이 멈출 수 있다.
이 차이를 확인하지 않으면 모든 실패가 잘못된 JSON으로만 기록된다. 모델이 스키마를 어긴 것인지, 응답이 잘린 것인지, 애플리케이션 검증 조건과 맞지 않는 것인지 구분할 수 없다.
출력 한도만 늘리지 않았다
가장 빠른 대응은 출력 한도를 크게 올리는 것이지만, 그것만으로는 생성량이 계속 늘어나는 문제를 남긴다. 정상적인 한 번의 로드맵 생성에 지나치게 긴 답변이 필요하다면 지연 시간과 비용도 함께 커지고, 다음에는 더 높은 한도에서 같은 문제가 반복될 수 있다.
먼저 생성해야 하는 분량을 줄였다. 각 인터뷰 모듈의 질문 관점, 기대 신호, 후속 질문, 평가 기준을 정확히 세 개의 간결한 항목으로 제한했고, 프롬프트에도 짧고 구체적으로 작성하라는 조건을 넣었다.
출력 한도는 8,192토큰으로 높였지만, 이는 평소에 전부 사용하라고 마련한 용량이 아니라 예외적인 응답을 위한 여유분이다. 정상 응답의 크기는 프롬프트와 스키마로 통제하고, 높은 한도는 예상보다 길어진 경우에 구조가 중간에서 끊기지 않도록 남겨 두었다.
OpenAI가 응답을 incomplete 상태로 반환하고 그 이유가 max output tokens라면, JSON 파싱을 시도하기 전에 명시적인 오류로 처리하도록 provider도 수정했다. 응답 문자열이 존재한다는 사실과 생성이 완료됐다는 사실은 다르기 때문이다.
작은 테스트 대신 실패했던 요청을 그대로 실행했다
앞선 수정이 두 번이나 실제 요청에서 다시 실패한 이유는 검증 범위가 좁았기 때문이다. 작은 서비스 테스트는 provider가 스키마를 받아 응답할 수 있다는 점을 확인했지만, 사용자가 실행하는 전체 경로를 증명하지는 못했다.
실제 흐름에는 인증과 controller routing, 긴 로드맵 문맥, LLM 생성, 애플리케이션 검증, 데이터베이스 저장이 모두 들어간다. 그래서 이전에 실패했던 blueprint를 그대로 사용해 인증된 로컬 HTTP endpoint를 호출하고, 응답 코드와 생성 상태뿐 아니라 토픽과 인터뷰 모듈이 데이터베이스에 저장됐는지까지 확인했다.
최종 요청은 약 4.9초 만에 완료됐고 출력은 457토큰이었다. 이전 요청이 4,096토큰을 모두 사용하고도 약 87초 뒤 실패했던 것과 비교하면, 출력 한도를 늘린 것보다 생성량을 통제한 영향이 더 컸다.
검증 기준도 함께 바뀌었다.
provider 응답 완료
→ 애플리케이션 스키마 검증
→ 인증된 HTTP 요청 성공
→ 데이터베이스 저장 확인
이 네 단계를 통과해야 로드맵 생성이 완료됐다고 판단할 수 있다. provider를 직접 호출한 작은 테스트는 원인을 좁히는 데 유용하지만, 실제 제품 경로를 대신하지는 못한다.
구조화 출력 문제를 고치면서 처음에는 JSON의 모양을 봤고, 다음에는 스키마 제한이 실제 요청에 적용되는지 확인했다. 마지막 실패에서야 응답의 완결성과 실제 저장 결과까지 검증 범위에 들어왔다.
LLM 응답에 텍스트가 들어 있다는 이유만으로 생성이 완료된 것은 아니다. 스키마가 엄격하더라도 출력 예산이 먼저 끝날 수 있고, 파싱에 성공하더라도 애플리케이션의 제한을 통과하지 못할 수 있다.
이번 수정 이후에는 모델이 무엇을 반환했는지만 확인하지 않는다. 응답이 끝까지 생성됐는지, 제품의 계약을 만족하는지, 사용자가 호출한 경로를 통과했는지, 결과가 실제로 저장됐는지까지 이어서 확인한다.