사내 LLM 데이터 에이전트 플랫폼
바딧 · 2026.04 – 현재 · 1인 설계·구현·운영 · 운영 중
- Claude API
- PostgreSQL · pgvector
- ClickHouse
- MCP
- Docker
자연어로 사내 데이터를 조회하는 어시스턴트. MVP에서 tool-calling 에이전트까지 전환했고, 아키텍처 선택을 A/B 실측으로 결정했다.
- 4개 접점사용자 진입
- MCP 게이트웨이 (SSO)
- Slack 봇
- REST API
- 앱 API
- tool-calling 에이전트능력을 툴로 노출
단일라벨 라우터(table-jaccard 0.86 → 0.69)는 A/B 실측 후 폐기
- RAG 지식베이스스키마 730건 + 쿼리 로그
- 3-tier 라우팅폴백 포함
- MySQL
- PostgreSQL
- ClickHouse
- fail-closed 비식별화마스킹 없으면 전송 차단
행 단위 선마스킹 + 정규식 백스톱 2중 방어
- 외부 LLM
문제
데이터를 보려면 SQL을 쓸 줄 알아야 했다. 그래서 실제 질문은 대부분 데이터를 다루는 사람에게 몰렸고, 그 사람의 시간이 병목이 됐다. 질문 자체는 대체로 단순했다 — 어떤 지표가 지금 얼마인지, 지난주와 뭐가 다른지.
한 일
설계·구현·배포·운영을 혼자 맡았다.
-
3-tier 데이터 라우팅(MySQL / PostgreSQL / ClickHouse + 폴백) 위에 RAG 지식베이스를 얹었다. MVP 때 스키마 문서 730건(95테이블·635컬럼)으로 시작해 데이터 소스를 늘려 왔고, 실사용 쿼리 로그를 자동 승격하는 학습 루프를 붙였다.
-
아키텍처 전환을 A/B 실측으로 결정했다. LLM이 질의를 단일 라벨로 분류해 라우팅하는 방식은 table-jaccard가 0.86 → 0.69로 회귀해 폐기했다. 각 능력을 도구로 노출하고 LLM이 직접 고르게 하는 방식은 실행 성공률이 47/47로 기존과 같았고, 조합 질의 5/5를 통과했다 — 기존 구조로는 아예 불가능하던 것이다. 지시 대상이 모호한 질문에는 약 5/6을 되물었다(기존 0/6).
대신 단일 질의의 table-jaccard는 5~7pp 떨어졌다. 질문을 과하게 분해하는 탓이고, 프롬프트로는 막히지 않았다. 그래서 채택 근거를 “정확도”가 아니라 능력 확장으로 못박고 이 손해를 같이 기록했다. 채택안의 단점을 적어두지 않으면 나중에 그 손해가 원인 불명의 버그로 돌아온다.
-
fail-closed 비식별화를 설계했다. 마스킹이 적용되지 않은 상태에서는 외부 LLM으로 전송하는 것 자체를 차단한다. 행 단위 선마스킹과 정규식 백스톱의 2중 방어라, 컬럼 맥락만으로는 못 잡는 식별자까지 걸린다.
결과
채택안의 단점을 적어두지 않으면 그 손해가 나중에 원인 불명의 버그로 돌아온다.
구성원 누구나 쓸 수 있게 접점을 4개로 넓혔다 — 원격 MCP 게이트웨이(SSO로 각자의 AI 클라이언트에서), Slack 봇, REST API, 앱 API. 30분 주기로 이상을 감지해 원인 분석을 붙여 알리는 능동 이상탐지도 라이브로 돌렸다가, 알림 임계치와 중복 제거를 다시 보기 위해 지금은 멈춰 두었다.
운영하면서 가장 크게 배운 건 조용한 고장이다. API 크레딧이 바닥났을 때 health 체크는 키가 있는지만 봐서 200 ok를 내고 있었고, 4시간 41분 동안 아무도 몰랐다. 소진 신호도 문서와 달리 400으로 와서 준비해 둔 예비 키 전환이 걸리지 않았다. 검증되지 않은 전환 분기는 지우고, health를 실제 호출(TTL 300초)로 바꾸고, 5분 주기 장애 알림을 붙였다.
측정 조건
- 대상
- 자체 eval 세트 (사내 실사용 질의 기반)
- 표본
- 단일 질의 47문항 · 조합 질의 5문항 · 모호성 되물음 6문항
- 비교 기준
- 기존 결정론 라우팅 구조 · 조합 질의는 구조상 불가 · 되물음 0/6
- 검증
- A/B 실측. 회귀한 접근(단일라벨 라우터 table-jaccard 0.86 → 0.69)은 폐기. 채택안은 실행 성공률 47/47 동률, 단일 질의 table-jaccard −5~7pp 손해까지 함께 기록 (런간 변동 ±4pp)