← 에디토리얼

AI 도입 · 2026.08.24 · 6분

RAG는 모델이 아니라 문서에서 막힙니다

사내 문서를 AI에게 물어보게 만들려다 멈추는 지점은 거의 다 문서 쪽에 있습니다. 시작 전에 뭘 세어봐야 하는지, 평가 세트는 어떻게 만드는지 적었습니다.

"우리 회사 문서를 AI가 읽고 답하게 해주세요."

요청은 한 줄인데, 실제 작업의 절반 이상은 모델을 붙이기 전에 끝납니다. 문서 쪽에서 막히거든요.

RAG가 실제로 하는 일

RAG는 이름 그대로 두 단계입니다.

질문 → [1] 관련 있는 문서 조각을 찾는다
      → [2] 그 조각을 모델에게 같이 넘겨 답을 만든다

그래서 답의 품질은 [1]에서 뭘 찾아왔느냐에 거의 다 걸립니다. 엉뚱한 걸 넘기면 아무리 좋은 모델도 엉뚱하게 답합니다. 도입이 실패했다고 할 때 열에 아홉은 모델이 아니라 검색이 엉뚱한 걸 물어온 경우입니다.

뒤집으면 이런 뜻입니다. 모델 고르는 건 나중 일이고, 문서 상태가 일정을 정합니다.

시작 전에 세어볼 네 가지

하나. 같은 내용인데 판본이 몇 개인가

업무규정_최종.docx
업무규정_최종_수정.docx
업무규정_최종_2024개정_진짜최종.docx

셋 다 넣으면 검색도 셋 다 물어옵니다. 모델은 뭐가 맞는지 모릅니다. 오래된 게 걸리면 틀린 답을 아주 자신 있게 합니다.

뭐가 현행인지 정하는 건 개발이 아니라 업무 정리입니다. 그런데 이걸 안 하면 나머지가 전부 헛돕니다. 최소한 문서마다 유효일자폐기여부를 붙여두고 검색에서 폐기된 걸 걸러야 합니다.

둘. 표와 스캔이 얼마나 되나

PDF 안의 표는 텍스트로 뽑으면 열이 뒤섞이기 쉽습니다.

원본 표
  구분      2024년     2025년
  A등급     120만원    135만원
  B등급     90만원     100만원

그냥 뽑으면
  구분 2024년 2025년 A등급 120만원 135만원 B등급 90만원 100만원

이 상태로 넣으면 "B등급 2024년 얼마냐"에 135만원이라고 답할 수 있습니다. 금액표가 섞이면 그 답은 분쟁이 됩니다.

스캔한 문서면 글자부터 인식해야 하고, 손글씨가 섞이면 그마저 어렵습니다. 시작 전에 전체 문서에서 표와 스캔이 몇 퍼센트인지 세어보세요. 그 비율이 곧 전처리 기간입니다.

셋. 누가 뭘 볼 수 있나

인사 문서와 급여 자료가 같은 폴더에 있으면 검색은 그걸 구분해주지 않습니다. 권한을 검색 단계에서 안 거르면 전 직원이 급여를 조회하는 도구가 됩니다.

중요한 건 답을 만든 뒤에 거르면 늦다는 점입니다. 모델에게 이미 넘어간 내용은 요약되어 새어 나옵니다. 권한은 검색 조건 자체에 들어가야 합니다.

// 나쁜 예 — 다 찾아놓고 나중에 거른다
const hits = await search(question);
const visible = hits.filter((h) => canRead(user, h));  // 이미 늦었다면?

// 이렇게 — 애초에 볼 수 있는 것만 찾는다
const hits = await search(question, {
  filter: { department: { in: user.departments }, level: { lte: user.level } },
});

넷. 문서가 얼마나 자주 바뀌나

매주 바뀌는 문서라면 색인을 갱신하는 구조가 있어야 합니다. 한 번 넣고 끝이 아닙니다. 주기가 짧을수록 "문서는 고쳤는데 AI는 옛날 답을 한다"는 민원이 빨리 옵니다.

어떻게 자르느냐가 절반입니다

긴 문서를 통째로 넘길 수 없으니 조각으로 나눠 넣습니다. 이 자르는 방식이 검색 품질을 크게 좌우합니다.

글자 수로 기계적으로 자르면 문장 중간이 끊기고 표가 반으로 갈립니다.

// 흔히 이렇게 하는데, 위험합니다
const chunks = text.match(/.{1,1000}/gs);

조항이나 소제목 단위로 자르고, 조각마다 어느 문서 어느 절인지를 붙여두는 편이 낫습니다.

type Chunk = {
  text: string;
  docId: string;
  docTitle: string;   // "2025 업무규정"
  section: string;    // "제3장 2절 출장비"
  effectiveFrom: string;
  page?: number;
};

이 정보가 있어야 두 가지가 됩니다.

출처를 보여줄 수 있습니다. 사용자가 답을 믿을지 판단하려면 근거 문서를 열어볼 수 있어야 합니다. 출처 표시는 선택이 아닙니다.

조각 앞에 맥락을 붙일 수 있습니다. "본 항의 금액은 별표 2에 따른다"만 잘라 넣으면 무슨 소린지 모릅니다. 앞에 문서명과 절 제목을 한 줄 붙이는 것만으로 정확도가 눈에 띄게 오릅니다.

[2025 업무규정 · 제3장 2절 출장비]
본 항의 금액은 별표 2에 따른다. ...

검색은 두 가지를 섞어야 합니다

벡터 검색만 쓰면 정확한 단어에 약합니다. "K-2025-114 건 처리 규정"을 물으면 문서번호를 못 찾습니다. 반대로 키워드 검색만 쓰면 "출장비"와 "여비"를 다른 말로 봅니다.

방식 잘하는 것 못하는 것
벡터 검색 표현이 달라도 뜻이 같은 것 문서번호, 고유명사, 코드
키워드 검색(BM25) 정확한 단어, 숫자 같은 뜻 다른 표현

둘을 같이 돌려 결과를 합치는 게 기본입니다. 여기에 상위 후보를 다시 정렬하는 단계를 붙이면 한 번 더 오릅니다.

재는 방법을 먼저 만드세요

이걸 건너뛰면 "좋아진 것 같다"는 느낌으로만 굴러갑니다. 그러면 아무도 손을 못 댑니다.

실무자에게 자주 나오는 질문 50개와 정답을 받아두세요. 이게 시험지입니다.

type Case = {
  question: string;
  mustCite: string[];   // 반드시 근거로 잡혀야 할 조각 id
  expected: string;     // 사람이 쓴 정답
};

// 검색 단계와 생성 단계를 나눠서 잰다
for (const c of cases) {
  const hits = await retrieve(c.question, { k: 5 });
  recall += c.mustCite.every((id) => hits.some((h) => h.id === id)) ? 1 : 0;
}

검색 정확도와 답변 정확도를 따로 재는 게 핵심입니다. 답이 틀렸을 때 검색이 못 찾아온 건지, 찾아왔는데 모델이 잘못 읽은 건지 나뉘어야 어디를 고칠지 압니다.

만드는 데 하루면 되고, 그다음부터 모든 판단의 기준이 됩니다. 조각 크기를 바꿀 때마다, 검색 방식을 바꿀 때마다 이 50개를 돌려 비교하면 됩니다.

모르면 모른다고 하게 하세요

RAG에서 제일 위험한 건 근거를 못 찾았는데 그럴듯하게 답하는 겁니다. 규정을 지어내면 그 답이 곧 분쟁이 됩니다.

  • 검색 점수가 기준 아래면 답하지 말고 담당자에게 넘긴다
  • 금액·기간·조항 번호는 모델이 쓰게 두지 말고 찾아온 원문을 그대로 인용한다
  • 답변에 출처를 반드시 붙인다. 못 붙이면 그 답은 내보내지 않는다

문서를 근거로 답할 때 인용 위치까지 같이 돌려주는 기능이 API 차원에서 제공되기도 합니다. 직접 만들기 전에 그쪽부터 보는 게 빠릅니다.

작게 열고 넓히세요

전사 문서를 한 번에 넣는 방식은 대체로 실패합니다. 문서 상태가 부서마다 다르고 권한 구조도 제각각이라 어디서 문제가 났는지 짚기 어렵습니다.

한 부서, 한 종류로 시작해 그 안에서 정확도를 올린 다음 넓히는 게 빠릅니다. 첫 범위에서 얻은 기준 — 조각 크기, 검색 방식, 임계값 — 이 다음 범위에 그대로 쓰입니다.

정리

RAG 일정은 모델 선택이 아니라 문서 상태가 정합니다. 발주 전에 이 넷만 세어보셔도 견적과 일정이 훨씬 정확해집니다.

  1. 현행 판본이 정리돼 있나
  2. 표와 스캔이 몇 퍼센트인가
  3. 권한을 검색 조건으로 걸 수 있나
  4. 얼마나 자주 바뀌나

그리고 평가 세트 50개. 없으면 고쳐도 좋아졌는지 알 수 없고, 알 수 없으면 결국 아무도 안 건드립니다.

관련 프로젝트

다음 글LLM 요금이 갑자기 뛰는 다섯 가지 이유