지난 글까지가 무엇을 넣고 무엇을 접을지를 정하는 얘기였다. 남은 결정이 하나 있다. 넣기로 한 것들을 어떤 순서로 늘어놓을 것인가. 이 결정은 검색 파이프라인을 만들 때 대개 무의식적으로 내려진다 — 검색기가 돌려준 순서를 그대로 이어 붙이는 것이다. 코드 한 줄이라 결정이라는 자각도 없다.
그런데 창 안에서 위치에 따라 정확도가 달라진다는 사실을 인정하고 나면, 이 한 줄이 공짜가 아니라는 것도 따라 나온다. 관련도 1위 문서를 가장 덜 읽히는 자리에 두는 배치가 실제로 존재하기 때문이다.
언제부터 문제가 되는가
먼저 범위를 좁혀 두자. 조각이 서넛뿐이면 어떻게 늘어놓든 차이가 거의 없다. 전부 창의 앞쪽에 몰려 있어 위치 효과가 생길 여지가 없다.
배치가 의미를 갖는 조건은 셋이다.
| 조건 | 왜 |
|---|---|
| 조각이 여덟 개를 넘는다 | 가운데라 부를 만한 구간이 생긴다 |
| 조각 하나가 길다 | 개수가 적어도 총 길이가 길면 같은 일이 벌어진다 |
| 관련도 편차가 크다 | 1위와 8위가 비슷비슷하면 순서를 바꿔도 얻을 게 없다 |
셋 다 아니라면 이 글의 나머지는 안 읽어도 된다. 검색 개수를 다섯으로 줄이는 편이 배치를 고민하는 것보다 확실하다.
세 가지 배치
순위 그대로. 검색기 출력을 그대로 쓴다. 1위가 창의 앞머리에 온다. 앞머리는 잘 읽히는 자리이므로 나쁜 선택이 아니고, 무엇보다 구현이 없다는 것이 장점이다.
역순. 1위를 질문 바로 앞에 둔다. 질문에 가장 가까운 자리라 모델이 마지막으로 본 내용이 되고, 답을 쓰기 직전의 맥락으로 작동한다. 한 조각만 근거로 삼으면 되는 단순 질의응답에서 이 배치가 유리한 경우가 많다.
샌드위치. 홀수 순위를 앞에서부터, 짝수 순위를 뒤에서부터 채워 상위권이 양 끝에 오게 한다. 앞과 뒤가 둘 다 잘 읽히는 자리라는 관찰을 그대로 이용하는 배치다. 조각이 열 개를 넘고 여러 근거를 종합해야 하는 과제에서 가장 안정적이다.
이 세 배치는 같은 목적함수의 서로 다른 해로 볼 수 있다. 조각 의 관련도를 , 그 조각이 놓인 위치의 읽힘 가중치를 라 하면 우리가 키우려는 것은 이 값이다.
가 앞뒤로 높고 가운데가 낮은 U자라면, 를 최대로 만드는 배치는 큰 를 큰 자리에 붙이는 것이다. 그게 샌드위치다. 반대로 가 뒤로 갈수록 단조 증가한다고 보면 답은 역순이 된다. 어느 쪽이 맞는지는 모델과 길이에 따라 달라지므로, 결국 재 봐야 안다.
관련도만으로 정하지 않는다
배치를 관련도 순서 문제로만 보면 놓치는 것이 있다. 조각들은 서로 독립이 아니다.
원문에서 이웃한 조각은 붙여 둔다. 같은 문서의 3절과 4절이 각각 2위와 6위로 뽑혔다면, 이 둘을 떼어 놓는 배치는 문맥을 끊는다. 4절의 "이 경우"가 3절을 가리키고 있는데 사이에 다른 문서가 끼면 모델이 그 지시어를 엉뚱한 곳에 연결한다. 그래서 실무에서는 문서 단위로 묶은 뒤 묶음끼리 배치하는 편이 안전하다.
def arrange(chunks):
# 1) 같은 문서의 조각을 묶고 원문 순서로 정렬
groups = defaultdict(list)
for c in chunks:
groups[c.doc_id].append(c)
for g in groups.values():
g.sort(key=lambda c: c.position)
# 2) 묶음의 대표 점수 = 그 안 최고 점수
ranked = sorted(groups.values(), key=lambda g: max(c.score for c in g), reverse=True)
# 3) 샌드위치로 펼친다
head, tail = [], []
for i, g in enumerate(ranked):
(head if i % 2 == 0 else tail).extend(g)
return head + list(reversed(tail))
시간 순서가 의미를 갖는 데이터라면 그쪽을 따른다. 회의록, 로그, 변경 이력처럼 순서 자체가 정보인 자료를 관련도 순으로 섞으면 모델이 시간 관계를 잘못 읽는다. 이런 자료는 관련도로 고르되 시간순으로 늘어놓는다. 고르는 일과 늘어놓는 일을 분리하면 이 구분이 자연스러워진다.
경계를 분명히 표시한다
조각 사이의 구분이 흐릿하면 모델이 두 문서의 내용을 하나로 이어 읽는다. 서로 다른 제품의 가격표 두 개가 붙어 있으면 답에서 값이 섞여 나온다.
--- [1] 문서: 배포 정책 v3 / 절: 2.1 / 갱신: 2026-05-14 ---
(본문)
--- [2] 문서: 배포 정책 v2 / 절: 2.1 / 갱신: 2025-11-02 ---
(본문)
머리말에 넣을 값은 세 가지면 충분하다. 어느 문서인지, 어느 부분인지, 언제 것인지. 특히 갱신일은 값이 크다. 같은 절의 옛 판과 새 판이 함께 뽑히는 일이 흔한데, 날짜가 적혀 있으면 모델이 새 쪽을 고른다. 없으면 앞에 있는 쪽을 고른다.
번호를 붙여 두는 것도 이유가 있다. 답에 근거를 인용하라고 지시할 때 모델이 이 번호를 쓴다. 그러면 답을 검증할 때 사람이 원문을 되짚기 쉽고, 자동 검증도 붙일 수 있다.
중복은 배치 전에 걷어 낸다
검색 결과에는 거의 같은 내용이 여러 조각으로 들어온다. 청킹이 겹치게 잘려 있거나, 같은 문단이 여러 문서에 복사돼 있어서다. 이걸 그대로 넣으면 두 가지 손해가 겹친다. 창을 먹고, 같은 말이 여러 번 나온다는 사실 자체가 그 내용을 더 중요해 보이게 만든다.
제거는 단순한 기준으로 충분하다. 이미 고른 조각들과의 최대 유사도가 임계를 넘으면 버린다.
def dedupe(ranked, threshold=0.92):
kept = []
for c in ranked:
if all(cosine(c.vec, k.vec) < threshold for k in kept):
kept.append(c)
return kept
임계를 너무 낮게 잡으면 관점이 다른 근거까지 날아가므로, 0.9 언저리에서 시작해 평가셋으로 조정한다. 그리고 버린 것을 로그로 남겨 두면 나중에 "왜 그 문서가 안 들어갔지"를 추적할 수 있다.
재순위와 배치는 다른 일이다
여기서 자주 뒤섞이는 개념을 갈라 두자. 재순위는 무엇을 넣을지 고르는 정확도를 올리는 일이고, 배치는 넣기로 한 것을 덜 낭비하는 일이다. 재순위 모델을 붙였다고 배치 문제가 사라지지 않고, 배치를 잘한다고 나쁜 검색이 좋아지지도 않는다.
순서로는 재순위가 먼저다. 잘못 뽑힌 문서를 어디에 두든 답은 안 나온다. 재순위로 상위권의 질을 확보한 다음, 남은 열 개를 어떻게 늘어놓을지가 배치의 몫이다.
재 보는 방법
배치는 코드 몇 줄로 바꿀 수 있어서 실험 비용이 아주 싸다. 대신 차이가 작아서 대충 재면 잡음에 묻힌다.
- 조각 개수를 고정하고 배치만 바꿔 세 가지를 각각 돌린다.
- 질문을 두 종류로 나눠 따로 집계한다 — 근거 하나로 답하는 것, 근거 여럿을 종합하는 것.
- 정답이 몇 번 조각에 있었는지를 함께 기록한다.
마지막 항목이 진단의 핵심이다. 정답이 상위권에 있었는데 틀렸다면 배치나 프롬프트 문제이고, 정답 조각이 아예 안 뽑혔다면 배치를 아무리 바꿔도 소용없다. 이 둘을 구분하지 않으면 검색이 문제인 시스템에서 배치만 몇 주씩 만지게 된다.
대개의 결론은 이렇다. 조각이 적으면 아무 배치나 쓰고, 많으면 샌드위치를 기본으로 두고, 시간 순서가 의미 있는 자료는 그쪽을 따른다. 그리고 이 결정보다 훨씬 큰 이득은 조각 개수 자체를 줄이는 데서 나온다.
읽어주셔서 감사합니다. 😊

