← 문제 해결 기록

개인 구축 · 동료 7명 사용 · 2026.06 - 현재

사내 지식 검색 MCP 서버

사내 지식을 개발 에이전트가 근거와 함께 찾도록 만들었습니다.

개요

기간
2026.06 - 현재
참여 형태
개인 구축 · 동료 7명 사용
담당
구축·검색 평가·운영
목적
개발 에이전트가 사내 고유 용어를 일반 웹 개념으로 해석해 틀린 전제로 작업하는 문제를 줄이는 것
대상
사내 개발자가 쓰는 코딩 에이전트 (일부 동료의 개발 도구에 연결)
사용 기술
Python · FastAPI · MCP · LangChain · Firestore Vector Search · Cloud Run
사내 용어를 잘못 이해하는 개발 도구를 보며 자료 수집·검색·MCP 서버를 구축했고, 일부 동료의 개발 도구에 연결했습니다. AI 보조 구현을 포함하며, 서버의 책임과 검증 기준을 정한 판단을 중심으로 씁니다.

구현 기능과 기술 선택

  • 사내 Q&A·매뉴얼·DB 스키마 통합 검색과 출처 반환
  • 테이블·컬럼 스키마 조회와 문서 원문 조회
  • 작업 전 주의사항 점검(preflight)과 업무 규칙 온톨로지 조회
  • 수집·청킹·임베딩 파이프라인과 문서 해시 기반 증분 색인
  • 벡터·BM25·식별자 정확 일치 3개 경로를 가중 RRF로 융합한 검색, 서버의 답변 생성을 제거하고 근거 반환으로 역할 축소
  • Cloud Run 배포, 환경 변수 기반 인덱스 전환·롤백, GitHub Actions 랭킹 회귀 게이트
자체 챗봇 대신 MCP 서버
서버가 저가 모델로 답변까지 생성하면 호출 비용이 늘고 품질이 낮았습니다. 동료들이 이미 쓰는 코딩 에이전트에 답변을 맡기고, 서버는 근거만 반환하도록 했습니다.
Cloud Run
요청이 없을 때 인스턴스가 0으로 줄어, 하루 백여 건 규모의 서비스에 맞았습니다.
가중 RRF (벡터 1.0 / BM25 0.2 / 식별자 0.5)
테이블명·컬럼명은 의미 유사도보다 정확 일치가 중요합니다. 균등 가중에서는 청크가 많은 매뉴얼이 BM25 상위를 독점해, 벡터를 기본 신호로 두고 식별자 일치에 가산했습니다.
LLM 리랭커 제외
상위 20건을 LLM이 재정렬하자 21문항 기준 recall@5 0.770 → 0.516, MRR 0.854 → 0.600으로 하락해 기본 검색 경로에서 뺐습니다.

트러블슈팅

답변을 만드는 서버에서 근거를 반환하는 서버로

문제 상황

처음에는 E-GENE 화면에 붙는 챗봇으로 만들어, 서버가 검색 결과를 LLM에 넘겨 답변까지 생성했습니다. 요청마다 LLM을 불러야 해서 개인 비용으로는 저가 모델을 쓸 수밖에 없었고, 사용자가 늘수록 비용도 늘었습니다. 서버 답변의 정답 일치 점수는 검색과 프롬프트를 고쳐도 0.5 부근에 머물렀고, 근거를 무시하고 없는 컬럼명으로 SQL을 만드는 경우도 있었습니다.

해결 과정

더 좋은 모델로 바꾸면 품질은 오르지만 비용이 사용자 수에 비례해 늘었습니다. 그런데 팀원들은 이미 Claude Code·Codex 같은 상용 코딩 에이전트로 개발하고 있었고, 같은 근거를 저가 모델과 상용 에이전트에 주고 블라인드로 비교하자 상용 에이전트가 분명히 앞섰습니다. 그래서 답변은 각자의 에이전트에 맡기고, 서버는 검색과 근거 반환만 하는 MCP 서버로 역할을 좁혔습니다. 답변 품질을 서버가 직접 통제할 수 없게 된 대신 근거마다 출처 링크를 함께 돌려줘 원문을 확인할 수 있게 했습니다. 상위 20건을 LLM이 다시 정렬하는 리랭커도 시험했지만 recall@5가 0.770에서 0.516으로 떨어져 뺐습니다.

결과

서버의 LLM 호출 비용이 0원이 됐고, 답변은 동료들이 쓰던 에이전트의 작업 맥락 안에서 근거와 출처를 받아 만들어집니다.

본인 역할: 지식 검색 도구 구축과 운영·평가. 구현에는 AI 보조를 활용했습니다.

검증 범위: 서버의 도구와 설정. 환각 제거·답변 정확도 개선율은 측정하지 않았습니다.

호출 횟수보다 연결 수명을 추적하기

문제 상황

하루 백여 건, 건당 약 0.5초면 CPU 사용은 하루 1분 미만이어야 했는데, 청구 내역에는 주 약 319K CPU초가 찍혀 있었습니다. 요청이 없을 때 인스턴스가 0으로 줄어드는 Cloud Run을 고른 이유가 사라지는 숫자였습니다.

해결 과정

요청 수가 아니라 연결 수명을 의심하고 Cloud Run 지표와 요청 기록을 맞춰 보니, Claude Code 등 에이전트 클라이언트가 도구 목록 변경 같은 서버 알림을 받으려고 여는 GET 스트림(SSE)이, 실제 검색(POST)과 별개로 약 5분마다 재연결하며 하루 종일 열려 있었습니다. Cloud Run은 열린 스트림을 처리 중인 요청으로 보기 때문에, 검색이 없어도 인스턴스가 내려가지 못하고 과금이 이어졌습니다. 최소 인스턴스는 이미 0이었고, 요청 타임아웃을 줄여도 클라이언트가 끊기자마자 다시 연결해 열린 연결이 늘 남았습니다. MCP 사양을 보니 이 스트림은 선택 기능이었고 제 서버는 보낼 알림이 없어, 단독 GET 요청은 405로 거부하고 POST 도구 호출은 그대로 뒀습니다.

결과

2026년 7월 실측에서 유휴 스트림은 Cloud Run CPU 과금의 82%를 차지했고, 차단 후 연결당 점유가 약 301초에서 3ms로 줄었습니다. 이후 사용량은 실제 검색 처리 시간에 비례해, 사용자가 늘어난 지금도 서버 비용은 월 몇천 원에서 몇만 원 사이입니다.

테스트 통과가 실제 코드의 회귀를 잡는지 확인하기

문제 상황

주간 워크플로의 검색 회귀 검사는 저장소에 커밋된 검색 결과 파일을 다시 채점할 뿐 실제 검색을 돌리지 않아, 랭킹 코드가 바뀌어도 숫자가 그대로였습니다. 감시 대상도 운영 중인 엔진이 아니라 이전 엔진이었습니다.

해결 과정

매번 실제 검색을 돌리면 임베딩 API와 벡터 DB를 불러야 해 자격 증명과 네트워크, 비용이 들었습니다. 그래서 벡터 검색 후보는 파일로 고정하고, 그 뒤의 BM25·정확 일치·융합·감점 단계는 운영 엔진 코드를 그대로 실행하는 검사로 바꿨습니다. LLM 호출 없이 약 4초에 끝나 PR마다 돌릴 수 있는 대신, 임베딩 모델과 벡터 검색 자체의 회귀는 잡지 못하는 것을 감수했습니다. 엔진을 일부러 망가뜨려 검사를 시험하자 홍보글 감점을 지운 변경은 지표가 그대로라 통과했고, 그래서 지표와 함께 문항별 상위 10건 순위를 기준값과 대조하게 했습니다.

결과

BM25 정규화를 지운 변경(MRR@10 0.917 → 0.732)과 홍보글 감점을 지운 변경(순위가 바뀐 문항 31개)이 모두 실패하고, 되돌리면 통과합니다. 의도한 순위 변경은 기준값 파일의 문항별 diff를 확인한 뒤 반영합니다.

검증 범위: 고정 후보 기반 랭킹과 설정 연결. 전체 검색 E2E 검사가 아닙니다.

후속 과제: 요청 deadline·취소·의존성 격리와 운영 지연 원인 분리. 개선 완료 성과로 적지 않습니다.

하이브리드 검색: 벡터·BM25·식별자 일치의 가중 RRF

문제 상황

벡터 검색만으로는 테이블명·함수명처럼 정확히 일치해야 하는 질의를 놓쳤습니다. BM25를 더해 균등 가중 RRF로 합치자, 청크가 많은 매뉴얼 문서가 BM25 상위 30개를 독점해 순위가 흔들렸습니다(21문항 스윕 기준 hit@5 0.857, MRR 0.676).

해결 과정

세 경로의 순위를 경로별 가중치로 융합했습니다. 벡터 경로는 Upstage 임베딩(2048차원)과 Firestore find_nearest를 사용합니다. BM25(k1 1.5, b 0.75)는 코퍼스 전체의 df·avgdl 인덱스를 미리 계산해 배포 이미지에 포함했습니다. 질의에서 정규식으로 식별자를 추출해 역색인과 정확 일치시키는 경로를 추가하고, RRF(k=60)의 가중치를 벡터 1.0 / BM25 0.2 / 식별자 0.5로 두었습니다. BM25·식별자 단독 후보의 본문은 최종 top-k에 한해서만 배치 조회해 조회 비용을 줄였습니다.

결과

가중치 조정 후 같은 스윕에서 hit@5 0.905, MRR 0.721로 올랐고, 문항 단위 적중률은 기존 엔진과 같은 수준이었습니다. 질의 변형은 최대 3개를 한 번에 받아 통합·중복 제거하고, 문서 해시 비교로 바뀐 문서만 다시 임베딩합니다(최초 증분 반영 시 12,378 → 12,545 청크).

검색 평가: Recall·MRR 회귀셋과 RAGAS

문제 상황

검색 엔진을 새 구성으로 바꾸자 p95 응답 시간은 2,972ms에서 642ms로 줄었지만, 순위 지표 MRR@10은 0.851에서 0.721로 떨어졌습니다. 처음에는 기존 엔진 결과로 만든 정답지가 새 엔진에 불리하다고 설명했지만, 검증하지 않은 가설이었습니다.

해결 과정

속도만 보고 바꾸거나 지표만 보고 되돌리는 대신, 무엇을 재고 있는지부터 나눴습니다. 검색은 Recall·Hit·MRR로, 답변은 RAGAS로 따로 재고, 사람이 정답 문서를 라벨링한 골든셋(25문항)과 변경 감지용 회귀셋(149문항)의 역할을 나눴습니다. 정답지를 보완해 다시 채점하자 격차는 오히려 -0.130에서 -0.159로 벌어져(p=0.002) 가설이 틀렸음을 확인하고 기록을 정정했습니다. 순위 지표는 정답이 몇 번째에 오는지만 재기 때문에, 결과 묶음의 쓸모는 두 엔진의 결과를 가린 채 LLM 심판에게 비교시켰습니다. 심판 판정의 22.8%가 내용 대신 위치를 따라, 순서를 뒤집어 두 번 판정하고 같은 쪽을 고른 경우만 승리로 셌습니다.

결과

블라인드 비교(38문항 × 3회)에서 새 엔진이 매회 18승, 기존 엔진이 6~9승이었고, 상위 10건 내 정답 포함률(hit@10)은 두 엔진 모두 0.96이었습니다. 에이전트는 상위 여러 건을 함께 읽으므로 1위 정확도보다 결과 묶음의 질과 응답 속도가 중요하다고 판단해, 1위 순위가 낮아지는 것과 메모리를 2GB로 올리는 비용을 감수하고 새 엔진으로 전환했습니다.

기각 사례: 형태소 분석 토크나이저는 149문항 중 102문항의 결과를 바꿨지만 지표 차이가 없어(recall@1 2승 2패) 반영하지 않았습니다.

표본과 소스 구성: 149문항 회귀셋에서는 MRR@10 격차가 -0.040(p=0.049), 정답지 보정 후 -0.018(p=0.42)로 줄었습니다. 이 세트의 정답이 두 엔진이 동률인 Q&A 문서로만 구성됐기 때문이며, 열세는 공식 매뉴얼 회수(8문항, recall@10 0.906 대 0.344)에 집중돼 있었습니다.

한계: 정답지를 두 엔진의 결과를 보며 보완해 편향을 완전히 제거하지 못했습니다.

온톨로지와 작업 전 점검(preflight) 도구

문제 상황

작업 중 확인한 규칙과 함정을 문서로 쌓아도, 에이전트가 관련 코드를 수정하는 시점에 조회하지 않으면 같은 실수가 반복됐습니다.

해결 과정

규칙을 스키마가 있는 노드로 정의하고 작업 전에 조회하는 도구를 제공했습니다. Markdown 정본에 필수 필드(id, type, status, summary, verified_at)와 관계:대상 형식의 typed 관계를 두고 로드 시 검증합니다. SQLite FTS5 검색, 깊이 지정 이웃 조회, 작업 설명과 대상 파일을 받아 관련 규칙·함정을 반환하는 점검 도구를 구현했습니다. 주간 GitHub Actions에서 근거 검증을 통과한 초안만 verified로 승격해 Cloud Run에 배포하고, 배포된 온톨로지 버전을 확인합니다.

결과

검증된 규칙이 특정 작업의 점검에서 조회되지 않는 반례를 확인하고, 실제 사건을 질의로 만든 회수 검사를 추가하는 방향으로 정리했습니다. 원본과 임베딩 스냅샷이 어긋나면 힌트를 비활성화하고 배포 단계에서 실패하도록 했습니다.

규모: 관측 시점 기준 23개 노드.

결과와 성과

  • 검색 응답 p95 2,972ms → 642ms (4.6배 단축), 상위 10건 내 정답 포함률(hit@10 0.96)은 유지
  • Cloud Run CPU 과금의 82%를 차지하던 유휴 GET 스트림 차단 (연결당 약 301초 → 3ms)
  • 제시 순서를 통제한 블라인드 평가로 LLM 심판의 위치 편향(판정의 22.8%) 검출, 일부러 성능을 떨어뜨려 회귀 검사가 실패하는지 확인

회고

배운 점
지표가 높은 쪽을 고르기 전에, 그 지표가 실제 사용 방식을 측정하는지 먼저 확인해야 한다는 점을 배웠습니다. 정답지 편향이라는 제 가설이 틀렸다는 것도 재채점으로 확인했습니다.
아쉬운 점
골든셋이 25문항으로 작고 정답지 편향을 완전히 제거하지 못했습니다. 새 엔진은 공식 매뉴얼 회수에서 기존 엔진보다 낮습니다(8문항, recall@10 0.906 대 0.344).
보완 계획
공식 매뉴얼 회수 개선과, 요청 deadline·취소·의존성 격리를 통한 운영 지연의 원인 분리가 다음 과제입니다.

관련 글

만든 과정과 시행착오는 블로그 연재로 썼습니다.

  1. 에이전트가 E-GENE를 이해하기까지
  2. Blue 구현부터 Green 개선까지
  3. 측정 및 개선
  4. 환경 구성 및 운영