지난 글에서 압축을 손실 크기로 줄 세우고 앞의 세 단계를 봤다. 필드를 쳐내고 형식을 바꾸고 오래된 도구 결과를 축약하고도 창이 모자라면, 그때 남는 방법이 요약이다. 그리고 요약은 앞의 셋과 성질이 다르다. 원문이 사라지고, 사라진 자리에 모델이 만든 문장이 들어간다. 그 문장이 무엇을 담고 무엇을 빠뜨릴지는 요약을 시키는 방식이 정한다.
여기서 한 가지를 먼저 갈라 두면 뒤가 쉬워진다. 긴 대화를 다루는 일은 접는 일과 기억하는 일 둘이고, 이 둘은 같은 장치로 하면 안 된다. 접기는 컨텍스트를 줄이는 압축이고, 기억은 창 밖에서도 살아남아야 하는 저장이다. 한 덩어리로 다루면 "지난주에 정한 배포 정책"이 요약 세 번을 거치며 조용히 사라진다.
언제 접을 것인가
요약을 돌리는 시점은 세 가지 신호 중 하나로 정한다.
| 트리거 | 쓰기 좋은 곳 | 주의 |
|---|---|---|
| 토큰 임계 초과 | 대부분의 대화형 시스템 | 임계를 낮게 잡으면 너무 자주 접힌다 |
| 턴 수 | 턴 길이가 고른 서비스 | 도구 결과가 섞이면 턴 수와 토큰이 안 맞는다 |
| 작업 경계 | 에이전트 | 하위 과제가 끝나는 자리라 손실이 가장 적다 |
셋 중 하나만 쓰기보다 토큰 임계를 기본으로 두고 작업 경계를 우선하는 조합이 낫다. 같은 800토큰을 접더라도 하위 과제가 끝난 자리에서 접으면 잃는 것이 훨씬 적기 때문이다.
임계값을 정할 때 캐시와 부딪히는 지점이 있다. 요약은 이력의 앞부분을 통째로 갈아 끼우는 조작이라 그 시점에 프리픽스 캐시가 무효가 된다. 남은 예산을 , 한 턴에 늘어나는 평균 토큰을 라 하면 다음 무효화까지 버티는 턴 수는 이렇게 된다.
임계를 창의 90%처럼 늦게 잡으면 가 커져 이 늘고, 캐시가 깨지는 간격도 벌어진다. 반대로 60%에서 접기 시작하면 요약 호출과 캐시 무효화가 둘 다 잦아진다. 요약을 조금씩 자주 하는 것보다 넉넉히 모았다가 한 번에 하는 편이 대개 싸다.
무엇을 원문으로 남길지 지시한다
"이 대화를 요약해 줘"라고만 하면 모델은 읽기 좋은 서사를 만든다. 그런데 우리가 필요한 것은 서사가 아니라 이후 대화가 딛고 설 발판이다. 그래서 요약 프롬프트에는 보존 목록을 못 박아 둔다.
아래 대화를 이어 갈 수 있도록 압축하라.
원문 표현 그대로 옮길 것:
- 사용자가 명시한 제약과 선호
- 확정된 결정과 그 이유
- 식별자, 파일 경로, 수치와 단위
- 시도했으나 실패한 방법과 실패 사유
줄여도 되는 것:
- 같은 내용의 재확인, 인사, 진행 상황 보고
- 이미 결정으로 대체된 중간 논의
없는 사실을 채우지 말고, 애매한 것은 애매한 채로 적어라.
마지막 줄이 생각보다 중요하다. 요약 모델은 빈틈을 매끄럽게 메우려는 성질이 있어서, 결론이 안 난 논의를 "~하기로 했다"로 정리해 버리는 일이 있다. 그렇게 굳어진 문장은 다음 턴부터 사실처럼 취급되고, 사용자가 아니라고 말하기 전까지는 아무도 모른다.
요약 결과를 어디에 넣는지도 정해야 한다. 시스템 블록 바로 뒤, 남은 최근 턴 앞이 자연스럽다. 이 자리에 두면 캐시 관점에서도 안정적이고, 모델이 읽는 순서도 "지침 → 지금까지의 경위 → 최근 대화 → 이번 질문"으로 사람이 읽는 순서와 같아진다.
요약의 요약이 쌓일 때
대화가 아주 길어지면 요약본 자체가 다시 접힘 대상이 된다. 이 계층 구조는 필요하지만 손실이 누적된다는 것을 알고 써야 한다. 한 번 접을 때 유지되는 정보 비율을 이라 하면 번 접은 뒤 남는 비율은 대략 이렇게 간다.
이 0.9로 꽤 좋아 보여도 다섯 번 접으면 0.59다. 그리고 실제로 잃는 것은 고르게 분포하지 않는다 — 구체적인 값과 예외 조건부터 먼저 떨어져 나가고, 추상적인 요지가 남는다. 그래서 계층 요약만으로 긴 대화를 버티려는 설계는 시간이 갈수록 "무엇을 하려던 것인지는 아는데 어떻게 하기로 했는지는 모르는" 상태로 수렴한다.
해법은 접는 횟수를 줄이는 것이 아니라 접히지 않는 자리를 하나 만드는 것이다.
구조화 메모리는 문장이 아니라 항목이다
요약이 산문이라면 메모리는 목록이다. 항목 단위로 들어가고, 항목 단위로 갱신되고, 항목 단위로 지워진다. 이 차이가 전부다 — 산문은 부분 수정이 안 되지만 목록은 된다.
MEMORY_SCHEMA = {
"kind": "decision | constraint | fact | failure",
"text": "한 문장. 원문 표현을 유지한다.",
"source_turn": 14, # 어디서 나왔는지
"confidence": "stated | inferred",
"expires": None, # 유효 기간이 있으면 채운다
}
confidence를 나눠 두는 이유가 있다. 사용자가 직접 말한 것과 모델이 대화에서 짐작한 것은 신뢰도가 다른데, 한 목록에 섞어 두면 다음 턴부터 구분이 사라진다. 짐작한 항목은 나중에 사용자 발언과 부딪힐 때 먼저 버리면 된다.
expires는 "이번 주에는 배포하지 않는다" 같은 항목을 위한 자리다. 유효 기간 없는 메모리는 시간이 지나면 틀린 사실 창고가 되고, 그 상태가 요약 손실보다 더 나쁘다. 틀린 것을 자신 있게 말하기 때문이다.
메모리를 뽑는 시점은 접는 시점과 같이 두는 편이 단순하다. 요약을 만들 때 같은 호출에서 항목도 함께 뽑게 하면 호출이 하나로 끝난다.
다시 넣을 때는 골라 넣는다
메모리를 쌓다 보면 항목이 수백 개가 된다. 이걸 매 요청 통째로 넣으면 처음 문제로 돌아온다 — 창을 아끼려고 만든 장치가 창을 먹는다.
그래서 메모리는 검색해서 넣는다. 다만 검색 문서와 다루는 방식이 조금 다르다.
| 종류 | 매 요청 넣는가 | 고르는 방법 |
|---|---|---|
| 제약·선호 | 넣는다 | 전부. 대개 몇 개 안 되고 어기면 바로 티가 난다 |
| 확정된 결정 | 관련된 것만 | 이번 질문과의 유사도 |
| 검증된 사실 | 관련된 것만 | 유사도 + 최신순 |
| 실패 기록 | 같은 도구를 쓸 때만 | 도구 이름으로 걸러 낸다 |
제약을 항상 넣는 것이 핵심이다. 제약은 개수가 적고 어겼을 때의 비용이 크므로 유사도 검색에 맡기면 안 된다. "코드에 주석을 달지 말라"는 지시가 유사도가 낮다는 이유로 빠지면 사용자는 같은 말을 세 번 하게 된다.
접은 다음에 무엇을 잃었는지 확인한다
요약과 메모리는 붙여 놓고 검증하지 않으면 문제를 아주 늦게 발견한다. 답이 그럴듯하게 나오기 때문이다. 검증은 단순한 방법이 잘 듣는다.
긴 대화 로그를 몇 개 골라, 접기 전 상태에서 답할 수 있던 질문 목록을 만든다. "아까 정한 배포 시각은?", "왜 A안을 버렸지?", "이미 시도해 본 방법은?" 같은 것들이다. 그다음 접은 상태에서 같은 질문을 던져 답이 유지되는지 본다.
이 테스트는 평균 점수보다 어떤 유형이 먼저 무너지는지를 보려고 하는 것이다. 대개 결정의 이유가 가장 먼저 사라지고, 실패 기록이 그다음이며, 수치는 요약 프롬프트에 보존 지시를 넣으면 꽤 잘 버틴다. 무너지는 유형이 확인되면 그 유형을 메모리 항목으로 승격시키면 된다. 요약 프롬프트를 더 정교하게 다듬는 것보다 이쪽이 확실하다.
접기는 아무리 잘해도 손실이다. 그래서 잃으면 안 되는 것을 접히지 않는 자리로 옮기는 일이 요약을 잘 시키는 일보다 먼저다.
읽어주셔서 감사합니다. 😊

