XGEN 15일 무료 체험 — 설치 없이 브라우저에서 바로 시작하세요체험 신청
PlateerAI Labs
·Reading Time | 3 min
작성 | 김진수

작업은 성공이라고 나왔는데 그래프가 비어 있었습니다(9편)

추출 모델을 로컬 소형 모델로 바꾸자 배치가 끝나지 않거나, 성공으로 기록된 작업이 빈 그래프를 남겼습니다. 잘린 JSON과 시간 초과가 빈 결과처럼 처리되고 있었습니다. 실패를 실패로 만든 과정입니다.

작업은 성공이라고 나왔는데 그래프가 비어 있었습니다(9편) — 커버 일러스트Series · 온톨로지 개발기시리즈 9번째 글 · 전체 10편 보기

온톨로지 추출 모델을 로컬 소형 모델로 바꾸자 같은 문서의 한 배치가 15분 가까이 끝나지 않았습니다. 더 곤란한 쪽은 따로 있었습니다. 작업은 성공으로 끝났는데 그래프가 비어 있었습니다. 잘린 JSON과 시간 초과가 빈 추출 결과와 같은 모양으로 처리되고 있었기 때문입니다. 실패를 실패로 만들고 처리 단위를 실행 시점에 정하게 바꾼 과정입니다.


실패가 빈 결과와 같은 모양으로 도착했습니다

모델을 바꾸기 전까지 고정된 1만 자 입력으로 잘 돌아가고 있었습니다.

로컬 소형 모델로 바꾸자 두 가지가 나왔습니다. 한 배치가 15분 가까이 끝나지 않는 경우와, 작업이 성공으로 끝났는데 그래프가 비어 있는 경우입니다.

앞의 것은 눈에 띄지만 뒤의 것은 그렇지 않습니다.

원인은 실패가 도착하는 모양에 있었습니다. 출력 토큰 한도에 걸려 JSON이 중간에 잘리면 파싱이 실패합니다. 그 파싱 실패가 빈 추출 결과와 같은 값으로 처리되고 있었습니다. 시간 초과도 마찬가지였습니다.

정상   문서에서 개체를 못 찾음   → 빈 결과
비정상 JSON이 잘림              → 파싱 실패 → 빈 결과
비정상 응답이 안 옴             → 시간 초과 → 빈 결과

세 경우가 같은 값으로 내려오니 상위 단계는 구분할 수 없었습니다. "이 문서에는 추출할 것이 없었다"로 기록하고 성공으로 넘어갔습니다.

고정된 1만 자 입력이 작은 모델의 컨텍스트와 출력 한도를 함께 압박하고 있었고, 그 압박이 조용한 실패로 나타난 것입니다.

값이 없는 상태를 반환하는 함수를 만들 때, 없는 것과 못 가져온 것을 같은 값으로 표현하면 그 구분은 영영 복구되지 않습니다. 호출하는 쪽에서는 둘 다 빈 목록이기 때문입니다.

상수를 낮췄더니 다른 모델에서 잘게 쪼개졌습니다

당장의 대응은 배치 크기와 출력 토큰을 낮추는 것이었습니다. 통과했습니다.

다른 모델에서 돌리자 불필요하게 잘게 쪼개졌습니다. 여유가 있는 모델까지 작은 모델에 맞춘 크기로 호출하면서 호출 수가 늘었습니다.

그다음 시도는 모델 이름별 상수 목록이었습니다. 이 모델은 이 크기, 저 모델은 저 크기로 적어뒀습니다.

이것도 틀어졌습니다. 모델의 능력과 현재 서빙 설정은 다릅니다. 같은 모델이라도 서버가 컨텍스트를 작게 잡아 띄워두면 목록에 적힌 값은 맞지 않습니다. 목록은 만든 날에만 정확합니다.

그래서 숫자를 외우는 방식을 버렸습니다. 추출기가 실행 시점에 현재 모델의 입력과 출력 예산을 읽어 배치 크기를 계산하게 했습니다.

OpenAI 호환 서버는 모델 목록 응답의 메타데이터에서 컨텍스트 길이 관련 값을 탐색합니다. 키의 위치와 이름이 서버마다 달라 중첩된 메타데이터도 함께 봅니다. 런타임 조회가 불가능한 클라우드 API에서는 운영 설정을 쓰고, 둘 다 없을 때만 검증된 기본값으로 돌아갑니다.

공급자 이름으로 "이 모델은 128K"라고 가정하지 않습니다.

환경마다 달라지는 값을 코드에 상수로 적는 순간, 그 값은 환경을 따라가지 못합니다. 실행 시점에 물어볼 수 있는 값이면 물어보는 편이 목록을 관리하는 것보다 쌉니다.

컨텍스트만 보면 출력에서 잘립니다

예산을 계산하면서 하나 더 알게 됐습니다.

온톨로지 추출은 일반 요약보다 출력이 큽니다. 입력에 개체와 관계가 많으면 JSON 배열이 빠르게 늘어납니다.

컨텍스트 윈도우만 보고 입력을 크게 묶으면 입력은 들어가는데 출력에서 잘립니다. 앞의 조용한 실패가 정확히 그 경로였습니다.

그래서 문자 예산을 두 값 중 작은 쪽으로 잡습니다.

배치 문자 예산
  = min(
      컨텍스트 윈도우에서 시스템 프롬프트와 출력 여유를 뺀 입력 예산,
      최대 출력 토큰으로 감당할 수 있는 추출량 예산
    )

이 값은 정확한 토큰 계산이 아닙니다. 문자당 토큰 비율과 구조화 출력의 팽창률을 운영 설정으로 조절해 잡는 보수적 추정치입니다. 모델과 언어가 바뀌면 실제 사용량을 다시 측정해 계수를 조정해야 합니다.

입력 한도만 보고 배치를 잡는 것은 흔한 기본값입니다. 산출물이 입력보다 커질 수 있는 작업에서는 그 기본값이 맞지 않습니다.

앞부분만 잘라 보내면 뒤쪽 행이 조용히 사라집니다

배치 예산을 줄여도 청크 하나가 예산보다 클 수 있습니다. HTML 표나 OCR 결과는 하나의 청크가 아주 길어집니다.

기존 경로는 문자열 앞부분만 잘라 보냈습니다. 그러면 뒤쪽의 행이 아무 표시 없이 사라집니다. 이것도 성공으로 기록되는 실패였습니다.

큰 청크는 표의 행 경계를 우선 사용해 나누고, 행 경계가 없으면 문자 윈도우로 분할합니다. 분할한 조각은 원래 청크 식별자를 유지합니다.

모델 호출 단위는 작아져도 결과가 돌아갈 원문 출처는 바뀌지 않습니다. 1편에서 만든 출처 연결이 여기서 깨지면 안 되기 때문입니다.

고정 길이로 입력을 잘라내던 경로는 제거했습니다. 배치 조립 단계가 예산 안에서 입력을 구성하므로, 추출 함수는 전달받은 내용을 임의로 다시 자르지 않습니다. 자르는 책임을 한 곳에만 두는 것이 목적이었습니다.

실패한 배치만 절반으로 나눠 다시 시도합니다

실패를 구분할 수 있게 되자 복구 방법도 정할 수 있었습니다.

JSON 호출이 시간 초과와 출력 절단, 파싱 실패를 각각 구분해 반환하도록 했습니다. 문제가 생긴 배치는 절반으로 나눠 다시 시도합니다. 정상 배치까지 되돌리지 않습니다.

배치 실행
  ├─ 성공 ─────────────→ 결과 병합
  └─ 시간 초과 / 절단
       ├─ 왼쪽 절반 ───→ 재실행
       └─ 오른쪽 절반 ─→ 재실행

재시도는 재귀 호출이 아니라 명시적인 스택으로 관리합니다. 동시 호출 수를 제한하는 슬롯을 잡은 상태에서 같은 함수를 재귀 호출하면 자기 자신이 새 슬롯을 기다리다 멈출 수 있기 때문입니다. 한 작업이 슬롯을 한 번만 확보하고, 내부에서 분할 배치를 순서대로 처리한 뒤 결과를 합칩니다.

합칠 때 클래스와 속성은 식별자로 중복을 제거하고, 인스턴스와 관계와 값은 출처를 유지한 채 이어 붙입니다.

조용한 실패를 다 없애지는 못했습니다

정직하게 남은 것을 적어두겠습니다.

더 나눌 수 없는 최소 단위에서 실패하면 현재 결과는 값 없음으로 끝나고, 상위 집계가 그 예외나 빈 결과를 건너뛸 수 있습니다.

분할과 병합 로직은 만들었지만, 배치별 실패 수와 식별자를 작업 오류로 올려서 부분 유실을 막는 계약은 아직 없습니다. 그러니까 이 편에서 저희가 고친 것은 조용한 실패의 대부분이지 전부가 아닙니다.

가장 흔한 경로에서 실패가 소리를 내게 만들었고, 가장 안쪽의 경로는 남아 있습니다.

검증 범위도 나눠 적어야 합니다. 분할과 병합, 절단 신호, 입력과 출력 예산 계산, 동시 실행 슬롯 종료는 실제 클래스를 불러온 단위 테스트로 확인했습니다. 기존 호출부가 새 선택 인자 없이 같은 동작을 유지하는지도 점검했습니다.

다만 당시 대상 로컬 모델 서버에 연결할 수 없어 실제 모델 메타데이터 조회와 전체 라이브 빌드는 확인하지 못했습니다. 확인된 범위는 계산과 복구 로직이고, 특정 로컬 모델에서의 처리 시간 개선은 서버가 준비된 뒤 따로 측정해야 합니다.

조용히 실패하는 시스템은 성공률이 높아 보입니다

이 편의 시작은 성능 문제처럼 보였습니다. 작은 모델이 느리다는 것이었습니다.

실제로 고친 것은 실패가 도착하는 방식이었습니다. 잘린 응답과 시간 초과가 빈 결과와 같은 값으로 내려오니, 시스템은 아무 문제 없이 돌아가는 것처럼 보였고 그래프만 비어 있었습니다.

조용히 실패하는 시스템은 지표상으로 성공률이 높습니다. 실패한 실행이 성공 쪽에 세어지기 때문입니다. 3편의 완료 상태, 5편의 자동 삭제와 같은 종류의 문제가 여기서도 나왔습니다.

문서 쪽에서는 이렇게 모델 차이를 흡수하는 것이 핵심이었습니다. 정형 데이터는 접근이 다를 수 있습니다. 데이터베이스가 이미 타입과 기본키, 외래키를 제공하므로 행을 해석하는 데 모델을 쓸 이유가 없습니다.

마지막 편에서는 조회 결과를 직접 그래프로 만들면서, 행의 정체성을 무엇으로 잡을지 다시 정한 이야기를 다룹니다.


이전 편 → 문서용 중복 정리가 정형 테이블을 합치고 있었습니다 (8편)

다음 편 → 이름으로 만든 URI가 동명이인을 한 사람으로 합쳤습니다 (10편)

#온톨로지#LLM 추출#장애 복구
블로그 목록으로