에이전트·RAG

AGENT / 45번째 글

예산을 넘겼을 때 무엇부터 버릴 것인가

토큰 추정이 실제와 얼마나 벌어지는지, 안전 여유를 데이터로 정하는 법, 예산 초과 시 되돌리기 쉬운 것부터 버리는 강등 순서를 코드로 고정하는 방법을 정리합니다.

PALDYN Team12 MIN READ

지난 글까지가 무엇을 넣고 어떻게 늘어놓을지에 대한 얘기였다면, 남은 것은 그 계획이 어긋나는 순간이다. 예산표를 아무리 잘 짜 두어도 실제 요청은 예산을 넘긴다. 사용자가 30페이지짜리 문서를 붙여 넣고, 도구가 예상의 열 배짜리 응답을 돌려주고, 대화가 예정보다 길어진다. 그때 시스템이 무엇을 하느냐가 품질을 가른다.

가장 나쁜 경우는 아무것도 정해 두지 않은 것이다. 그러면 API가 오류를 던지거나, 프레임워크가 임의로 앞부분을 잘라 내거나, 답이 중간에 끊긴다. 셋 다 사용자에게는 같은 모습으로 보인다 — 이유를 알 수 없는 실패다.

토큰 추정은 생각보다 자주 틀린다

예산 관리의 출발점은 지금 요청이 몇 토큰인지 아는 것인데, 이 값부터 정확하지 않다. 흔한 추정 방식과 그 오차는 대략 이렇다.

방법 정확도 비용 쓸 자리
글자 수 ÷ 상수 낮다 없다 대략의 감만 잡을 때
다른 모델의 토크나이저 중간 낮다 후보를 거를 때
해당 모델의 토크나이저 높다 낮다 조립 단계의 기본값
제공자의 토큰 계산 API 가장 높다 호출 한 번 임계 근처에서 확인할 때

한국어는 특히 편차가 크다. 같은 글자 수라도 자주 쓰는 표현은 토큰이 적게 붙고, 고유명사나 코드가 섞이면 훨씬 많이 붙는다. 그래서 "한글 한 글자 ≈ 1.5토큰" 같은 상수는 평균에서는 맞아도 개별 요청에서는 크게 어긋난다.

중요한 것은 오차를 없애는 것이 아니라 오차의 크기를 알고 있는 것이다. 실제 요청 몇백 건에서 추정값과 응답에 실려 온 실제값을 함께 로그로 남기면 상대 오차 분포가 나온다.

ϵ=T실제−T추정T추정\epsilon = \frac{T_{\text{실제}} - T_{\text{추정}}}{T_{\text{추정}}}

안전 여유 SS는 이 분포에서 정한다. ϵ\epsilon의 상위 99 백분위가 4%라면, 창의 5% 정도를 여유로 두면 백 번에 한 번도 넘치지 않는다. 감으로 "넉넉하게 10%"를 잡는 것과 결과는 비슷할 수 있지만, 데이터로 정하면 나중에 줄여도 되는지를 판단할 수 있다. 여유는 그냥 버리는 창이므로 근거 없이 크게 잡으면 계속 손해다.

버리는 순서를 코드로 못 박는다

예산을 넘겼을 때 무엇부터 버릴지는 원칙이 하나다 — 되돌리기 쉬운 것부터. 앞선 글들에서 본 압축 사다리가 그대로 강등 순서가 된다.

예산 초과 시 강등 순서

이 순서를 문서가 아니라 코드에 두는 것이 중요하다. 문서에만 있으면 급할 때 지켜지지 않고, 무엇보다 조립하는 자리가 여러 곳으로 흩어지면 경로마다 다른 순서로 버리게 된다.

def enforce_budget(parts, budget):
    steps = [
        lambda p: cap_documents(p, max_docs=DOC_CAP),      # ①
        lambda p: shrink_old_results(p, keep_full=3),      # ②
        lambda p: drop_old_turns(p, keep_pairs=6),         # ③
        lambda p: summarize_dropped(p),                    # ④
    ]

    for i, step in enumerate(steps):
        if estimate(parts) <= budget:
            return parts, i                                # 몇 단계까지 갔는지 함께 돌려준다
        parts = step(parts)

    if estimate(parts) > budget:
        raise BudgetExceeded(estimate(parts), budget)      # ⑤ 호출부가 결정한다
    return parts, len(steps)

return parts, i가 이 함수의 두 번째 역할이다. 몇 단계까지 강등했는지를 호출부로 돌려주면 그 값을 로그와 응답 메타데이터에 실을 수 있다. 이게 없으면 강등이 조용히 일어난다 — 답의 품질이 떨어져도 원인이 안 보인다.

절대 잘리지 않는 것을 먼저 정한다

강등 순서만큼 중요한 것이 그 반대편이다. 어떤 단계에서도 손대지 않는 집합을 명시해 두지 않으면, 압박이 심할 때 결국 아무거나 잘린다.

항목 왜 남기는가
시스템 지시 없으면 모델이 자기 역할을 잃는다
최초의 과제 서술 에이전트가 무엇을 하던 중인지 모르게 된다
사용자가 명시한 제약 어기면 사용자가 같은 말을 반복하게 된다
마지막 사용자 메시지 이걸 자르면 애초에 답할 것이 없다
직전 도구 호출과 그 결과 짝이 깨지면 대화 구조 자체가 무너진다

마지막 줄은 실수가 잦은 자리다. 토큰이 큰 순서로 자르는 코드를 짜면 큰 도구 결과가 먼저 사라지는데, 그 결과에 대응하는 호출 메시지가 남으면 대화가 앞뒤가 안 맞는 상태가 된다. 자를 때는 항상 짝 단위로 자른다.

PROTECTED = ("system", "task_brief", "constraints", "last_user")

def drop_old_turns(parts, keep_pairs):
    body = [m for m in parts.messages if m.tag not in PROTECTED]
    kept = body[-keep_pairs * 2:]              # 사용자·어시스턴트 짝으로만 남긴다
    parts.messages = [m for m in parts.messages if m.tag in PROTECTED] + kept
    return parts

모델마다 예산표가 다르다

경로에 따라 다른 모델을 쓴다면 예산을 상수로 두면 안 된다. 창 크기만 다른 것이 아니라 출력 상한, 토크나이저, 캐시 조건이 전부 다르다. 그래서 모델별 표를 하나 두고 조립 함수가 그 표를 읽게 한다.

BUDGETS = {
    "fast":     {"window": 128_000, "max_output":  4_000, "doc_cap":  8},
    "standard": {"window": 200_000, "max_output":  8_000, "doc_cap": 12},
    "long":     {"window": 1_000_000, "max_output": 16_000, "doc_cap": 20},
}

def input_budget(tier):
    b = BUDGETS[tier]
    return b["window"] - b["max_output"] - int(b["window"] * SAFETY_RATIO)

여기서 한 가지 함정이 있다. 창이 큰 모델로 라우팅하면 예산 문제가 해결되는 것처럼 보이지만, 앞서 본 대로 창이 크다고 그 안의 정보가 다 쓰이지는 않는다. 창이 큰 모델은 넘침을 막는 수단이지 품질을 올리는 수단이 아니다. 강등 사다리를 건너뛰는 용도로 쓰면 비용은 오르고 정확도는 그대로인 조합이 된다.

doc_cap을 모델마다 따로 둔 것도 같은 이유다. 창이 여덟 배 크다고 문서를 여덟 배 넣으면 안 된다. 그 값은 창 크기가 아니라 평가로 정한다.

강등을 소리 나게 만든다

강등의 가장 큰 위험은 그것이 조용하다는 점이다. ④단계까지 갔는데도 답은 그럴듯하게 나오고, 사용자는 그 답이 원문 절반을 잃은 상태에서 만들어졌다는 것을 모른다. 그래서 남길 것은 세 가지다.

  • 어느 단계까지 갔는가. 위 함수가 돌려주는 값을 그대로 지표로 올린다.
  • 무엇이 빠졌는가. 잘라 낸 문서 번호, 축약한 도구 호출 이름 정도면 충분하다.
  • 추정과 실제의 차이. 응답의 사용량 정보와 조립 시점 추정값을 함께 남긴다.

첫 값을 경로별로 집계하면 운영 판단이 바로 나온다. 어떤 경로의 요청 중 30%가 ③단계를 넘어간다면 그 경로는 애초에 예산 설계가 틀린 것이다. 강등 코드를 손볼 것이 아니라 그 경로에서 넣는 것을 줄여야 한다.

반대로 모든 경로가 ①단계에서 끝난다면 여유가 과한 것일 수 있다. 안전 여유를 줄이거나 문서 상한을 올려 볼 여지가 있다는 신호다.

마지막 단계는 실패가 아니다

사다리 끝의 ⑤단계, 즉 작업을 나누거나 사용자에게 알리는 선택은 패배처럼 보여서 자주 생략된다. 그 대신 요약을 한 번 더 돌리거나 문서를 더 잘라 억지로 창에 밀어 넣는다.

그런데 그 지점을 넘어가면 답의 근거가 남아 있지 않은 상태가 되고, 모델은 근거가 없어도 답을 만든다. 사용자 입장에서 틀린 답을 자신 있게 받는 것보다 "이 문서는 한 번에 처리하기에 너무 깁니다. 장별로 나눠 진행할까요?"가 낫다. 나누는 방법을 제안할 수 있으면 더 낫고, 자동으로 나눠 처리한 뒤 합칠 수 있으면 가장 낫다.

정리하면 컨텍스트 예산 관리는 결국 세 문장이다. 넘칠 것을 전제로 순서를 미리 정해 둔다. 그 순서를 코드 한 곳에 두고 절대 안 잘리는 집합을 명시한다. 그리고 강등이 일어났다는 사실이 로그에 남게 한다. 이 셋이 있으면 예산 초과는 장애가 아니라 관측 가능한 정상 동작이 된다.


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

LATEST

에이전트·RAG의 최신 글

에이전트·RAG2026.08.23

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

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

11 MIN
에이전트·RAG2026.08.23

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

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

11 MIN
에이전트·RAG2026.08.23

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

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

13 MIN