Post

Rag 검색 설정

Rag 검색 설정

RAG 검색 설정 정하기

무엇을 정하는가
chunk_size문서를 몇 자씩 자를 것인가
chunk_overlap앞뒤 chunk를 몇 자나 겹칠 것인가
k (top-k)질문 하나당 chunk를 몇 개 가져올 것인가


이 값들은 정답이 없다.

문서 성격에 따라 달라지기 때문에 남이 쓴 값을 가져와도 내 문서에서 좋다는 보장이 없다.





재는 방법 정하기

검색

RAG는 검색해온 문서를 근거로 답을 만들기 때문에

검색이 정답 문서를 못 가져오면 답이 맞을 수가 없다.


세 값은 전부 검색 단계에 붙는 값이라, 검색 결과만 봐도 값끼리 비교가 된다.



평가셋 : 질문 25개

실제로 챗봇에 물어봤던 질문들을 유형별로 5개씩 골라 25개를 만들었다.


유형질문
A 단일 영웅 정보리퍼 스킬 알려줘
 겐지 튕겨내기로 뭘 막을 수 있어?
 오리사 궁극기가 뭐야?
 아나 생체 수류탄 어떤 효과야?
 루시우 소리 방벽은 언제 쓰는 거야?
B 상성·카운터리퍼 카운터 영웅 추천해줘
 겐지 상대하기 너무 어려운데 뭘로 카운터쳐?
 파라 못 잡겠어. 카운터 픽 알려줘
 트레이서한테 강한 영웅이 누구야?
 윈스턴 카운터 뭐가 있어?
C 맵 운영도라도 공격 운영법 알려줘
 왕의 길 수비할 때 어떻게 해야 해?
 부산에서 이기려면 어떻게 운영해야 해?
 눔바니 공격 팁 좀 알려줘
 콜로세오는 어떻게 플레이해야 해?
D 상황 서술형상대가 멀리서 계속 견제해서 접근을 못 해
 상대가 높은 곳을 다 잡고 있어서 접근을 못 하겠어
 상대 딜러가 뒤로 와서 힐러를 죽여서 우리팀 힐러가 힐을 못해
 좁은 골목에서 계속 싸워야 하는 맵인데 조합을 어떻게 짜?
 상대가 계속 우리 본대로 달려드는데 뭘로 받아쳐야 해?
E 멀티턴 후속그럼 궁극기는 언제 써?
 망령화는 언제 쓰는 게 좋아?
 걔 궁은 어떻게 피해?
 그 영웅 궁은 언제 써?
 수비할 때는 어떻게 해?


D유형은 영웅 이름도 맵 이름도 질문에 없다.

보통은 “파라 카운터”처럼 영웅 이름을 대고 묻는데 그건 B유형이 담당한다.

D유형은 영웅 이름을 모르거나 상황부터 말하는 경우를 보려고 넣었다.


E유형은 직전 대화를 같이 넣어서 검색한다.

“걔 궁은 어떻게 피해?”만 검색하면 아무것도 안 걸리기 때문에

앞 질문(“파라 상대법 알려줘”)을 이어붙여서 질의로 쓴다.



문서로 답할 수 없는 질문 3개

  • 오버워치 리그 2023 우승팀이 어디야?
  • 발로란트에서 제트 잘 쓰는 법 알려줘
  • 오버워치2 배틀패스 가격이 얼마야?


이건 점수에서 빼고 1위로 뭘 가져오는지만 기록했다.

문서에 없는 걸 물었을 때 엉뚱한 문서를 자신 있게 가져오는지 보려고 넣었다.



정답 라벨링 : chunk 번호 대신 헤더 조합

처음엔 “이 질문의 정답은 몇 번 chunk”라고 적으려고 했다.

그런데 이렇게 하면 테스트 자체가 성립하지 않는다.


chunk 크기를 400에서 800으로 바꾸는 순간 chunk 경계가 전부 달라진다.

3번 chunk가 가리키던 내용이 다른 조합에서는 2번이 되거나 두 chunk로 쪼개진다.


그래서 정답을 (H1, H2, H3) 헤더 조합으로 적었다.

1
2
3
4
5
6
7
8
9
10
{
  "id": "A1",
  "question": "리퍼 스킬 알려줘",
  "gold": [
    { "H1": "공격 영웅", "H2": "리퍼", "H3": "스킬 데이터" },
    { "H1": "공격 영웅", "H2": "리퍼", "H3": "좌 Shift — 망령화" },
    { "H1": "공격 영웅", "H2": "리퍼", "H3": "E — 그림자 밟기" },
    { "H1": "공격 영웅", "H2": "리퍼", "H3": "Q — 죽음의 꽃" }
  ]
}


chunk를 어떻게 자르든 그 chunk가 어느 섹션에서 나왔는지는 metadata에 남아 있으니

조합을 바꿔가며 재도 같은 기준으로 채점할 수 있다.



지표

지표의미
Hit@k가져온 k개 안에 정답 문서가 들어왔는가 (0 또는 1)
Recall@k정답 문서가 여러 개일 때 그중 몇 개나 들어왔는가
MRR정답이 몇 번째로 올라왔는가 (1위면 1.0, 2위면 0.5)


여기에 평균 컨텍스트 토큰을 같이 쟀다.

검색을 많이 가져올수록 지표는 오르지만 그만큼 LLM에 넣는 토큰이 늘어난다.

지표만 보고 고르면 무조건 제일 많이 가져오는 설정이 이긴다.





테스트 순서 : chunk → overlap → k

변수는 하나씩 바꿔야 한다.

그런데 이 세 값은 서로 독립적이지 않아서 어떤 순서로 정하느냐가 결과를 바꾼다.


  • overlap은 chunk 크기에 대한 비율이다. 1300의 10%는 130자고 800의 10%는 80자다.
  • k는 chunk 크기에 따라 필요한 개수가 달라진다. chunk가 작으면 같은 내용을 담는 데 더 많은 chunk가 필요하다.


둘 다 chunk에 따라 달라지는 값이다.

그래서 chunk를 먼저 정하고, 정해진 chunk 위에서 overlap을 재고, 다시 그 위에서 k를 쟀다.


1
① chunk_size  →  ② chunk_overlap  →  ③ top-k





첫번째 test : chunk 크기

overlap은 크기의 10%, k=7로 고정하고 chunk만 바꿨다.


chunkchunk 수MRRHit@kRecall@k평균 토큰
4001,2340.4460.6000.4871,131
8006650.4850.6800.5532,189
13004940.5260.6800.5533,001


수치를 보니 400은 800, 1300과 비교해볼 때 전부 좋지 않았다.

즉 영웅 한명의 스킬 설명도 여러 chunk로 쪼개져서 정답이 흩어진다.


800과 1300을 보면 Hit@k와 Recall@k는 소수점까지 똑같고, MRR만 1300이 0.041 앞선다.

하지만 토큰은 1300이 37% 더 든다(2,189 → 3,001).


따라서 해당 답은 비슷하게 가져오지만 값이 비싼 느낌이라 800으로 선택하게 되었다.

MRR 차이가 없어서가 아니라, 0.041을 얻으려고 토큰을 37% 더 쓸 이유가 없다고 봤다.





두번째 test : overlap

chunk를 800으로 고정하고 overlap만 바꿨다.


overlapchunk 수MRRHit@kRecall@k평균 토큰
06510.5010.6400.5332,110
80 (10%)6650.4850.6800.5532,184
160 (20%)7130.4240.6400.5132,258


수치를 보니 20%가 MRR과 Recall@k에서 제일 낮고, Hit@k도 최고값에 못 미친다.


이유는 chunk 수를 보면 알 수 있다.

overlap 160자는 chunk 하나의 20%나 되는 분량이라

앞뒤 chunk가 서로 같은 문장을 품은 중복이 늘어난다(651개 → 713개).


검색은 이 중복 chunk들을 나란히 상위로 올린다.

따라서 서로 다른 정답 섹션이 그만큼 상위 7개 밖으로 밀려난다.

10%에서 20%로 갈 때 Hit@k가 떨어진 것도 같은 이유다.


0%와 10%는 여기서 갈린다. MRR은 0%가 앞서고 Hit@k·Recall@k는 10%가 앞선다.

이 시점에서는 판단이 안 서서 k를 정할 때 두 값을 같이 들고 갔다.



같은 overlap도 chunk가 바뀌면 결과가 뒤집힌다

overlap 비율은 chunk와 묶여 있어서 고정된 값이 아니다.

같은 테스트를 chunk 1300 위에서 돌리면 이렇게 나온다.


overlapchunk 1300chunk 800
0%0.4950.501
10%0.5260.485
20%0.5330.424


결과가 정반대로 나왔다.

1300에서는 20%가 제일 좋은데 800에서는 제일 안좋게 나왔다.


같은 160자라도 1300짜리 chunk에서는 12%지만 800짜리에서는 20%다.

chunk가 작을수록 overlap이 차지하는 비중이 커지고, 그만큼 중복의 손해도 커진다.


비록 k=7에서 잰 값이긴 하지만 chunk를 800으로 정해놓고 overlap은 1300에서 잰 결과를 가져다 썼다면

800에서 가장 안좋은 값을 쓰고 있었을 것이다.



분할 경계 확인

overlap은 원래 chunk가 잘린 자리의 문맥을 이어주려고 주는 값이다.

1
2
3
chunk 1: ...A B C D E
chunk 2:         D E F G H    ← D, E가 overlap
chunk 3:                 G H I J K


overlap을 주면 두 번째 chunk만 꺼내 봐도 앞 문맥이 이어진다.

그러면 0으로 두면 문장이 중간에 잘리지 않을까?


overlap을 0으로 두면 문장이 잘릴 수 있어서 경계를 전부 세어봤다.

chunk 800 / overlap 0에서 생긴 분할 경계는 376개였다.


경계가 떨어진 자리건수비율
줄바꿈34692.0%
표·목록·제목 사이287.4%
한 문단 안20.5%


문장이 중간에 끊긴 사례는 0건이었다.

한 문단 안에서 갈린 2건도 마침표 뒤에서 갈렸거나 제목만 앞 chunk 끝에 남은 경우였다.


그럴 수밖에 없는 이유가 세 가지 있었다.


1. 문서가 표와 번호 목록 위주라 한 줄이 짧다

RecursiveCharacterTextSplitter는 자를 자리를 \n\n\n → 공백 순으로 찾는다.

800자 안에 줄바꿈이 흔하면 거의 항상 줄 경계에서 끊긴다.


2. 애초에 안 잘리는 섹션이 절반이 넘는다

헤더로 나눈 섹션 273개 중 144개(53%)가 800자 이하다.

섹션 길이 중앙값이 697자라 대부분은 통째로 chunk 하나가 된다.


3. 문맥을 잡아주는 건 overlap이 아니라 헤더였다

1
2
3
4
MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "H1"), ("##", "H2"), ("###", "H3")],
    strip_headers=False,
)

strip_headers=False라 모든 chunk가 헤더를 달고 있다.

표 중간에서 잘린 chunk도 “공격 영웅 / 겐지 / 상성 및 궁합 데이터”라는 소속을 그대로 갖는다.


다만 이 결과는 이 문서라서 나온 것이다.
줄바꿈 없이 긴 문단이 이어지는 문서였다면 다르게 나왔을 것이다.





세번째 test : top-k

마지막으로 몇 개를 가져올지 정했다.

overlap 0%와 10% 둘 다 k 곡선을 뽑았고, 아래는 0% 결과다.


kMRRHit@kRecall@k평균 토큰
30.4870.5600.473960
50.5050.6400.5331,527
70.5050.6400.5332,106
100.5230.8000.6232,949


수치를 보니 7에서 10으로 올릴 때 Hit@k가 0.640에서 0.800으로 뛴다.

즉 25개 질문 중 정답 문서를 못 가져오던 9개가 5개로 줄었다.


k=10에서는 두번째 test에서 갈렸던 우열도 정리된다.

overlap 0%가 MRR 0.523 / Hit 0.800 / Recall 0.623 / 2,949 토큰이고

10%가 0.497 / 0.760 / 0.613 / 3,108 토큰이라 0%가 전 지표에서 앞선다.

overlap으로 문맥을 메우던 효과는 k를 늘려 chunk를 더 가져오는 것으로 대체되고, 중복의 손해만 남는다.





네번째 test : 세 chunk를 같은 조건으로 다시

두번째 test에서 overlap이 chunk에 따라 달라지기 때문에 chunk는 고정이 아니다.

첫번째 test에서 overlap 10% / k=7 라는 한 조건에서만 쟀는데 해당 조건이 각 chunk의 최적이 아닐 수도 있다.


그래서 세 chunk 전부 overlap 0/10/20% × k 3·5·7·10을 돌려서 각자의 최적을 찾고 다시 비교했다.


k는 chunk·overlap 조합마다 3·5·7·10을 전부 쟀다. 36개 조합에서 k=10이 전부 최고이거나 동률이었다.

동률이 나온 건 chunk 400의 0%와 20% 두 곳뿐이고, 여기서는 k=7과 지표가 같으면서 토큰만 더 든다.

그래서 아래 비교는 k=10 기준으로 뽑았다.


overlapchunk 400chunk 800chunk 1300
0%0.6000.8000.800
10%0.6800.7600.720
20%0.6400.7200.760


k가 바뀌면 최적 overlap도 바뀐다

같은 chunk인데도 k를 바꾸면 제일 좋은 overlap이 달라졌다.

Hit@k 기준으로 k=7과 k=10을 나란히 놓으면 이렇다.


chunkk=7 최적 overlapk=10 최적 overlap
40020% (0.640)10% (0.680)
80010% (0.680)0% (0.800)
13000%·20% 동률 (0.720)0% (0.800)


세 chunk 전부 k=7에서 고른 overlap과 k=10에서 고른 overlap이 다르다.

즉 overlap을 먼저 확정하고 k를 나중에 정하면 이미 틀린 overlap 위에서 k를 재는 셈이다.


k=10에서는 세 chunk 모두 0% 아니면 10%로 모인다.

overlap으로 문맥을 메우던 효과가 k를 늘려 chunk를 더 가져오는 것으로 대체되는 게

800뿐 아니라 1300에서도 같은 방향으로 나왔다.


그런데 400은 예외였다. chunk가 제일 작은데도 0%가 아니라 10%가 제일 좋게 나왔다.

k=10에서 0%는 0.600, 10%는 0.680으로 차이가 있었다.


chunk가 작을수록 overlap이 크게 나오면 안된다 라고 정리하기 애매했다.

최적 overlap은 chunk와 k 둘 다 반영되어있다.


k를 올려서 얻는 이득도 chunk마다 달랐다.

각자 최적 overlap에서 k를 7에서 10으로 올렸을 때 Hit@k 상승폭은 800이 0.160으로 제일 크고, 400과 1300은 둘 다 0.080이었다.

chunk가 작을수록 k를 올리는 값이 크다고 보기도 애매했다.



각자 최적 조합끼리 비교


조합MRRHit@kRecall@k평균 토큰
chunk 400 / overlap 10% / k=100.4550.6800.5671,596
chunk 800 / overlap 0 / k=100.5230.8000.6232,957
chunk 1300 / overlap 0 / k=100.5110.8000.6334,388


400은 토큰이 800의 54%밖에 안 되지만 Hit@k가 0.120 낮다.

25개 중 정답을 못 가져오는 질문이 5개에서 8개로 늘어나는 차이라 토큰을 아낀 만큼 잃는다.


800과 1300은 Hit@k가 같다.

Recall@k는 1300이 0.010 높고 MRR은 800이 0.012 높은데, 둘 다 질문 하나가 순위 몇 칸 움직이면 뒤집히는 폭이다.

하지만 토큰은 800이 33% 싸다.


첫번째 test에서 “정확도는 사실상 같은데 토큰만 비싸다”고 판단해 800을 골랐는데,

각 chunk를 제 최적으로 맞춰놓고 봐도 같은 결과가 나왔다.


같은 조합(chunk 800 / overlap 0 / k=10)인데 세번째 test 표에는 토큰이 2,949로, 이 표에는 2,957로 적혀 있다. 두 test는 인덱스를 각각 새로 만들어 돌렸는데, 점수가 거의 같은 chunk끼리는 순위가 조금 바뀔 수 있다. 10위 근처에서 그런 일이 생기면 가져오는 chunk가 한두 개 달라지고 길이도 달라져서 토큰 평균이 8 차이 났다. MRR·Hit@k·Recall@k는 소수점까지 같아서 같은 설정이 재현된 것으로 봤다.





최종 설정

1
2
3
chunk_size=800,
chunk_overlap=0,
search_k=10,


⚠️ 주의: 이 셋 중 하나라도 바꾸면 VectorDB를 다시 만들어야 한다.
chunk 경계가 달라지면 기존 인덱스와 섞이면 안 되기 때문이다.





마무리

정리하면 이런 순서로 정했다.

1
2
3
4
5
6
① 답변 품질 대신 검색을 재기로 하고, 질문 25개로 평가셋을 만든다
② 정답은 chunk 번호가 아니라 헤더 조합으로 라벨링한다 (조합을 바꿔도 채점되도록)
③ chunk 크기를 후보로 좁힌다 → 800
④ 정해진 chunk 위에서 overlap을 잰다 → 0
⑤ 같은 조건에서 k를 정한다 → 10
⑥ 세 chunk를 같은 격자로 다시 재서 ③의 선택을 확인한다


③에서 chunk를 정하고 ④에서 overlap을 쟀는데, chunk가 바뀌면 최적 overlap도 바뀌었다.

그러면 고정된 overlap 위에서 고른 ③도 다시 봐야 한다. 그게 ⑥이다.


⑥까지 하고 나니 두번째 test에서 적었던 “chunk가 작을수록 overlap이 손해”라는 정리도 400에서는 맞지 않았다.

즉 한 chunk에서 본 결과를 다른 chunk로 그대로 옮겨 쓰면 안 된다.


overlap도 0이 제일 좋게 나왔지만, 경계를 세어보니 이 문서가 표와 목록 위주라서 나온 결과였다.

This post is licensed under CC BY 4.0 by the author.