Micro-compaction: 에이전트 루프에서의 분할 상환 방식 컨텍스트 압축
요약
에이전트 세션의 컨텍스트 압축 시 발생하는 긴 지연 시간을 해결하기 위해 매 턴마다 조금씩 요약하는 'Micro-compaction' 방식을 소개합니다. 배치 압축 대신 순환 요약을 사용하여 지연 시간을 분산시키고 컨텍스트 유지 능력을 향상시킵니다.
핵심 포인트
- 배치 압축의 긴 일시 정지 문제를 분할 상환 방식으로 해결
- 매 턴마다 가장 오래된 대화를 순환 요약하여 지연 시간 최소화
- 사용자 의도 보존을 위해 사용자 메시지는 압축 대상에서 제외
- 시스템 프롬프트와 최신 대화(Tail)는 원문 그대로 유지하여 정확도 확보
어떤 프레임워크에서든 긴 에이전트 세션을 실행해 보았다면, 한계에 부딪힌 적이 있을 것입니다. 컨텍스트(context)가 가득 차고, 프레임워크가 압축(compact)할 시점이라고 판단하면, 요약기(summarizer)가 전체 기록을 처리하는 동안 모든 것이 멈춰버립니다. hermes-agent에서는 이 배치 압축(batch compaction)이 컨텍스트 임계값의 약 80% 지점에서 발생하며, 긴 세션에서는 이 일시 정지 시간이 몇 분 단위로 측정됩니다. 에이전트가 멈춘 것은 아니며 필요한 작업을 수행 중인 것이지만, 외부에서 보기에는 작동이 중단된 것처럼 보입니다. 그리고 압축을 늦게 하면 늦출수록, 결국 지불해야 할 비용(lump)은 더 커집니다.
Micro-compaction은 동일한 비용을 지불하는 다른 방식입니다. 용량이 거의 찼을 때 한 번에 큰 배치를 처리하는 대신, 매 턴(turn)마다 아직 흡수되지 않은 가장 오래된 교환 내용을 순환 요약(rolling summary)으로 접어 넣습니다. 전체 요약 작업량은 대략적으로 동일합니다. 변하는 것은 비용의 형태입니다. 최악의 순간에 발생하는 한 번의 긴 지연 대신, 세션 전체에 걸쳐 분산된 작은 증가분으로 바뀝니다.
이례적인 상황이라 원문의 노트를 덧붙입니다. 이 기능의 초안은 Hermes가 자신의 코드베이스를 위해 직접 작성했습니다. 이어지는 측정, 버그 수정, 그리고 업스트리밍(upstreaming) 과정은 그 초안을 병합할 가치가 있는 상태로 만들기 위해 필요한 과정들이었습니다.
작동 방식
매 턴이 끝난 후, 파이널라이저(finalizer)는 아직 흡수되지 않은 가장 오래된 에이전트 턴을 찾아 이를 누적 순환 요약(cumulative rolling summary)으로 요약하고, 해당 내용이 있던 기록 위치에 단일 요약 마커(summary marker)를 삽입합니다. 어떤 시점에도 마커는 정확히 하나만 존재합니다. 각 패스(pass)는 새로운 마커를 추가하는 대신 기존 마커를 교체합니다. 보호된 헤드(protected head, 시스템 프롬프트 및 초기 설정)와 보호된 테일(protected tail, 가장 최근의 턴들, 기본적으로 윈도우의 16%)은 절대 건드리지 않으므로, 모델은 현재 작업 중인 내용에 대해 항상 원문 그대로의 접근 권한을 가집니다.
언급할 만한 가치가 있는 한 가지 설계 결정은 다음과 같습니다: 사용자 턴(user turns)은 절대 압축하지 않습니다. 어시스턴트(Assistant)의 출력은 "이런 방식으로 수행했다"는 식으로 요약될 수 있고, 세부 사항은 대개 이후의 작업으로부터 재구성될 수 있기 때문에 압축이 잘 됩니다. 하지만 사용자의 의도(User intent)는 그렇지 않습니다. 사용자가 실제로 요청한 내용을 요약하여 없애버리면, 이후의 어떤 컨텍스트로도 이를 복구할 수 없습니다. 사용자 메시지는 원문 그대로 누적되기 때문에, 이 방식은 대화 중간 부분이 얼마나 줄어들 수 있는지에 대한 하한선(floor) 역할을 합니다. 저는 이것이 올바른 트레이드오프(trade-off)라고 생각합니다. 만약 사용자가 20K 토큰 분량의 프롬프트를 던진다면, 그것은 사용자의 책임입니다.
이 기능은 선택 사항(opt-in)입니다:
compression:
micro_compact: true
실행 빈도를 조절하고 싶다면 micro_compact_every_n_turns 옵션도 사용할 수 있습니다.
수치가 말해주는 것
이 기능의 핵심 주장은 단순히 "토큰을 절약한다"가 아닙니다. 초기에는 저도 그렇게 표현했으나, 그것은 잘못된 헤드라인이었습니다. 진짜 가치는 두 가지입니다: 긴 일시 정지(pause) 시간이 분할 상환(amortized)되어 사라지고, 임계값(threshold)에 도달하기 전까지 컨텍스트가 더 오래 지속된다는 점입니다.
검증 실행은 모든 패스(pass)에 대한 텔레메트리(telemetry)를 포함한, 프로젝트 전체 코드 리뷰를 수행하는 실제 3.5시간 세션이었습니다. 두 가지 결과가 나왔습니다:
약 75K 토큰의 트랜스크립트(transcript) 전체에서 배치 압축(batch compactions)이 단 한 번도 발생하지 않았습니다. 몇 분씩 걸리던 일시 정지도 단 한 번도 실행되지 않았습니다.
점유율(Occupancy, 압축 임계값 대비 사용된 컨텍스트의 비율)은 8.7% → 15.0% → 17.5% → 21.8% → 22.0%로 상승하다가 평탄해졌습니다. 마지막 구간에서 대화에 4,841 토큰이 추가되었지만, 마이크로 압축(micro-compaction)이 4,395 토큰을 다시 확보했습니다. 이는 80%를 향해 꾸준히 상승하는 대신 22%에서 평형(equilibrium)을 이룬 것입니다. 이 세션은 실제 진행된 것보다 훨씬 더 오래 지속될 수 있었습니다.
직접 이를 측정(instrument)할 때 주의할 점이 하나 있습니다: 첫 번째 패스(pass)에서는 토큰 비용이 발생합니다. 요약 마커(summary marker)는 약 411 토큰의 고정된 스캐폴딩(scaffolding)을 수반하며, 첫 번째 패스에서는 이것이 흡수된 단일 교환(exchange)에 대해 청구되므로, 차이(delta)는 일반적으로 양수입니다. 제 측정치로는 약 +330 정도였습니다. 두 번째 패스부터는 마커가 추가되는 대신 교체되는 방식이 되며, 각 교환은 거의 순수하게 절약 효과를 냅니다. 손익분기점(Break-even)은 두 번째 또는 세 번째 패스 즈음에 도달합니다. 단일 텔레메트리 라인이 아니라 전체적인 궤적(trajectory)을 판단하십시오.
신뢰할 수 있는 숫자를 얻는 것이 솔직히 가장 큰 작업이었습니다. 원래 초안에는 매 패스마다 새 마커를 추가하고 이전 마커들을 제자리에 남겨두는 버그가 있었습니다. 롤링 요약(rolling summary)은 누적되기 때문에, 각 오래된 마커는 다음 마커의 거의 중복되는 접두사였고, 스크립트는 매 턴마다 커졌습니다. 6개 턴에 걸쳐 측정했을 때: 4,104 토큰이 4,797 토큰으로 부풀어 올랐습니다. supersede 수정 덕분에 같은 대화가 4,104에서 2,572로 줄었습니다. 두 번째 버그는 재개 커서(resume cursor)가 스플라이스 이전 인덱스를 가리키게 하여, 도구 사용이 많은 대화에서는 흡수했어야 할 교환의 절반 정도를 조용히 건너뛰게 했습니다. 이 두 가지 버그는 단일 패스 단위 테스트에서는 보이지 않습니다. 둘 다 480개의 다양한 대화 형태(conversation shapes)를 각각 25번의 패스로 실행하고 매 패스마다 불변성(invariants)을 검증하는 무작위 스트레스 하네스(randomized stress harness)에서 발견되었습니다: 마커는 최대 하나여야 하고, 스크립트는 API 유효성을 유지해야 하며, 메시지 수는 절대 증가해서는 안 되고, 사용자 턴은 온전해야 합니다. 이 클래스로 무엇이든 구축하고 있다면, 압축 버그는 드리프트(drift)를 누적시키고 있으므로, 여러 턴을 시뮬레이션하는 것이 이를 찾는 유일한 테스트 형태입니다.
솔직한 비용
Micro-compaction은 패스를 커밋할 때마다 이미 전송된 기록을 다시 작성하며, 이는 제공업체의 프롬프트 캐시 접두사(prompt-cache prefix)를 깨뜨립니다. 배치 압축(Batch compaction)도 그렇게 하지만, 매 턴이 아니라 한 번만 그렇습니다. hermes-agent 자체 설정은 대화별 캐싱을 거의 신성한 것으로 취급하며, 그 선제적 가지치기 로직(proactive-prune logic)은
패스 (pass) 자체도 공짜는 아닙니다. 검증 실행(validation run)에서의 중간 패스 지속 시간은 약 31초였으며, 턴 종료(turn finalization) 시 동기식(synchronous)으로 작동합니다. 그 시점에는 이미 응답이 스트리밍되었으므로 사용자가 멈춰 있는 답변을 바라보게 되지는 않지만, 패스가 완료될 때까지 턴은 닫히지 않습니다. 총 3.5시간 동안 약 2분 정도의 요약 작업이 분산되어 수행되었는데, 이는 여전히 몇 분씩 걸리는 한꺼번에 처리하는 방식보다는 낫지만, 31초 단위의 증분은 "작은 증분"이라는 프레임워크에 부담을 줍니다. 제 설정에서 병목 현상(bottleneck)은 입력 크기가 아니라 공유 서버에서 실행되는 4-bit Qwen 7B 요약 모델(summarizer model)이므로, 더 빠른 보조 모델(auxiliary model)을 사용한다면 이 시간을 상당히 줄일 수 있을 것입니다. 아마도 말이죠.
또한 아직 회수 크기 게이트(reclaim-size gate)는 없습니다. 패스는 단 한 번의 교환(exchange)만으로 저장된 내용이 단 몇 백 토큰에 불과하더라도 무엇이든 커밋(commit)하며, 각 커밋은 캐시 단절(cache break)을 일으킵니다. 명확한 후속 작업은 흡수된 교환들을 버퍼링(buffering)했다가, 회수량이 임계값(threshold)을 넘었을 때만 병합(splicing)하는 것이며, 이는 기존의 가지치기 로직(prune logic)이 수행하는 방식과 유사합니다. 현재는 그런 기능이 존재하지 않습니다.
결과물
이 기능은 hermes-agent에 병합되었으며 v0.19.1 버전으로 출시되었고, 위의 설정 플래그(config flag)를 통해 선택적으로 사용할 수 있습니다. 원래의 PR은 #74522였으며, 커밋과 저작권을 유지하면서 일부 내부 구조를 제가 보기에도 곳곳에서 더 나은 설계로 재작업한 유지 관리자의 구조 작업(salvage) PR (#75345)을 통해 반영되었습니다. 이제 마커(marker)는 어시스턴트 역할(assistant-role)의 메시지이며, 교환(exchange)은 사용자 메시지로 경계가 지어지는 전체 에이전트 턴(agent turn)입니다. 또한 마커는 그 내용이 롤링 요약(rolling summary)에 확실히 포함되어 있음이 증명될 때만 대체되므로, 오래된 마커(stale-marker) 버그가 구조적으로 다시 발생하기 어렵게 만들었습니다. 저장소의 문서(docs)에서 그 메커니즘을 더 자세히 다루고 있습니다.
만약 에이전트 루프 (agent loop)를 유지 관리하고 있는데 배치 압축 (batch compaction) 일시 중단 현상으로 인해 어려움을 겪고 있다면, 코드가 동일하지 않더라도 이 아이디어의 형태는 그대로 적용될 수 있습니다: 턴 (turn)마다 요약 비용을 지불하고, 헤드 (head)와 테일 (tail)을 보호하며, 사용자의 의도 (user intent)는 절대 건드리지 말고, 신뢰하기 전에 반드시 계측 (instrument)하십시오. 특히 마지막 항목이 중요합니다. 이 기능에서 발견된 모든 실제 버그는 코드를 읽어서가 아니라 계측을 통해 발견되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기