RAG 품질은 모델보다 검색 설계에서 먼저 갈립니다

RAG를 도입할 때 가장 흔한 실수는 임베딩 모델만 바꾸면 답변 품질이 올라갈 것이라고 보는 것입니다. 실제 운영에서는 검색 대상 문서를 어떻게 나누고, 어떤 메타데이터를 함께 저장하며, 질의 시 어떤 조건으로 후보를 좁히는지가 더 큰 차이를 만듭니다. 문서가 너무 크게 잘리면 관련 없는 문맥이 섞여 모델이 엉뚱한 내용을 인용하고, 너무 작게 잘리면 답변에 필요한 전후 관계가 사라집니다. 또한 부서, 제품, 버전, 공개 범위 같은 정보를 메타데이터로 남기지 않으면 벡터 유사도는 높지만 현재 업무에는 맞지 않는 조각이 검색될 수 있습니다.

청킹 기준은 글자 수가 아니라 의미 단위입니다

청킹은 단순히 1,000자마다 자르는 작업이 아닙니다. 운영 문서, 약관, 장애 대응 매뉴얼, API 명세처럼 구조가 있는 문서는 제목, 소제목, 표, 코드 예제, 절 번호를 기준으로 먼저 나누는 것이 좋습니다. 한 조각은 독립적으로 읽어도 질문 하나에 답할 수 있어야 하며, 동시에 너무 많은 주제를 담지 않아야 합니다. 예를 들어 결제 API 문서에서 인증, 요청 파라미터, 오류 코드, 환불 절차가 한 조각에 들어가면 검색 결과는 넓어지지만 답변 근거는 흐려집니다.

실무 기준으로는 한 청크에 하나의 주제와 짧은 부가 설명을 담고, 앞뒤 문맥을 100자에서 250자 정도 겹치게 두는 방식이 안정적입니다. 단, 중복 문맥을 무조건 크게 잡으면 같은 정보가 여러 번 검색되어 상위 결과를 차지하므로 주의해야 합니다. 문서 제목과 상위 섹션명은 본문에 반복해서 붙이기보다 메타데이터로 저장하고, 검색 결과를 모델에 넘길 때 함께 조립하는 방식이 관리하기 쉽습니다.

필수 메타데이터를 먼저 정합니다

메타데이터는 나중에 붙이는 태그가 아니라 검색 정책의 일부입니다. 최소한 문서 ID, 문서 제목, 섹션 제목, 원본 URL 또는 파일 경로, 버전, 작성일 또는 수정일, 접근 권한, 업무 도메인을 저장해야 합니다. 회사 내부 검색이라면 부서, 고객사, 제품군, 프로젝트 코드도 중요합니다. 같은 질문이라도 영업팀이 볼 수 있는 공개 제안서와 개발팀만 볼 수 있는 장애 보고서는 함께 검색되면 안 됩니다. 권한 필터를 검색 후에 적용하면 상위 결과가 제거되어 빈약한 컨텍스트만 남을 수 있으므로, 가능하면 벡터 검색 단계에서 먼저 필터링해야 합니다.

  • 문서 구조 메타데이터: title, section, heading_path, page_number
  • 운영 메타데이터: version, updated_at, owner, source_url
  • 검색 필터 메타데이터: product, department, customer, visibility, language
  • 품질 관리 메타데이터: chunk_hash, ingestion_run_id, parser_version

저장 구조 예시

PostgreSQL과 pgvector를 쓰는 경우에도 핵심 원리는 같습니다. 본문, 임베딩, 메타데이터를 한 테이블에 넣되, 자주 필터링하는 항목은 별도 컬럼으로 빼고 나머지는 JSONB에 보관합니다. 이렇게 하면 제품군이나 공개 범위처럼 자주 쓰는 조건은 일반 인덱스로 빠르게 거르고, 세부 속성은 JSONB로 유연하게 확장할 수 있습니다.

CREATE TABLE rag_chunks (
  id bigserial PRIMARY KEY,
  document_id text NOT NULL,
  chunk_index integer NOT NULL,
  title text NOT NULL,
  section text NOT NULL,
  content text NOT NULL,
  product text NOT NULL,
  visibility text NOT NULL,
  updated_at timestamptz NOT NULL,
  metadata jsonb NOT NULL DEFAULT '{}',
  embedding vector(1536) NOT NULL
);

CREATE INDEX rag_chunks_product_visibility_idx
  ON rag_chunks (product, visibility, updated_at DESC);

CREATE INDEX rag_chunks_metadata_gin_idx
  ON rag_chunks USING gin (metadata);

CREATE INDEX rag_chunks_embedding_idx
  ON rag_chunks USING hnsw (embedding vector_cosine_ops);

검색 시에는 유사도와 업무 조건을 같이 봅니다

RAG 검색은 대체로 질문 임베딩을 만든 뒤 가장 가까운 청크를 찾는 흐름입니다. 하지만 운영 환경에서는 단순 top k 검색보다 조건 필터, 최신성, 중복 제거, 재정렬을 함께 적용해야 합니다. 먼저 사용자의 권한과 제품군으로 후보를 줄이고, 벡터 유사도로 20개에서 50개 정도를 넓게 가져온 뒤, 같은 문서의 비슷한 청크가 과도하게 몰리면 일부를 제거합니다. 이후 키워드 점수나 재정렬 모델을 붙이면 숫자, 오류 코드, 함수명처럼 벡터만으로 약한 검색도 보완할 수 있습니다.

SELECT id, document_id, title, section, content,
       1 - (embedding <=> $1) AS score
FROM rag_chunks
WHERE product = $2
  AND visibility IN ('public', $3)
  AND updated_at > now() - interval '18 months'
ORDER BY embedding <=> $1
LIMIT 30;

위 쿼리는 예시이지만 중요한 점은 권한과 제품 조건이 벡터 정렬 전에 들어간다는 것입니다. 사용자가 접근할 수 없는 문서를 검색한 뒤 애플리케이션에서 지우는 방식은 보안과 품질 양쪽에서 좋지 않습니다. 또한 오래된 문서가 답변을 오염시키지 않도록 updated_at 조건이나 버전 우선순위를 두는 것이 안전합니다.

컨텍스트 조립은 답변 품질의 마지막 관문입니다

검색 결과를 그대로 모델에 붙이면 같은 문장이 반복되거나 출처가 불명확해질 수 있습니다. 컨텍스트를 만들 때는 각 청크 앞에 문서 제목, 섹션, 수정일, 출처를 짧게 붙이고, 본문은 필요한 길이만 유지합니다. 모델에게는 답변에 사용한 출처를 함께 표시하게 하고, 검색 결과 안에 근거가 없으면 모른다고 답하도록 지시해야 합니다. 특히 사내 규정, 가격 정책, 계약 조건처럼 민감한 문서는 추론으로 빈칸을 채우게 두면 안 됩니다.

  • 같은 document_id의 청크가 많으면 상위 1개에서 2개만 유지합니다.
  • 점수가 낮은 결과는 모델에 넘기지 말고 재질문을 유도합니다.
  • 문서 제목과 수정일을 컨텍스트에 포함해 오래된 근거를 구분합니다.
  • 답변 생성 후 인용한 chunk_id를 로그에 남겨 품질 점검에 활용합니다.

운영 점검 체크리스트

RAG 시스템은 한 번 색인하고 끝나는 기능이 아닙니다. 문서가 추가될 때마다 청킹 품질이 유지되는지, 삭제된 문서가 검색에서 빠지는지, 권한 변경이 즉시 반영되는지 확인해야 합니다. 또한 실제 사용자 질문을 익명화해 실패 사례를 모으고, 검색된 청크가 답변에 충분했는지 주기적으로 점검해야 합니다. 품질 개선은 모델 교체보다 테스트 질문 세트, 검색 로그, 메타데이터 정비에서 더 빨리 시작되는 경우가 많습니다.

  • 청크 하나가 하나의 질문에 답할 수 있는 의미 단위인지 확인합니다.
  • 권한, 제품, 버전, 수정일 필터가 검색 단계에서 적용되는지 확인합니다.
  • 같은 문서가 검색 결과를 독점하지 않도록 중복 제거 규칙을 둡니다.
  • 검색 실패 질문을 모아 청킹 규칙과 메타데이터 누락을 개선합니다.
  • 문서 재색인 시 chunk_hash와 ingestion_run_id로 변경 이력을 추적합니다.