업로드마다 LLM을 부르는 빌드는 자동으로 켜 둘 수 없었습니다
온톨로지가 문서를 올릴 때마다 만들어지려면 기본 빌드에는 호출 비용이 없어야 했습니다. 표 구조와 형태소, 한국어 패턴만으로 클래스·개체·상하위를 세우고 관계 추출은 옵션 단계로 분리한 과정과, 그 뒤 드러난 문서 500개 상한 결함을 정리합니다.
현장에서 다시 잰 것들 시리즈 · 6/6전체 목차 펼치기
문서를 올리면 온톨로지가 저절로 생겨야 한다는 요구가 있었습니다. 그런데 빌드는 청크마다 LLM을 호출해 개념과 관계를 뽑고 있었고, 그 상태로는 업로드마다 자동 실행을 활성화해 둘 수 없었습니다. 기본 빌드에서 LLM을 빼고 무엇으로 판정했는지, 자동 실행되기 시작한 빌드에서 어떤 결함이 드러났는지 정리합니다.
호출마다 시간과 비용이 붙었습니다
기존 빌드는 청크를 모델에 보내 클래스, 인스턴스, 관계를 한 번에 받았습니다. 문서 4개(청크 113개)를 gpt-4o-mini로 빌드하는 데 941초가 걸렸고, 같은 입력을 로컬 모델로 실행하면 배치 하나에 약 6분씩 전체 2~3시간이 예상됐습니다.
비용만의 문제도 아니었습니다. LLM 추출은 실행마다 결과가 달라 재실행할 때마다 개념 수가 늘 수 있었고, 모델에 따라서도 갈렸습니다. 한 클라우드 모델은 고유명사마다 전용 클래스를 만들었는데, 같은 문서에서 로컬 모델은 제대로 된 상위어를 뽑았습니다.
배치 크기를 실행 시점의 모델 한도에 맞추고 추론 모델의 thinking 출력을 비활성화하는 식으로 호출을 다듬어 왔지만, 업로드마다 호출이 붙는 구조는 그대로였습니다.
무엇을 LLM 없이 판정할 수 있는가
그래서 빌드를 두 갈래로 나눴습니다. 기본 빌드는 LLM을 호출하지 않고 항상 실행되고, 비용이 큰 관계 추출은 사용자가 활성화한 컬렉션에서만 실행됩니다.
문서 업로드 → 기본 빌드 (LLM 0회, 새 청크만, 자동)
└ 관계 형성을 활성화한 컬렉션 → 관계 형성 (LLM, 이미 뽑힌 개체 사이만)기본 빌드의 판정 근거는 형태소 태그, 원문에 실제로 나온 것, 코퍼스 통계 세 가지로 제한했습니다. 도메인 단어 목록이나 예시 문자열은 판정 규칙에 두지 않았습니다.
- 표와 행 덤프는 구조로 읽습니다. 행이 개체, 열 이름과 셀이 속성이나 관계, 표 제목이 행의 클래스가 됩니다.
- 산문은 형태소 경계에서 명사구를 끊어 개체 후보로 씁니다.
- 상하위는 "A, B 등의 C" 같은 한국어 Hearst 패턴과 복합명사의 머리("내부감사규정"의 "규정")로 세웁니다.
관계 형성은 활성화한 시점에 이미 적재된 문서에도 소급 적용되고, 중단해도 이어서 실행됩니다. 이미 뽑힌 개체와 술어 목록을 참고로 줘 같은 이름을 다시 쓰도록 유도하되 강제하지는 않습니다.
문서 6개 테스트에서 기본 빌드는 0.3~1.6초에 LLM 0회, 관계 형성은 10초에 LLM 7회(15,194토큰)였습니다. 업로드 직후에는 새 청크만 기본 빌드해 문서 2개가 30초 안에 반영됩니다.
이름 하나가 틀리면 관계도 틀립니다
LLM이 빠진 만큼 이름을 끊는 규칙이 그래프 품질을 좌우했습니다.
산문이 표로 오인되는 일이 있었습니다. "줄마다 단어 수가 같으면 표"라는 판정에 산문이 우연히 걸리면, 공백으로 분할한 어절이 조사와 마침표가 붙은 채 개체가 됐습니다. 마침표로 끝나는 줄이 과반이면 산문으로 판정하도록 바꿨습니다.
이름이 잘리기도 했습니다. "현물환거래"가 "현물"과 "거래"로 갈렸는데, 형태소 분석기가 가운데 "환"을 어근으로 분류해 명사만 이어 붙이는 규칙에서 빠졌기 때문입니다. 어근과 접미사는 앞에 명사가 있을 때만 붙이게 했습니다. 이름이 틀리면 그 이름을 주어로 한 관계까지 틀어집니다. 실제로 "준법감시인"이 "준법감시"로 잘린 뒤 주어가 바뀐 관계가 만들어져 있었습니다.
업로드 때 청크 앞에 붙는 검색용 머리말도 문제였습니다. 머리말이 400자 청크의 31%를 차지하며 개체로 들어오고, 관계 추출과 답변 프롬프트까지 채우고 있었습니다. 누출 경로가 네 곳이라, 한 함수로 모아 모든 경로가 같은 것을 쓰게 했습니다.
빌드 이력 102건이 모두 같은 숫자였습니다
자동 빌드를 활성화하자 다른 결함이 보였습니다. "빌드가 왜 이렇게 느리냐"는 제보를 따라가 보니, 한 컬렉션의 빌드 이력 102건이 거의 전부 164초, 청크 10,056개, 트리플 54,809개로 같았고 11.6분 간격으로 끝없이 반복되고 있었습니다.
원인은 SDK 조회 함수의 기본값 limit=500이었습니다. 빌드가 문서를 읽는 네 경로가 모두 limit을 넘기지 않았습니다. 컬렉션에는 문서 1,186개, 청크 29,506개가 있었지만 빌드는 문서 500개 몫인 청크 10,056개만 봤습니다. 나머지 문서 686개, 청크로는 19,450개(66%)가 그래프에 들어가지 못했습니다. 벡터 검색으로는 찾히는데 그래프에는 없는 문서가 생긴 것입니다.
반복도 같은 원인이었습니다. 자동 스윕은 벡터 저장소의 전체 청크 수(29,506)와 빌드가 처리한 기준 수(10,056)를 비교했는데, 빌드가 500개만 읽는 한 기준이 자라지 않아 비교가 영원히 참이었습니다.
limit을 명시하고, 상한(100,000)에 닿으면 경고를 남기게 했습니다. 조용히 잘리면 왜 문서가 빠졌는지 아무도 찾지 못합니다. "빌드 경로가 SDK 조회를 직접 호출하지 않는다"는 것을 소스에서 검사하는 테스트도 두었습니다. 한 곳만 고치면 나머지 경로로 되살아나기 때문입니다.
판정을 청크에서 코퍼스로 옮겼습니다
9월 말에는 그래프 뷰어에서 "정보가" 같은 조사 붙은 클래스와 뒤집힌 상하위가 보였습니다. 금융 내규·법령 문서 764건(청크 51,006개)을 로컬에서 다시 빌드해 층마다 원인을 잡았습니다.
| 층 | 원인 | 결과(같은 764건) |
|---|---|---|
| 용어 | 마침표 없는 조문 문단을 행 덤프로 오판 | 조사로 끝나는 개체명 7.6%에서 3.5% |
| 분류 | 괄호 속 법령 제목 안의 "등"에서 패턴 발화 | 제목 안 상하위 쌍 2,003에서 9 |
| 개념 | 흔한 머리 명사를 무조건 부모로 | 흔한말 부모 상하위 16,239에서 9,770 |
개념 층이 핵심이었습니다. 흔한 말인지를 청크 단위로 세면 가장 흔한 말도 16.6%라 30% 기준에 걸리지 않았습니다. 문서 단위로 세니 "사항"은 0.71, "업무"는 0.59였고 "공원"은 0.02였습니다. 머리와 그 구성원이 문서의 30% 넘게 퍼져 있으면 분류가 아니라 연관 관계로 낮췄습니다. 청크라는 지역 정보로 내리던 전역 판단을 코퍼스 단위로 옮긴 것입니다. 이 빌드는 LLM 호출 없이 1,313초, 약 22분이 걸렸습니다.
넣었다가 뺀 규칙도 있습니다. 순서만 바뀐 고유명사를 동의어로 합치는 규칙이었는데, 764건의 고유명사 1,646개에서 걸린 두 쌍(금융리스와 리스금융, 영업양도와 양도영업)이 모두 서로 다른 말이었습니다. 얻는 것 없이 잘못 합칠 위험만 있어 제거했습니다.
확인한 것과 남은 것
764건 수치는 빌드 라이브러리의 기본 모드로 로컬에서 재현한 결과이고, 운영 컬렉션은 재빌드한 뒤 다시 봐야 합니다. 개발 서버 그래프에서 보인 허브 클래스는 로컬 일괄 빌드로는 재현되지 않아, 이어서 빌드하는 경로를 의심하고 있지만 아직 원인을 찾지 못했습니다. 관계 형성이 검색 품질에 주는 이득도 그것을 직접 측정하는 문항 세트가 없어 아직 말하기 어렵습니다.
LLM을 빼는 결정은 비용을 줄이려고 시작했지만, 결과적으로는 같은 입력에서 같은 그래프가 나오게 하는 일이었다고 봅니다. 결과가 고정되어야 층마다 원인을 짚는 비교가 가능했던 것 같습니다.