지식그래프 중복, 같은 뜻인데 이름이 달라서 온톨로지가 뻥튀기됐다
pending 목록이 늘어나는데, 이거 다 다른 타입이 맞아?
온톨로지 거버넌스를 도입하고 나면, LLM이 발견한 미매핑 타입들이 pending 목록에 쌓이기 시작합니다. 거버넌스가 뭔지는 이전 글에서 정리해뒀는데, 요약하면 "LLM이 새 타입을 발견해도 바로 온톨로지에 넣지 않고 대기시키는 프로세스"입니다.
이전글:
그런데 목록을 보다 보면 이상한 게 눈에 들어옵니다. HAS_HA_PAIR와 HA_PAIR가 각각 따로 등록돼 있고, Error와 ErrorCode도 별개 타입으로 잡혀 있어요. 이게 진짜 다른 타입인지, 아니면 이름만 다른 같은 타입인지 사람이 하나하나 확인해야 하는 상황이 된 겁니다.
문자열만 보면 될까, LLM까지 써야 할까
처음에는 "이름이 비슷하면 같은 타입 아닌가?"라는 단순한 접근으로 시작했습니다. 문자열 비교만으로 충분할 것 같았거든요.
그런데 실제로 돌려보니까 두 종류의 중복이 있더라고요.
하나는 글자가 비슷한 중복입니다. HAS_HA_PAIR ↔ HA_PAIR처럼 접두사만 다르거나, Error ⊂ ErrorCode처럼 한쪽이 다른 쪽을 포함하는 경우요. 이건 문자열 비교로 잡을 수 있습니다.
다른 하나는 글자는 완전히 다른데 뜻이 겹치는 중복입니다. RECOVERY_CONTROLLED_BY와 MANAGES는 글자가 하나도 안 겹치지만, 의미적으로는 "누가 뭘 관리/통제한다"로 겹칠 수 있거든요. 이건 문자열을 아무리 비교해도 절대 못 잡습니다.
그래서 2단계로 나눠서 설계하게 됐습니다.
1단계: 문자열 유사도 — 비용 없이 즉시 잡는 것들
1단계는 LLM 호출 없이 즉시 처리할 수 있는 방법들입니다. 비용이 0이라 모든 타입 생성 시 바로 돌릴 수 있어요.
접두사 정규화
관계 타입에는 IS_, HAS_, BELONGS_TO_ 같은 접두사가 자주 붙습니다. 이걸 제거하고 비교하면 HAS_HA_PAIR ↔ HA_PAIR를 바로 잡을 수 있어요. 접두사가 의미를 바꾸는 게 아니라 문법적 장식에 가까운 경우가 많거든요.
포함 관계
정규화된 이름이 서로 포함 관계에 있는지 체크합니다. Error ⊂ ErrorCode처럼 한쪽이 다른 쪽의 부분 문자열이면 유사 타입 후보로 올립니다.
Levenshtein 거리
정규화 후 편집 거리가 전체 길이의 30% 이내면 유사로 판단합니다. 오타 수준의 차이나 복수형/단수형 차이를 잡아내는 용도예요.
이 세 가지를 조합하면 "글자가 비슷한 중복"은 대부분 걸러집니다. 여기서 중요한 규칙이 하나 있는데, 같은 종류끼리만 비교한다는 점이에요. 관계는 관계끼리, 엔티티는 엔티티끼리만 비교합니다.
관계와 엔티티를 교차 비교하면 안 되는 이유가 있거든요. HAS_CONTACT_PERSON이라는 관계에 Person이라는 엔티티 이름이 포함되는 건 완전히 자연스러운 겁니다. "Person에 대한 관계"니까 이름에 Person이 들어가는 거지, 중복이 아니에요. 교차 비교를 켜면 이런 정상적인 관계가 전부 유사 타입 경고로 뜨면서 노이즈가 폭발합니다.
2단계: LLM 의미적 판단 — 문자열로 절대 못 잡는 것들
1단계로 잡을 수 없는 케이스가 분명히 있습니다. RECOVERY_CONTROLLED_BY와 MANAGES는 글자가 완전히 다르니까요.
이걸 위해 별도 LLM 호출을 추가하면 비용이 늘어나는데, 여기서 한 가지 설계 트릭을 썼습니다. 미매핑 타입을 생성할 때 이미 _generate_reason이라는 LLM 호출이 있거든요. "이 타입이 왜 발견됐는지" 설명을 생성하는 용도인데, 이 호출에 기존 타입 목록을 함께 넘겨서 의미적 중복 판단도 같이 하게 한 겁니다.
추가 비용 없이 의미적 중복 감지를 끼워넣은 셈이에요.
예: RECOVERY_CONTROLLED_BY 생성 시
→ LLM에 기존 타입 목록(MANAGES, OPERATES, SUPERVISES...)을 함께 전달
→ LLM이 "MANAGES와 의미적으로 겹칠 수 있음"이라고 판단
→ similar_to 컬럼에 기록
두 결과를 합쳐서 카드에 보여준다
1단계와 2단계 결과는 similar_to 컬럼에 병합됩니다. pending 타입 카드에 "⚠ 유사: MANAGES" 같은 경고가 표시되고, 사용자는 이걸 보고 세 가지 판단을 내릴 수 있어요.
진짜 같은 타입이다 → 기각하고 기존 타입을 사용
비슷하지만 다른 타입이다 → 그대로 승격
판단이 어렵다 → 문서가 더 쌓일 때까지 보류
최종 판단은 항상 사람이 합니다. LLM이 "겹친다"고 판단해도 도메인에 따라서는 별개 타입이 맞을 수 있거든요. 시스템은 판단 근거를 모아서 보여주는 역할까지만 하고, 결정권은 사용자에게 둔 겁니다.
왜 처음부터 LLM만 쓰지 않았나
"2단계가 더 정확하면 처음부터 LLM만 쓰면 되지 않나?"라는 질문이 나올 수 있는데, 이유가 있습니다.
문서를 임포트할 때마다 미매핑 타입이 생기고, 그때마다 기존 타입 전체와 비교해야 합니다. 온톨로지에 타입이 100개 있으면 매번 100개를 LLM 컨텍스트에 넣어서 비교하는 건 비용도 비용이지만, 응답 시간도 느려지거든요.
1단계 문자열 비교로 명확한 중복을 먼저 걸러내면, LLM은 문자열로 못 잡는 애매한 케이스만 판단하면 됩니다. 그리고 그 LLM 호출도 reason 생성에 끼워넣어서 추가 비용이 0이고요.
결국 "비용 없이 잡을 수 있는 건 문자열로, 문자열로 못 잡는 건 LLM으로, 최종 결정은 사람이" 하는 3단 구조가 된 겁니다. 유사 타입 감지에서 가장 비싼 자원은 LLM 토큰이 아니라 사용자의 판단 시간이었거든요. 시스템이 노이즈를 최대한 줄여서 보여줘야 사용자가 핵심 판단에 집중할 수 있습니다.