context 압축 전략 — 우리가 시도한 3가지
LLM을 결제·정산 자동화에 연결할 때 컨텍스트 비용이 폭증하는 이유와, HEDVION이 슬라이딩 윈도우·요약 삽입·상태 분리를 순서대로 시도하며 얻은 실전 교훈을 수치와 함께 공유한다.
결제·정산 자동화에서 컨텍스트 비용은 왜 특히 위험한가
일반적인 챗봇 서비스와 정산 자동화 에이전트는 컨텍스트 문제의 결이 다르다. 챗봇은 컨텍스트가 길어지면 응답이 느려지는 정도로 끝나지만, 정산 에이전트는 컨텍스트 오염이 곧 금전 오류로 이어진다. HEDVION 팀이 운영하는 자동화 파이프라인을 예로 들면, 에이전트 세션 하나가 수십 건의 트랜잭션 내역, 환불 조건, 수수료 정책, 파트너사별 정산 규칙을 동시에 다룬다. 이 정보들이 뒤섞이거나 잘못 압축되면 단순한 응답 품질 저하가 아니라 오정산, 중복 처리, 규칙 누락이 발생한다. 결제 도메인에서 "맥락이 잘린다"는 것은 단순한 불편이 아니다.
비용 구조도 마찬가지다. 우리 팀이 초기 프로토타입을 운영할 때, 에이전트 세션 하나가 30분을 넘기면 누적 컨텍스트가 32K 토큰을 초과하기 시작했다. Claude Sonnet 기준 입력 토큰 비용이 $3/1M 수준인데, 에이전트가 하루 200세션을 처리하고 세션당 평균 40K 입력 토큰을 소비하면 월 입력 비용만 약 $720에 달한다. 여기에 멀티턴 대화를 반복 참조하는 패턴이 더해지면 동일 토큰을 여러 번 청구받는 구조가 된다. 컨텍스트 압축은 UX 문제가 아니라 운영 비용과 정확도의 문제다. 이걸 늦게 깨달을수록 리팩터링 비용이 커진다.
전략 1 — 슬라이딩 윈도우: 단순함의 대가
가장 먼저 시도한 것은 슬라이딩 윈도우다. history[-MAX_HISTORY:]로 최근 N개 메시지만 유지하는 방식으로, 구현에 30분도 걸리지 않는다.
MAX_HISTORY = 10
def build_context(history: list[Message]) -> list[Message]:
return history[-MAX_HISTORY:]
짧은 단일 태스크 — "이 송장의 부가세를 계산해줘" 같은 요청 — 에서는 최근 10개 메시지면 충분했다. 문제는 세션이 길어지는 경우였다. 파트너사 담당자가 "아까 말한 수수료 예외 조건에서 이 케이스는 어떻게 처리해?"라고 물으면, 그 '아까'가 윈도우 바깥에 있다. 모델은 모른다고 하거나, 더 나쁜 경우 잘못된 기본값으로 계산한다. 정산 로직에서 예외 조건 누락은 소액이라도 누적되면 월말 정산 불일치로 직결된다.
슬라이딩 윈도우의 핵심 문제는 무엇을 버릴지를 모델이 아니라 순서가 결정한다는 점이다. 대화 초반에 설정한 고정 조건(계약 조건, 파트너별 예외 규칙)이 시간 순서상 오래됐다는 이유만으로 잘린다. 이 방식은 태스크가 stateless하고 짧을 때만 안전하다. 결제·정산처럼 세션 내내 초기 조건을 참조해야 하는 도메인에서는 구조적으로 맞지 않는다.
전략 2 — 요약 삽입: 비용과 신뢰도의 충돌
슬라이딩 윈도우의 단점을 보완하기 위해 요약 삽입으로 넘어갔다. 대화가 일정 길이를 초과하면 이전 대화를 모델에게 요약시키고, 그 요약을 system 메시지 앞에 고정해두는 방식이다.
system:
[이전 대화 요약]
- 사용자는 A 파트너사 수수료 3.5% 조건을 적용 요청
- B 케이스에서 환불 예외 처리 완료
- 현재 C 배치 정산 검토 중
user: [최근 메시지들]
정보 보존 측면에서는 분명 나아졌다. 하지만 두 가지 문제가 곧 드러났다. 첫째, 요약 자체가 모델 호출이라는 점이다. 세션당 요약을 2–3회 수행하면 요약 비용이 총 호출 비용의 15–20%를 추가로 차지한다. 처음 기대했던 절감 효과가 요약 비용에 상쇄된다.
둘째 문제가 더 심각했다. 숫자와 고유명사는 요약이 신뢰할 수 없다. 실제 테스트에서 "수수료율 3.5%"가 요약 과정에서 "약 3–4%"로 뭉개지거나, 특정 파트너사 코드가 유사한 다른 코드와 혼용되는 현상을 확인했다. 요약은 의미의 흐름을 보존하는 데는 강하지만, 정밀도가 요구되는 정산 로직에서는 허용 오차가 0에 가까워야 한다. "약 3–4%"와 "3.5%"의 차이는 대화 맥락에서는 사소해 보이지만, 월 1억 원 규모의 정산에서는 수십만 원의 오차로 나온다. 요약은 서사를 요약하는 도구지, 수치를 보존하는 도구가 아니다.
전략 3 — 구조화된 상태 분리: 우리가 정착한 방식
현재 HEDVION 에이전트 파이프라인에 적용 중인 방식이다. 핵심 아이디어는 대화 히스토리와 작업 상태를 완전히 분리하는 것이다. 모델에는 히스토리를 넘기지 않는다. 애플리케이션 레이어가 관리하는 구조화된 상태 객체만 조립해서 전달한다.
# 상태 객체 — 애플리케이션이 외부에서 관리
state = {
"partner_id": "PARTNER_A",
"fee_rate": 0.035, # 원본 값 그대로, 요약 경로 없음
"exceptions": ["CASE_001"], # 처리 완료 예외 목록
"current_step": "batch_review",
"pending_items": [...]
}
def build_context(state: dict, current_message: str) -> list[Message]:
system = f"""
파트너: {state['partner_id']}
적용 수수료율: {state['fee_rate']}
처리 완료 예외: {state['exceptions']}
현재 단계: {state['current_step']}
"""
return [{"role": "system", "content": system},
{"role": "user", "content": current_message}]
모델이 어떤 결정을 내리면, 그 출력을 파싱해 상태 객체를 업데이트하고 다음 호출에서 다시 조립한다. 대화 히스토리 전체는 컨텍스트에 넣지 않는다. 검색이 필요한 경우에만 벡터스토어에서 관련 청크를 가져와 추가한다.
이 방식으로 전환한 뒤 입력 토큰 사용량이 세션당 평균 40K에서 약 6K 수준으로 줄었다. 같은 작업 품질을 유지하면서 85% 토큰을 절감한 수치다. 응답 지연도 평균 4.2초에서 1.8초로 개선됐다. 더 중요한 것은 수수료율, 파트너 코드 같은 정밀 수치가 상태 객체에서 직접 넘어가므로 요약으로 인한 왜곡이 구조적으로 불가능해졌다는 점이다. 단점도 명확하다 — 상태 설계가 복잡하다. 어떤 정보를 상태로 추출할지, 모델 출력에서 상태 변화를 어떻게 파싱할지를 사전에 명세해야 한다. 우리 팀은 초기 설계에 약 2주를 썼고, 새 정산 규칙이 추가될 때마다 상태 스키마를 수정한다. 이 유지보수 비용을 과소평가하면 곤란하다.
세 전략 수치 비교
실제 운영 데이터를 기반으로 정리하면 다음과 같다.
| 전략 | 세션당 평균 입력 토큰 | 응답 지연(평균) | 정밀 수치 보존 | 설계 복잡도 |
|---|---|---|---|---|
| 슬라이딩 윈도우 | ~12K | 2.1초 | 낮음 (순서 의존) | 낮음 |
| 요약 삽입 | ~18K (요약 포함) | 3.4초 | 중간 (수치 왜곡 위험) | 중간 |
| 상태 분리 | ~6K | 1.8초 | 높음 (구조적 보장) | 높음 |
슬라이딩 윈도우가 토큰 수는 중간처럼 보이지만, 이는 컨텍스트 자체가 잘리기 때문이다. 장기 세션에서 누락 오류가 발생하면 수동 수정이나 재처리 비용이 따라오므로 실질 운영 비용은 가장 높을 수 있다. 요약 삽입은 요약 호출 자체가 추가 비용이고 지연도 늘어난다. 상태 분리는 초기 설계 비용이 있지만 운영 규모가 커질수록 단위 비용이 가장 안정적이다. 세 전략 모두 "컨텍스트에 무엇을 넣을지"를 명시적으로 결정하는 방식이지, 그냥 히스토리를 쌓아두는 것과는 다르다. 처음부터 이 결정을 설계의 일부로 다루지 않으면 서비스가 성장하면서 반드시 벽을 만난다.
지금 바로 적용할 수 있는 시사점
1. 지금 당장 컨텍스트 토큰을 측정하라. 느낌이 아니라 숫자로 봐야 한다. tiktoken 또는 anthropic.count_tokens()로 세션별 입력 토큰을 로깅하고, 10K를 초과하는 세션의 비율과 평균 세션 길이를 파악하는 것이 시작이다. 이 데이터 없이 전략을 고르면 과잉 설계하거나 과소 대응하게 된다.
2. 정밀 수치는 절대 요약 경로에 태우지 마라. 수수료율, 계좌번호, 날짜, 건수 같은 정보는 요약을 거치는 순간 신뢰도가 떨어진다. 이런 정보는 별도 구조체로 관리하고, 모델 컨텍스트에는 포맷된 문자열로 직접 주입해야 한다. "요약은 흐름에만, 사실 데이터는 직접 참조"를 코딩 컨벤션으로 명시해두는 것이 좋다.
3. 상태 분리를 도입한다면 스키마 먼저, 코드 나중. 상태 객체의 필드 목록과 각 필드의 업데이트 트리거를 먼저 문서화하라. "모델 출력에서 어떻게 추출할 것인가"를 코드 전에 정의하지 않으면, 추출 로직이 케이스마다 임시방편으로 쌓인다. 우리 팀은 JSON Schema로 상태 스키마를 먼저 정의하고, 모델 출력도 같은 스키마로 structured output을 강제하는 방식을 쓴다.
4. 전략은 서비스 단위가 아니라 태스크 단위로 선택하라. 동일 서비스 안에서도 "단발 계산 요청"과 "멀티턴 정산 검토 세션"은 최적 전략이 다르다. 태스크 유형별로 전략을 분기하고, 각각의 컨텍스트 예산 상한을 별도로 관리해야 장기적으로 유지보수가 쉽다. 단일 전략으로 모든 케이스를 커버하려다 두 배로 리팩터링하는 상황은 우리 팀도 이미 한 번 겪었다.
— by slecs
* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.