기술

LLM이 새 개념을 발견할 때마다 스키마에 넣었더니, 스키마가 엉망이 됐습니다

2026.03.267분 읽기

문서를 넣을수록 스키마가 엉망이 됐다

지식그래프에 문서를 임포트하면, LLM이 텍스트에서 엔티티와 관계를 추출합니다. "이 문서에 Person이 있고, Organization이 있고, 둘 사이에 WORKS_AT 관계가 있다" 식으로요.

문제는 기존 온톨로지에 없는 타입을 LLM이 발견했을 때입니다. 예를 들어 Incident라는 엔티티나 CONTROLLED_BY라는 관계가 처음 나타났을 때, 이걸 어떻게 할 건지가 결정돼 있지 않았거든요.

gdh-2026-03-26T02-03-07.png


발견 즉시 스키마에 넣으면 안 되는 이유

처음에는 단순하게 접근했습니다. LLM이 새 타입을 발견하면 바로 온톨로지에 추가하는 거죠.

결과는 예상과 달랐습니다. 문서 10개만 넣어도 스키마에 타입이 수십 개씩 쌓이더라고요. Error, ErrorCode, ErrorMessage처럼 의미가 겹치는 타입이 각각 등록되고, HAS_HA_PAIRHA_PAIR처럼 접두사만 다른 타입이 따로 존재하고요.

"추출은 잘 되는데 관리가 안 된다"는 게 핵심 문제였습니다. LLM은 문서마다 독립적으로 추출하니까, 전체 스키마의 일관성은 신경 쓸 수가 없는 거거든요.


핵심 깨달음: 바로 넣지 않고 대기시킨다

해결 방향은 의외로 단순했습니다. 발견 즉시 스키마에 넣지 않고, 일단 대기(pending) 상태로 놔두는 것이었거든요.

비유하자면 채용 프로세스와 비슷합니다. 이력서 한 장 받았다고 바로 채용하지 않잖아요. 여러 면접관(문서)에게서 같은 후보(타입)가 반복적으로 언급되면, 그때 정식으로 뽑는(승격하는) 겁니다.

구체적인 흐름은 이렇습니다.

문서 임포트 → LLM이 기존 온톨로지에 없는 타입을 발견

대기 상태로 등록 → 바로 스키마에 넣지 않음

시그널 누적 → 다른 문서에서도 같은 타입이 발견되면 건수가 올라감

검토 → 사용자가 누적된 근거를 보고 승격(스키마 추가) 또는 기각 판단

재임포트 → 새 스키마 기준으로 출처 문서의 그래프 데이터를 다시 추출

이 프로세스를 "온톨로지 거버넌스"라고 부르기로 했습니다.

2.png


같은 타입인데 이름만 다른 경우를 잡아내기

대기 상태로 놔두는 것만으로는 부족했습니다. HAS_HA_PAIRHA_PAIR가 따로 pending에 쌓이면, 사용자 입장에서는 이게 같은 건지 다른 건지 판단이 어렵거든요.

그래서 유사 타입 감지를 2단계로 설계했습니다.

1단계는 문자열 유사도입니다. 비용 없이 즉시 처리할 수 있는 방법이에요. IS_, HAS_ 같은 접두사를 제거하고 비교하면 HAS_HA_PAIRHA_PAIR를 잡을 수 있고, 포함 관계를 체크하면 ErrorErrorCode도 걸러집니다. Levenshtein 거리로 정규화 후 30% 이내 차이까지 감지하고요.

2단계는 LLM 의미적 판단입니다. 문자열로는 절대 잡을 수 없는 케이스가 있거든요. 예를 들어 RECOVERY_CONTROLLED_BYMANAGES는 글자가 완전히 다르지만 의미적으로는 겹칠 수 있습니다. 이건 LLM이 기존 타입 목록을 함께 보고 판단하게 했어요.

두 결과를 합쳐서 카드에 "⚠ 유사: MANAGES" 같은 경고를 띄워주면, 사용자가 "이건 기존 타입으로 충분하겠다" 판단을 내릴 수 있습니다.

한 가지 주의할 점이 있는데, 관계와 엔티티를 교차 비교하면 안 됩니다. HAS_CONTACT_PERSON이라는 관계에 Person이라는 엔티티 이름이 포함되는 건 자연스러운 거지 중복이 아니거든요. 같은 종류끼리만 비교해야 합니다.

3.png


승격 후에는 재임포트가 필요하다

타입을 승격하면 온톨로지에 새 버전이 만들어집니다. 그런데 이미 임포트된 문서들은 이전 버전 스키마로 처리된 상태잖아요. 새 스키마 기준으로 그래프 데이터를 다시 뽑아야 합니다.

여기서 "뭘 지우고 뭘 남기나"가 중요한 설계 결정이었거든요.

결론부터 말하면, Neo4j/Qdrant 그래프 데이터는 삭제 후 재생성하되, 거버넌스 데이터(시그널, 타입 상태)는 보존합니다.

비유하자면, 이력서(시그널)는 남기고 면접(추출)만 다시 보는 겁니다. 시그널은 "이 타입이 왜 발견됐는지"의 근거 데이터인데, 재임포트한다고 그 근거가 바뀌지 않으니까요.

비유하자면, 이력서(시그널)는 남기고 면접(추출)만 다시 보는 겁니다. 시그널은 "이 타입이 왜 발견됐는지"의 근거 데이터인데, 재임포트한다고 그 근거가 바뀌지 않으니까요.

시그널을 삭제하면 실제로 UX 문제가 생깁니다. 타입 A, B, C가 pending인데 A만 승격하고 재임포트하면, 승격된 A는 이제 "매핑된 타입"으로 인식되어 새 시그널이 안 생깁니다. 그런데 B, C의 기존 시그널까지 삭제하면 B, C가 0건으로 보여요. 사용자 입장에서는 "아까 있던 근거가 왜 사라졌지?" 하고 혼란스러워지죠.


재임포트 필요 여부는 버전 ID로 판단한다

"이 문서가 재임포트가 필요한가?"를 판단하는 방법도 고민이 있었습니다. 타임스탬프를 비교하는 방법도 있지만, 명시적 버전 ID를 쓰기로 했어요.

문서를 처리할 때 현재 활성 스키마 버전 ID를 같이 기록하고, 승격으로 새 버전이 만들어지면 활성 ID가 바뀝니다. 조회 시 문서의 버전 ID와 현재 활성 ID가 다르면 재임포트가 필요한 것이고, 같으면 최신 상태인 거죠.

이 패턴은 Palantir Foundry의 stale asset 추적이나 Confluent Schema Registry의 스키마 버전 관리에서 쓰는 접근과 동일합니다. 타임스탬프 비교는 시간 동기화 이슈가 생길 수 있는데, 버전 ID는 그런 문제가 없거든요.


급하게 승격하지 않는 게 기본 전략이다

거버넌스를 운영하면서 가장 중요했던 원칙은 "대기에 놔두는 것이 기본"이라는 점입니다.

판단 기준은 간단합니다. 여러 문서에서 반복 발견되면(docCount가 높으면) 승격 근거가 충분하고, 1개 문서에서만 발견됐으면 보류합니다. 추가 문서를 임포트하면서 누적되는지 지켜보는 거죠. 유사 타입 경고가 있으면 기존 타입으로 표현 가능한지 검토 후 기각할 수도 있고요.

결국 온톨로지 거버넌스의 핵심은 "추출을 잘 하는 것"이 아니라, 추출 결과를 어떻게 걸러서 스키마에 반영할 것인가에 있었습니다. LLM이 아무리 정확하게 추출해도, 그 결과를 바로 스키마에 넣으면 일관성이 깨집니다.

시그널 누적 → 유사 타입 감지 → 사용자 검토 → 승격 또는 기각 → 재임포트. 이 흐름을 갖추고 나서야 문서를 계속 넣어도 스키마가 안정적으로 유지됐습니다. 지식그래프를 운영 수준으로 쓰려면 추출 파이프라인만큼 거버넌스 설계에 시간 투자하는 걸 추천합니다.