
답변에 실제로 필요한 내용만 남기도록 RAG 컨텍스트를 가지치기(Pruning)하는 방법
요약
RAG 시스템에서 답변에 불필요한 컨텍스트를 제거하여 비용을 절감하는 가지치기(Pruning) 기법을 소개합니다. 작은 LLM을 활용해 재현율 96%를 유지하면서도 컨텍스트의 68%를 제거하여 쿼리 비용을 3분의 1로 줄이는 방법을 다룹니다.
핵심 포인트
- 작은 LLM을 활용해 제너레이터 전달 전 불필요한 청크 제거
- 재현율 96% 유지하며 컨텍스트의 약 68% 압축 성공
- 쿼리 비용을 기존 대비 약 3분의 1 수준으로 절감
- 에이전트의 컨텍스트 부패 방지 및 효율적인 정보 공간 확보
우리가 작은 LLM에게 RAG 컨텍스트의 68%를 버리도록 가르친 방법
재현율(Recall)의 96%를 유지하면서, 에이전트 컨텍스트를 답변에 실제로 필요한 내용으로 가지치기(Pruning)하기
작성자
Lars Baltensperger
Kapa는 방대한 제품 지식 베이스(Knowledge bases)에 대해 복잡한 질문에 답하는 AI 어시스턴트를 구축합니다. 기술 문서, API 레퍼런스, PDF, 포럼, 지원 스레드 등을 생각하면 됩니다. 개발자들은 우리의 검색(Retrieval) API를 사용하여 에이전트에게 제품에 대한 컨텍스트를 제공하며, 동일한 검색 레이어가 우리의 엔드투엔드(End-to-end) 어시스턴트를 구동합니다.
2026년 내내 에이전트에게 여전히 RAG가 필요한지에 대한 논쟁이 있었지만, 우리의 도메인에서는 지식 베이스가 크고 복잡해질 때 RAG를 대체할 수 있는 것이 없습니다. 우리의 검색은 에이전트 방식(Agentic)부터 단일 패스(Single-pass) 방식까지 여러 형태로 제공되지만, 모두 동일한 구조를 공유합니다. 즉, 질문과 관련된 문서 청크(Chunks)를 찾는 리트리버(Retriever)와, 이를 바탕으로 답변을 작성하는 LLM인 제너레이터(Generator)로 구성됩니다.
이 포스트의 요약: 우리는 이 두 단계 사이에 세 번째 단계를 추가했습니다. 작고 저렴한 LLM이 질문과 검색된 모든 청크를 함께 읽고, 비용이 많이 드는 모델이 이를 보기 전에 답변에 필요하지 않은 청크를 버립니다. 이 방식은 컨텍스트의 약 68%를 제거하면서도 재현율(Recall)의 약 96%를 유지하며, 자체 비용을 제외하고 쿼리 비용을 3분의 1로 절감합니다. 이 포스트에서는 우리가 어떻게 이 단계에 도달했는지 설명합니다.

무시된 청크도 비용이 발생합니다
리트리버는 깔때기와 같습니다. 임베딩(Embedding) 및 키워드 검색(Keyword search)은 수십만 개의 청크로 구성된 지식 베이스를 수백 개의 후보로 줄이고, 리랭커(Reranker)가 이를 정렬하며, 상위 15개 정도가 체인에서 가장 크고 비용이 많이 드는 모델인 제너레이터에 도달합니다. 그럼에도 불구하고 제너레이터가 읽는 내용의 대부분은 질문에 필요하지 않습니다. 이는 의도적인 것입니다. 리트리버는 최대 재현율(Recall)을 목표로 하며, 제너레이터가 노이즈를 무시할 것이라고 신뢰하기 때문입니다.
하지만 제너레이터(Generator)는 무시하는 모든 청크(Chunk)에 대해 비용을 지불합니다. 당사의 어시스턴트(Assistants)의 경우, 검색된 청크는 쿼리(Query) 비용의 약 3분의 2를 차지하며, 이는 답변, 대화 기록, 시스템 프롬프트(System Prompt)를 모두 합친 것보다 더 많습니다. 청크를 하나씩 줄일 때마다 쿼리 비용을 약 4%씩 절감할 수 있습니다. 또한 에이전트(Agent)의 경우, 모든 도구 호출(Tool call)이 동일한 컨텍스트(Context)에 출력을 쏟아붓기 때문에 컨텍스트가 빠르게 증가합니다. 따라서 더 정교한 검색 결과는 에이전트가 유지해야 할 다른 모든 정보를 위한 공간을 확보해주며, 부패(Rot)할 컨텍스트를 줄여줍니다.

문제는 재현율(Recall)입니다. 답변에 필요한 청크를 누락하면, 단 몇 센트를 아끼려다 잘못된 답변을 얻게 됩니다. 프루너(Pruner, 가지치기 도구)의 성능은 정확히 이 트레이드오프(Tradeoff)에 달려 있습니다. 즉, 손실된 재현율 1포인트당 얻은 압축률이 성능의 척도입니다.
명백해 보이는 해결책은 작동하지 않습니다
우리는 이미 상위 K개를 반환하기 전에 리랭크(Rerank)를 수행하므로, 때때로 리랭크 점수를 그대로 공개하여 호출자가 이를 기준으로 자를 수 있게 해달라는 요청을 받습니다. 예를 들어, 0.7 이상의 것은 유지하고 나머지는 버리는 식입니다. 하지만 이는 두 가지 이유로 실패하며, 두 번째 이유가 우리가 구축한 모든 것의 형태를 결정했습니다.
첫째, 리랭크 점수는 순위(Ordering)를 나타내는 것이지 측정값(Measurement)이 아닙니다. 이는 단지 청크 A가 이 쿼리에 대해 청크 B보다 낫다는 것을 의미할 뿐, 그 이상도 이하도 아닙니다. 점수는 쿼리 전반에 걸쳐 보정(Calibrated)되어 있지 않으며, Cohere 또한 이를 명시하고 있습니다. 따라서 고정된 임계값(Cutoff)은 작동하지 않습니다. 랭킹(Ranking)이 지원하는 유일한 임계값은 위치 기반의 Top-N 방식뿐이며, 이는 마지막 청크가 노이즈인지 답변인지와 상관없이 무조건 버리게 됩니다.
둘째, 이는 완벽한 보정(Calibration)이 이루어지더라도 여전히 남는 문제입니다. 관련성(Relevance)은 단일 청크의 속성이 아닙니다. 대부분의 파이프라인에 있는 리랭커(Reranker)는 포인트와이즈 크로스 인코더(Pointwise Cross-encoders)입니다. 이들은 각 쿼리-청크 쌍을 단독으로 점수 매기며, 함께 검색된 다른 청크들과 함께 고려하지 않습니다. 다음은 익명화된 실제 운영 사례입니다:

두 번째 청크는 감사 로그(audit logs)를 전혀 언급하지 않으므로 노이즈로 점수가 매겨지지만, 실제로는 답변의 절반을 차지하고 있습니다. 하지만 포인트와이즈(pointwise) 점수 방식으로는 이를 파악할 수 없는데, 해당 청크는 첫 번째 청크와 함께 있을 때만 관련성이 있기 때문입니다. 또한 청크들이 다중 부분 질문(multi-part questions)을 나누어 가질 경우, 각 청크는 단독으로는 무용지물이 됩니다. 진짜 문제는 청크가 단독으로 관련이 있느냐가 아니라, 질문에 함께 답할 수 있는 세트에 속해 있느냐 하는 것입니다.
동일한 방식으로 실패한 영리한 해결책
리랭커(reranker)를 포기하기 전, 우리는 앵커 문서(anchor documents, Sinhababu et al.) 방식을 시도했습니다. 즉, 순위 산정 과정에 알려진 관련성을 가진 합성 청크(synthetic chunks)를 심어 리랭커의 척도를 절대적으로 만드는 방식입니다. '필수적(Essential)'부터 '관련 없음(Unrelated)'까지 각 단계별로 하나씩 작성된 청크를 배치한 뒤, 유지하고자 하는 최하위 단계의 앵커보다 낮은 순위를 기록하는 모든 실제 청크를 제거하는 것입니다. 이미 실행 중인 리랭크 과정에 LLM 호출을 한 번 더 추가하는 것으로, 진정으로 우아한 방식이었습니다.
하지만 근본적인 이유로 인해 이 방식도 작동하지 않았습니다. 앵커는 캘리브레이션(calibration)은 고정할 수 있지만 점수 자체를 고칠 수는 없으며, 리랭커는 부분적이고 간접적으로 관련 있는 청크를 명백히 무관한 청크보다 낮은 순위에 계속 배치했습니다. 이들을 유지하려면 앵커를 너무 낮게 설정해야 해서 거의 아무것도 가지치기(pruning)할 수 없게 됩니다.
그 실패가 오히려 유용한 결과가 되었습니다. 가지치기를 수행하는 무엇이든 질문과 모든 청크를 동시에 보아야 한다는 사실을 깨달았기 때문입니다. 왜냐하면 판단 대상은 개별 청크가 아니라 '세트(set)'이기 때문입니다.
그래서 우리는 LLM이 청크를 채점하도록 했습니다
우리가 실제로 적용한 방식은 리랭커와 생성기(generator) 사이에 리스트와이즈(listwise) LLM 호출을 한 번 수행하는 것입니다. 이 호출은 질문과 모든 청크를 전달받아, 프롬프트에 작성된 5단계 척도에 따라 각 청크를 채점합니다.
|
|
|
5 | ESSENTIAL (필수) | 이 청크가 직접적인 답변을 제공하거나, 정의를 제공하거나, 다른 청크가 의존하는 전제 조건인 경우 등, 이 청크 없이는 답변을 생성할 수 없음. |
4 | CONTRIBUTING (기여) | 단독으로는 답변할 수 없으나, 다른 청크와 결합하여 완전한 답변에 필요한 요소를 제공함. |
3 | SUPPORTING (지원) | 주제와 관련이 있고 유용할 가능성이 높으나, 이 청크 없이도 답변이 완성될 가능성이 높음. |
2 | TANGENTIAL (지엽적) | 동일한 도메인이거나 용어를 공유하지만, 구체적인 기여는 없음. |
1 | UNRELATED (무관) | 의미 있는 연결 고리가 없음. |
임계값(Threshold) 이상의 점수를 받은 청크들만 살아남습니다. 이 설계는 앞서 언급한 두 가지 실패 사례를 모두 해결합니다. 각 단계가 언어로 정의되어 있기 때문에, 4점은 모든 쿼리에서 동일한 의미를 가지며, 따라서 고정된 컷오프(Cutoff)가 마침내 작동하게 됩니다. 또한 모델이 질문과 모든 청크를 함께 보기 때문에 전체 집합을 판단할 수 있으며, 결과적으로 부분적이고 간접적인 관련성도 분류될 수 있는 위치를 갖게 됩니다.
세 가지 조절 요소(Knobs)가 중요합니다:
모델 (The model): 가지치기(Pruning) 과정에 드는 비용은 절감된 비용 내에서 충당되어야 하므로, 구조적으로 플래그십(Flagship) 모델은 제외됩니다. 모든 모델이 유사하게 평가되므로, 우리는 추론 노력(Reasoning effort)이 낮으면서 가장 빠르고 저렴한 모델을 선택했습니다.
임계값 (The threshold): 압축(Compression)과 재현율(Recall) 사이를 조절하는 주요 다이얼입니다.
keep-top-k: 점수와 상관없이 상위 몇 개의 재순위화(Reranked)된 청크를 유지하여, 가장 강력한 청크들이 채점 실수로 인해 탈락하는 것을 방지합니다.
우리는 객관성을 유지하기 위해 두 가지 더 단순한 설계도 실행해 보았습니다. Budget-select: 상위 몇 개를 유지하고, LLM이 최대 N개를 추가하도록 합니다. 크기는 예측 가능하지만, 예산이 소진되면 관련성에 상관없이 이후의 모든 청크는 버려집니다. 그리고 가장 단순한 가지치기 방식: 척도 없이 LLM에게 어떤 청크를 유지할지 직접 묻습니다. 만약 어떤 방식이 직접 묻는 것보다 나은 결과를 내지 못한다면, 그것을 구축할 가치가 없습니다.
결과
우리는 답변에 정확히 어떤 청크가 필요한지 알고 있는 실제 질문의 레이블링된 세트(Labeled set)에서 재현율(Recall)을 측정했습니다. 그 다음, 실제 생성기(Generator)로 전송된 각 쿼리의 정확한 청크들을 대상으로, 무작위로 추출된 한 달간의 운영 대화 데이터를 재현하여 압축률, 비용 및 지연 시간(Latency)을 검증했습니다.

각 점은 하나의 설정을 나타내며, 중요한 두 가지 요소를 기준으로 플로팅(plotting)되었습니다. x축의 압축률(Compression)은 프루너(pruner)가 버리는 검색된 청크(chunk)의 비율을 의미합니다. y축의 보존된 재현율(Recall preserved)은 프루닝(pruning) 후에도 답변에 필요한 모든 청크를 여전히 보유하고 있는 질문의 비율입니다. 100%는 어떤 질문도 필요한 청크를 잃지 않았음을 의미하며, 90%는 열 개 중 하나가 청크를 잃었음을 의미합니다. 그래프에서 오른쪽 상단으로 갈수록 더 좋습니다. 선들은 각 전략의 최적 설정들을 연결하며, 점선 형태의 회색 선은 모든 프루너가 넘어서야 할 기준점(baseline)인 단순 Top-N 절단(truncation, 리랭커(reranker)로부터 더 적은 수의 청크만 반환하는 방식)을 나타냅니다.
모든 전략이 이 기준점을 압도적으로 앞섭니다. 재현율을 98%로 유지할 경우, 절단(truncation) 방식은 청크를 하나 줄일 수 있으며 압축률은 약 7%에 그칩니다. 반면 모든 LLM 전략은 30% 이상의 압축률을 달성하며, 관련성 점수 산출(relevance scoring) 방식은 청크의 거의 절반을 제거합니다. 또한 점수 산출 방식은 모든 압축 수준에서 나머지 두 방식보다 우세하므로, 남은 결정 사항은 해당 선 위의 어느 지점에 위치할 것인가뿐이었습니다.
우리는 공격적인(aggressive) 끝단 근처의 지점을 선택했습니다. 보존된 재현율은 약 96%, 제거된 청크는 약 68%입니다. 25개 질문 중 하나가 필요한 청크를 잃게 되지만, 그 대가로 컨텍스트(context)의 3분의 2가 사라지며, 프루너 자체 비용을 제외한 쿼리당 비용은 약 34% 감소합니다.
지연 시간(Latency) 측면에서의 비용
프루너는 검색(retrieval)과 생성(generation) 사이의 임계 경로(critical path)에서 실행되므로, 프루너의 모델 호출(model call)은 모든 쿼리에 추가되며 그 속도가 비용을 결정합니다. 실제 운영 데이터 세트 전체를 대상으로 했을 때, 우리가 선택한 설정은 쿼리당 약 0.7초가 소요되었습니다. 더 무거운 설정은 시간이 급격히 늘어나므로, 추가되는 시간을 1초 미만으로 유지하기 위해서는 추론 노력(reasoning effort)이 낮은 소형 모델(small model)을 사용하는 것이 핵심입니다.
그 대가로 생성 속도는 거의 빨라지지 않습니다. 청크 수가 적어지면 생성기(generator)를 위한 입력 토큰(input token)이 줄어들어 응답을 아주 조금 더 빨리 시작하기는 하지만, 이는 불과 0.x초 수준이며 프루너의 호출 시간을 상쇄하기에는 턱없이 부족합니다.
따라서 프루닝 (Pruning)은 우리가 제공하는 설정 기준으로 1초 미만의 작고 고정된 지연 시간 (Latency)을 대가로 압축을 얻습니다. 이는 지연 시간에 민감한 싱글샷 (Single-shot) 경로에서는 실제로 고려해야 할 비용입니다. 하지만 턴당 이미 여러 번의 모델 호출을 수행하는 에이전트 (Agent) 내부에서는, 한 번의 더 가벼운 호출은 미미한 수준입니다.
적용 사례
우리는 검색 (Retrieval)이 여러 도구 중 하나로 사용되는 곳에 이 기능을 가장 먼저 출시했습니다. 즉, 우리의 검색 기능을 기반으로 에이전트를 구축하는 고객들입니다. 에이전트는 수십 개의 도구를 보유하며, 모든 호출은 동일한 컨텍스트 (Context)에 출력을 쏟아붓습니다. 이때 문서 검색 결과가 3분의 2 정도 줄어든다면 다른 모든 것을 위한 공간을 확보할 수 있습니다. 또한 그곳에서는 재현율 (Recall)의 손실도 덜 위험합니다. 무언가 누락된 것을 감지한 에이전트는 다시 검색할 수 있기 때문입니다.
프루닝은 당사 Product Agent SDK의 지식 베이스 검색 (Knowledge base search)에서 기본적으로 활성화되어 있으며, 검색 API (Retrieval API) 및 MCP 서버에서는 선택 사항입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기