왜 문제가 되는가
롱컨텍스트 모델은 입력을 넣는 만큼 잘 쓴다는 보장이 없다. 여러 문서를 프롬프트에 이어 붙이면, 모델은 맨 앞과 맨 뒤에 놓인 정보는 잘 활용하지만 가운데에 놓인 정보는 자주 놓친다. 이 U자형 성능 곡선을 흔히 "lost in the middle"이라 부른다. RAG 파이프라인에서 관련 문서를 20~40개씩 넣는데도 답이 엉뚱하다면, 검색 품질이 아니라 배치 순서가 원인인 경우가 많다.
실무에서 이 편향은 조용히 비용을 늘린다. 정답 근거가 컨텍스트 안에 분명히 있는데도 모델이 무시하고, 이를 검색 실패로 오진해 top-k를 더 키우면 가운데 구간만 두꺼워져 상황이 더 나빠진다.
먼저 측정하라
대응 이전에 우리 파이프라인이 실제로 편향을 겪는지 확인해야 한다. 정답을 담은 "needle" 문서를 컨텍스트의 각 위치에 심고 정확도를 측정하는 간단한 프로브가 유효하다.
import numpy as np
def probe_position_bias(client, question, answer, gold_doc, fillers):
# gold_doc을 0..N 위치에 각각 삽입해 정확도 측정
scores = []
for pos in range(len(fillers) + 1):
docs = fillers[:pos] + [gold_doc] + fillers[pos:]
ctx = "\n\n".join(f"[{i}] {d}" for i, d in enumerate(docs))
resp = client.ask(context=ctx, question=question)
scores.append(int(answer.lower() in resp.lower()))
return np.array(scores) # U자형이면 가운데 인덱스가 낮게 나온다
위치별 정확도가 평평하면 편향이 없는 것이고, 가운데가 꺼지면 아래 대응이 필요하다.
대응 1: 재정렬(reranking + 배치)
가장 비용 대비 효과가 큰 방법이다. 크로스 인코더로 관련도를 매긴 뒤, 가장 관련 높은 문서를 양 끝에 배치하고 낮은 것을 가운데로 몰아넣는다.
def edge_pack(docs_scored):
# (doc, score) 리스트를 관련도 내림차순으로 정렬
ranked = sorted(docs_scored, key=lambda x: x[1], reverse=True)
head, tail = [], []
for i, (doc, _) in enumerate(ranked):
(head if i % 2 == 0 else tail).append(doc)
# 높은 것: 앞/뒤 끝, 낮은 것: 중앙
return head + list(reversed(tail))
이 "edge packing"은 U자 곡선의 강한 두 지점에 중요한 근거를 정확히 얹는다.
대응 2: 컨텍스트를 줄여라
가운데를 아무리 잘 배치해도 넣는 문서가 많으면 신호가 희석된다. 관련도 임계값으로 top-k를 잘라내고, 문서 내부도 관련 문단만 남기는 것이 낫다. "많이 넣으면 안전하다"는 직관은 이 문제에서는 틀린다.
| 전략 | 중앙 근거 적중 | 토큰 비용 | 구현 난이도 |
|---|---|---|---|
| 단순 top-k 확장 | 낮음 | 높음 | 낮음 |
| rerank + edge packing | 높음 | 중간 | 중간 |
| 임계값 컷 + 문단 압축 | 중간 | 낮음 | 중간 |
대응 3: 근거를 명시적으로 인용시켜라
각 문서에 ID를 붙이고, 답변에 사용한 문서 ID를 반드시 인용하게 하면 모델이 가운데 구간을 강제로 훑는 효과가 있다. 인용이 비면 재검색 트리거로도 쓸 수 있다.
SYSTEM = """각 문서는 [id] 형식이다.
답변 근거로 사용한 모든 문서 id를 답변 끝에 CITED: [id,...] 로 나열하라.
근거가 없으면 CITED: [] 로 표기하고 모른다고 답하라."""
주의점
몇 가지 함정이 있다. 첫째, 위치 편향의 강도는 모델·버전마다 다르므로 재정렬 로직을 한 모델에 과적합시키지 마라. 모델 교체 시 프로브를 다시 돌려야 한다. 둘째, edge packing은 문서 간 순서가 의미를 갖는 경우(시계열 로그, 코드 diff)에는 오히려 해가 된다. 이때는 순서를 보존하되 top-k를 줄이는 쪽을 택한다. 셋째, 인용 강제는 지연시간과 토큰을 늘리므로 정확도가 실제로 개선되는지 A/B로 확인하고 적용한다.
정리하면, 검색을 더 키우기 전에 위치를 먼저 의심하라. 측정 → 재정렬 → 압축 → 인용 순으로 접근하면 대부분의 lost in the middle은 코드 몇 줄로 눈에 띄게 완화된다.