한 번의 압축, 네 가지 액션, 하나의 블록: 압축 안전성은 쌍(pair)의 속성이다
요약
컨텍스트 압축의 안전성은 압축 자체의 품질이 아니라, 이후 수행될 특정 액션과의 관계에 따라 결정된다는 점을 분석합니다. 압축기가 에이전트의 미래 액션을 예측할 수 없기에 발생하는 안전성 문제를 수학적 검증 관점에서 다룹니다.
핵심 포인트
- 압축의 안전성은 액션(action)에 종속적인 쌍(pair)의 속성임
- 액션에 눈먼(action-blind) 검증은 과도한 차단(false positive)을 유발함
- 좋은 압축기라는 개념 대신 특정 액션에 대한 안전한 압축이 존재함
- 검증은 느낌이 아닌 산술적(arithmetic) 데이터로 이루어져야 함
**컨텍스트 압축 (Context compaction)**은 특정하게 제안된 액션(action)에 대해서만 안전하거나 안전하지 않습니다. 일반적으로는 그렇지 않습니다. 압축기(compactor)는 에이전트가 무엇을 할지 알기 전에 에이전트가 무엇을 잊을지를 결정하므로, 압축 시점에는 "이것이 중요한 무언가를 잃었는가?"라는 질문에 대한 답이 아직 존재하지 않습니다. 만약 거짓이 있다면, 그것은 나중에 에이전트가 액션을 제안할 때 비로소 존재하게 됩니다.
철학적인 이야기처럼 들리겠지만, 여기에는 종료 코드(exit code)가 있습니다.
AI 공개 (AI disclosure). 나는 AI 어시스턴트와 함께
compaction_omission_gate.py를 작성했으며 직접 실행했습니다: Python 3.13.5, 오프라인, 표준 라이브러리만 사용, 네트워크 없음, 키 없음, 자금 없음. 아래의 모든 판결, 종료 코드 및 해시(hash)는 실제 로컬 실행에서 복사되었습니다. 데모 전체를 두 번 실행했으며 두 개의output.txt파일은 바이트 단위로 동일합니다 (sha256 2f0ae2c6d54b1d35caefb3da12af3a4189aff05e5d8c59321b63150f27eef376). 피스처(fixtures)는 합성된 것이며 나의 것입니다. Anannya Roy Chowdhury, Prasad T 및 Ratel 팀으로부터 인용한 수치는 그들의 게시물에서 나온 그들의 주장이며, 본문 내에 출처를 밝혔습니다. 나는 그들의 시스템을 재현하지 않았습니다.
요약하자면:
- 압축을 고정합니다. 액션(action)만 변경합니다. 동일한 52개의 레코드, 동일하게 유지된 16개 (
모든 행에 kept-sha256=3bfe38c85fcf), 동일하게 삭제된 36개, 그리고 네 가지 모두에서 누락된 동일한 규칙: 1 BLOCK, 3 PASS. 압축이 전혀 움직이지 않았음에도 안전성이 이동했습니다. - 따라서 "좋은 압축기"라는 것은 가질 수 있는 대상이 아닙니다. 오직 "이 압축은 저 액션에 대해 안전하다"만 존재할 뿐입니다.
- 검증은 느낌(vibes)이 아니라 산술(arithmetic)입니다:
amount 5000은 통과(pass)하고,amount 5001은 차단(block)합니다. 동일한 단어이지만, 단 한 단위 차이입니다. - 이것은 이틀 전 내가 공개적으로 던진 질문에 대한 **부분적인 부정 (partial no)**입니다. 액션에 눈먼(action-blind) 버전의 검증은 게이트(gate)가 4개 중 1개를 차단하는 것과 달리 4개 중 4개 모두를 차단합니다. 그 4개의 차단 중 3개는 잘못된 것이며, 아무것도 그것들을 복구할 수 없습니다.
- 나의 설계 중 두 가지가 이 포스트에서 무너졌습니다: 하나는 다른 사람의 포스트에 나온 숫자 때문이었고, 다른 하나는 나의 리뷰어 때문이었습니다. 세부 사항은 아래에 있습니다.
세 사람이 같은 주에 발견한 격차
Anannya Roy Chowdhury는 7월 16일에 My Multi-Agent AI Cost $1,847 in One Weekend를 게시했습니다. 그녀가 제시한 수치는 다음과 같습니다: 1,847달러 소모, 82% 절감, 컨텍스트 (context)가 "12,000 토큰에서 340 토큰으로" 압축됨. 이는 컨텍스트의 97%가 사라진 것입니다. 나는 댓글을 통해 그녀에게, 턴 (turn)이 실행되기 _전_에 잘못된 압축을 잡아낼 방법을 찾았는지, 아니면 승률 (win rate)을 통해 알게 되는 것인지 물었습니다.
그녀의 답변은 나의 질문보다 그 허점을 더 잘 짚어냈습니다:
"잘못된 압축기 (compressor)는 누락을 통해 확실히 '거짓말'을 할 수 있으며, 이는 단순한 재생 (naive replay)보다 훨씬 더 위험할 수 있습니다. 왜냐하면 실패가 소리 없이 일어나기 (the failure is silent) 때문입니다."
그녀는 이를 자신의 파트 2(Part 2)의 핵심 과제로 꼽았는데, 제가 이 글을 쓸 당시에는 아직 공개되지 않은 상태였습니다. 따라서 이 포스트는 무엇인가에 대한 반박이 아닙니다. 그녀도 미결 과제라는 점에 동의했던 질문에 답하려는 시도입니다.
다른 두 사람도 같은 주에 동일한 벽에 부딪혔습니다. Prasad T의 Teaching a Qwen agent to forget는 메모리 레코드에 superseded_by 링크를 추가하여, 모순된 사실이 회상 (recall)에서는 제외되되 감사 추적 (audit trail)에는 남도록 했습니다. 그의 포스트 아래 스레드에서 제가 계속 되새기게 되는 문장은 이것입니다: 검사할 수 없는 망각은 그저 데이터 손실일 뿐이다. 그리고 Ratel의 Show HN (제가 확인했을 때 19점, 18개 댓글)은 BM25/임베딩 (embeddings)/하이브리드 (hybrid) 방식을 사용하여 점진적 도구 공개 (progressive tool disclosure)를 수행하며, 최대 81%의 토큰 절감을 주장합니다. 겉모습은 다르지만 문제는 동일합니다: 무언가가 모델이 보지 못할 것을 결정하고 있으며, 그 결정이 모델이 수행하려는 작업과 일치하는지 확인하는 장치는 아무것도 없습니다.
나의 첫 번째 설계를 무너뜨린 숫자
나의 첫 번째 설계는 아주 당연한 것이었습니다: 전체 컨텍스트 (full context)에서 결정을 내리고, 압축된 컨텍스트 (compacted context)에서도 결정을 내린 뒤, 만약 결정이 달라진다면 차단 (BLOCK)하는 것이었습니다. 그리고 진실 (truth)과 비교하는 것이었죠.
Prasad의 포스트는 결정적이었습니다. 그의 FAMA 측정치(제 수치가 아닌 그의 수치)에 따르면 recency-only (최신성 전용) = 0.64 및 never-forgets (망각 없음) = 0.64로 보고되었습니다. 동일합니다. Never-forgets가 바로 전체 컨텍스트 (full context)입니다. 따라서 제가 비교하고자 했던 "진실 (truth)" 자체가 36%의 확률로 틀린 것입니다. 3분의 1의 사례에서 거짓을 말하는 오라클 (oracle)은 오라클이 아니며, 그의 포스트를 읽은 독자라면 단 한 줄만으로 제 탭을 닫아버릴 것입니다.
이 동일한 두 수치는 또 다른 것, 즉 제가 반사적으로 선택했을 프레임워크(framing)를 무너뜨립니다: 압축 (compaction)은 볼륨을 잃기 때문에 해롭다. 거의 모든 것을 버리는 것과 아무것도 버리지 않는 것이 모두 0.64에 도달한다면, 볼륨은 변수가 아닙니다. Prasad는 이를 명확하게 말합니다: "승패는 컨텍스트를 다듬는 것에 있지 않습니다. 무엇이 오래된 것인지 아는 것에 있습니다."
따라서 게이트 (gate)에는 오라클이 없습니다. 그것은 모델이 무엇을 결정했을지 묻지 않습니다. 그것은 당신이 선언한 술어 (predicates)를 당신의 저장된 기록 (records)과 대조하여 해결합니다. 그것은 예측이 아니라 구조적 사실입니다.
압축 누락 게이트 (compaction omission gate)는 실제로 무엇을 묻는가?
하나의 폐쇄형 질문: 압축기 (compactor)가 삭제한 기록들 중에서, 이 액션 (action)을 구속하는 기록이 있는가?
입력값은 압축 전의 전체 컨텍스트, 압축기가 유지한 ID들, 그리고 구체적인 액션입니다. 삭제된 집합 (dropped set)은 계산되는 것이지, 제공되는 것이 아닙니다. 저는 그 누구의 "여기 제가 제거한 목록입니다"라는 리스트도 믿지 않습니다. 기록들은 다음과 같이 유형화됩니다: policy (범위, 술어, 효과), fact (필드, 값, 선택적 supersedes), chatter. 순서는 선언된 seq (시퀀스)이며, 절대 벽시계 시간 (wall clock time)이 아니므로, 판결이 시간대에 따라 표류할 수 없습니다.
전체 해결 (resolution) 단계는 다음과 같이 매우 작습니다:
def resolve(field, params, facts):
"""action.params가 우선하며, 그렇지 않으면 해당 필드에 대해 가장 최신으로 저장된 fact를 사용합니다."""
if field in params:
...
만약 특정 필드가 어디에서도 해결되지 않는다면, 게이트(gate)는 무해하다고 가정하는 대신 '닫힘(fails closed)' 상태가 됩니다. 판결 경로(verdict path) 어디에도 점수 산정(scoring)은 없습니다. PASS, BLOCK, ERROR. 퍼센트(%)는 없습니다. 이는 의도된 것입니다. 7월 16일, ReasonGate라는 이름의 Show HN 게시물이 정규 표현식(regex) 기반으로 구축된 "게이트"를 출시했고, Simon Willison은 한 시간도 채 되지 않아 해당 스레드에서 이를 해체해 버렸습니다. _"76-96%... 결코 100%는 아니다"_라는 회상은 게이트가 아니며, _"이 정규 표현식 목록은 신뢰를 주지 못한다"_라는 평을 듣습니다. 추측하는 게이트는 홍보(PR)만 잘 된 필터일 뿐입니다.
컨텍스트 압축(context compaction)은 안전한가? 압축을 고정하고 액션을 변화시켜라
다음은 실험으로서의 주장이며, 비교 대상은 필요하지 않습니다. 한 번의 압축. 네 가지 액션.
대상(fixture)은 52개의 레코드로 구성된 지원 스레드입니다. 환불 요청이 스레드를 시작하고, 해결(resolution)이 이를 종료하며, 중간 부분에는 내부 메모, 티어 업데이트, 그리고 seq 11에 있는 건조한 문장 하나인 Refunds over 5,000 need a human approver.(5,000 이상의 환불은 사람의 승인이 필요함)가 포함되어 있습니다. 압축 방식은 head+tail 방식으로, 처음 8개와 마지막 8개를 유지하는 것이며, 이는 대부분의 윈도잉(windowing) 스키마가 수렴하는 형태입니다. 이 규칙은 52개 중 11번 위치에 있습니다. 이는 삭제됩니다. 네 가지 행 모두에서 말입니다.
| 제안된 액션 (Proposed action) | 판결 (Verdict) | 종료 (Exit) |
|---|---|---|
refunds.create {amount: 8400} | BLOCK dropped-binding-constraint | 1 |
| ... |
"동일한 압축"은 하나의 주장이며, 수치에 대한 주장은 값싸기 때문에, 도구는 유지된 **집합(set)**의 핑거프린트(fingerprint)를 출력합니다:
compaction: 52 records, 16 kept, 36 dropped (retention=given, kept-sha256=3bfe38c85fcf)
해당 다이제스트(digest)는 네 가지 행 모두에서 동일합니다. 동일한 52개 레코드, 동일하게 유지된 16개 레코드(레코드 단위로 일치), 동일하게 삭제된 36개 레코드. 하나는 차단(block)합니다. 압축에 관한 그 어떤 것도 변하지 않았지만, "이 압축은 안전한가?"라는 질문에 대한 답은 변했습니다. 이것이 논지의 전부이며, 여기에는 기준점(baseline)이 없습니다. 왜냐하면 이 주장은 다른 도구가 더 못한다는 것이 아니기 때문입니다. 이 주장은 액션(action)을 명시하기 전까지는 질문 자체가 잘못되었다는 것입니다.
120번 행은 제가 좋아하는 문장을 출력합니다:
note : permissive: dropped policy 'f_020' (allow) resolves TRUE here (amount = 120 lt 500)
but losing a permission cannot license an action; not a block reason
폐기된 allow 규칙은 위험이 아닙니다. 그것은 자동 승인(auto-approval) 기회를 잃는 것일 뿐, 그 이상도 이하도 아닙니다. 게이트(gate)는 원칙에 따라 차단하는 대신 그렇게 말하고 있습니다.
"좋은 압축기(compactor)"라는 것은 존재할 수 없습니다. 오직 "이 압축은 해당 액션(action)에 대해 안전하다"라는 사실만이 존재할 뿐입니다.
경계선, "그것이 술어(predicate)를 해결한다"라는 것 또한 하나의 주장이기 때문에
5000번 행은 파헤쳐 볼 가치가 있는 행입니다. 5000 gt 5000은 FALSE이므로 규칙이 실행되지 않았을 것이고, 따라서 해당 규칙의 부재는 아무것도 바꾸지 못합니다. 하지만 게이트가 패턴 매칭(pattern-matching)을 통해 도달한 것이 아니라, 실제로 계산을 수행했다는 것을 어떻게 알 수 있을까요? 경계선을 따라가 봅시다:
amount | 판결 (Verdict) |
|---|---|
| 4999 | 통과 (PASS) |
| ... |
이름값을 하는 그 어떤 토크나이저(tokenizer)에게도 5000과 5001은 동일한 단어입니다. 판결이 다른 이유는 게이트가 액션의 파라미터(params)에서 amount를 해결(resolve)하고 저장된 술어(predicate)에 대해 산술 연산을 수행했기 때문입니다. 어휘적 유사성(Lexical similarity)은 8400 > 5000이라는 사실을 알 방법이 없습니다. 그것은 단어에 관한 사실이 아니기 때문입니다.
스스로의 질문에 "아니오"라고 답하는 부분
이틀 전, 저는 공개적으로 턴(turn)이 실행되기 전에 잘못된 압축을 잡아낼 수 있는지 물었습니다. 제가 원하지 않았던 정직한 답변은 대체로 아니오이며, 그렇지 않은 척했을 때 치러야 할 비용을 저는 측정할 수 있습니다.
--precheck는 액션을 판결에서 제외하고 대신 일반적인 질문을 던집니다: 폐기된 레코드가 이 도구들의 어떠한 도달 가능한 호출(reachable call)과도 결합될 수 있는가?
$ python3 compaction_omission_gate.py --precheck --retention given fixtures/fx1_refund_thread.json
action : (아래의 판결은 이를 절대 참조하지 않습니다 -- 이것이 실험의 핵심입니다)
compaction: 52 records, 16 kept, 36 dropped (retention=given, kept-sha256=3bfe38c85fcf)
...
네 가지 액션 각각에 대해 실행해 보면 동일한 라인이 네 번 나타납니다. 왜냐하면 모델이 그것들을 전혀 읽지 않기 때문입니다. 게이트(gate)는 4개 중 1개를 차단합니다. 프리체크(Precheck)는 4개 중 4개를 차단합니다. 이 네 개의 차단 중 세 개는 거짓(false)이며, 더 영리하게 행동한다고 해서 해결될 수 있는 문제가 아닙니다. 120이 없다면 120 > 5000 = FALSE를 복구할 방법은 아무것도 없습니다.
따라서 제어 지점(control point)은 제가 희망했던 것보다 한 단계 늦고, 비용이 발생하는 시점보다 한 단계 앞서 위치합니다. 전환(turn)은 이미 소모되었습니다. 모델은 훼손된 컨텍스트(context)를 읽었고 제안(proposal)을 생성했습니다. 아직 아무것도 _실행(executed)_되지 않았습니다. 이것이 제 실행 전 게이트(pre-execution gate)가 항상 그어온 선입니다: 사고(thinking) 전이 아니라, 액션(action) 전입니다. 드롭된 레코드(dropped records)를 읽어 확인하는 데는 토큰이 전혀 들지 않습니다. 압축(compaction)은 디스크에 무엇이 있는지가 아니라, 모델로 무엇이 전송되는지를 제어하기 때문입니다. (비용 문제는 별도의 포스트에서 다룹니다: 재청구 곡선(re-billing curve)은 여기 있습니다.))
위험은 대칭적이지 않으며, 나의 게이트는 그것을 틀렸다
여기에 리뷰어가 제 도구에서 찾아낸 버그가 있습니다. 이는 제가 자랑스러워했던 버그의 거울 쌍입니다.
저에게는 다음과 같은 규칙이 있었습니다: 권한(permission)을 잃는 것이 액션을 허가할 수는 없다. 맞는 말이며, 게이트는 이 규칙에 의존합니다. 드롭된 allow는 결코 차단하지 않습니다. 하지만 저는 유지된(kept) 규칙 검사에도 동일한 로직을 적용하여 필터링했고, 그 필터가 잘못되었습니다. 유지된 allow 규칙은 당신이 여전히 가지고 있는 권한입니다. 만약 압축기(compactor)가 _그 권한이 읽는 값(value)_을 드롭한다면, 권한은 당신이 이미 회수한 값에 대해 실행됩니다:
$ python3 compaction_omission_gate.py --retention given fixtures/fx6_stale_permit.json
verdict : BLOCK (dropped-superseder-stale-permit) exit 1
reason : dropped-superseder-stale-permit: kept policy 'f_003' (allow) reads
...
수정 전에는 해당 피스처(fixture)가 PASS exit 0을 반환했습니다. 조용히 말이죠. 20개 레코드 전에 취소된 gold 티어에 대해 4,000달러의 환불이 자동으로 승인되었음에도, "실패는 조용히 일어나므로 이를 포착하라"는 것이 핵심 논지인 나의 게이트(gate)는 아무 말도 하지 않았습니다. 나는 일반적인 원칙을 작성해 놓고는, 그것을 잘못된 명사에 적용했던 것입니다.
이를 수정하면서 내가 소홀히 다루었던 구분을 강제하게 되었습니다. 오래된 값(stale value)은 두 가지 방향성을 가지며, 이들은 동일한 이벤트가 아닙니다:
- 유지된 규칙이 오래된 값에서는 **허용(permits)**하지만 최신 값에서는 허용하지 않는 경우: 에이전트가 당신의 기록이 중단하라고 말하는 지점에서 행동합니다. 위험(Hazard). 차단(BLOCK).
- 유지된 규칙이 오래된 값에서는 **거부(denies)**하지만 최신 값에서는 거부하지 않는 경우: 에이전트가 당신의 기록이 진행하라고 말하는 지점에서 중단합니다. 비용(Cost). 통과(PASS), 메모와 함께.
두 번째 사례 역시 실제 피스처(fx7)이며, 게이트는 이를 차단하기를 거부합니다:
note : stale value, safe direction: kept policy 'f_003' (deny) reads 'customer.tier'.
[...] 규칙 자체의 술어(predicate) (customer.tier eq restricted)가 유지된
값에서는 TRUE이고 최신 값에서는 FALSE입니다. 이 압축(compaction)은 에이전트를 더 신중하게 만듭니다
...```
_무언가_가 변할 때마다 차단하는 게이트는 집 전체에 연결된 알람과 같습니다. 중요한 것은 방향입니다.
## "올바른" 유지(retention) 체계가 있는가? 측정해 보았습니다. 없습니다.
동일한 피스처, 동일한 `k`, 액션과 독립적인 두 가지 체계:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기