티스토리 뷰

반응형

Python으로 공부하는 OpenAI 10편 — RAG가 붙었는데 답이 별로라면, chunking과 top-k부터 다시 봐야 합니다

9편에서 RAG를 붙이면, 처음엔 되게 뿌듯합니다.
“이제 우리 문서를 찾아서 답하네?” 싶거든요.

근데 그 다음에 거의 꼭 오는 순간이 있어요.

검색은 되는 것 같은데 답이 좀 엉성하다.
문서가 분명 있는데 못 찾는다.
찾긴 찾는데 엉뚱한 조각을 들고 온다.
관련 문서를 너무 많이 넣으니까 오히려 답이 흐려진다.

이쯤 되면 많은 분이 바로 모델 탓을 합니다.
근데 실제로는 모델보다 먼저 봐야 할 게 있어요.

문서를 어떻게 쪼갰는지(chunking), 몇 개를 가져오는지(top-k), 그리고 검색 결과를 어떻게 정리해서 넣는지
이 세 가지에서 품질이 갈리는 경우가 정말 많습니다.

OpenAI 문서도 Retrieval API가 관련 chunks, similarity score, file of origin을 반환한다고 설명하고 있고, File Search는 결과 수를 max_num_results로 조절할 수 있다고 안내합니다. 또 Accuracy 가이드는 retrieval에서 잘못된 문맥을 주거나, 너무 많은 잡음을 주면 환각이 늘 수 있다고 분명히 말합니다. (OpenAI 개발자)

이번 글에서는 바로 이걸 다룹니다.

  • chunking을 왜 신경 써야 하는지
  • chunk를 너무 크게/작게 자르면 무슨 일이 생기는지
  • top-k를 몇 개로 시작하면 좋은지
  • rerank 감각은 왜 필요한지
  • RAG가 “붙긴 했는데 답이 별로”일 때 어디부터 봐야 하는지

1. RAG 품질은 모델보다 검색 파이프라인에서 먼저 무너집니다

이건 진짜 한 번 겪어보면 확 와요.

모델이 멍청한 게 아니라,
모델 앞에 넣어준 자료가 애매해서 답이 이상한 경우가 많습니다.

예를 들어 이런 일이 생깁니다.

  • 질문은 “환불 조건”인데, 검색 결과엔 “배송 지연” chunk가 먼저 뜸
  • FastAPI 배포 절차를 물었는데, 검색 결과가 Docker 개념 설명 위주로 나옴
  • 긴 문서를 너무 크게 잘라놔서 필요한 한 문장이 거대한 잡음 속에 묻힘
  • top-k를 10개로 너무 크게 잡아 irrelevant chunk까지 다 들어감

OpenAI Accuracy 가이드는 retrieval에서 흔한 문제로 wrong contexttoo much irrelevant context를 직접 언급합니다. 즉, 답이 안 좋을 때는 “검색이 틀렸거나, 잡음이 너무 많아서 모델이 핵심 정보를 못 본다”는 얘기예요. (OpenAI 개발자)

솔직히 저도 처음엔
“RAG 붙였는데 왜 더 이상하지?”
이 생각을 꽤 했습니다.

근데 지나고 보니까, RAG는 그냥 붙인다고 좋아지는 기능이 아니라
검색 결과를 얼마나 깨끗하게 주느냐가 핵심이더라고요.


2. chunking은 문서를 자르는 기술이 아니라, 검색 품질을 설계하는 일입니다

처음엔 chunking이 되게 사소해 보여요.
“어차피 알아서 나눠지겠지” 싶죠.

근데 이게 정말 중요합니다.

OpenAI Vector Store 파일 API 레퍼런스에 따르면, 파일을 vector store에 넣을 때 chunking strategy를 지정할 수 있고, 기본 auto 전략은 현재 max_chunk_size_tokens=800, chunk_overlap_tokens=400을 사용합니다. 또 static 전략으로 chunk size와 overlap을 직접 지정할 수도 있고, chunk size는 100~4096 토큰 범위에서 설정할 수 있습니다. (OpenAI 개발자)

이 말은 곧 이런 뜻입니다.

  • 문서는 검색 가능한 단위로 쪼개져 저장된다
  • 검색 결과는 그 “조각” 단위로 돌아온다
  • 조각이 너무 이상하면 검색 품질도 같이 이상해진다

즉, chunking은 단순 전처리가 아니라
“모델이 어떤 단위의 근거를 받게 할 것인가”를 정하는 설계입니다.


3. chunk가 너무 크면 생기는 문제

이건 의외로 많이 합니다.

문서를 너무 크게 자르면,
검색 하나로 너무 많은 문맥이 한꺼번에 들어와요.

예를 들어 2,000~3,000토큰짜리 chunk 안에:

  • 정의
  • 예외사항
  • 예제
  • unrelated 배경설명
  • 다른 섹션 일부

가 다 섞여 있으면, 검색은 맞는 chunk를 찾았더라도
모델이 그 안에서 진짜 필요한 부분만 바로 집어내기 어려워집니다.

OpenAI Accuracy 가이드는 long context에서 모델이 프롬프트 전체에 고르게 주의를 주지 못할 수 있고, 이 현상을 “lost in the middle”이라고 설명합니다. 아주 긴 프롬프트에서는 필요한 정보가 중간에 묻혀 보이지 않을 수 있다는 뜻입니다. (OpenAI 개발자)

RAG에서도 비슷한 일이 납니다.

검색 chunk가 너무 크면:

  • 핵심 문장이 잡음에 파묻히고
  • 모델이 관련 없는 주변 정보까지 같이 먹고
  • 답이 장황하거나 흐려집니다

4. 반대로 chunk가 너무 작아도 안 좋습니다

이건 또 다른 함정입니다.

너무 작은 chunk, 예를 들어 한두 문단보다도 더 잘게 쪼개면
문맥이 끊어져버립니다.

예를 들어 이런 식이죠.

  • “예외: 단, 결제 후 7일 이내”
  • “이 경우에는 고객센터 승인 필요”
  • “전자상품권은 제외”

이게 각각 따로 떨어져 있으면,
검색 결과 하나만 봤을 때 의미가 반쯤 잘린 상태가 됩니다.

그래서 chunking은 크게 두 가지를 동시에 만족해야 합니다.

  • 검색 질의와 semantic하게 잘 매칭될 만큼 작아야 하고
  • 의미가 깨지지 않을 만큼은 커야 합니다

OpenAI의 기본 auto chunking이 800토큰에 400토큰 overlap을 두는 것도,
완전히 잘라버리지 말고 앞뒤 맥락을 어느 정도 겹쳐서 유지하라는 철학으로 볼 수 있습니다. (OpenAI 개발자)

즉, overlap은 괜히 있는 게 아니에요.
문서를 자르면서 의미가 부러지는 걸 조금 완화해줍니다.


5. 처음 시작할 때 chunk 크기는 어떻게 잡는 게 현실적이냐

반응형

이건 정답 하나는 없어요.
문서 성격에 따라 달라집니다.

그래도 시작점은 잡을 수 있습니다.

이런 문서라면 중간 크기 chunk가 무난합니다

  • 기술 블로그
  • FAQ
  • 개발 가이드
  • 사내 문서
  • 정책 문서

이 경우엔 OpenAI 기본 auto 전략처럼
중간 크기 chunk + 적당한 overlap으로 시작하는 게 꽤 합리적입니다. 기본 auto는 현재 800/400입니다. (OpenAI 개발자)

이런 문서라면 좀 더 구조 기반으로 자르는 게 낫습니다

  • 제목/소제목이 명확한 문서
  • 표/리스트 중심 문서
  • API 문서
  • 법률/정책 예외조항 문서

이 경우엔 토큰 수만 기준으로 자르기보다
섹션 단위, heading 단위로 먼저 나누고, 그 안에서 추가 분할하는 쪽이 더 잘 맞을 때가 많습니다.

개인적으로 초반엔 이렇게 권합니다.

  • 그냥 막연하면 auto로 시작
  • 품질이 애매하면 static으로 조정
  • 문서 구조가 뚜렷하면 구조 기반 split을 먼저 고려

6. top-k는 “많이 가져오면 좋다”가 아닙니다

RAG 입문자들이 정말 자주 하는 실수 중 하나가
top-k를 과하게 크게 잡는 겁니다.

“혹시 놓칠까 봐 10개, 15개 넣자”
이렇게 가기 쉬워요.

근데 OpenAI Accuracy 가이드는 retrieval에서 too much irrelevant context가 오히려 hallucination을 늘릴 수 있다고 경고합니다. File Search 가이드도 max_num_results를 줄이면 latency와 token usage는 줄지만 품질 tradeoff가 있을 수 있다고 설명합니다. 즉, 많이 넣는 게 무조건 좋은 게 아닙니다. (OpenAI 개발자)

실무 감각으로 풀면:

  • top-k가 너무 작으면 정답 chunk를 놓칠 수 있음
  • top-k가 너무 크면 잡음이 섞여서 모델이 흔들림

그래서 처음엔 작게 시작해서 늘리는 방식이 좋습니다.

예를 들면:

  • FAQ/짧은 문서: top-k 2~4
  • 일반 기술 문서: top-k 3~5
  • 문서가 난해하거나 표/예외가 많음: 5 전후에서 실험

저는 대부분 3~5에서 먼저 봅니다.
이 범위가 생각보다 균형이 좋아요.


7. Retrieval API와 File Search에서 top-k를 어떻게 다루나

OpenAI Retrieval API는 semantic search 결과를 반환하고, 각 결과에는 relevant chunks, similarity scores, file of origin이 포함된다고 안내합니다. File Search tool은 max_num_results를 직접 지정할 수 있습니다. (OpenAI 개발자)

그러니까 구조적으로는 이렇게 볼 수 있습니다.

Retrieval API

  • 검색 결과를 직접 받고
  • 내가 top-k를 slice 하거나
  • score 기반으로 추가 필터링할 수 있음

File Search tool

  • max_num_results로 상한을 먼저 지정
  • 모델이 그 범위 안에서 문서를 참고하게 함

빠르게 붙일 때는 File Search의 max_num_results가 편하고,
세밀하게 다룰 때는 Retrieval API로 직접 결과를 보고 조정하는 편이 더 좋습니다.


8. rerank는 “한 번 더 생각해서 다시 줄 세우기”입니다

RAG를 하다 보면 이런 순간이 옵니다.

1차 검색 결과는 얼추 맞는데,
정말 제일 중요한 문서가 3등이나 4등에 있어요.

이럴 때 필요한 감각이 rerank입니다.

OpenAI 공식 cookbook에는 search API 결과를 임베딩 기반으로 다시 점수화해 re-rank하는 예제가 있고, 또 cross-encoder를 써서 검색 결과를 재정렬하는 예제도 있습니다. Cookbook 예시는 retrieval 후 다시 관련도를 계산해 정렬하는 흐름을 보여줍니다. (OpenAI 개발자)

실무 감각으로 풀면 rerank는 보통 이런 상황에서 씁니다.

  • 1차 검색으로 후보 10개를 뽑는다
  • 그중에서 질문과 가장 잘 맞는 3개만 다시 고른다
  • 모델에는 그 3개만 넣는다

즉,

search는 넓게 찾고, rerank는 정밀하게 줄인다

이 느낌입니다.

처음 서비스에선 꼭 바로 넣을 필요는 없어요.
하지만 “검색은 되는데 우선순위가 자꾸 이상하다” 싶으면
rerank를 생각할 타이밍입니다.


9. 직접 embeddings를 쓴다면 similarity 계산 감각도 알아야 합니다

OpenAI Embeddings 가이드는 text-embedding-3-small과 text-embedding-3-large를 안내하고, 기본 embedding 길이는 각각 1536, 3072라고 설명합니다. 또 예제에서 embedding을 만들고 유사도 검색에 활용하는 흐름을 보여줍니다. (OpenAI 개발자)

직접 벡터DB를 운영할 때는 결국 이런 흐름입니다.

  1. 문서 chunk를 embedding으로 만든다
  2. 사용자 질문도 embedding으로 만든다
  3. cosine similarity 같은 방식으로 가까운 chunk를 찾는다

OpenAI embeddings 가이드에도 코드 검색 예제에서 query embedding과 code embedding 사이 cosine similarity를 계산하는 흐름이 나옵니다. (OpenAI 개발자)

즉, top-k와 rerank 문제는 결국
“어떤 similarity 기준으로 어느 정도 후보를 남기느냐”의 문제로 이어집니다.


10. FastAPI에서 실험 가능한 구조는 이렇게 잡으면 편합니다

이번 편은 RAG 품질 튜닝이니까,
코드도 “실험 가능한 구조”가 중요합니다.

프로젝트 구조 예시:

app/
├── main.py
├── core/
│   └── config.py
├── schemas/
│   └── rag.py
├── services/
│   ├── retrieval_service.py
│   ├── rerank_service.py
│   ├── context_builder_service.py
│   └── rag_chat_service.py
└── api/
    └── rag_router.py

핵심은 이거예요.

  • retrieval_service: 1차 검색
  • rerank_service: 2차 정렬
  • context_builder_service: 최종적으로 모델에 넣을 문맥 조립

이걸 분리해두면 나중에:

  • chunk 크기 바꾸기
  • top-k 바꾸기
  • rerank on/off 하기
  • score threshold 넣기

를 아주 편하게 실험할 수 있습니다.


11. 실행 가능한 1차 Retrieval + top-k 제한 예제

아래 코드는 FastAPI 서비스 계층 안에 넣기 좋게 최소화한 예제입니다.

from dataclasses import dataclass
from typing import Any

from openai import OpenAI


@dataclass
class RetrievedChunk:
    source: str
    content: str
    score: float | None = None


class RetrievalService:
    def __init__(self, api_key: str, vector_store_id: str) -> None:
        self.client = OpenAI(api_key=api_key)
        self.vector_store_id = vector_store_id

    def search(self, query: str, top_k: int = 4) -> list[RetrievedChunk]:
        results = self.client.vector_stores.search(
            vector_store_id=self.vector_store_id,
            query=query,
        )

        chunks: list[RetrievedChunk] = []

        for item in results.data[:top_k]:
            source = getattr(item, "filename", "unknown")
            score = getattr(item, "score", None)

            text = ""
            content_items: Any = getattr(item, "content", None)
            if isinstance(content_items, list):
                parts: list[str] = []
                for c in content_items:
                    maybe_text = getattr(c, "text", None)
                    if maybe_text:
                        parts.append(maybe_text)
                text = "\n".join(parts).strip()
            elif isinstance(content_items, str):
                text = content_items.strip()

            if text:
                chunks.append(
                    RetrievedChunk(
                        source=source,
                        content=text,
                        score=score,
                    )
                )

        return chunks

OpenAI Retrieval 가이드가 vector_stores.search(...)와 semantic search 흐름을 공식적으로 설명하고 있으니, 이 코드는 그 흐름에 맞는 FastAPI 서비스형 래퍼라고 보면 됩니다. (OpenAI 개발자)


12. 아주 단순한 rerank 예제도 이렇게 시작할 수 있습니다

처음부터 cross-encoder까지 안 가도,
간단한 규칙 기반 rerank만으로도 체감이 올 때가 있습니다.

예를 들면:

  • filename 가중치
  • score threshold
  • 최신 문서 우선
  • 특정 문서 타입 우선
from dataclasses import dataclass


@dataclass
class RetrievedChunk:
    source: str
    content: str
    score: float | None = None


class SimpleRerankService:
    def rerank(self, query: str, chunks: list[RetrievedChunk], top_k: int = 3) -> list[RetrievedChunk]:
        def sort_key(chunk: RetrievedChunk) -> tuple[float, int]:
            score = chunk.score if chunk.score is not None else 0.0
            faq_bonus = 0.1 if "faq" in chunk.source.lower() else 0.0
            return (score + faq_bonus, len(chunk.content))

        ranked = sorted(chunks, key=sort_key, reverse=True)
        return ranked[:top_k]

이건 아주 초보적인 버전이지만,
실무에선 이런 “작은 규칙”도 꽤 도움이 됩니다.

그리고 검색 정확도가 더 중요해지면,
그때 cookbook 수준의 embedding 기반 rerank나 cross-encoder rerank를 검토하면 됩니다. (OpenAI 개발자)


13. chunking을 직접 제어하고 싶다면 vector store file 생성 시 전략을 명시할 수 있습니다

OpenAI API 레퍼런스에 따르면 vector store file 생성 시 chunking_strategy를 지정할 수 있고, static에서는 max_chunk_size_tokens와 chunk_overlap_tokens를 커스터마이즈할 수 있습니다. 기본값은 800/400이고, chunk size 허용 범위는 100~4096입니다. (OpenAI 개발자)

예를 들면 이런 식입니다.

from openai import OpenAI

client = OpenAI()

vector_store_file = client.vector_stores.files.create(
    vector_store_id="vs_abc123",
    file_id="file_abc123",
    chunking_strategy={
        "type": "static",
        "static": {
            "max_chunk_size_tokens": 600,
            "chunk_overlap_tokens": 200,
        },
    },
)

print(vector_store_file.status)

이 코드가 좋은 이유는 하나예요.
RAG 품질이 애매할 때, “문서 자르는 방식” 자체를 실험할 수 있게 해주거든요.


14. RAG가 붙었는데 답이 별로일 때, 저는 이 순서로 봅니다

이건 진짜 실전 체크리스트예요.

1) 검색 결과가 맞는가

정답이 들어 있는 chunk가 top-k 안에 들어오나?

2) chunk가 너무 크거나 작지 않은가

핵심 문장이 묻히거나, 문맥이 잘리지 않았나?

3) top-k가 너무 크지 않은가

잡음이 너무 많이 들어가진 않나?

4) 검색 결과를 그대로 다 넣고 있진 않은가

중복/불필요한 부분을 정리하고 있나?

5) 모델 프롬프트가 근거 사용 방식을 분명히 요구하나

“문서에 없는 내용은 추측하지 마라” 같은 지시가 있나?

6) long context에 묻히는 건 아닌가

문맥이 너무 길어져서 lost-in-the-middle가 생기진 않나? (OpenAI 개발자)

OpenAI Accuracy 가이드가 retrieval 튜닝, noise 줄이기, 제공 정보 조절을 핵심 해법으로 말하는 것도 결국 같은 방향입니다. (OpenAI 개발자)


15. 처음 시작할 때 추천하는 기본값

완벽한 정답은 아니지만,
주니어 개발자가 처음 RAG 품질 튜닝할 때는 이 정도가 괜찮습니다.

  • chunking: 기본 auto로 먼저 시작
  • top-k: 3 또는 4
  • rerank: 처음엔 off, 결과가 어정쩡하면 on
  • overlap: 기본값 유지 후 문제 생기면 조정
  • 응답 프롬프트: “검색 문서를 우선 참고, 없으면 불확실하다고 말하기”

OpenAI 기본 auto chunking이 이미 800/400으로 제공되기 때문에,
처음부터 무리하게 수동 튜닝하기보다 baseline으로 삼기 좋습니다. (OpenAI 개발자)

그 다음엔 꼭 평가 질문 세트를 만들어서 비교하세요.

예를 들면:

  • “환불 조건이 뭐야?”
  • “배포 절차를 요약해줘”
  • “FastAPI 의존성 주입 장점은?”
  • “문서에 없는 질문을 했을 때 어떻게 답하나?”

이렇게 10~20개만 있어도,
chunk size와 top-k 바꿨을 때 체감 차이가 꽤 잘 보입니다.


16. 오늘 글의 핵심 요약

이번 글에서 꼭 가져가야 할 건 이것입니다.

RAG 품질은 모델보다 먼저 chunking, top-k, 검색 결과 정리에서 갈린다.

정리하면:

  • 검색 결과는 chunk 단위로 돌아온다. (OpenAI 개발자)
  • OpenAI 기본 auto chunking은 현재 800토큰 크기, 400토큰 overlap이다. (OpenAI 개발자)
  • top-k는 무조건 크게 잡는다고 좋은 게 아니고, noise가 늘면 오히려 성능이 흔들린다. (OpenAI 개발자)
  • long context에서는 lost-in-the-middle도 생길 수 있다. (OpenAI 개발자)
  • 결과가 애매하면 rerank나 score filtering을 검토할 만하다. (OpenAI 개발자)

즉,
RAG는 붙이는 것보다 튜닝하는 순간부터 진짜 시작입니다.


다음 편 예고

다음 글에서는 여기서 더 실무적으로 들어가겠습니다.

Python + FastAPI에서 RAG 응답에 출처(citation)까지 붙여 신뢰도를 높이는 방법

이 주제로,

  • source를 어떻게 응답에 포함할지
  • chunk와 파일명을 사용자에게 어떻게 보여줄지
  • “문서 근거 기반 답변” UX를 어떻게 만들지
  • citation-friendly 응답 포맷 설계

까지 이어가보겠습니다.


출처

  • OpenAI Retrieval 가이드 — vector_stores.search(...), semantic search, relevant chunks / similarity scores / file of origin 반환. (OpenAI 개발자)
  • OpenAI File Search 가이드 — max_num_results 지원. (OpenAI 개발자)
  • OpenAI Accuracy 가이드 — retrieval에서 wrong context, too much irrelevant context가 문제이며 retrieval tuning이 중요함. (OpenAI 개발자)
  • OpenAI Accuracy 가이드 — long context에서 “lost in the middle” 현상 설명. (OpenAI 개발자)
  • OpenAI Embeddings 가이드 — text-embedding-3-small, 기본 1536차원, cosine similarity 기반 검색 예시. (OpenAI 개발자)
  • OpenAI Vector Store File API 레퍼런스 — auto chunking 기본값 800/400, static chunking 전략 커스터마이즈 가능, chunk size 범위 100~4096. (OpenAI 개발자)
  • OpenAI Cookbook — search API 후 embedding 기반 re-rank 예제, cross-encoder rerank 예제. (OpenAI 개발자)

 

Python, OpenAI, FastAPI, RAG, chunking, top-k, rerank, Retrieval, File Search, Vector Store, Embeddings, OpenAI Python SDK, semantic search, AI 백엔드, 주니어 개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함
반응형