grep

Engineering

사내 지식으로 Agentic RAG 만들기 (1/3) : RAG 파이프라인의 바닥 다지기

Pete여기어때

2026년 8월 20일

원문에서 보기 ↗

글. 문혜승(Pete) / 공통플랫폼개발팀

안녕하세요, 여기어때컴퍼니 공통플랫폼개발팀 피트입니다.

공통플랫폼개발팀은 사내 여러 조직이 함께 쓰는 시스템을 만듭니다. 그러다 보니 하루에도 몇 번씩 이런 Slack DM이 옵니다.

하나하나는 1분이면 답할 수 있는 질문입니다. 문제는 같은 질문이 계속 돌아온다는 것이었습니다. 답을 아는 사람은 정해져 있고, 그 사람이 자리를 비우면 응답은 그대로 밀립니다. 물어보는 쪽도 매번 누군가의 시간을 빌려야 합니다.

관련된 문서가 없어서 생기는 일이 아니었습니다. 문서는 정리돼 있지만 그 문서로 가는 길이 담당자의 머릿속에만 있었습니다. 조직 어디를 봐도 같은 모양이었고, 기록을 성실히 남기는 팀일수록 오히려 필요한 문서를 찾기가 어려웠습니다. 지식이 문서가 아니라 사람에게 종속돼 있었습니다.

Overview : 무엇을 왜 만들었나

챗봇이 아니라 MCP

이러한 문제를 해결하기 위해 처음 제안된 건 단순한 RAG 챗봇이었습니다. 그런데 챗봇으로 만들면 사내 지식에 닿는 길이 슬랙 하나뿐입니다. 이미 사내에서 활발하게 사용되고 있는 에이전트들은 이 지식에 접근할 방법이 없습니다.

검색 자체를 MCP로 떼어내면 이야기가 달라집니다. 이미 사내에서 쓰이고 있는 에이전트들을 그대로 가져다 쓸 수 있고, 앞으로 새로운 에이전트가 만들어지더라도 대응할 수 있습니다. 그래서 챗봇 대신 Agentic RAG MCP를 만드는 방향으로 정했습니다. 사용자에게 보이는 얼굴은 슬랙봇이든 사내 에이전트든 상관없고, 저희가 책임지는 건 “사내 지식에서 정확한 근거를 찾아 돌려주는 일” 하나입니다.

MCP(Model Context Protocol) 는 에이전트가 외부 도구를 가져다 쓸 때 지키는 공통 규격입니다. USB 포트처럼, 규격만 맞으면 어떤 에이전트든 “사내 문서 검색”이라는 도구를 꽂아 쓸 수 있습니다.

사실, 사내 지식을 검색하는 MCP 자체는 이미 있었습니다. Atlassian이 제공하는 Confluence/Jira MCP입니다. 다만 이 도구가 하는 일은 검색보다 조회에 가까웠습니다. 에이전트가 키워드를 골라 질의문을 만들면, 그 키워드가 본문에 그대로 들어 있는 페이지와 이슈를 돌려줍니다. 다만 두 가지 문제가 있었습니다.

첫째 , 맞는 키워드를 모르면 계속 헤맵니다. 키워드 검색은 에이전트가 고른 단어가 문서에 그대로 적혀 있어야 걸립니다. 에이전트는 적당한 문서를 찾기 위해 여러 번 단어를 바꿔 검색하고, 걸린 문서를 열어보고, 아니다 싶으면 다시 검색하는 과정을 반복합니다. 실제 trace를 보면 검색과 열람을 6~7번씩 왕복하다 답을 내는 경우가 흔했습니다. 왕복이 쌓이는 만큼 응답은 느려지고 토큰은 나갑니다.

둘째 , 이미지는 아예 보이지 않습니다. 사내 문서는 아키텍처 구성도나 작업 화면 스크린샷, 간트 차트에 핵심을 담는 경우가 많습니다. 그런데 기존 MCP가 돌려주는 본문에서 이미지는 파일명과 링크로만 남습니다. 프로젝트 일정이 간트 차트 한 장에 정리된 WBS 페이지에 “5월에 어떤 작업이 계획돼 있나요”라고 물었더니, 모델은 그림 옆의 텍스트만 읽고 그럴듯한 일정을 지어냈습니다. 문서는 제대로 찾아왔는데 틀리는, 가장 알아채기 어려운 오답입니다.

이 두 가지를 해결하는 것이 이번 프로젝트의 목표였습니다. 단어매칭이 아닌 의미로 문서를 더 정확하게 찾고 , 이미지에 담긴 내용까지 답할 수 있는 MCP를 직접 만들기로 했습니다.

전체 구조

프로젝트의 추상화된 전체 아키텍처

프로젝트의 전체 구조는 크게 세 덩어리로 이뤄져있습니다.

  1. 데이터 파이프라인 : Confluence와 Jira에서 문서를 증분으로 수집하고, RAG가 쓸 수 있는 형태로 가공해 적재합니다.
  2. 문서 리트리버 : 의미(semantic)로 찾는 LightRAG와, 문서가 놓인 자리를 따라 걷는 Navigator입니다. 더 자세한 이야기는 2편에서 다룹니다.
  3. MCP 인터페이스 : 두 리트리버를 하나의 도구 묶음으로 서빙합니다. 에이전트는 질문의 성격에 따라 필요한 툴을 골라 씁니다.

두 개의 리트리버

RAG 파이프라인을 한 번이라도 구축해봤다면, 아키텍처 그림에서 리트리버(질문과 관련된 문서들을 가져오는 도구) 역할을 수행하는 도구가 두 개나 존재하는 것이 의아하게 느껴질 수 있습니다. 전통적인 RAG 구조에서는 리트리버가 하나입니다. 문서를 잘게 잘라 벡터로 만들어 두고, 질문이 들어오면 가장 가까운 청크 몇 개를 꺼내 모델에게 넘기면 끝입니다.

저희도 처음엔 그렇게 만들었습니다. 그런데 사내 질문은 한 문서만 읽어서는 답이 안 되는 것이 대부분이었습니다. 예를 들어 “공통 게이트웨이에 필터를 하나 추가하려면 어떻게 하나요”라는 질문의 답은 게이트웨이 가이드 문서에도, 업무 요청 프로세스 문서에도, 비슷한 작업을 했던 과거 Jira 이슈에도 조금씩 나뉘어 있습니다. 비슷한 청크를 다섯 개 꺼내 나열하면 조각은 다 모이지만, 그 조각들이 서로 어떤 관계인지는 아무도 알려주지 않습니다. 모델은 문맥 없이 나열된 다섯 덩어리를 받아 그럴듯하게 이어 붙이고, 그 과정에서 틀립니다.

LightRAG 도입

필요한 건 “비슷한 문서”가 아니라 “이 개념이 어디에서 어떻게 이어지는지”였습니다. 이런 상황에 걸맞은 도구가 GraphRAG였고, 그 계열 중 하나인 LightRAG를 리트리버로 쓰게 됐습니다. 왜 LightRAG였는지는 2편에서 자세히 풀겠습니다.

GraphRAG란 문서에서 개념(Entity)과 그 사이의 관계(Relation)를 미리 뽑아 지식 그래프로 만들어 두고, 검색할 때 벡터 유사도만 보는 게 아니라 그래프를 따라 관련된 개념까지 함께 끌어오는 방식입니다.

Navigator 도입

LightRAG를 얹고 나니 검색은 확실히 좋아졌습니다. 다만 여전히 틀리는 것들이 남았고, 거기엔 공통점이 있었습니다. 제목도 내용도 닮은 문서가 여러 폴더에 흩어져 있을 때, 문서가 어디에 놓여 있는지가 답을 가르는 질문들이었습니다. 의미 기반 검색은 결국 비슷해 보이는 것을 고르는 일이라, 임베딩을 바꾸거나 후보를 늘려도 어느 폴더 아래 문서인지까지는 가려내지 못합니다.

반면 Confluence의 폴더 트리도, Jira의 Project·Epic·Story·Subtask 계층도 사람이 손으로 정리해 둔 구조입니다. 추측할 게 아니라 읽으면 되는 값입니다. 그래서 원본 계층 구조를 그대로 그래프 DB에 옮겨 담고 결정론적으로 탐색하는 Navigator를 따로 만들었습니다. 어떤 문제를 어떻게 풀었는지는 2편에서 다루겠습니다.

두 개를 함께 두기

정리하면 역할이 다릅니다. LightRAG가 “어디를 봐야 하는가 ”를 의미로 찾아내면, Navigator가 “그 주변에 무엇이 더 있는가”를 구조로 채웁니다. 하나로 합치지 않고 두 도구로 나눠 둔 이유이기도 합니다. 에이전트가 질문의 성격에 따라 골라 쓰고, 필요하면 둘을 번갈아 호출하면서 근거를 좁혀 갑니다.

결과 미리보기

본격적인 이야기에 들어가기 전에 결과부터 잠깐 보여드리겠습니다. 평가 방법이나 측정 방식(LLM-as-a-judge)에 대한 디테일은 3편에서 자세히 다루는 걸로 하고, 우선은 가볍게 지표만 공유하고자 합니다.

앞서 이야기한 두 리트리버를 붙인 결과입니다. 기존 도구보다 정확한 답을 내면서, 토큰까지 함께 아낄 수 있었습니다. 필요한 문서를 곧바로 찾으니 헤매는 왕복이 줄었고, 모델에게 넘기는 것도 문서 전체가 아니라 근거가 될 부분만으로 충분했기 때문입니다.

여기까지가 프로젝트의 전체 그림입니다. 이제부터는 그 조각들을 하나씩 열어보려 합니다. 이 글에서 다룰 것은 그중 첫 번째, 데이터 파이프라인입니다.

데이터 파이프라인

여기어때에서는 가이드와 작업 문서를 Confluence에 남기고, 업무 요청과 진행 상황은 Jira로 추적합니다. 그래서 데이터파이프라인의 수집 대상은 Confluence 페이지와 Jira 이슈로 선정했습니다. 둘 다 Atlassian에서 공식 API를 제공하므로, 이를 이용해 배치 단위로 문서를 적재하는 파이프라인을 구성했습니다.

설계 단계에서 한 가지를 미리 정해두었습니다. 데이터를 가져다 쓰는 곳이 하나가 아니라는 것입니다. 수집과 소비를 처음부터 분리하고, 소비처가 추가돼도 수집 단계를 건드리지 않도록 파이프라인을 설계했습니다. 이 구조가 왜 필요했는지는 이후 편에서 다시 이야기하겠습니다.

데이터 파이프라인 전체 구성

파이프라인은 크게 네 단계로 동작합니다.

  1. 수집 : Confluence와 Jira에서 증분 데이터를 요청한 뒤 raw JSON 형태로 DB에 적재합니다.
  2. 변환 : JSON 원문을 평탄화하고 본문의 내용을 RAG에 걸맞은 형태로 변형합니다.
  3. 대체 텍스트 생성 : 문서 내부에 있는 이미지의 대체 텍스트를 생성하고 삽입합니다.
  4. 동기화 : RAG에 사용할 최종 DB로 적재를 수행합니다.

네 가지 단계 모두를 자세히 다루기에는 이야기가 길어지니, 만들면서 특히 고민이 있었던 세 가지만 풀어보려 합니다.

어떻게 매번 전부 다시 적재하지 않을 것인가

문서를 어떤 형태로 바꿔야 잘 검색될 것인가

텍스트가 아닌 이미지는 어떻게 다룰 것인가

어떻게 매번 전부 다시 적재하지 않을 것인가

사내에는 2010년에 작성된 문서도 남아 있고, 매일 수백 개의 문서가 새로 만들어집니다. 이걸 배치마다 전부 다시 적재하면 시간도 비용도 감당이 되지 않습니다. 특히 뒤에 이어지는 변환과 대체 텍스트 생성은 LLM 호출을 동반하기 때문에, 어제와 한 글자도 달라지지 않은 문서에 매일 같은 돈을 다시 낼 이유가 없었습니다.

결국 무엇이 바뀌었는지를 알아야 했습니다. 가장 확실한 건 원본 DB를 직접 보는 것이지만, Confluence와 Jira의 DB에 접근할 수는 없습니다. 다만 Atlassian이 데이터 조회 API를 제공하고, 여기에 CQL과 JQL이라는 질의 문법이 있습니다. 마치 DB에 쿼리를 던지듯 조건을 걸어 문서를 요청할 수 있습니다.

이를 이용해 마지막 배치 시각 이후 생성되거나 수정된 문서만 가져오도록 했습니다.

-- CQL: 특정 시각 이후 생성·수정된 페이지
type = page AND space = "ABC" AND lastModified >= "2026/08/01 03:00"
ORDER BY lastModified ASC
-- JQL: 특정 시각 이후 생성·수정된 이슈
project = "ABC" AND updated >= "2026/08/01 03:00"
ORDER BY updated ASC

전체 문서 17만건 중 하루 배치에서 실제로 처리되는 건 평균 1,300건입니다. LLM 호출(임베딩, 대체 텍스트 생성)이 그만큼 줄어들면서, 전량 재적재 대비 비용은 100분의 1 수준이 됐습니다.

문서를 어떤 형태로 바꿔야 잘 검색될 것인가

수집한 JSON을 그대로 청킹해 임베딩할 수도 있었습니다. 다만 raw JSON은 본문보다 구조를 표현하는 필드가 훨씬 많습니다. 임베딩할 때도, 검색 결과를 LLM 컨텍스트에 넣을 때도 그 비용을 계속 지불하게 됩니다. revision 번호처럼 에이전트에게 필요 없는 값이 섞이면 벡터가 문서의 의미와 무관한 방향으로 흔들리기도 합니다.

그렇다고 본문만 평문으로 남길 수도 없었습니다. Heading 구조나 코드 블록은 그 자체가 정보입니다. 청킹 단위를 나눌 때도, LLM에 문맥을 전달할 때도 쓸 수 있는 신호인데 평문으로 눌러버리면 이걸 잃습니다.

Markdown 정규화 Before After

결국 필요한 건 구조는 남기면서 부피는 줄이는 형식이었습니다. Markdown이 그 조건에 맞았습니다. Heading과 코드 블록을 그대로 살리면서 JSON보다 훨씬 가볍고, LLM이 가장 익숙하게 다루는 형식이기도 합니다.

텍스트가 아닌 이미지는 어떻게 다룰 것인가

사내 문서에는 이미지가 많습니다. 팀마다 문서를 쓰는 스타일은 제각각이지만, 대부분 이미지를 적극적으로 활용하는 편입니다. 아키텍처 구성도, 작업 화면 스크린샷, 플로우차트 같은 것들입니다. 글로 설명하기 어려운 것일수록 이미지에 담겨 있습니다.

기본적으로 RAG는 텍스트 단위의 도구입니다. 이미지는 색인되지 않으니 검색에서 빠지고, 핵심 정보가 이미지에만 있는 문서는 사실상 빈 문서로 취급됩니다. 이미지까지 검색 대상에 넣는, 멀티모달 RAG가 필요했습니다.

방법은 두 가지를 놓고 비교했습니다.

결론부터 말하면 대체 텍스트를 택했습니다. 이유는 둘입니다.

첫째, 어차피 이미지를 컨텍스트에 실어야 합니다. 멀티모달 임베딩으로 이미지를 찾아냈다고 해서 LLM이 그 이미지를 본 건 아닙니다. 결국 원본을 컨텍스트에 넣어야 하는데, 문서 한 편에 이미지가 수십 장씩 들어 있는 경우도 있습니다. 토큰 관점에서는 미리 텍스트로 바꿔두는 쪽이 유리했습니다.

둘째, 사내 지식은 이미지와 텍스트가 섞인 형태입니다. 이미지가 문서의 어느 위치에서 어떤 역할을 하는지가 중요한데, 멀티모달 임베딩은 이미지를 독립된 대상으로 다루면서 그 맥락을 놓쳤습니다. 내부 테스트에서 대체 텍스트 방식보다 검색 품질이 눈에 띄게 낮았습니다.

대체 텍스트 생성 파이프라인

대체 텍스트 생성 파이프라인

실제로는 이렇게 동작합니다.

  1. Markdown 파싱 : 앞 단계에서 변환된 Markdown 문서를 파싱해 이미지와 텍스트를 분리합니다. 이미지는 자리 표시만 남기고 따로 빼둡니다.
  2. 대체 텍스트 생성 : 각 이미지를 gemini-3.1-flash-lite 모델에게 넘겨 설명을 생성합니다.
  3. RAG Document 조립 : 원래 텍스트와 생성된 대체 텍스트를 합쳐 하나의 문서로 만듭니다. 이때 대체 텍스트는 이미지가 원래 있던 자리에 그대로 삽입됩니다. 앞에서 말한 “이미지의 위치와 역할”이 보존됩니다.

다만 이렇게 만들고 나니 문제가 하나 있었습니다. 생성된 대체 텍스트가 기대만큼 검색에 도움이 되지 않았습니다.

문서에 들어있던 삽화 예시 (gemini 생성)

예를 들어 사내에 멀티모달 RAG의 어려움을 정리한 문서가 하나 있었습니다. 서로 다른 데이터 형식 사이의 의미적 격차를 설명하면서, 근거로 인파가 빽빽하게 그려진 삽화 한 장을 실어두었습니다. 이미지 한 장의 정보량이 텍스트 수십 뭉치와 맞먹는다는 걸 보여주려는 것이었습니다.

이 삽화를 모델에 넘겨 받은 대체 텍스트는 이랬습니다.

해변에 많은 사람들이 모여 물놀이, 일광욕, 휴식을 즐기는 모습. 요트, 보트, 증기선이 바다에 떠 있고, 
해변에는 파라솔, 비치 체어, 수영복을 입은 사람들이 보인다.

그림 자체에 대한 설명으로는 틀린 게 없습니다. 문제는 이 이미지가 해변을 이야기하려고 들어간 게 아니라는 점입니다. 원래 문서에서 이 그림은 의미적 격차를 설명하기 위한 예시였습니다.

그런데 대체 텍스트에는 그 역할이 한 글자도 남지 않았습니다. 누군가 “멀티모달 검색이 어려운 이유”를 물었을 때 이 문서가 검색될 수 있게 해주는 정보는 전부 사라지고, 대신 검색될 일이 없는 해변 이야기만 남았습니다.

이런 문제를 해결하기 위해서 이미지와 함께 주변 맥락을 넣어줬습니다. 문서 제목, 이미지 앞뒤의 본문입니다. 무엇을 설명하려는 그림인지 알려주면, 모델은 같은 이미지에서 다른 것을 봅니다.

이 이미지는 "월리를 찾아라" 스타일의 그림으로, 해변에 많은 사람들이 모여 있는 복잡한 장면을 보여줍니다. 
텍스트 검색에서 이러한 이미지의 복잡성과 정보량은 "의미적 격차"의 어려움을 잘 보여줍니다. 
픽셀 데이터인 이미지는 수많은 텍스트 뭉치에 해당하는 정보를 담고 있어, 
텍스트 기반 검색 시스템이 이미지의 의미를 파악하고 관련 정보를 추출하는 데 어려움이 있습니다.
**핵심 키워드:** 이미지 검색, 멀티모달 검색, 의미적 격차, 복잡한 이미지, 정보량, 텍스트 매칭, 픽셀 데이터.

같은 이미지인데 설명이 완전히 달라졌습니다. 그림이 무엇을 그리고 있는지가 아니라, 문서에서 무슨 역할을 하고 있는지가 텍스트에 남았습니다.

다만 대체 텍스트가 이미지의 모든 정보를 담지는 못합니다. 그래서 변환할 때 식별키를 함께 남겼습니다.

<img id="..." alt-text="..." />

대체 텍스트만으로 부족하다고 판단되면 Agent가 이 id로 원본 이미지를 직접 불러옵니다. 평소에는 텍스트만 읽고, 필요할 때만 이미지를 가져오는 구조입니다.

마치며

여기까지가 검색을 만들기 전에 다져둔 바닥입니다.

Confluence와 Jira에 흩어져 있던 문서를 증분으로 모았고, 그대로는 검색에 쓸 수 없는 JSON을 Markdown으로 바꿨습니다. 텍스트만 보던 파이프라인에 이미지를 태우면서 대체 텍스트도 붙였습니다.

글 앞에서 기존 MCP의 문제로 두 가지를 꼽았습니다. 그중 이미지 문제는 여기까지로 답이 됐습니다. 남은 하나, 맞는 키워드를 몰라 헤매는 문제는 문서를 잘 쌓아두는 것만으로는 풀리지 않습니다. 그 위에 얹을 검색이 따로 필요합니다.

돌아보면 이 편에는 RAG다운 이야기가 거의 없습니다. 임베딩 모델을 고르거나 청킹 전략을 비교하는 대목이 나오지 않습니다. 다만 검색은 들어간 문서 이상을 돌려주지 못합니다. 이미지가 빠진 채 색인된 문서는 어떤 리트리버를 붙여도 이미지를 답할 수 없고, 구조가 뭉개진 문서는 어디를 잘라 보여줘야 할지 알 수 없습니다. 그래서 검색보다 문서가 먼저였습니다.

다음 편에서는 이 문서들 위에 검색을 얹습니다.

시리즈 구성

읽어주셔서 감사합니다.