본문 바로가기

이 포트폴리오의 원본은 https://sec.iruyo.com (심재빈) 입니다 · 출처 식별자 jbx-7f3a2e9b

← 기술 블로그

AI·RAG

방치된 맥스튜디오로 사내 RAG 만들기

외부 전송이 금지된 사내 환경에서 3년간 방치됐던 맥스튜디오를 활용해 임베딩, 리랭킹, 생성 모델 스택을 온프레미스로 구성한 과정. 코드·문서 유출 없이 검색 정확도를 높이기 위해 골든셋으로 recall@5 87%까지 튜닝했고, 완성된 RAG 엔진을 사내 검색, MCP 모듈, MR 리뷰봇에서 공용 인프라로 통합한 전체 설계와 검증 절차를 다룬다.

제약이 먼저 설계를 정했다

대부분의 RAG 관련 글은 “어떤 임베더가 좋은가”라는 질문에서 출발한다. 하지만 여기서는 “세 모델을 한 장비 안에 어떻게 넣을 수 있을까”라는 문제부터 시작했다. 보안 제품을 만드는 회사였기 때문에, 설계보다 제약이 먼저 정해졌고 그 제약이 구조를 거의 결정했다.

제약은 두 가지 층위로 존재한다. 첫째, 사내 코드와 내부 문서는 외부로 내보낼 수 없다. 따라서 RAG를 접목하려는 순간 흔히 사용하는 선택지들이 막힌다 — 외부 임베딩 API도, 외부 생성 API도 사용할 수 없다.

질의 한 줄이라도 코드 조각이나 문서 본문이 함께 나가는 구조는 허용되지 않는다. 검색에서 생성까지 모든 처리가 사내 환경 안에서 끝나야 한다. 클라우드 GPU는 자연히 제외되고, 추론은 오직 사내 장비에서만 수행해야 한다.

둘째 제약은 장비의 메모리다. 임베딩, 재정렬, 생성의 세 모델을 동시에 상주시킬 용량이 부족하다. 외부로 한 바이트도 보낼 수 없고, 동시에 모든 모델을 올릴 수도 없는 상황이다.

추론을 올린 곳은 사내에서 사용하던 맥스튜디오(Apple Silicon Mac Studio) 다. 새 장비를 구매하려던 계획은 없었다. 사내 RAG가 필요해졌고, 마침 3년 넘게 구석에 있던 맥스튜디오가 거의 사용되지 않고 있었다. 그 장비를 처음으로 인프라로 채택한 셈이다.

도구의 뿌리는 원래 취미 삼아 만들던 로컬 RAG였다. 흩어진 사내 자료와 이슈 히스토리를 빠르게 찾고 싶어서 직접 만들었다. 그 개인 도구를 맥스튜디오 위에 올려 사내 본체로 확장한 것이 TextRAG다.

메모리 예산: 장비를 둘로 나누다

생성은 사내에 상주하는 로컬 LLM으로 처리하고, 임베딩과 재정렬도 모두 사내 모델로 돌렸다. 핵심 과제는 세 모델을 한 장비 안에서 동시에 운용하지 못하는 제약을 해소하는 일이었다.

결론은 장비를 둘로 분리하는 것이었다. 무거운 인덱싱(임베딩 일괄 생성)은 인덱싱 전용 장비에서 수행하고, 사용자 질의를 받는 서빙 장비는 검색과 생성을 전담하도록 했다.

인덱싱 장비 (오프라인 · 무거움) 임베딩 일괄 생성 코퍼스 색인

서빙 장비 (질의 처리 · 예산 빠듯) 검색 (임베딩 질의 + 재정렬) 생성 (로컬 LLM)

인덱스 전달

그럼에도 서빙 측 메모리가 여전히 한계에 가까워, 세 가지 조정으로 예산을 최소화했다.

  • 모델별 상주 토글: 모델의 메모리 상주 시간(keep_alive)을 각각 다르게 설정한다.
  • 재정렬 선택적 off: 재정렬 모델이 불필요한 시나리오에서는 해당 모델을 내린다.
  • 생성 KV cache 축소: 생성 모델의 KV cache 크기를 줄여 메모리 점유를 낮춘다.

이후 각 시나리오별로 모델이 차지하는 메모리 양을 표로 정리했다. 동시 상주가 필요 없는 조합을 골라 예산 내에서 운용했다.

색인 단위에서도 다시 한 번 구분을 두었다. 코드를 심볼 청크와 패시지 청크로 겹쳐 나눴다.

이는 동일한 코드베이스를 함수·심볼 단위 검색과 서술형 답변 양쪽에 동시에 활용하기 위한 구조다. 그 결과 수많은 제품 코드베이스와 사내 문서들을 하나의 코퍼스 체계로 색인해, 상시 검색 가능한 환경을 구성할 수 있었다.

골든셋으로 recall@5 87%까지

“잘 된다”는 감만으로는 튜닝을 할 수 없다. 대표 질의 30문항으로 직접 골든셋을 구성하고, 회귀 스크립트를 연결했다.

스크립트가 내는 지표는 세 가지다.

  • recall@k: 정답 문서가 상위 k개 안에 포함됐는가.
  • MRR: 정답 문서가 평균 몇 번째 위치에 떴는가.
  • 무관 출처 혼입률: 엉뚱한 출처가 섞여 들어온 비율.

청크 크기나 재정렬 여부를 바꿀 때마다 기준선 대비 회귀 결과를 바로 비교했다.

이 과정에서 반드시 남겨야 할 부분은 측정 방식 자체에 두 가지 허점이 있었다는 점이다. 평가 코드가 틀려 있으면 그 위에서 모델을 아무리 조정해도 방향이 어긋난다.

두 허점을 찾아 수정하고 나서야 수치를 신뢰할 수 있게 됐다. 최종 결과는 recall@5 87%, recall@10 90%, MRR 0.76이며, 코드 검색 정확도는 거의 100% 수준에 도달했다. 약한 구간은 덮지 않고 원인을 정리해 개선 항목으로 남겼다.

현재는 검색과 답변 요청마다 지연 시간, 결과 수, 점수 분포, 호출원을 append-only 형식으로 기록하는 계측 레이어가 추가돼 있다. 검색 파이프라인은 그대로 두고, 쿼리 원문은 해시 형태로만 저장했다. 로깅이 실패해도 서비스가 중단되지 않도록 분리한 결과다.

엔진 하나를 셋이 공유한다

여기까지 만들고 나니, 같은 검색 엔진을 함께 쓰고 싶어 하는 자리가 셋이었다.

  • 웹 사내 검색
  • 개발 도구 — Claude Code에 MCP로 붙는 검색 도구
  • GitLab MR 리뷰봇

각자 따로 색인을 관리하면 코퍼스가 세 갈래로 나뉘고, 품질도 달라진다. 그래서 단일 서버 엔진을 공용 인프라로 두고, 그 위에 소비자별 어댑터만 얇게 얹는 구조로 정했다.

웹 사내 검색 MCP 검색 도구 (Claude Code) MR 리뷰봇 단일 서버 엔진 색인·검색 품질을 한 곳에서 관리 접근 제어 — 구글 로그인 + 그룹↔코퍼스 매핑 그룹 하나에 N개 코퍼스 · 검색 범위와 권한을 같은 축에서 제어

색인과 검색 품질은 한 곳에서 관리되고, 소비자는 셋이다. 접근 제어는 구글 계정 로그인 위에 그룹↔코퍼스 매핑(그룹 하나에 N개 코퍼스)을 붙여, 검색 범위와 권한을 동일한 기준에서 제어한다.

하지만 장비를 분리하는 결정이나 평가 방식의 편차를 확인하는 일은 오롯이 내 역할이었다. 제안부터 설계, 구현, 서버 구축, 패키징, 그리고 iOS 팀 도입 교육까지 한 주기를 직접 수행했다.

외부로 한 바이트도 내보낼 수 없다는 제약이 출발점이었다. 결과적으로 그 제한 덕분에 엔진을 공유 자원 형태로 설계하게 됐다. 제약이 좁은 길을 만들었고, 그 길 위에서 재사용 가능한 구조가 나왔다.