Agent Mentor Learn
에이전트 메모리와 상태 · 2 / 6강

레슨 2: 대화 히스토리 관리: 추가, 잘라내기, 요약

학습 목표:

  • 대화 히스토리가 기본적으로 커지기만 하고 줄어들지 않는 이유 설명하기
  • 잘라내기가 무엇을 버리고, 무엇을 남기며, 어떤 구조를 깨뜨릴 수 있는지 말하기
  • 컴팩션과 도구 결과 비우기가 각각 푸는 문제를 구분하기
  • 윈도우 사용량과 무엇이 윈도우를 부풀리고 있는지에 따라 어떤 메커니즘을 꺼낼지 판단하기

전제: 레슨 1을 마치고 컨텍스트 윈도우가 무엇으로 이루어지는지 이해 | 이전: 레슨 1 << | 다음: 레슨 3 >>

추가가 기본값이다: 히스토리가 계속 커지는 이유

레슨 1에서는 모델이 보는 히스토리란 호스트 앱이 매 턴 다시 보내는 것이라고 짚었습니다. 그렇다면 그것은 실제로 어떻게 보내질까요? 가장 단순한 구현은 **추가(append)**입니다: 한 턴이 끝나면 그 턴의 새 메시지들(사용자의 말, 모델의 응답, 도구 호출과 그 결과)을 기존 messages 배열 끝에 이어 붙이고, 다음 턴에는 그 배열 전체를 그대로 다시 보냅니다.

공식 문서는 이 기본 동작을 직설적으로 서술합니다: 대화가 진행됨에 따라 각 사용자 메시지와 모델 응답이 컨텍스트 윈도우에 쌓이고, 이전의 모든 턴은 온전히 유지됩니다. "As the conversation advances through turns, each user message and assistant response accumulates within the context window, and previous turns are preserved completely."1 (대화가 턴을 거쳐 진행됨에 따라, 각 사용자 메시지와 어시스턴트 응답은 컨텍스트 윈도우 안에 누적되고, 이전 턴들은 완전하게 보존됩니다) 아무도 능동적으로 무언가를 삭제하지 않으므로 히스토리는 오르기만 합니다 — 열 턴이 지나면 윈도우는 열 턴 분량의 내용을 전부 담고 있으며, 최신 턴의 요약도 아니고 자동으로 걸러진 하이라이트 모음도 아닙니다.

짧은 대화에서는 문제가 되지 않습니다. 하지만 오래 실행되는 에이전트에서는 문제가 눈덩이처럼 불어납니다: 모든 도구 호출의 전체 인자와 전체 반환값이 히스토리에 채워지고, 파일을 반복해서 읽고 명령을 실행하는 작업은 몇십 턴 뒤면 messages 배열을 수만 토큰까지 쉽게 밀어 올립니다. 레슨 1에서 윈도우에는 딱딱한 용량 상한이 있고, 가득 찰수록 그 한계에 가까워진다는 것을 다뤘습니다. 더 미묘한 대가는 컨텍스트 로트입니다 — 히스토리가 길고 지저분할수록, 지금 실제로 중요한 그 한 줄을 모델이 그 안에서 찾아내기가 더 어려워집니다1. 히스토리를 방치해 커지게 두면 결국 두 청구서를 모두 치르게 됩니다.

잘라내기: 가장 단순하고 무딘 선택지

가장 직접적인 대응은 **잘라내기(truncation)**입니다: 윈도우가 거의 가득 차면 가장 오래된 메시지 묶음을 통째로 잘라내고 가장 최근의 N턴만 남깁니다. 만들기 가장 쉬운 방식입니다 — 추가 모델 호출도 없고, 설계할 요약 포맷도 없습니다. messages.slice(-N) 한 줄이면 됩니다.

하지만 잘라내기가 버리는 것은 영영 사라집니다. 잘라낸 묶음에 사용자가 3번째 턴에 밝힌 핵심 제약('예산은 $5,000 이하로 유지')이 들어 있었고 에이전트가 지금 40번째 턴에서 주문을 넣으려 한다면, 그 정보는 그냥 증발합니다. 모델은 그것을 한때 '봤다가' '잊었다'는 것조차 알지 못합니다 — 그냥 애초에 들은 적 없는 것처럼 행동합니다.

잘라내기에는 더 숨은 함정도 있는데, 이전 코스인 '에이전트 도구 호출: 에이전트가 실제로 일하게 만들기'의 레슨 2 '도구 호출의 완전한 왕복'에서 다룬 왕복 프로토콜과 직접 이어집니다: '마지막 N개 메시지'로 순진하게 잘라내면, tool_use / tool_result 쌍의 중간을 쉽게 자를 수 있습니다 — 호출을 발생시킨 assistant 메시지는 남기면서 바로 뒤따라온 tool_result 메시지를 잘라내는 식입니다. 그 히스토리를 모델에 보내면 프로토콜 자체가 깨집니다. 문서는 명시합니다: "Tool result blocks must immediately follow their corresponding tool use blocks in the message history."2 (도구 결과 블록은 메시지 히스토리에서 대응하는 도구 사용 블록 바로 뒤에 와야 합니다) "tool_use ids were found without tool_result blocks immediately after"2 같은 에러는 짝짓기가 깨졌다는 신호입니다 — 모델은 자신이 '호출을 시작했지만' 그 호출의 결과를 결코 받지 못한 것을 보게 되고, 다음 요청은 곧바로 실패합니다.

컴팩션: 윈도우를 하나의 요약으로 압축한다

잘라내기의 문제는 통째로 버린다는 것입니다. 정보를 완전히 버리지 않으면서 공간을 확보하는 방법은 없을까요? 그것이 **컴팩션(compaction)**이 푸는 문제입니다. 공식 쿡북은 이렇게 정의합니다: "Compaction distills the contents of a context window into a high-fidelity summary, letting the agent continue with minimal performance degradation when the conversation gets long."3 (컴팩션은 컨텍스트 윈도우의 내용을 고충실도 요약으로 증류하여, 대화가 길어질 때 에이전트가 최소한의 성능 저하로 계속할 수 있게 합니다)

잘라내기의 '한 구간을 통째로 삭제'와 달리, 컴팩션은 '전체를 다시 쓰기'입니다: 앞선 대화 히스토리가 하나의 고충실도 요약으로 압축되어, 원본 메시지의 긴 흐름을 대체하고 윈도우 앞쪽에 자리합니다. 요약이 남기는 것은 '무슨 일이 있었고 무엇이 결론이었는가'이고, 버리는 것은 한 마디 한 마디의 원본 대화 세부입니다.

문서는 이 메커니즘의 매개변수를 명확히 밝힙니다. 기본 발동 임계값이 있습니다 — 컴팩션은 윈도우 사용량이 150K 토큰에 이르면 자동으로 발동하며, 임계값은 설정 가능하지만 50K 토큰 아래로는 내려갈 수 없는, 서버가 강제하는 하한이 있습니다3 4. 매 발동은 개별적인 대체입니다: 히스토리의 큰 구간이 요약으로 교체되고, 새 메시지는 그 뒤에 정상적으로 계속 추가됩니다. 이것은 한 번으로 끝나는 이벤트가 아닙니다 — 문서는 긴 대화가 한 번 이상 컴팩션될 수 있다고 명시하며, "The last compaction block reflects the final state of the prompt, replacing content prior to it with the generated summary."4 (마지막 컴팩션 블록은 프롬프트의 최종 상태를 반영하며, 그 이전의 내용을 생성된 요약으로 대체합니다) 다시 컴팩션할 때는, 앞선 컴팩션 블록이 나머지 히스토리와 함께 새 요약으로 접혀 들어갑니다. 컴팩션은 전체 트랜스크립트에 대한 연산이며, 여기서 "user messages, assistant messages, tool calls, tool results, even prior compaction blocks are all flattened into the summary."3 (사용자 메시지, 어시스턴트 메시지, 도구 호출, 도구 결과, 심지어 이전 컴팩션 블록까지 모두 요약으로 평탄화됩니다)

컴팩션은 공짜가 아닙니다. 컴팩션하는 행위는 추가 모델 호출 비용이 들고(요약기 모델이 실행됩니다)3, 아무리 신중히 작성해도 요약은 원본의 손실된 버전입니다 — "The summary preserves key decisions and facts but may drop specific numbers or exact phrasing."3 (요약은 핵심 결정과 사실을 보존하지만, 특정 숫자나 정확한 표현은 누락할 수 있습니다) 나중 단계가 하필 요약 과정에서 사라진 작은 세부(이를테면 어떤 변수의 정확한 철자)에 의존한다면, 그 세부는 없어졌을 수 있습니다. 이것이 컴팩션이 '전체 컨텍스트가 너무 커졌다'는 거친 입자의 문제에 맞고, 모든 종류의 히스토리 부풀림에 대한 만병통치약으로 쓰이지 않는 이유이기도 합니다.

도구 결과 비우기: 낡아 가는 부분만 비운다

히스토리 부풀림의 큰 기여자는 도구 호출 그 자체입니다. 에이전트가 파일을 읽거나 명령을 실행할 때마다 전체 반환값이 히스토리에 채워집니다 — 수천 줄짜리 파일을 읽으면 그 수천 줄이 messages 배열에 그대로 앉아 있으며, 아무도 그 세부를 더 이상 필요로 하지 않는 열 턴 뒤에도 그렇습니다. 쿡북은 이를 직접 짚습니다: "Tool-result clearing addresses the bloat from tool use itself. As an agent pulls in tools and calls them, the results pile up, and deciding how much of that tool output to keep becomes an increasingly important part of managing context."3 (도구 결과 비우기는 도구 사용 자체에서 오는 부풀림을 다룹니다. 에이전트가 도구를 끌어와 호출함에 따라 결과가 쌓이고, 그 도구 출력을 얼마나 남길지 결정하는 일이 컨텍스트 관리에서 점점 더 중요한 부분이 됩니다)

**도구 결과 비우기(tool-result clearing)**는 바로 이것을 정조준한 메커니즘입니다: 그것은 "drops old, re-fetchable results while keeping the record that the call happened."3 (호출이 일어났다는 기록은 유지하면서, 다시 가져올 수 있는 오래된 결과를 버립니다) 이것이 핵심 구분입니다 — 비우기는 도구가 반환한 구체적 내용(그 수천 줄의 파일 내용)은 버리지만, '에이전트가 이 경로로 read_file을 호출했다'는 기록은 지우지 않습니다. 그 내용이 나중에 다시 필요해지면, 에이전트는 어떤 도구를 무슨 인자로 호출했는지 알고 있으므로, 그 내용을 되가져오기 위해 다시 호출할지 판단할 수 있습니다.

이것의 발동 임계값과 보존 정책에도 명확한 기본값이 있습니다: 비우기는 윈도우 사용량이 100K 토큰에 이르면 발동하고, 기본적으로 가장 최근 3개 도구 호출의 전체 결과는 남기고 더 오래된 도구 결과를 비웁니다3. 100K 발동은 컴팩션의 150K보다 낮은데, 이는 그 역할에 들어맞습니다 — '가장 부풀기 쉽고 되가져오기 가장 쉬운' 부분인 도구 출력을 먼저 처리하고, 그것으로 충분하지 않으면 전체 윈도우를 컴팩션에 넘깁니다.

셋 중 고르기: 사고 모델

이제 두 메커니즘이 있고, 레슨 3이 다루는 외부 메모리를 더하면 셋이 됩니다. 쿡북은 이들의 분업을 정리하는 간결한 사고 모델을 제시합니다: "compaction compresses the whole window when it grows too large, clearing drops stale re-fetchable data inside the window, and memory moves information out of the window so it survives across sessions."3 (컴팩션은 윈도우가 너무 커지면 윈도우 전체를 압축하고, 비우기는 윈도우 안의 낡고 다시 가져올 수 있는 데이터를 버리며, 메모리는 정보를 윈도우 밖으로 옮겨 세션을 넘어 살아남게 합니다)

이들의 우선순위와 쓰임새는 경쟁하지 않습니다 — 층으로 쌓입니다:

  • 도구 결과 비우기는 '이 내용은 아직 윈도우 안에 있지만 낡았고, 다시 가져올 수 있으니 버려도 괜찮다'를 다룹니다 — 가장 표적화되어 있고 가장 저렴합니다.
  • 컴팩션은 '윈도우 전체가 너무 커졌다'를, 내용이 어디서 왔는지와 무관하게 다뤄, 그 모두를 하나의 요약으로 다시 씁니다 — 도달 범위는 넓지만 손실이 있고 추가 모델 호출 비용이 듭니다.
  • 메모리(다음 레슨의 주제)는 '이 정보는 이 하나의 대화에만 있어서는 안 되고, 다음 세션까지 이어져야 한다'를 다룹니다 — '윈도우가 전부를 담을 수 없다'를 푸는 것이 전혀 아니라 '이 대화가 끝나면 윈도우 안의 모든 것이 사라진다'를 풉니다.

이 레슨이 열었던 질문으로 돌아가 봅시다: 히스토리가 커지기만 하는 이유는 아무도 능동적으로 비우지 않기 때문입니다. 잘라내기, 컴팩션, 도구 결과 비우기는 서로 다른 비용과 서로 다른 상황에서 그것을 비우는 세 가지 방법입니다 — 어느 것을 고를지는 무엇을 남기고 싶은지, 그리고 그것을 남기기 위해 얼마를 치를 의향이 있는지에 달려 있습니다.

정리

  • 대화 히스토리는 기본적으로 커지기만 한다: 각 턴의 메시지가 윈도우에 쌓이고 이전 턴은 온전히 유지되며, 아무도 능동적으로 비우지 않으면 한없이 오른다
  • 잘라내기는 가장 단순하지만 버리는 것은 되돌릴 수 없고, 잘라내는 지점이 tool_use / tool_result 쌍의 중간에 떨어지면 도구 호출의 프로토콜 구조를 깨뜨린다
  • 컴팩션은 윈도우 전체 히스토리를 하나의 고충실도 요약으로 다시 쓰며, 기본적으로 150K 토큰에서 발동한다(임계값은 50K 아래로 내려갈 수 없고 서버가 강제한다). 대가는 손실이 있다는 것과 추가 모델 호출 한 번이다. 긴 대화는 한 번 이상 컴팩션될 수 있고, 앞선 요약 블록은 새 요약으로 접혀 들어간다3 4
  • 도구 결과 비우기는 낡고 다시 가져올 수 있는 도구 출력만 버리고 호출 기록은 남기며, 기본적으로 100K 토큰에서 발동해 최근 3개 호출의 결과를 남긴다 — 컴팩션보다 더 표적화되어 있다
  • 셋은 하는 일이 다르다: 비우기는 낡고 다시 가져올 수 있는 데이터를, 컴팩션은 너무 커진 전체 윈도우를, 메모리는 세션을 넘어 살아남기를 다룬다 — 어느 것을 고를지는 부풀림의 구체적 원천과 세부를 잃어도 되는지에 달려 있다

>> 레슨 3: 외부 메모리: 파일과 검색

Footnotes

  1. Context windows — https://platform.claude.com/docs/en/build-with-claude/context-windows 2

  2. Handle tool calls — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  3. Context engineering: memory, compaction, and tool clearing — https://platform.claude.com/cookbook/tool-use-context-engineering-context-engineering-tools 2 3 4 5 6 7 8 9 10

  4. Compaction — https://platform.claude.com/docs/en/build-with-claude/compaction 2 3

연습

01

아래 세 시나리오 각각에 대해, 잘라내기·컴팩션·도구 결과 비우기 중 무엇이 가장 맞는지 판단하고 이유를 설명하세요(이유는 '부풀림의 구체적 원천'이나 '세부를 잃어도 되는가'에 착지해야 하며, 단지 '이게 더 맞아 보인다'가 아니어야 합니다).

레벨 1: 세 시나리오에 맞는 메커니즘 고르기
  1. 코드 리뷰 에이전트가 grep_code를 반복 호출해 코드베이스를 검색하고, 매 호출마다 몇십 줄의 매치를 반환합니다. 윈도우 사용량이 상한에 가깝지만 리뷰 작업은 끝나지 않았습니다.
  2. 지원 에이전트가 한 사용자와 60턴째 대화 중입니다. 윈도우가 전체적으로 커졌고, 도구 호출과 사용자·에이전트 대화의 긴 구간을 모두 담고 있습니다 — 내용이 사방에서 옵니다.
  3. 메모 작성 보조 에이전트가 매일 새 세션을 시작할 때마다, 사용자가 지난주 언급한 장기 선호('매운 음식 안 먹음')를 알아야 합니다. 이 세션 자체는 이제 막 시작했고, 윈도우는 가득 차려면 한참 멀었습니다.
완료 기준 · 로컬에서 확인
02

아래 의사 코드는 윈도우 사용량이 너무 높아지면 히스토리를 비우려 하며, 가장 최근 메시지만 남깁니다:

레벨 2: 잘라내다 버그를 내는 코드 진단하기

어느 시점의 messages 배열이 정확히 이 순서라고 합시다: [..., { role: "assistant", content: [tool_use block] }, { role: "user", content: [tool_result block] }, ...], 그리고 maxKeep이 하필 그 두 메시지를 갈라놓아, tool_result 쪽만 남기고 tool_use 쪽을 잘라낸다고 합시다.

이것이 모델에 보내질 때 어떤 문제를 일으키는지 설명하고, 그것을 피하는 수정안을 제시하세요(전체 코드는 필요 없습니다 — 아이디어만 분명히 하세요).

완료 기준 · 로컬에서 확인