Engineering · Agent Engineering

은행 관리 콘솔 에이전트 온톨로지: 운영 테이블에서 LLM 없이 구성하고 분포로 판정하기

관리자가 300여 개 에이전트와 컬렉션·DB·도구의 관계를 탐색하고 질문할 수 있도록, 문서를 다시 추출하지 않고 운영 테이블에서 에이전트 그래프를 결정론적으로 구성했습니다. 상태 서명 기반 증분, 분포 기반 인사이트 규칙, 그래프 근거 질의, 서버 측 그래프 요약까지 설계 판단과 실측을 정리합니다.

기간
2026.09
소속
고객사 구축 · 은행
역할
설계·구현(백엔드·프론트엔드)

Key Results

3,373 노드 / 3,867 엣지
에이전트 306개 그래프 구성외부망 개발 DB 읽기 전용 드라이런, LLM 0회. 소요 시간은 별도 기록 없음
4.5초 → 0.23초
온톨로지 탭 첫 렌더링 long task실제 에이전트 309개 응답을 Playwright로 재현, 클라이언트 요약 단계 적용 전후
7/7
집계형 관리 질의 정답Qwen 27B 계열, 정답을 그래프에서 직접 계산해 대조, 응답 2~5초

은행 고객 환경의 XGEN 플랫폼에는 개발 환경 기준으로 에이전트가 300개 넘게 쌓여 있었습니다. 에이전트가 어떤 모델·컬렉션·도구·DB 연결을 쓰는지는 캔버스 설정에, 배포 단계와 승인은 배포 테이블에, 누가 얼마나 실행했는지는 실행 로그에 흩어져 있었습니다. 은행과의 후속 협의에서 관리 설정의 온톨로지는 관계를 보여 주는 뷰어가 아니라 관리자용 인사이트 도구여야 한다는 방향이 정해졌습니다.

이 글은 그 요구를 에이전트 단위 지식 그래프로 구현한 과정입니다. 출발점의 판단은 에이전트마다 문서를 다시 추출하지 않고 이미 있는 운영 테이블에서 그래프를 LLM 호출 없이 구성한다는 것이었습니다. 그 위에서 그래프가 운영 상태를 따라가지 못하던 문제, 고정 임계값이 환경마다 성립하지 않던 문제, 자연어 질의가 엉뚱한 근거를 고르던 문제, 실제 규모에서 렌더링이 멈추던 문제를 순서대로 다룹니다.

전제: 저장소부터 바꿔야 했습니다

고객 환경의 온톨로지는 RDF 트리플스토어(Apache Jena Fuseki)를 전제로 한 이전 세대 코드였습니다. 고객 환경에서는 트리플스토어를 운영하지 않기로 했는데, 기존 코드는 Fuseki가 없으면 빈 그래프를 HTTP 200으로 돌려주고 빌드는 트리플 0개인 채 완료로 기록했습니다. 실패가 성공처럼 보이는 상태였습니다. 그래서 에이전트 그래프에 앞서 제품 기본 라인의 PostgreSQL 네이티브 온톨로지 엔진(빌드 파이프라인·저장소·검색·뷰)을 고객 배포 라인으로 이식했습니다. Fuseki는 설정으로 켤 때만 동작하는 선택적 미러로 남기고, 고객 환경 전용 기능인 text-to-SQL과 OpenTelemetry 연동은 보존했습니다.

기획 시점에 외부망 개발 DB를 읽어 규모를 확인했습니다. 에이전트 307개, 사용자 76명이었고, 60일 넘게 수정이 없는 에이전트가 196개, 한 번도 실행되지 않은 에이전트가 100개, 지워진 자산을 가리키는 끊어진 참조가 14건이었습니다. 에이전트들이 참조하는 컬렉션은 중복을 빼면 약 40개였습니다. 최종 구조는 아래와 같습니다.

운영 테이블 (워크플로 메타 · 배포 · 사용자 · 벡터 DB · 도구 · DB 연결 · 실행 로그)
      │  결정론적 구성 (LLM 0회)
      ▼
agent:<id>        L0 구조층: 소유 · 부서 · 자산 참조 · 배포 · 승인 · 실행
                  L1 정렬층: 멤버 컬렉션 사이 sameAs
assets:platform   어떤 에이전트도 참조하지 않는 자산까지 원본 테이블에서
topics:platform   L2 업무 주제 (LLM, 텍스트 해시가 바뀐 에이전트만)
      │  전부 기존 노드·엣지 테이블, collection_id 를 스코프 키로 재사용
      ▼
조회 = 스코프 합집합 (노드는 URI로 중복 제거)
      ▼
관리 콘솔 온톨로지 탭: 요약 칩 · 필터 · 그래프 · 자원 검색 · 자연어 질의 · 운영 인사이트

에이전트마다 빌드하지 않은 이유

그때까지 온톨로지의 빌드 단위는 컬렉션이었습니다. 에이전트에 그래프를 주는 방법으로 세 가지를 비교했습니다.

선택지 장점 단점 판단
에이전트마다 전체 빌드 에이전트별 그래프가 독립적 문서 추출 LLM이 에이전트 수 × 컬렉션 참조 수만큼 실행되고, 같은 문서 지식이 스코프마다 복제되며, 컬렉션이 바뀌면 쓰는 에이전트 전부 재빌드 기각
요청 시점 파생만 비용 0, 드리프트 없음 검색·정렬·학습층을 얹을 저장 위치가 없음 구조층으로 흡수
자산은 빌드, 에이전트는 구성 문서 추출은 컬렉션 수만큼, 에이전트 구성은 결정론적 여러 스코프를 한 번에 읽도록 저장소 조회를 바꿔야 함 채택

규모로 보면 "에이전트 307개 × 문서 재추출"이 아니라 "컬렉션 약 40개 빌드 + 에이전트 307개 구성"이 맞는 문제였습니다. 컬렉션이 재빌드되면 그 컬렉션을 쓰는 에이전트는 별도 작업 없이 다음 조회부터 새 그래프를 봅니다.

저장 구조에서도 새 테이블을 만들지 않았습니다. PostgreSQL 그래프 테이블은 이미 collection_id로 분할돼 있었고 엣지 테이블에는 노드 외래 키 없이 (스코프, 주어, 술어, 목적어) 유니크 제약만 있었습니다. 그래서 collection_id를 스코프 키로 일반화하고 에이전트·자산·업무 주제를 접두어가 다른 키로 같은 테이블에 넣었습니다. 초기 구현에서는 조회가 collection_id 단일 조건이라 여러 스코프를 함께 볼 때 스코프 사이의 정렬 엣지가 그려지지 않았고, 조회 함수가 접두어별 스코프를 합집합으로 읽고 노드를 URI로 중복 제거하도록 고쳤습니다.

범위에서 뺀 것도 있습니다. 에이전트 스코프로 문서를 검색하는 실행 기능(RAG 노드에서 "이 에이전트의 온톨로지"를 고르는 옵션)을 기획했지만 관리 목적과 맞지 않아 제외했고, 그 자리를 자원 통합 검색과 미사용 자산 찾기로 채웠습니다.

운영 상태가 바뀌어도 그래프가 그대로였습니다

구성은 LLM을 쓰지 않으므로 요청 경로에서 실행해도 비용 문제가 없었습니다. 그래서 갱신 버튼 없이 자동으로 따라가게 만들었습니다. 1분 주기 스윕이 낡은 에이전트만 재구성하고, 온톨로지 탭을 열면 낡은 에이전트를 최대 30개까지 그 자리에서 갱신하며(잠금과 최소 간격 15초), 에이전트 상세를 열면 그 하나만 갱신합니다. 지워진 에이전트의 스코프는 스윕이 정리합니다.

처음의 낡음 판정은 세 가지였습니다. 아직 구성되지 않았거나, 캔버스가 마지막 구성 이후 저장됐거나, 멤버 자산 집합이 바뀐 경우입니다. 그런데 배포 승인이 나거나 실행이 쌓여도 그래프의 배포 단계와 실행 횟수가 바뀌지 않는다는 문제가 확인됐습니다. 원인은 판정이 캔버스 저장에만 반응한다는 데 있었습니다. 배포 승인·회수, 실행 실적, 공유 여부는 캔버스를 다시 저장하지 않아도 바뀌는 값입니다.

그래서 상태 서명을 추가했습니다. 배포 단계, 승인, 실행 횟수, 실행 사용자 수, 공유, 소유자를 묶은 해시를 구성 메타데이터에 싣고, 현재 값으로 다시 계산한 서명과 다르면 낡은 것으로 봅니다. 여기에 하루 슬롯을 넣어 "미수정 일수"처럼 날짜에서 파생되는 값도 하루 한 번은 다시 계산되게 했습니다. 슬롯 경계를 모든 에이전트가 공유하면 자정에 재구성이 몰리므로, 에이전트별 해시로 슬롯 시각을 흩었습니다.

검증은 외부망 DB를 읽기 전용으로 읽고 쓰기는 메모리 저장소로 대신하는 드라이런으로 했습니다. 낡음 전이 8가지가 모두 기대값대로 판정됐고, 탭을 열 때의 첫 갱신(낡은 에이전트 30개와 자산 목록)은 0.96초였습니다. 이후 스윕이 매번 모든 에이전트의 캔버스 JSON을 다시 읽던 것을 수정 시각 기준 캐시로 바꿔, 배포 환경에서 읽기 전용으로 잰 스윕 준비가 첫 회 0.14초, 두 번째 0.03초(다시 읽은 캔버스 0개)가 됐습니다.

고정 임계값 대신 그 환경의 분포로

인사이트는 구성된 그래프 위에서 규칙으로 계산합니다. LLM은 호출하지 않고 결과는 60초 캐시합니다. 첫 판은 Louvain 군집 기반 "묶으면 시너지"와 의존이 몰린 자산을 짚는 "지식 병목"을 포함한 8종이었고 임계값은 고정 숫자였습니다. 외부망 드라이런은 0.3초에 확인 필요 32, 공유 권장 4, 정리 대상 171을 냈지만, 부서·승인 데이터가 적은 개발망에서는 현황판 후보·부서 공백·군집 카드가 0건이었습니다.

검토에서 두 가지 지적을 받았습니다. 군집과 병목 카드는 관리자가 행동으로 옮기기 어려우니 빼고, 규칙 기준은 환경에 따라 움직여야 한다는 것이었습니다. 분포를 실제로 보니 고정값이 성립하지 않는 이유가 분명했습니다. 실행 이력이 있는 에이전트 219개의 실행 횟수 중앙값은 1회, P90은 7회였고 상위 15개가 11~287회에 몰려 있었습니다. 기준을 P75로 잡으면 "실행 5회 이상"이 되어 공유 권장 카드가 23장 나오고, P90으로 잡으면 "11회 이상"으로 10장이 나옵니다. 폐쇄망은 이 분포가 또 다릅니다.

최종 규칙 7종과 기준은 다음과 같습니다. 기준값은 응답에 함께 실어 카드 이유 끝에 "(기준: 실행 상위 10% = 11회 이상)"처럼 표시합니다.

종류 조건
확인 필요 승인 없이 배포, 개인정보 컬렉션인데 위험도 미평가, 끊어진 참조
현황판 후보 배포·승인 상태이고 실행 상위 10%(하한 5회), 실행 부서 2곳 이상(부서 정보가 없으면 실행 사용자 2명)
공유 권장 비공유, 실행 상위 10%(하한 5회), 실행 사용자 1명
부서 공백 사용자 수가 부서 크기 중앙값 이상(하한 2명)인 부서에 소유·실행 에이전트 0개
실행 변화 최근 2주가 직전 2주의 2.5배 이상 또는 1/2.5 이하(2주 합 상위 25%, 하한 10회). 직전 0회면 "새로 쓰이기 시작"
같은 업무 LLM 업무 주제가 같은 에이전트의 소유자가 2명 이상
정리 대상 정리 후보 에이전트와 미사용 자산

카드 표현도 바꿨습니다. 첫 판은 제목·이유·AI 설명 세 문단이라 무엇을 보라는 것인지 읽히지 않는다는 피드백을 받았습니다. 서버가 짧은 제목과 숫자 2~3개를 함께 보내고 프론트엔드는 숫자 카드로 렌더링하며, 이유는 펼쳐야 보이게 했습니다. 이후 카드 문장도 서버가 만든 한국어 문자열 대신 종류 코드와 파라미터로 내려보내 프론트엔드가 로케일 템플릿으로 구성하게 바꿨습니다. 외부망에서 규칙 7종은 확인 필요 1장(대상 32개), 공유 권장 10장, 실행 변화 3장, 정리 1장을 냈습니다.

자연어 질의가 엉뚱한 근거를 고른 세 경로

자연어 질의는 컬렉션 검색과 같은 omnifuse 엔진을 관리 그래프 위에 얹어 구현했습니다. 항목 하나를 이름·종류·요약 속성·1홉 관계 이름으로 된 근거 한 줄로 만들어 라이브러리의 인메모리 어휘 인덱스(한글 어간 BM25F)에 넣고, 이름 정확 일치·종류 별칭·1홉 관계·MMR로 근거를 고른 뒤 온톨로지 빌드와 같은 LLM 공급자로 답합니다. 검증은 질의마다 정답을 그래프에서 직접 계산해 모델 답과 대조하는 방식으로 했습니다. 이 과정에서 근거 선택이 실패하는 경로가 세 가지 나왔습니다.

첫째는 집계 질문이었습니다. "컬렉션을 가장 많이 쓰는 에이전트"처럼 특정 항목의 이름이 없는 질문은 시드가 0개였습니다. 종류 단어(에이전트·컬렉션·도구)를 해당 클래스의 전체 목록으로 바꾸고 질문에 맞는 집계값으로 정렬해 시드로 넣었더니, 이번에는 이름에 "가장"이나 "에이전트"가 들어간 항목이 근거를 차지했습니다. 라이브러리의 관계 정렬이 질문 단어와 겹치는 개수로만 순위를 매기기 때문이었습니다. 순수 집계 질문은 어휘 검색을 비우고 정렬된 상위 항목만 시드로 쓰게 했고, 관리자 계정이 소유한 113개가 관계 상한 80개에 잘리던 것도 상한을 풀었습니다.

둘째는 라이브러리 버전이었습니다. 고객 배포 라인이 omnifuse 0.6 이상으로 올라가면서, 검색 함수가 프롬프트 빌더 인자를 먼저 찾고 기존의 내부 프롬프트 메서드는 별칭으로만 남게 됐습니다. 서브클래스에서 그 메서드를 오버라이드하던 어댑터는 조용히 무시됐고, 호스트에 설치된 0.7.0으로 드라이런하자 근거 항목이 0개였습니다. 오버라이드를 버리고 공개 API(프롬프트 빌더 인자와 합성 생략 옵션)로 옮긴 뒤 관계 80개·항목 40개·프롬프트 7.7~13.6k자가 복구됐고, 같은 7문항에 다시 모두 맞게 답했습니다.

셋째는 계층이었습니다. "본부 산하 부서가 쓰는 에이전트는 몇 개인가"에 모델이 "확인되지 않습니다"라고 답했습니다. 원인은 두 겹이었습니다. 부서의 상위 부서는 사용자 속성으로만 있고 엣지가 없었고, 프롬프트에 싣는 관계 상한 80개에 하위 부서 에이전트 190개가 들어가지 않았습니다. 부서→상위 부서 partOf와 역할 supervises 관계를 추가하고, 부서·역할 시드에서 구조 관계를 3홉까지 BFS로 펼쳐 하위 부서·사용자·에이전트를 프롬프트의 계층 요약 블록에 따로 실었습니다(60개까지). 근거 재정렬에는 omnifuse의 PPR을 가중치 0.3으로 섞었습니다. 외부망 DB에는 부서 계층이 없어서 본부 하나와 하위 부서 둘, 사용자 4명을 얹은 드라이런으로 확인했고, 필터는 190/190, 두 질문 모두 정답이었습니다. 이 데이터에서는 PPR이 순서를 바꾸지 않아 안전망으로만 남겼습니다.

배포 뒤에는 답이 중간에 끊긴다는 보고가 있었습니다. 질의 경로가 출력 상한 700토큰으로 모델을 호출하고 있었는데, 같은 플랫폼의 채팅 경로는 상한을 두지 않았습니다. 질의 경로도 상한을 없애고, 대신 생성 시간이 길어질 수 있어 질의 전용 타임아웃(기본 180초)을 분리했습니다. 배경 작업만 짧은 상한을 유지합니다.

3D를 걷어내고 그래프 요약을 서버로

그래프 렌더링은 여러 번 바뀌었습니다. 사내 2D 엔진은 기본 군집 접기 때문에 첫 렌더링이 "기타 10" 말풍선으로 접혔고, 3D 엔진(블룸 효과)을 기본 뷰로 바꿨다가 관리 목적에는 3D가 필요 없다는 피드백으로 철회했습니다. 최종적으로 React Flow와 dagre로 사람·부서 → 에이전트 → 자산 → 거버넌스·업무 주제가 왼쪽에서 오른쪽으로 흐르는 계층 레이아웃을 쓰고, 그 위에 d3-force를 얹어 dagre 좌표를 닻으로 삼게 했습니다. 끌면 이웃이 따라오고 놓으면 제자리로 돌아오는 정도의 움직임입니다. 반발력을 키운 첫 시도는 층 구조가 뭉개져 원복했습니다.

실제 규모에서 문제가 드러났습니다. 실제 에이전트 309개의 API 응답을 Playwright로 로컬 빌드에 흘려 측정하니, 탭을 열고 렌더링까지 10.8초, long task 4.5초였고 그래프는 세로 한 줄로 길게 늘어졌습니다. 클라이언트에 요약 단계(노드가 많으면 자산을 종류별 허브로 접고, 그래도 많으면 에이전트를 소유자 아래로 접기)를 넣자 요약 그래프 58노드·221선, long task 0.23초가 됐습니다.

그런데 접는 기준이 120노드·60노드·30개 같은 프론트엔드 상수였고, "이렇게 하드코딩한 기준으로는 운영에서 쓸 수 없다"는 지적을 받았습니다. 접기 판단을 서버로 옮겼습니다. 서버가 노드 예산(기본 200)에 맞춰 전체 → 허브 접기 → 소유자 접기 순으로 단계를 정하고, 레이아웃 종류(물리 시뮬레이션은 60노드 이하, 그 밖은 격자)와 접기 전 종류별 개수를 함께 응답합니다. 프론트엔드는 서버가 정한 단계를 그대로 그리고, 상수는 옛 서버를 위한 대비책에만 남겼습니다. 배포 환경의 전체 675노드가 0.04초에 58노드·220선 요약으로 내려왔습니다.

결과와 남은 과제

지표 이전 이후 측정 조건
에이전트 그래프 구성 없음 에이전트 306개 → 노드 3,373 · 엣지 3,867 외부망 DB 읽기 전용 드라이런, LLM 0회, 시간 기록 없음
탭 진입 시 첫 갱신 해당 없음 0.96초 낡은 에이전트 30개와 자산 목록, 같은 드라이런
낡음 판정 캔버스 저장만 반응 전이 8종 모두 기대값 상태 서명 추가 후 드라이런
어휘 인덱스 구축 / 탐색 해당 없음 939항목 0.03초 / 0.03~0.08초 외부망 DB 드라이런
집계형 관리 질의 시드 0개 또는 오답 근거 7/7 정답, 응답 2~5초 Qwen 27B 계열, 정답은 그래프에서 직접 계산
업무 주제 추출 해당 없음 표본 40건 중 38건 유효(잡음은 빈 결과), 5호출 17초 Qwen 27B 계열
첫 렌더링 long task 4.5초 0.23초 실제 309개 응답을 Playwright로 재현, 클라이언트 요약 적용
서버 요약 675노드 전체 58노드 · 220선, 0.04초 배포 환경 읽기 전용

관리용 온톨로지는 새 지식을 뽑는 일보다 이미 있는 설정을 같은 어휘로 엮는 일에 가까웠습니다. 그렇게 엮어 두니 관리자 질문의 상당수는 LLM 없이 그래프 위의 규칙과 집계로 답할 수 있었고, LLM은 업무 주제와 문장 설명처럼 덧붙이는 역할만 맡았습니다.

남은 과제도 분명합니다. 외부망 데이터에는 부서 정보가 사용자 3명분뿐이라 현황판 후보와 부서 공백 규칙이 실제로 성립하는지는 폐쇄망 데이터에서 확인해야 합니다. 에이전트에서 자산 방향의 다홉 탐색은 프롬프트 폭증을 막으려고 아직 막아 두었습니다.

배포에서 얻은 교훈도 적어 둡니다. 이 작업이 기본값으로 켜 둔 자동 백필은 온톨로지가 없는 컬렉션을 일괄 빌드하면서, 그동안 실행되지 않던 기존 CSV→DB 동기화 코드의 결함(영숫자가 없는 컬렉션 이름에서 표 접두사가 빈 문자열이 되어 삭제 대상 패턴이 모든 표에 맞는 문제)을 실행 경로에 올렸습니다. 직접 원인은 기존 코드였지만 방아쇠는 기본값으로 켠 일괄 작업이었습니다. 그 뒤 패턴 삭제를 소유 표시가 정확히 일치하는 표만 지우는 방식으로 바꾸고, 전체 데이터를 훑는 자동 작업과 배경 LLM 작업은 기본값을 끔으로 바꿨으며, 공유 환경 배포 전에는 DB 덤프를 먼저 남기는 절차를 고정했습니다.