RAG기반 온보딩 챗봇 구축하기

---
개발

도메인 이해도를 높이기 위해 다양한 학습방법을 시도해보며 RAG와 MCP까지 구축한 경험을 공유합니다

들어가며

WMS팀에 합류하여 업무를 시작한 지 이제 한 달 정도 되었다.

지금은 입하에서 출고로 이어지는 물류 프로세스의 큰 흐름이 어느 정도 파악되었고, 팀원들과의 기술적 논의나 커뮤니케이션에도 큰 무리가 없다. 하지만 본격적으로 티켓을 처리하기 시작했던 2주차 때만 해도 상황은 많이 달랐다.

당시 나는 도메인 지식이 부족한 상태에서 방대한 레거시 코드를 파악함과 동시에 운영 업무를 해야 하는 과제를 안고 있었다.

그중 기억에 남는 건 “패킹 작업 페이지에서 placeholder가 잘리는 CSS 문제 수정” 티켓을 받았을 때다. 단순히 CSS를 수정하는 매우 간단한 업무였지만 나는 이 문제를 해결하는 데 며칠을 소요했다.

문제는 CSS가 아니라 해당 페이지에 진입하는 방법 자체를 몰랐다는 점이었다.

알 수 없는 비즈니스 로직에 의해 계속 메인 페이지로 리다이렉트 되어버렸고, 이 페이지까지 도달하기 위해서는 어떤 프로세스를 거쳐야하는지 알 기 힘들었다.

결국 당장의 티켓들을 쳐내기 위해 리다이렉션 코드를 강제로 주석 처리하고, 억지로 페이지를 렌더링 시켜 작업을 마무리했다. 그렇게 티켓은 처리하긴 했지만 개운하지 않았다.

당시에 남아있는 라우팅 주석에 대한 커밋 내역…

"도메인 맥락을 이해하지 못한 채, 편법으로 코드를 수정하는 것이 과연 옳은가?"

라는 질문을 계속하게 되었고, 결국 저 질문의 답을 찾기 위해 MCP(Model Context Protocol)까지 만들면서 겪었던 시행착오와 과정들을 기록해보려고 한다.


문제의 본질과 다양한 시도들

물류 프로세스는 생각한 것 보다 더 복잡했다. 사용자 가이드 문서는 충분히 제공되었지만, 어느정도 물류 흐름을 이해한 창고주나 셀러들이 보기에 딱 좋은 가이드였고, 당장 이 시스템을 개발해야하는 개발자들이 보기에는 이해하기가 쉽지 않았다.

즉 화면 중심의 설명만으로는 물류 흐름에 필요한 전체 맥락을 이해하기가 어려웠다.

"이 버튼을 누르면 이 기능이 동작한다"는 알겠는데, "왜 사용자는 이 시점에 이 액션을 해야 하는가?"에 대한 의문은은 해소되지 않았다는 뜻이다.

또 다른 형태의 내가 이해하기 쉬운 문서가 필요했고, 이를 AI를 활용해서 한번 생성해보자 생각했다.

시도 1: 스펙 문서 만들자

가장 먼저 시도한 것은 AI에게 코드를 던져주고 "내가 이해할 수 있게 스펙 문서를 작성해줘"라고 요청하는 것이었다. 하지만 이 방식은 한계가 명확했다.

  1. 데이터 구조와 상태 전이만 나열될 뿐, 비즈니스 맥락이나 정책같은것들이 없었다.
  2. 코드베이스의 의존성이 복잡해 AI가 전체 흐름을 읽지 못했다.

결과적으로 만들어진 문서는 "데이터가 어떻게 변한다"는 말해줬지만, "왜 변해야 하는가"는 설명해주지 못했다.

지금 보면 꽤나 잘 만들어진 데이터 흐름이긴한데 이때 당시에는 해당 흐름이 Why인 관점에서는 이해가 되지 않았다.

다른 접근이 필요했다.

시도 2: 반대로 기획문서를 뽑을 수 있을까?

여기서 문득 한 가지 아이디어가 떠올랐다.

일반적인 개발 프로세스는 [기획 → 디자인 → 개발]로 이어진다.

그렇다면 이 과정을 역으로 수행하면 [코드 → 기획 문서]를 복원할 수 있지 않을까? 하는 단순한 아이디어를 떠올렸다.

나는 단일 에이전트로는 벅찬 작업을 역할별로 쪼개어 서브 에이전트들을 만들었다.

  • 코드 분석가: 코드베이스에서 페이지 구조, 스키마, API 엔드포인트를 추출 (Fact 위주)
  • 도메인 분석가: 추출된 팩트에 비즈니스 맥락을 입힘 (Context 부여)
  • 기획자: 이를 종합하여 사람이 읽기 좋은 '기능 명세서' 작성

대충 이런식의 워크플로우로 구성했고, 중앙 오케스트레이터가 workflow 파일과 config 파일을 읽어온 뒤 각각의 서브에이전트들을 호출하도록 구성했다.

이렇게 만들고 나니 결과는 생각보다 놀라웠다.

완성된 문서는 이전보다 훨씬 체계적이었고 각 기능이 어떤 비즈니스 가치를 제공하는지, 사용자 유즈케이스, 사용자 정책 등 꽤나 그럴싸한 기획문서를 뽑아주었다.


검색을 효율적으로 해보자 - RAG 구축

문서가 쌓이자 새로운 문제가 생겼다.

파일이 수백 개가 넘어가니 내가 원하는 내용을 찾는 게 또 다른 일이 되었다. 단순히 Ctrl+F로 키워드를 찾는 것으로는 한계가 있었고, 파일을 찾았다 해도 문서 양이 방대하기 때문에 하나씩 읽는 비용이 훨씬 컸다.

그래서 이 문서들을 기반으로 내가 궁금한 것들을 질문하면 답변을 해주는 RAG(Retrieval-Augmented Generation)를 구축해보자는 결론에 도달했다.

시스템 아키텍처

설계 단계에서 정의한 요구사항(팀 스택 유지, 로컬 환경 친화성, 비용 효율성)을 기반으로, RAG는 NestJS, Qdrant, Ingestion Pipeline, MCP Server를 중심으로 구축했다.

전체 아키텍쳐는 다음과 같다.

  • Gateway Layer (NestJS): API Gateway 역할을 수행하며, REST API와 MCP 프로토콜 요청을 통합 처리한다.보통 Python이 주류지만 TypeScript 환경을 선택했다.
  • Vector Store (Qdrant): 청킹 및 임베딩된 문서를 저장하는 벡터 데이터베이스다. Docker 컨테이너 하나로 가볍게 실행되면서도 Rust 기반이라 검색 속도가 매우 빨라 간단하게 구현해보긴 제격이었다.
  • Ingestion Pipeline: 문서 수집부터 전처리, 임베딩, 적재까지의 과정을 담당한다. 비용 효율성을 위해 증분 업데이트(Incremental Update)와 결정적 ID 생성을 지원한다.
  • LLM Core: text-embedding-3-smallgpt-4o-mini를 사용하여 비용 대비 준수한 성능을 확보했다.

증분 업데이트

RAG 구축의 출발점은 "비용을 최소화하면서 어떻게 최신 상태를 유지할 것인가"였다.

매번 서버를 켤 때마다 전체 코드를 다시 임베딩하는 것은 API 비용과 시간 낭비다. 이를 해결하기 위해 파일 해시를 기반으로 변경된 데이터만 처리하는 파이프라인을 설계했다.

Data Sources — 기획서와 코드 결합

도메인 맥락을 완벽히 이해하기 위해 두 가지 형태의 데이터를 수집했다.

  1. 기획서 (Markdown): AI 에이전트를 통해 레거시 코드에서 역으로 추출한 비즈니스 로직 및 정책 문서.
  2. 코드 컨텍스트 (XML via Repomix): 단순히 파일만 읽으면 디렉토리 구조를 알 수 없다. 파일 구조와 트리 정보를 보존하기 위해 Repomix로 압축한 소스 코드 메타데이터를 활용했다.

Data Loader — 수집·해시 감지·적재

수집된 데이터를 검색에 적합한 형태로 변환하기 위해 다음과 같은 3단계 파이프라인을 구축했다.

  1. 변경 감지

DocumentLoader가 파일을 로드할 때 SHA-256 해시를 계산한다. 기존 Qdrant에 저장된 해시와 비교하여, 변경된 파일만 선별적으로 처리함으로써 API 비용을 크게 절감할 수 있었다.

// 변경 감지 로직 (IngestionService.ts)
const changedFiles = documents.filter((doc) => {
  const currentHash = doc.metadata.fileHash;
  const existingHash = existingFileHashes.get(source);

  // 1. 해시가 다르면 변경된 파일
  if (existingHash !== currentHash) return true;
  
  // 2. 해시는 같지만 청크 개수가 다르면 불완전 학습으로 간주하고 재처리
  if (existingChunkCount !== expectedChunkCount) return true;

  return false; // 변경 없음 (Skip)
});

2. 결정적 ID 생성

데이터 무결성을 위해 UUID v5를 도입했다. 파일 경로와 청크 인덱스를 시드로 사용하여, 언제 실행하더라도 동일한 청크는 동일한 ID를 갖도록 보장(Idempotency)했다.

`// 청크 ID 생성 로직
// namespace와 입력값이 같으면 언제나 동일한 UUID가 생성됨
const uniqueIdString = `${filePath}_${chunkIndex}`;
const pointId = uuidv5(uniqueIdString, UUID_NAMESPACE);

이 덕분에 재시도나 부분 실패 상황에서도 데이터 중복이 발생하지 않는다.

3. 적재 (Upsert)

변경되거나 삭제된 파일의 벡터를 정리하고, 새로운 벡터를 Qdrant에 적재한다.


검색 최적화

데이터 수집 후, RAG의 검색 품질을 높이기 위해 고민한 것은 "AI가 코드를 사람처럼 이해하게 만드는 것" 이었다. 단순한 텍스트 매칭을 넘어, 코드 속에 숨겨진 비즈니스 맥락, 의미를 복원하는걸 목표했다.

할루시네이션 제거

현재 코드에서는 비즈니스 용어가 아닌 다국어 키(W0001101)만 존재하여, AI가 문맥을 파악하지 못하고 할루시네이션을 일으키는 문제가 있었다.

예를 들어 “피킹 지시서 생성” 기능인데 “송장 번호 발급” 이런식으로 잘못된 정보를 주었다.

이를 해결하기 위해 소스 코드 전처리(Preprocessing) 단계를 추가했다. 임베딩 직전에 getLang("KEY") 패턴을 감지하고, 실제 언어 팩 파일(lang_ko.json)을 참조하여 주석을 강제로 주입했다.

// 전처리 로직: 다국어 키를 실제 텍스트로 치환하여 주석 주입
content = content.replace(/getLang\("([A-Z_]+)"\)/g, (match, key) => {
    const translation = langMap[key];
    // 변환 전: getLang("W0001101")
    // 변환 후: getLang("W0001101") /* 피킹 지시 */
    return translation ? `${match} /* ${translation} */` : match;
});

이 과정을 통해 AI는 코드의 구조(How)의미(What)를 동시에 챙길 수 있었다.

청킹 전략

청크가 너무 작으면 문맥이 끊기고, 너무 크면 검색 정확도가 떨어진다.

나는 800자(Character) 단위로 문서를 분할하되, 200자의 오버랩(Overlap)을 적용했다. 이는 문장이 중간에 잘리더라도 앞뒤 문맥을 보존하여 의미의 연결성을 유지하기 위함이다.

tiktoken과 같은 토크나이저 기반의 정교한 분할도 고려했으나, 한글이 섞인 문서 특성상 문자 수 기반 분할이 오히려 예측 가능하고 관리가 쉬웠다. 복잡한 도구 도입보다는 빠른 실험과 검증에 집중하기 위해 단순한 방식을 택했다.

테스트

실제로 Postman으로 서버에 요청을 보내보면 아래와 같이 답변을 생성해준다.


MCP 인터페이스 설계

이제 RAG를 잘 사용하는 방법에 대해서 고민하다가 UI로 채팅 인터페이스를 설계 하는 것 보다는 IDE나 내가 자주쓰는 LLM 서비스에 연동해서 쓰면 더 좋을 것 같다는 생각을 했다.

이를 위해 MCP(Model Context Protocol)를 구축하여 RAG 서버를 단순 API가 아닌 개발 도구로 확장했다.

MCP Server 구성

별도의 서버를 띄우는 대신, 기존 NestJS 애플리케이션의 RagService 로직을 그대로 재사용하여 MCP 프로토콜(Stdio)로 래핑했다.

  • REST API: 웹 인터페이스나 Postman 테스트용.
  • MCP Server: Cursor IDE 연동용. (@cgs-rag 호출)

도구 호출 흐름

이제 Cursor나 Cluade에서 자연스럽게 질문을 던지기만 하면 된다.

실제로 RAG를 단순한 검색기를 넘어서 MCP를 연동하면 아래처럼 Cursor에서도 사용하면서 온보딩 챗봇처럼 활용할 수 있었다.


앞으로 해볼만한 것들

지금 단계의 RAG는 “동작하는 온보딩 봇” 수준까지는 왔다. 하지만 실제로 팀에 붙여서 오래 굴리려면, 결국 품질은 검색 정밀도, 근거 신뢰도, 운영 안정성에서 갈린다. 아래는 다음 실험으로 해볼만한 것들이다.

청킹 전략 고도화

현재는 800자/200자 overlap으로 충분히 빠르게 검증했다. 다음 단계는 “문서 유형별” 청킹이다.

예를 들어 PRD/정책서는 문단 단위가 의미 경계고, 코드는 함수/컴포넌트 단위가 의미 경계다. 그래서 동일한 규칙으로 자르면 어떤 타입에서는 문맥이 끊긴다.

추천 방향은 구조 기반 청킹(Markdown heading, 코드 AST, 함수/클래스 경계)을 적용해 검색 단위를 의미 단위로 맞추는 것이다. 당장 시도해볼만하다.

Rerank 적용 (정답은 Top-K가 아니라 Top-1에서 결정된다)

벡터 Top-K는 “후보군”을 가져오는 데 강하지만, “정확히 맞는 문단”을 1등으로 올리는 건 약한 경우가 많다.

그래서 보통 1차 Retrieval(Top 20~50) → 2차 Rerank(Top 3~5)로 가면 체감 품질이 확 좋아진다고한다.

특히 WMS는 비슷한 맥락이나 단어가 많아서(출고/피킹/패킹) rerank가 효과가 클거같다.

멀티모달 RAG

WMS에서 가장 정보가 많은 건 특히 “화면”이다. 운영 가이드, 플로우 차트, 상태 다이어그램, 실제 UI 캡처가 핵심 맥락이다.

다음은 이미지(스크린샷/다이어그램)를 인덱싱하고, 질문 시 관련 이미지를 함께 가져와 설명하게 만드는 멀티모달 RAG다.

특히 “이 버튼이 왜 여기 있지?” 같은 질문은 텍스트보다 스크린샷 기반이 더 빠르게 맥락을 잡아줄거같다.

그리고 이걸 기반으로 화면기획서나 정책문서 같은것도 자동화로 뽑아볼 수 있을 것 같다.

메타데이터 필터링/스코핑

문서가 많아질수록 “찾긴 찾는데 엉뚱한 도메인 문서”를 가져오는 일이 늘어난다.

인덱싱 시점에 메타데이터를 더 빡세게 붙여두면(예: domain=outbound, feature=packing, role=worker/admin, source=policy/code) 검색 시에 스코핑이 가능해진다.

이건 품질뿐 아니라 보안/권한 분리에도 바로 연결할 수 있다.

피드백 루프 (사용 중에 자동으로 개선되는 구조)

온보딩 봇을 실제로 쓰면 “이 답은 맞았는지/도움이 됐는지/틀렸는지” 데이터가 쌓인다.

이 피드백을

  • 잘못된 답변 → 어떤 청크가 문제였는지 기록
  • 유용한 답변 → 그 조합을 강화(캐싱/프롬프트 패턴)
  • 자주 나오는 질문 → 문서 보강(역기획 문서 업데이트)
  • 이런 식으로 연결하면, RAG는 시간이 갈수록 운영 지식이 축적되는 시스템을 만들 수 있을 것 같다.

그래프 기반 RAG

WMS 도메인은 엔티티 관계가 핵심이다. (주문 ↔ 출고 ↔ 작업 ↔ 상태 ↔ 사용자 역할)

문서를 그냥 청크로만 보면 “관계”가 희미해진다. 그래서 도메인 키 엔티티를 추출해서

  • 화면(페이지) ↔ API ↔ 상태값 ↔ 이벤트 ↔ 정책 문서
  • 를 그래프로 연결하고, 질문 시 그래프를 따라가며 근거를 모으는 Graph RAG를 붙이면 “왜 이 단계가 필요한가?” 같은 질문에 특히 강해질거같은데 난이도가 꽤 어려울 것 같다


마치며

이 도구를 팀에 좀더 도움될 수 있는 도구로 발전시켜보고 싶다. Playwright MCP나 Figma MCP를 연동해서 화면 기획서 같은것들을 뽑아볼 수 도 있고 혹은 Slack이나 컨플루언서를 연동해서 온콜이 들어오면 바로 원인을 찾아 보고서를 생성할 수 있는 봇 같은걸로도 발전시켜볼 수 있을 것 같다.

그리고 한가지 배운것은 역시 도구는 도구일 뿐이다. 중요한 것은 익숙하지 않은 도메인을 좀더 효율적으로 학습할 수 있는 태도와 시도들을 해볼 수 있어서 좋았다.

편법으로 티켓을 처리하던 순간에서 시작된 이 작은 실험이 앞으로 우리 팀의 업무를 개선하는 의미 있는 자산이 되기를 바란다.

목차