에이전트 자기개선 하네스 (9/12) — memoryFlush: 500토큰에 세션 핵심 담기
자유형식 메모리의 비대화 문제를 6필드 고정 포맷과 500토큰 압축으로 해결한 과정
핵심 요약
- AI 에이전트의 자유형식 메모리는 세션이 쌓일수록 비대해지고, 토큰을 낭비하며, 정작 필요한 정보를 찾기 어렵게 만든다
- Goal/Progress/Decisions/Changed Files/Blockers/Next Steps 6필드 고정 포맷으로 구조화하면 일관성과 검색성이 동시에 확보된다
- 300토큰은 "왜"가 빠지고, 700토큰은 군더더기가 붙는다 — 500토큰이 균형점이었다
배경
멀티 에이전트 시스템에서 세션 간 상태 유지는 핵심 과제입니다. 에이전트가 종료되면 작업 맥락이 사라지기 때문에, memoryFlush — 세션 종료 시 핵심 정보를 압축·저장하는 메커니즘 — 가 필요합니다. 문제는 그 형식이었습니다.
자유형식 메모리는 세 가지 구조적 결함을 가집니다.
첫째, 비대화. 에이전트가 판단하는 "중요도"와 실제 기록 길이가 비례하지 않습니다. 사소한 버그 수정에 장문이 붙고, 아키텍처 변경이 한 줄로 끝나는 역전이 빈번합니다.
둘째, 토큰 낭비. 세션 재개 시 이전 메모리를 컨텍스트에 로드합니다. LLM의 컨텍스트 윈도우는 유한합니다. 비대한 메모리는 실제 작업에 할당 가능한 토큰을 직접적으로 잠식합니다.
셋째, 검색 불가. 구조가 없으면 특정 결정의 이유를 찾기 위해 메모리 전체를 순차 탐색해야 합니다.
이 세 문제를 해결하기 위해 memoryFlush의 구조를 설계 수준에서 재정의했습니다.
본문
1. 6필드 고정 포맷 — 무엇을 저장할지 정한다
자유형식의 근본 문제는 flush 시점마다 "무엇을 저장할지"가 에이전트의 즉흥 판단에 달린다는 것입니다. 판단 기준이 없으면 일관성도 없습니다.
해결책은 필드를 고정하는 것입니다. memoryFlush가 트리거될 때 에이전트는 반드시 아래 6개 필드를 채웁니다.
Goal: 이 세션에서 달성하려 한 목표
Progress: 실제로 완료한 것
Decisions: 내린 결정과 그 이유
Changed Files: 변경된 파일 목록
Blockers: 막힌 것, 해결 못한 것
Next Steps: 다음 세션에서 해야 할 것
고정 포맷이 가져오는 효과는 세 가지입니다.
일관성. 모든 세션의 메모리가 동일한 스키마를 공유합니다. 세션 번호와 무관하게 형식이 동일합니다.
검색성. "이전에 왜 이 구조를 바꿨지?" → Decisions 필드만 읽으면 됩니다. "다음에 뭘 해야 하지?" → Next Steps만 읽으면 됩니다. 필드가 인덱스 역할을 합니다.
강제 사고. 빈 필드는 즉시 가시화됩니다. "Blockers가 비어 있는데, 정말 막힌 게 없었나?" 에이전트가 누락을 스스로 점검하게 됩니다.
2. 트리거 조건 — 언제 flush가 실행되는가
memoryFlush는 두 가지 조건에서 자동 트리거됩니다.
세션 종료 시. 에이전트가 명시적 종료 신호를 받으면 즉시 flush를 실행합니다. 이때 컨텍스트에 남아 있는 모든 작업 상태가 6필드로 압축됩니다.
컨텍스트 임계치 도달 시. 컨텍스트 윈도우 사용량이 설정된 임계치(기본 80%)에 도달하면 중간 flush가 실행됩니다. 이후 메모리를 로드하고 컨텍스트를 초기화하여 작업을 계속합니다. 이 과정에서 flush 이전의 원시 컨텍스트는 버려집니다 — 저장된 6필드 메모리만 지속됩니다.
무엇이 flush되는가. Goal, Progress, Decisions, Changed Files, Blockers, Next Steps 6개 필드로 압축된 세션 요약이 저장됩니다. 도구 출력 원본, 중간 추론 과정, 대화 이력은 저장하지 않습니다.
무엇이 지속되는가. 6필드 구조체 자체가 영속 스토리지(파일 또는 DB)에 기록됩니다. 다음 세션에서 이 구조체를 로드하면 에이전트는 작업 맥락을 복원합니다.
3. 도구 출력 요약 규칙 — 원본이 아니라 요약을 저장한다
메모리 비대화의 또 다른 원인은 도구 출력의 원본 저장이었습니다. 파일 diff 전체, 터미널 출력 전체, API 응답 전체가 메모리에 유입됩니다.
규칙을 하나 추가했습니다: 50줄 이상의 도구 출력은 요약만 저장한다.
예를 들어, 100줄짜리 에러 로그가 나왔다면:
(100줄 전체 로그)
Error: SQLite migration failed at step 3/7.
Root cause: column name mismatch (expected 'user_id', got 'userId').
Fix: ALTER TABLE + re-run migration.
원본이 필요하면 파일을 다시 읽으면 됩니다. 메모리에는 "무슨 일이 있었는지"만 남기면 충분합니다. 원본 복구는 파일 시스템의 역할이고, 맥락 보존은 메모리의 역할입니다.
4. 500토큰 압축 목표 — 300은 부족하고 700은 과잉이다
6필드 포맷과 요약 규칙을 정한 후, 다음 질문은 "얼마나 짧게?"였습니다.
첫 시도: 300토큰. 극도로 압축했습니다. Goal, Progress, Changed Files, Next Steps는 문제없이 들어갔습니다. 하지만 Decisions 필드가 사실상 비었습니다. "SQLite로 변경"이라고만 적혀 있고, 왜 SQLite로 변경했는지가 빠졌습니다.
이것이 치명적이었습니다. 같은 맥락의 후속 작업에서 "왜 이 결정을 내렸지?"를 재구성할 수 없었습니다. 메모리의 핵심 가치는 의사결정 이유의 보존인데, 300토큰에서는 그 여유가 없었습니다.
두 번째 시도: 700토큰. Decisions에 이유까지 충분히 적을 수 있었습니다. 하지만 중복이 발생했습니다. "~를 시도했으나 ~해서 ~로 변경했다"가 Progress와 Decisions에 동시에 기술되고, Next Steps에 Progress의 맥락이 반복됐습니다.
최종: 500토큰. Decisions에 "무엇을 + 왜"를 한 줄로 적을 수 있으면서, 중복 없이 6필드를 모두 채울 수 있는 지점. 충분한 맥락을 담되 군더더기가 시작되기 직전의 경계선입니다.
500토큰은 기본값입니다. 아키텍처 변경처럼 결정 맥락이 중요한 세션은 600~650까지 허용합니다. 단순 버그 수정은 300이면 충분합니다. 핵심은 기본값을 500으로 잡고, 상한을 두되 내용에 따라 유연하게 조정하는 것입니다.
시행착오
"전부 저장하면 되지 않나?" 저장 공간은 무한하지만 LLM의 병목은 컨텍스트 윈도우입니다. 메모리를 로드하는 순간 토큰을 소비합니다. 1000토큰짜리 메모리 10개를 로드하면 그것만으로 10K 토큰. 실제 작업에 쓸 공간이 그만큼 잠식됩니다.
"AI가 알아서 요약하게 하면?" 요약 지시 없이 "메모리 저장해"라고만 하면, 에이전트마다 결과가 달랐습니다. 코드 블록 전체를 포함하거나, 핵심 결정이 누락되거나. 6필드 포맷과 토큰 상한을 명시적으로 지정해야 일관된 flush 결과가 나왔습니다.
300토큰의 교훈. "짧을수록 좋다"는 직관이 틀렸습니다. 메모리에서 가장 가치 있는 정보는 "무엇"이 아니라 "왜"입니다. Changed Files는 git log로 복구 가능하지만, 결정의 이유는 기록하지 않으면 사라집니다. 압축의 하한선은 "왜"가 살아남는 지점입니다.
마무리
memoryFlush 구조화의 핵심은 세 가지입니다.
- 필드를 고정한다. 자유형식은 일관성이 없다. 6필드로 고정하면 모든 세션의 메모리가 동일한 스키마를 가진다.
- 원본이 아닌 요약을 저장한다. 도구 출력 50줄 이상은 요약만. 원본 복구는 파일 시스템에 위임한다.
- 500토큰을 기본값으로 잡는다. "왜"가 살아남는 최소 지점이자, 군더더기가 시작되기 직전의 균형점.
세션 메모리는 에이전트가 다음 세션에서 복원할 수 있는 유일한 상태입니다. 그 구조가 비정형이면 복원 품질이 세션마다 달라집니다. 6필드 포맷 + 500토큰 상한은 flush가 일관된 결과를 내도록 강제하는 가장 단순한 방법입니다.
시리즈 전체 안내: 시리즈 목차
댓글
댓글 쓰기