에이전트·RAG

AGENT / 39번째 글

컨텍스트를 예산으로 다루기

컨텍스트 창에 들어가는 다섯 항목을 성질로 나누고, 항목마다 다른 관리법을 한자리에 정리한다. 캐시·잘라내기·요약·외부 메모리·개수 조절·출력 예약이 각각 어느 항목의 도구인지 짚는다.

PALDYN Team35 MIN READ

지난 글에서 도구 스키마가 매 요청마다 컨텍스트에 실린다는 얘기를 했다. 스키마만 그런 것이 아니다. 시스템 프롬프트, 대화 이력, 검색해 온 문서, 도구가 돌려준 결과가 전부 같은 창을 나눠 쓴다. 그런데 이것들을 한 덩어리 "프롬프트"로 보면 문제가 터졌을 때 어디를 줄여야 할지가 안 보인다. 항목마다 자라는 속도도, 줄이는 방법도, 줄였을 때 잃는 것도 다르기 때문이다.

이 글은 그 항목들을 성질로 갈라 놓고, 갈래마다 어떤 도구가 붙는지를 정리한다. 캐시·잘라내기·요약·외부 메모리·개수 조절이 각각 서로 다른 갈래를 겨냥한 방법이라는 것을 보고 나면, 「무엇을 써야 하나」가 「지금 부푼 것이 어느 갈래인가」로 바뀐다.

컨텍스트 창의 항목

창의 상한

컨텍스트 창(context window)은 모델이 한 번의 요청에서 다룰 수 있는 토큰 수의 상한이다. 창 밖에 있는 것은 모델에게 없는 것과 같다. 어제 나눈 대화도, 방금 잘라 낸 문단도 모델은 보지 못한다. 「LLM이 기억한다」는 말이 오해를 부르는데, 모델은 아무것도 기억하지 않는다. 요청마다 우리가 창 안에 다시 넣어 준 것만 본다. 그래서 「긴 대화에서 기억을 유지하는 법」은 사실 기억의 문제가 아니라 매 요청마다 무엇을 다시 넣을 것인가의 문제다.

창 크기는 모델마다 다르고 세대마다 커진다. 수만 토큰짜리가 있고 백만 토큰짜리도 있으며, 그 숫자는 반년이면 바뀐다. 특정 숫자를 외워 두는 것은 쓸모가 적고, 대신 알아 둘 것은 창이 커져도 이 글의 문제가 사라지지 않는다는 점이다. 이유가 셋이다. 입력 토큰은 창의 크기와 무관하게 넣은 만큼 값을 치른다. 창을 많이 채울수록 첫 토큰이 나오기까지 오래 걸린다. 그리고 뒤에서 볼 「중간에서 흐려지는」 현상 탓에, 넣어 둔 것을 모델이 다 읽는다는 보장도 없다.

정말 달라지는 것은 창이 커지면 선택지가 늘어난다는 것뿐이다. 예전에는 잘라 내는 수밖에 없던 자리에서 이제는 요약할지 그대로 둘지 고를 수 있다. 고를 수 있다는 말은 여전히 골라야 한다는 말이다.

항목의 성질

컨텍스트를 예산표처럼 보면 관리 방법이 저절로 갈린다.

컨텍스트 창의 항목별 예산 배분

항목 성질 자라는 속도 주된 관리법
시스템 프롬프트 고정 안 자란다 캐시, 주기적 감량
도구 스키마 고정 도구 추가 시에만 캐시, 노출 범위 조절
대화 이력 누적 턴마다 잘라내기, 요약
도구 결과 누적 호출마다 반환 필드 축소, 오래된 것 축약
검색 문서 선택 요청마다 새로 개수·순서 조정
출력 몫 예약 — 먼저 떼어 둔다

표는 여섯 줄이다 — 창을 나눠 쓰는 다섯 항목에 아직 생기지도 않은 출력 몫을 한 줄 더했다. 성질은 넷이다. 고정은 요청마다 똑같이 들어가는 것, 누적은 대화가 길어질수록 자라는 것, 선택은 요청마다 새로 고르는 것, 예약은 자리만 비워 둬야 하는 것이다. 이 넷이 이 글의 나머지 절을 그대로 나눈다.

갈래를 나누는 이유는 도구가 겹치지 않아서다. 캐시가 가장 크게 듣는 자리는 고정 항목이다 — 요청마다 앞부분이 통째로 같으니 첫 요청 뒤로는 읽기만 하면 된다. 누적 항목에도 걸 수는 있지만 매 턴 새로 붙은 만큼은 그때마다 값을 다시 치른다. 요약은 누적 항목의 도구다 — 시스템 프롬프트를 요약하는 사람은 없다. 개수 스윕은 선택 항목에만 쓴다. 지금 창이 넘친다면 어느 갈래가 부푼 것인지부터 정해야 하고, 그게 정해지면 쓸 방법은 대개 하나로 좁혀진다.

토큰 세기

표를 만들어 두고 실제 요청 하나의 토큰을 항목별로 세어 보면, 대개 예상과 다른 곳이 부풀어 있다. 흔한 범인은 도구 결과다. 조회 API 응답을 손대지 않고 그대로 넘기는 코드가 몇 군데 있으면 대화 열 턴 만에 이력의 절반을 차지한다. 사람이 쓴 시스템 프롬프트는 눈에 보이니까 자꾸 다듬게 되는데, 기계가 만들어 넣는 도구 결과는 아무도 안 읽어서 그대로 자란다.

그래서 첫 작업은 줄이는 것이 아니라 세는 것이다. 요청 하나를 골라 항목별 토큰 수를 로그로 남긴다. 토큰 수를 정확히 알고 싶으면 눈대중이나 글자 수 나누기 대신 그 모델의 토큰 세기 기능을 쓴다. 특히 한국어는 토크나이저에 따라 추정치와 실제가 꽤 벌어지므로, 「한글 한 글자에 몇 토큰」 같은 어림값을 예산의 근거로 삼으면 나중에 창 끝에서 터진다.

세어 본 결과를 요청 종류별로 남겨 두면 그다음이 쉽다. 「검색이 붙는 요청은 입력이 평균 8천 토큰인데 그중 5천이 문서」 같은 문장이 하나 생기면, 다음에 무엇을 손볼지가 논쟁거리가 아니라 사실이 된다.

고정 항목과 캐시

프리픽스 캐싱

시스템 프롬프트와 도구 스키마는 매 요청 똑같이 들어간다. 그래서 프리픽스 캐싱(prefix caching)의 효과가 가장 큰 자리다. 프리픽스 캐싱은 요청의 앞부분이 지난번과 바이트 단위로 같으면 그 구간의 계산 결과를 재사용하는 기능이다. 모델이 같은 앞부분을 다시 읽고 계산하는 일을 건너뛰므로 값이 싸지고 응답도 빨라진다.

값이 얼마나 싸지는지는 제공자마다 다르지만 구조는 비슷하다. Claude API를 예로 들면 캐시에 쓸 때는 그 토큰이 기본 입력가의 1.25배로 계산되고, 캐시에서 읽을 때는 0.1배다. 첫 요청에 25%를 더 내고 이후 요청에서 90%를 아끼는 셈이라, 같은 앞부분으로 요청을 두 번만 보내도 이미 이득이다(1.25 + 0.1 대 2). 유지 시간은 기본이 5분이고 더 긴 옵션을 지정할 수 있는데, 긴 쪽은 쓰기 값이 그만큼 오르므로 그 값을 메울 만큼 재사용이 잦은지 보고 고른다.

효과가 가장 극적인 것은 긴 문서 하나에 여러 질문을 던지는 작업이다. 100페이지짜리 계약서를 넣고 「면책 조항이 있나」·「계약 기간은」·「위약금은」 같은 것을 열 개쯤 차례로 물으면, 문서 부분은 첫 질문에서 한 번 캐시되고 나머지 아홉 질문은 그 구간을 10분의 1 값으로 읽는다.

resp = client.messages.create(
    model="claude-opus-5",
    max_tokens=512,
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": contract,          # 100페이지 계약서
             "cache_control": {"type": "ephemeral"}},    # 여기까지 캐시
            {"type": "text", "text": question},          # 질문만 매번 바뀐다
        ],
    }],
)

주의할 것이 두 가지 있다. 캐시가 걸리는 최소 프리픽스 길이가 모델마다 정해져 있어서(대략 512~4096 토큰) 그보다 짧은 앞부분은 아무 오류 없이 그냥 캐시되지 않는다. 그리고 캐시가 정말 듣고 있는지는 짐작하지 말고 응답의 사용량 정보에서 캐시로 읽은 토큰 수를 확인한다. 같은 앞부분으로 여러 번 불렀는데 그 값이 계속 0이면, 아래에서 볼 조용한 무효화가 어딘가에서 일어나고 있는 것이다.

프리픽스 순서

캐시가 듣게 하려면 규칙이 하나다 — 바뀌지 않는 것을 앞에, 바뀌는 것을 뒤에 둔다. 프리픽스 캐싱은 이름 그대로 앞부분이 같아야 걸리므로, 앞쪽에서 한 바이트가 달라지면 그 뒤의 캐시가 통째로 무효가 된다.

여기서 순서는 우리가 메시지를 적는 순서가 아니라 요청이 실제로 조립되는 순서다. Claude API는 도구 정의를 먼저, 시스템 프롬프트를 그다음, 메시지 목록을 마지막에 이어 붙인다.

[잘 안 바뀌는 쪽]                          [매번 바뀌는 쪽]
도구 정의 → 시스템 프롬프트 → 대화 이력 → 검색 문서 → 이번 질문

이 원칙을 깨는 가장 흔한 패턴이 시스템 프롬프트에 현재 시각이나 사용자 이름을 넣는 것이다. 「지금은 2026년 8월 11일 14시입니다」 한 줄 때문에 그 뒤의 모든 캐시가 매분 무효화된다. 도구 목록의 순서가 매번 달라지는 것도 같은 사고다 — 딕셔너리를 그대로 순회해 도구 배열을 만들면 순서가 실행마다 바뀔 수 있고, 그러면 첫 바이트부터 어긋난다. 시각이 정말 필요하다면 시스템 프롬프트가 아니라 대화의 마지막 사용자 메시지 쪽에 붙이고, 도구 목록은 정렬해서 고정한다.

프리픽스 캐싱과 무효화

캐시가 안 듣는다고 바로 알려 주는 오류는 없다. 값이 그냥 원래대로 나올 뿐이다. 그래서 캐시는 「걸어 두었다」가 아니라 「읽힌 토큰 수로 확인했다」까지 가야 끝난다.

캐시의 한계

고정 항목은 캐시가 듣더라도 무한정 늘려도 되는 것은 아니다. 캐시는 값과 지연을 줄여 주지만 모델이 읽어야 할 양은 그대로다. 창에서 차지하는 자리도 그대로고, 지시가 서로 부딪히는 문제도 그대로다.

지시가 서른 줄을 넘어가면 앞에서 「간결하게」라고 해 놓고 뒤에서 「근거를 빠짐없이」라고 하는 자리가 생긴다. 사람이 읽으면 둘 다 지키려고 애쓰지만 모델은 그중 하나를 고르고, 어느 쪽을 고를지는 그날 프롬프트의 사소한 차이가 정한다. 「가끔 답이 왜 이렇게 짧지」의 원인이 대개 여기 있다.

그래서 시스템 프롬프트는 쌓아 두는 문서가 아니라 주기적으로 줄여야 하는 자산이다. 규칙을 하나 더할 때마다 기존 줄과 부딪히는지 훑고, 분기마다 한 번은 통째로 다시 읽으면서 이제 안 쓰는 지시를 뺀다. 도구 스키마도 같다 — 지금 이 작업에서 절대 안 쓰는 도구까지 매 요청 붙이고 있다면, 그것은 캐시로 싸게 만드는 것이 아니라 애초에 안 넣는 것이 맞다.

누적 항목의 관리

대화 이력과 도구 결과는 아무것도 안 하면 계속 자란다. 여기가 방치했을 때 가장 먼저 창을 먹는 자리이고, 그만큼 방법도 많이 나와 있다. 크게 네 갈래이고, 실제 시스템은 대개 둘 이상을 함께 쓴다.

슬라이딩 윈도우·요약 압축·외부 메모리·계층 요약의 장단점 비교

잘라내기와 슬라이딩 윈도우

가장 단순한 방법은 슬라이딩 윈도우다. 최근 N턴만 남기고 그보다 오래된 것은 버린다. 큐 하나로 구현되고, 창이 절대 넘치지 않으며, 요청 하나의 값이 대화 길이와 무관하게 일정하다. 값을 예측할 수 있다는 것은 생각보다 큰 장점이다 — 대화가 길어질수록 값이 오르는 구조에서는 악성 사용자가 대화를 끝없이 늘리는 것만으로 청구서를 부풀릴 수 있다.

대신 잃는 것이 분명하다. 창 밖으로 나간 것은 그대로 사라진다. 스무 턴 전에 사용자가 「저는 파이썬 초보예요」라고 말했어도 이제 모델은 그것을 모른다. 그래서 잘라낼 때 반드시 남겨야 하는 것이 있다. 시스템 메시지와 최초의 과제 서술이다. 이게 잘려 나가면 에이전트는 자기가 뭘 하던 중인지를 잃고, 그때부터 하는 일이 전부 엉뚱해진다.

버리는 단위도 규칙이 있다. 한 쌍씩 버린다. 어시스턴트 메시지만 남기고 그에 대응하는 사용자 메시지를 지우면 대화가 앞뒤가 안 맞게 된다. 도구 호출과 그 결과도 마찬가지로 짝이라, 호출은 남고 결과만 사라지면 모델이 대답을 기다리다 같은 도구를 다시 부른다.

각 턴이 앞 맥락에 크게 기대지 않는 작업이면 이 방법으로 충분하다. 일반 챗봇, 단발성 고객 지원이 그렇다.

요약 압축과 계층 요약

요약 압축은 오래된 구간을 모델에게 요약시켜 한 덩어리로 대체한다. 스무 턴이 대여섯 문장이 되므로 잘라내기보다 훨씬 오래 맥락을 들고 갈 수 있다. 보통은 「기존 요약 + 새로 밀려난 대화 → 통합 요약」 꼴로 굴린다. 요약이 요약을 삼키며 계속 갱신되는 구조라, 대화가 백 턴이 되어도 요약본의 길이는 일정하게 유지된다.

값을 치르는 자리는 둘이다. 요약 자체가 모델 호출이라 값과 지연이 붙고, 요약 과정에서 사실이 뭉개진다. 앞의 것은 요약을 값싼 모델에게 맡기고 매 턴이 아니라 몇 턴에 한 번만 돌려서 줄인다. 뒤의 것은 지시로 줄인다 — 결정된 사항과 사용자가 명시한 제약을 원문 그대로 보존하라고 못 박으면 손실이 크게 준다. 「고객이 예산을 300만 원이라고 했다」가 「예산 제약을 언급했다」로 뭉개지는 것이 요약의 전형적인 사고이고, 그런 사고는 대개 몇 턴 뒤에야 드러난다.

계층 요약은 이 구조를 시간 축으로 한 번 더 쌓은 것이다. 현재 세션은 원문 그대로, 오늘 것은 일 단위 요약, 이번 주는 주 단위 요약, 그보다 오래된 것은 월 단위 요약으로 둔다. 아래로 갈수록 압축률이 높고 위로 갈수록 세부가 살아 있다. 「지난달에 뭘 하기로 했더라」와 「방금 뭐라고 했지」를 한 창 안에서 동시에 답할 수 있다는 것이 이 구조의 값이고, 대신 요약 계층을 언제 어떻게 승격시킬지를 전부 우리가 정해야 한다. 몇 주에 걸쳐 같은 사용자를 상대하는 개인 비서형 도구가 아니면 과한 구조다.

외부 메모리와 검색 주입

앞의 둘은 창 안에서 어떻게든 줄이는 방법이었다. 외부 메모리는 방향이 반대다. 대화를 전부 밖에 저장해 두고, 새 질문이 들어올 때마다 그와 관련 있는 과거 대화만 검색해 창에 넣는다. 저장은 보통 벡터 데이터베이스에 한다 — 문장을 벡터로 바꿔 두고 질문 벡터와 가까운 것을 찾는 저장소다.

이론상 기억의 양에 상한이 없다는 것이 이 방법의 매력이다. 대화가 만 턴이 되어도 창에 들어가는 것은 최근 몇 턴과 검색된 두세 조각뿐이다. 「석 달 전에 얘기했던 그 프로젝트 이름이 뭐였죠」에 답할 수 있는 것은 이 방법뿐이기도 하다 — 슬라이딩 윈도우는 이미 버렸고, 요약은 이름을 뭉갰을 것이다.

값은 복잡도로 치른다. 저장소가 하나 더 붙고, 임베딩 계산과 검색 지연이 매 턴 붙으며, 무엇보다 검색이 틀리면 엉뚱한 과거가 창에 들어온다. 관련 없는 옛 대화가 주입되면 모델은 그것을 지금 맥락으로 읽고 답을 그쪽으로 튼다. 그래서 외부 메모리를 쓸 때도 최근 몇 턴은 검색과 무관하게 무조건 넣는다. 지금 하던 얘기가 검색 결과에 밀려나면 안 되기 때문이다.

도구 결과의 축약

네 번째는 도구 결과 전용 기법이다. 대화 이력과 달리 도구 결과에는 다시 부를 수 있다는 성질이 있다. 그래서 최근 두세 개는 전문을 두고, 그보다 오래된 것은 「이 조회는 3건을 반환했다」 수준으로 줄인다. 정말 필요하면 모델이 그 도구를 다시 호출하면 되므로 손실이 가장 적다.

축약보다 먼저 할 일은 애초에 덜 담는 것이다. 조회 API 응답을 통째로 넘기는 대신 이 작업에 쓰이는 필드만 골라 반환하면, 축약할 것 자체가 줄어든다. 도구 결과가 이력의 절반을 먹고 있다는 진단이 나왔을 때 가장 값이 싼 수선이 여기다.

아래는 예산이 넘칠 때 축약과 잘라내기를 순서대로 적용하는 모습이다. 도구 결과를 먼저 줄이고, 그래도 모자라면 그때 대화 턴을 버린다. 시스템 프롬프트는 Claude API에서 messages 밖의 자리에 들어가므로 여기서 지켜야 하는 것은 최초의 과제 서술뿐이다.

def trim_history(messages, budget_tokens):
    head = messages[:1]                      # 최초의 과제 서술은 항상 남긴다
    tail = messages[1:]

    while count_tokens(head + tail) > budget_tokens and len(tail) > 4:
        # 가장 오래된 도구 결과부터 축약, 그다음이 오래된 대화 턴
        idx = next((i for i, m in enumerate(tail)
                    if is_tool_result(m) and not m.get("shrunk")), None)
        if idx is not None:
            tail[idx] = shrink_tool_result(tail[idx])
        else:
            tail = tail[2:]                  # 사용자·어시스턴트 한 쌍씩 버린다
    return head + tail

순서가 이렇게 잡힌 이유는 잃는 것의 크기가 다르기 때문이다. 축약된 도구 결과는 다시 부르면 되지만, 버려진 대화 턴은 사용자가 다시 말해 주지 않는 한 돌아오지 않는다.

선택 항목의 개수와 순서

검색 개수 스윕

검색해 온 문서는 요청마다 새로 고르는 항목이다. 여기서 가장 흔한 오해가 「창이 크니까 넉넉히 넣자」는 것이다. 실제로는 개수를 늘리면 정확도가 오르다가 어느 지점부터 떨어진다. 관련 없는 문서가 늘면 모델이 근거를 잘못 고를 확률도 같이 오르기 때문이다. 문서를 열 개 넣어 답이 나빠졌을 때, 그 열 개 안에 정답 근거가 들어 있었다는 사실은 위로가 되지 않는다.

몇 개가 적당한지는 데이터마다 다르지만, 정하는 방법은 같다. 검색 개수를 3·5·8·12로 바꿔 가며 같은 평가셋을 돌리고 정확도가 꺾이는 지점을 찾는다. 그 값이 그 시스템의 상한이다. 대개 생각보다 작고, 개수를 줄이면 정확도와 값과 지연이 동시에 좋아지는 흔치 않은 자리이기도 하다.

스윕을 한 번 돌려 두면 나중에 검색기를 바꿀 때도 기준이 생긴다. 새 검색기가 좋아졌는지는 「같은 개수에서 정확도가 올랐는가」로도 볼 수 있고 「같은 정확도를 더 적은 개수로 내는가」로도 볼 수 있는데, 뒤쪽이 값까지 함께 재는 지표다.

Lost in the Middle

개수를 정했다면 다음은 순서다. Liu 등이 2023년에 보고한 Lost in the Middle 현상이 여기 걸린다. 긴 컨텍스트를 준 모델이 앞쪽과 뒤쪽에 있는 정보는 잘 쓰면서 중간에 놓인 정보는 상대적으로 흘린다는 관찰이다. 정답이 담긴 문서를 목록의 첫째나 마지막에 두면 잘 맞히는데, 같은 문서를 한가운데로 옮기면 정확도가 눈에 띄게 떨어졌다.

이 현상이 실무에서 무서운 이유는 증상이 검색 실패와 똑같이 보인다는 데 있다. 답이 틀렸을 때 우리는 보통 검색이 근거를 못 찾았다고 짐작하고 검색기부터 손댄다. 그런데 로그를 열어 보면 근거 문서는 멀쩡히 다섯 번째 자리에 들어가 있다. 넣었는데 안 읽힌 것과 안 넣은 것을 구별하려면, 답이 틀린 요청에서 근거 문서가 목록의 몇 번째였는지를 함께 남겨 두어야 한다.

문서 배치

고치는 방법은 간단하다. 검색 점수 순서대로 늘어놓지 말고 가장 관련 높은 것을 앞과 뒤에, 덜 중요한 것을 가운데에 둔다. 1등을 맨 앞에, 2등을 맨 뒤에, 나머지를 그 사이에 채우는 식이다. 코드로는 배열 하나를 다시 늘어놓는 몇 줄이라 값이 거의 들지 않는다.

검색 문서의 배치

한 가지 헷갈리기 쉬운 것은 이 배치가 검색 순위를 바꾸는 것이 아니라는 점이다. 무엇이 1등인지는 검색기가 그대로 정하고, 우리는 그 1등을 어느 자리에 놓을지만 바꾼다. 순위 자체가 틀렸다면 배치를 아무리 손봐도 소용이 없다.

그리고 배치는 앞 소절의 개수 문제를 대신하지 못한다. 관련 없는 문서 열 개를 가운데에 몰아넣어도 그것들은 여전히 창을 먹고 값을 올리며, 모델이 그중 하나를 근거로 고를 위험도 남는다. 순서는 넣기로 한 것들 사이의 문제이고, 개수는 무엇을 넣을지의 문제다. 순서를 손보기 전에 개수를 먼저 정한다.

예산표

출력 몫

컨텍스트 창은 입력과 출력이 나눠 쓰는 공간이다. 입력을 창 끝까지 채우면 답이 중간에 잘린다. 이 사고가 성가신 것은 조용히 일어나기 때문이다. 오류가 나는 것이 아니라 그럴듯한 문장이 문장 한가운데서 끊길 뿐이고, JSON을 받는 자리였다면 파싱만 실패한다. 그래서 예산은 출력부터 떼고 짠다.

B입력=Cmax⁡−T출력−SB_{\text{입력}} = C_{\max} - T_{\text{출력}} - S

Cmax⁡C_{\max} 는 모델의 창 크기, T출력T_{\text{출력}} 은 이 작업에서 필요한 최대 출력 길이, SS 는 안전 여유다. 토큰 계산이 항상 정확하지는 않으므로 SS 를 창의 5% 정도 두는 편이 안전하다. 한국어에서 이 여유가 특히 값을 하는데, 토크나이저에 따라 같은 문장의 토큰 수가 꽤 벌어지기 때문이다.

T출력T_{\text{출력}} 은 작업마다 정직하게 잡는다. 분류 결과 한 단어를 받는 요청에 수천 토큰을 비워 두는 것도, 긴 보고서를 시키면서 몇백 토큰만 남겨 두는 것도 같은 크기의 실수다. 앞쪽은 넣을 수 있었던 문서를 못 넣는 것이고, 뒤쪽은 답이 잘리는 것이다.

항목별 비율

출력과 고정분을 뺀 나머지를 누적 항목과 선택 항목에 나눈다.

def build_context(model_window, max_output):
    budget = model_window - max_output - int(model_window * 0.05)

    fixed = count_tokens(SYSTEM_PROMPT) + count_tokens(TOOL_SCHEMAS)
    budget -= fixed                                   # 고정분을 먼저 뺀다

    docs_budget = int(budget * 0.4)                   # 검색 문서 몫
    history_budget = budget - docs_budget             # 나머지가 이력 몫
    return docs_budget, history_budget

비율은 작업 성격이 정한다. 문서 질의응답이면 검색 문서 쪽이 크고, 긴 협업 대화라면 이력 쪽이 크다. 중요한 것은 비율의 정확한 값이 아니라 어딘가에 명시적으로 적혀 있다는 사실이다. 적어 두지 않으면 자라는 항목이 나머지를 조용히 밀어낸다 — 이력이 길어져 문서 다섯 개가 세 개로 줄었는데 아무도 그걸 모르는 상태가 된다.

숫자를 하나 넣어 따라가 보자. 창이 20만 토큰이고 출력에 4천을 잡으면 안전 여유 1만을 뺀 입력 예산이 18만 6천이다. 시스템 프롬프트 1천5백과 도구 스키마 3천5백을 빼면 18만 1천이 남고, 4대 6으로 나누면 문서에 7만 2천, 이력에 10만 9천이다. 문서 한 편이 1천5백 토큰이라면 마흔여덟 편이 들어가는 셈인데 — 앞 절에서 본 스윕은 아마 여덟 편쯤에서 꺾였을 것이다. 예산이 허락한다고 다 쓰는 것이 아니라, 예산과 스윕 결과 중 작은 쪽이 실제 상한이다.

수선 순서

컨텍스트 문제는 대개 「토큰이 모자라다」로 보고되지만, 실제로는 어느 항목이 자기 몫을 넘긴 것이다. 세어 본 뒤 손대는 차례는 대체로 이렇다. 도구 결과의 불필요한 필드를 쳐낸다. 시스템 프롬프트에서 중복되는 지시를 합친다. 검색 문서 개수를 스윕으로 정한다. 고정 항목에 캐시를 걸고 무효화되지 않는지 확인한다. 그다음에야 이력 요약이나 외부 메모리 같은 정교한 기법이 값을 한다.

컨텍스트 수선 순서

이 순서에는 이유가 있다. 앞의 넷은 잃는 것이 없거나 오히려 품질이 좋아지는 수선이고, 뒤의 둘은 새 부품과 새 실패 모드를 들여오는 일이다. 요약을 붙이면 요약이 사실을 뭉갤 위험이 생기고, 외부 메모리를 붙이면 검색이 엉뚱한 과거를 끌어올 위험이 생긴다. 공짜인 것부터 다 쓰고 나서 값을 치르는 것으로 넘어간다.

그리고 이 모든 계산의 바닥에는 「창에 넣은 것은 모델이 읽는다」는 가정이 깔려 있다. 앞에서 본 Lost in the Middle이 그 가정에 낸 첫 번째 금이었다. 창이 백만 토큰이라는 말과 백만 토큰을 고르게 잘 읽는다는 말이 같은 말인지, 긴 컨텍스트를 재는 시험들이 실제로 무엇을 재고 있는지는 다음 글에서 따로 본다.


읽어주셔서 감사합니다. 😊

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

에이전트는 어디서 어긋나기 시작하는가

에이전트가 이상한 답을 낼 때 증상은 마지막에 보이지만 어긋난 자리는 그보다 앞입니다. 한 걸음이 무너지는 여섯 자리를 나누고, 증상에서 원인을 거슬러 찾는 방법과 자리별 처방을 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트가 무엇을 했는지 나중에 알 수 있게 만들기

에이전트는 같은 입력에도 다른 경로로 갑니다. 그래서 로그 몇 줄로는 왜 그렇게 됐는지 복원이 안 됩니다. 트레이스를 어떻게 나누고 구간마다 무엇을 붙이며 어떤 지표를 볼지 정리합니다.

11 MIN
에이전트·RAG2026.08.23

에이전트 비용은 걸음 수보다 빨리 늘어난다

걸음이 두 배면 비용은 두 배가 아니라 서너 배입니다. 왜 그렇게 되는지, 어디서 새는지, 상한을 몇 겹으로 어떻게 거는지와 실제로 효과가 큰 순서대로의 대응을 정리합니다.

13 MIN