긴 평가 데이터를 LLM에 한 번에 보내면 안 될까
평가 기록이 늘어도 더 어려운 판단이 필요한 것은 아니었다#
문제를 간단히 정리하면, 한 사람에게 쌓인 평가 전반의 메타데이터와 서술형 결과를 보고서 형식에 맞춰 정리하는 것이었습니다. 서비스가 평가 데이터를 조회해 보고서 작성 지침과 함께 전달하면, LLM은 전체 내용을 바탕으로 하나의 보고서를 생성합니다.
입력이 길어진 이유는 LLM에 요구하는 판단이 더 복잡해졌기 때문이 아니라, 보고서 하나에 포함해야 할 평가 기록이 늘었기 때문입니다. 필요한 것은 모델의 추론 능력을 높이는 일이 아니라, 같은 정리 작업을 입력 크기에 맞게 실행하는 방법이었습니다.
특히 연말이나 반기말에는 한 사람에 대한 평가성 텍스트가 최대 약 16만 줄에 이른 경우도 있었습니다. 이런 이유로 짧은 입력을 전제로 만든 단일 호출 경로만으로는 모든 보고서를 처리할 수는 없었습니다.
큰 컨텍스트 윈도우를 지원하는 모델을 사용하면 일부 긴 입력은 한 번에 받을 수도 있습니다. 하지만 실제 요청에는 평가 데이터뿐 아니라 LLM을 호출할 때 함께 전달해야 하는 시스템 프롬프트와 사용자 프롬프트가 들어가고, 보고서를 생성할 출력 공간도 필요합니다. 데이터의 규모가 커지면 모델 한도에 맞춰 같은 판단을 반복해야 하므로, 입력량만으로 모델을 바꾸기보다 애플리케이션에서 처리 방법을 선택하기로 했습니다.
긴 입력을 여러 번 나눠 보내면 청크별 결과만 얻게 됩니다. 최종 결과는 하나의 평가 보고서여야 하므로, 마지막에는 중복을 정리하고 전체 관점에서 다시 합치는 과정도 필요했습니다.
그래서 긴 입력은 여러 청크로 나누어 각각 처리하고(Map), 그 결과를 다시 LLM에 전달해 하나의 보고서로 통합했습니다(Reduce). 입력이 짧을 때는 기존의 한 번 호출을 유지하고, 길어질 때만 MapReduce로 바꾸는 구조입니다. 이 글에서는 두 경로를 나눈 기준과, 입력 하나가 여러 LLM 호출로 늘어나면서 함께 달라진 조건을 정리해보려고 합니다.
짧은 입력은 한 번에 처리했다#
SimpleChain은 입력 길이와 관계없이 LLM을 한 번 호출합니다. 데이터가 짧을 때는 전체 맥락을 한 번에 전달할 수 있고, 호출 횟수도 가장 적습니다.
그래서 짧은 입력은 기존 경로에 그대로 두고, 입력이 일정 크기를 넘을 때만 MapReduce로 전환했습니다. 호출하는 서비스가 체인을 직접 고르게 하지 않고 AdaptiveChain이 입력 크기를 보고 결정하도록 했습니다.
-return simpleChain.execute(toSimpleChainInput(input));
+int estimatedTokens = tokenEstimator.estimate(input.additionalContent());
+
+if (estimatedTokens >= 10_000) {
+ List<ChainResult> mapped = mapReduceChain.map(
+ toMapReduceChunkInputs(input)
+ );
+ return mapReduceChain.reduceWithLLM(mapped, context);
+}
+
+return simpleChain.execute(toSimpleChainInput(input));최대 16만 줄을 한 번에 보내기 어려운 이유#
현재 모델의 컨텍스트 윈도우는 당시보다 훨씬 커졌습니다. 2026년 8월 기준 OpenAI 최신 모델은 최대 105만 토큰, Claude 최신 모델은 최대 100만 토큰의 컨텍스트 윈도우를 지원합니다.
줄 수만으로 실제 토큰 수를 확정할 수는 없습니다. 다만 현재 TokenEstimator의 계산식으로 컨텍스트 한도에 들어갈 수 있는 평균 줄 길이를 역산할 수는 있습니다.
100만 토큰을 16만 줄로 나누면 한 줄에 사용할 수 있는 값은 6.25토큰입니다. 줄바꿈에 적용되는 0.3토큰을 제외하면 한글은 평균 약 3.96자까지 들어갑니다. 한 줄이 평균 한글 4자라고 계산해도 줄바꿈을 포함한 추정값은 약 100만 8천 토큰입니다.
160,000줄 × (한글 4자 × 1.5토큰 + 줄바꿈 0.3토큰)
= 1,008,000토큰105만 토큰 모델에서도 같은 입력을 넣으면 시스템 프롬프트와 보고서 출력에 남는 공간은 약 4만 2천 토큰입니다. 한 줄이 평균 5자라면 추정값은 약 124만 8천 토큰으로 두 모델의 컨텍스트 한도를 모두 넘습니다.
160,000줄 × (한글 5자 × 1.5토큰 + 줄바꿈 0.3토큰)
= 1,248,000토큰이 값은 실제 Provider의 tokenizer로 측정한 결과가 아니라 현재 서비스의 추정식으로 계산한 경계입니다. 정확한 입력 크기는 실제 평가 데이터와 대상 모델의 tokenizer로 다시 확인해야 하지만, 최대 16만 줄을 항상 단일 호출로 처리할 수 있다고 가정하기는 어렵습니다.
최신 모델의 컨텍스트 윈도우는 커졌지만, 실제로 유효하게 처리할 수 있는 길이는 더 짧을 수 있습니다. Claude 컨텍스트 문서는 입력이 길어질수록 정확도와 재현율이 낮아질 수 있다고 설명하고, RULER도 모델이 표기된 컨텍스트 길이 전체에서 같은 성능을 내는 것은 아니라고 보여줍니다. 최대 16만 줄의 입력을 처리하려면 여전히 분할 경로가 필요합니다.
10,000토큰부터 MapReduce로 전환했다#
당시 구현은 추가 입력이 10,000토큰 이상으로 추정되면 MapReduce를 선택하고, Map에 전달할 입력은 8,000토큰을 목표로 나눕니다. 10,000토큰은 8,000토큰짜리 청크 목표를 넘기므로, 줄바꿈을 기준으로 나눌 수 있는 입력이라면 두 개 이상의 Map 입력이 생기는 지점입니다.
TokenEstimator는 한글 한 글자를 1.5토큰, 영문 한 단어를 1.3토큰, 그 밖의 문자를 0.3토큰으로 계산합니다. 이 계산식에서 한글 1,000자는 약 1,500토큰이고, 10,000토큰 분기값은 한글 약 6,667자에 해당합니다. Provider가 계산한 실제 토큰 수는 아니지만, 입력마다 같은 규칙으로 실행 경로와 청크 수를 정할 수 있었습니다.
10,000토큰이 현재 모든 모델에 적용할 수 있는 권장값이라는 의미는 아닙니다. 모델이 바뀌면 OpenAI의 tiktoken이나 Claude의 Token Counting API로 시스템 프롬프트와 입력, 예상 출력까지 다시 계산해야 합니다. 그보다 이른 시점에 MapReduce로 전환할지는 실제 평가 데이터의 길이 구간별로 두 경로의 결과 품질, 응답 시간과 비용을 비교해 조정할 수 있습니다.
최대 16만 줄의 입력을 처리하려면 MapReduce 경로는 필요합니다. 다만 10,000토큰부터 이 경로를 사용할지는 평소 입력 분포와 두 경로의 실행 결과를 기준으로 다시 조정할 수 있습니다.
입력을 나누자 한 요청이 여러 LLM 호출로 바뀌었다#
MapReduce 경로에서는 긴 입력을 여러 청크로 나누고 각 청크를 LLM에 전달합니다. 평가 서술처럼 청크별 결과에 중복이 생기거나 전체 우선순위를 다시 정해야 하는 작업은 Map 결과를 이어 붙이는 것만으로 끝내기 어려웠습니다. 이 경우에는 Map 결과를 모아 LLM으로 한 번 더 정리하는 Reduce 단계를 사용했습니다.
이 구조에서 입력이 길어질수록 LLM 호출 수도 늘어납니다. 예를 들어 토큰 추정값이 45,000이고 청크 목표 크기가 8,000이라면 다음과 같이 계산됩니다.
ceil(45,000 / 8,000) = 6개 청크
Map 6회 + Reduce 1회 = LLM 7회45,000토큰은 호출 수를 설명하기 위한 계산 예시이며 운영에서 측정한 입력값은 아닙니다. 중요한 점은 한 번의 요청이 Map 청크 수와 Reduce 호출을 합한 만큼 외부 요청으로 늘어난다는 것입니다. Map을 병렬로 실행하면 대기 시간을 줄일 수 있지만, 동시에 Provider로 나가는 요청도 많아집니다. 이 구조를 적용하려면 executor 크기뿐 아니라 Provider의 rate limit과 기능별 동시 실행 수를 함께 제한해야 합니다.
Map 결과가 많아지면 Reduce 입력도 길어진다#
각 Map 호출은 자신에게 전달된 청크만 봅니다. 여러 청크에 흩어진 평가 내용의 일관성을 확인하거나 상반된 의견을 조정하는 작업은 이 단계에서 하기 어렵습니다. LLM Reduce는 청크별 결과의 중복을 정리하고 하나의 보고서로 통합하지만, Map 결과에서 빠진 원본 내용을 다시 복원할 수는 없습니다.
현재 구조는 모든 Map 결과를 모아 한 번의 reduceWithLLM() 호출로 전달합니다. 청크 수가 많거나 Map 결과가 길면 Reduce 입력도 컨텍스트 한도에 도달할 수 있습니다. Map 프롬프트에서 출력 길이를 제한해도 충분하지 않은 규모라면 Map 결과를 여러 그룹으로 나누어 단계적으로 Reduce해야 합니다.
병렬 Map은 CompletableFuture.allOf()로 모든 청크의 처리를 기다립니다. 한 청크에서 모든 Provider 호출이 실패하면 Future가 예외로 종료되고 Reduce는 실행되지 않습니다. 일부 결과만 반영된 보고서를 반환하지 않는 구조입니다. 부분 결과를 허용해야 한다면 누락된 청크와 범위를 최종 결과에 함께 표시하는 별도 계약이 필요합니다.
호출 수가 늘면 프롬프트도 Map마다 반복됩니다. 총 입력 토큰에는 원본 평가 데이터뿐 아니라 청크마다 전달되는 프롬프트와 Map 결과를 다시 전달하는 Reduce 입력이 포함되므로, MapReduce는 비용을 줄이는 구조가 아니라 긴 입력을 처리하기 위해 호출과 토큰 사용량을 늘리는 구조입니다. 기능별 호출량과 응답 규모는 사용량 추적에서 함께 확인해야 합니다.
단위 테스트는 체인 선택과 호출 수를 확인했다#
AdaptiveChainImplTest에서는 토큰 추정값을 고정해 두 실행 경로를 나눠 확인했습니다. 추정값이 100이면 SimpleChain.execute()만 호출하고, 12,000이면 MapReduce를 선택하도록 했습니다. 12,000토큰을 두 청크로 나누는 조건에서는 Map 결과 두 개가 한 번의 reduceWithLLM()으로 전달되는지도 확인했습니다.
추정값 100 → Simple Chain → LLM 1회
추정값 12,000 → Map 2회 + Reduce 1회 → LLM 3회이 테스트가 보장하는 범위는 입력 크기에 따른 체인 선택, 청크 전달과 Reduce 호출까지입니다. 실제 Provider가 계산한 토큰 수, 결과의 품질, 응답 시간과 비용이 좋아졌다는 뜻은 아닙니다. 이 항목까지 판단하려면 같은 입력을 두 경로로 실행하고 Provider 사용량과 원본 응답을 함께 비교해야 합니다.
현재처럼 줄바꿈을 기준으로 청크를 나누는 방식에는 적용 조건도 있습니다. 한 줄이 청크 목표 크기보다 길면 해당 줄을 더 작게 자르지 못합니다. 입력 한 줄의 길이를 통제할 수 없다면 문장이나 tokenizer 기준의 추가 분할이 필요합니다.
입력 크기는 검증 조건이 아니라 실행 계획이었다#
이 작업을 하기 전에는 긴 입력을 모델의 토큰 제한에 걸리지 않게 자르는 문제로만 봤습니다. MapReduce를 적용하고 나니 입력 크기는 단순한 검증 조건이 아니라 LLM 호출 횟수와 동시성, 비용, 실패 지점을 바꾸는 실행 계획에 가까웠습니다.
최대 16만 줄이라는 입력 규모는 분할 경로가 필요한 이유였고, 10,000토큰은 그 경로로 전환하는 시점이었습니다. 두 판단을 구분하고 나니 모델의 컨텍스트가 커져도 설계 전체를 버릴 필요는 없었습니다. 모델 사양이 바뀌면 전환점은 다시 조정할 수 있지만, 최대 입력을 여러 호출로 처리하고 하나의 결과로 합치는 경로는 계속 필요합니다.
입력의 상한은 어떤 실행 경로가 필요한지를 결정하고, 평소 입력 분포와 품질·응답 시간·비용은 언제 그 경로로 바꿀지를 결정합니다. 이후에는 긴 입력을 나눌 수 있는지만 보기보다 이 두 기준을 따로 확인해야 할 것 같습니다.