← 모든 글

context 압축 전략 — 우리가 시도한 3가지

LLM 컨텍스트 창이 길어질수록 비용과 지연이 늘어난다. 우리 팀이 실제 서비스에서 시도한 세 가지 압축 전략을 정리한다.

LLM 을 서비스에 연결하면 초반에는 컨텍스트 관리가 쉽다. 대화가 짧고, 토큰이 적고, 비용도 낮다. 그런데 사용 시간이 길어질수록 컨텍스트 창이 쌓이고, 어느 순간부터 응답 지연이 눈에 띄게 늘거나 비용 그래프가 꺾인다. 우리 팀은 이 문제를 다루면서 세 가지 전략을 순서대로 시도했다.

전략 1 — 슬라이딩 윈도우

가장 단순한 방법이다. 최근 N개의 메시지만 컨텍스트로 유지하고 나머지는 버린다.

MAX_HISTORY = 10

def build_context(history: list[Message]) -> list[Message]:
    return history[-MAX_HISTORY:]

장점은 구현이 간단하고 컨텍스트 크기가 예측 가능하다는 것이다. 단점은 오래된 정보가 통째로 잘린다는 것이다. 사용자가 대화 초반에 입력한 조건이나 맥락이 필요한 상황에서 모델이 그걸 모르는 채로 답하는 문제가 생겼다.

전략 2 — 요약 삽입

일정 길이를 초과하면 이전 대화를 모델에게 요약시키고, 그 요약을 컨텍스트 앞에 고정해 두는 방식이다.

system: <이전 대화 요약>
  - 사용자는 A 조건을 원함
  - 지금까지 B 를 시도했고 실패함
  - 현재 C 를 논의 중

user: [최근 메시지들]

이 방식은 슬라이딩 윈도우보다 정보 보존이 낫다. 하지만 요약 자체가 모델 호출이기 때문에 요약 비용이 추가되고, 요약 품질에 따라 중요한 세부 정보가 손실될 수 있다. 특히 숫자나 고유명사 같은 정밀도가 필요한 정보는 요약 과정에서 왜곡될 가능성이 있어서, 요약 결과를 그대로 신뢰하기 어려웠다.

전략 3 — 구조화된 상태 분리

현재 우리가 사용하는 방식이다. 대화 히스토리 전체를 컨텍스트로 넣는 대신, 애플리케이션이 상태를 별도로 관리하고 모델에는 현재 작업에 필요한 정보만 조립해서 넘긴다.

system:
  - 사용자 설정: {user_settings}
  - 현재 작업 단계: {current_step}
  - 관련 조건: {relevant_constraints}

user: [현재 메시지 1개]

상태는 대화 외부에서 구조체로 관리한다. 모델이 결정을 내리면 그 결정을 추출해서 상태를 업데이트하고, 다음 호출에는 업데이트된 상태를 다시 조립해서 보낸다. 대화 히스토리는 검색이 필요한 경우 별도 벡터스토어에서 가져온다.

이 방식의 장점은 컨텍스트 크기가 대화 길이와 무관하게 일정하다는 것이다. 비용이 예측 가능해지고, 응답 지연도 안정된다. 단점은 상태 설계가 복잡하고, 모델이 히스토리 전체를 참조해야 하는 상황에서는 별도 검색 로직이 필요하다는 것이다.

어떤 전략을 선택할 것인가

세 전략은 복잡도와 유연성의 트레이드오프가 명확하다. 짧은 태스크 단위 대화라면 슬라이딩 윈도우로 충분하다. 장기 대화가 필요하지만 구조화가 어렵다면 요약 삽입이 차선이다. 서비스 레벨에서 안정적인 비용과 지연을 유지해야 한다면 상태 분리가 맞는 방향이다.

어느 방식이든 “컨텍스트에 무엇을 넣을 것인가”를 의식하지 않으면 서비스가 성장하면서 반드시 벽을 만난다. 처음부터 컨텍스트 관리를 설계의 일부로 다루는 것이 이후 리팩터링 비용을 아낀다.

— by slecs