실무 Multi-Agent 오케스트레이션 17장 · 장기 기억 관리 2 / 7 ← 이전목차다음 → TechLead Cro

17장. 장기 기억 관리

요약 메모리 — 버리지 말고 줄여서 남긴다

한 줄 요약

요약 메모리(summary memory) 는 이력이 길어졌을 때 오래된 부분을 지우는 대신 LLM으로 요약해 짧은 글로 남겨 두는 방법입니다. 그 뒤로 모델에게 들어가는 것은 "과거 요약 + 최근 대화 원문 + 새 문의"입니다.


1. trim과 무엇이 다른가

같은 자리에서 일하지만 하는 일이 다릅니다.

trim (16장) 요약 메모리 (이번 장)
언제 이력이 한도를 넘을 때 이력이 한도를 넘을 때
오래된 부분을 버린다 요약으로 바꾼다
추가 비용 없음 요약하는 LLM 호출 1회
남는 것 최근 턴의 원문 과거 요약 + 최근 원문

2. 요약도 프롬프트로 시킨다

요약은 LLM 호출 한 번입니다. 무엇을 남길지는 프롬프트에 적습니다.

def summarize_history(history: list[dict]) -> str:
    """대화 이력(오래된 부분)을 상담 맥락 요약으로 압축한다."""
    transcript = "\n".join(f"{h['role']}: {h['text']}" for h in history)
    r = client.models.generate_content(
        model=MODEL,
        contents=f"다음 상담 대화를, 이후 상담에 필요한 사실 위주로 3줄 이내 요약하라.\n"
                 f"고객 요청·주문번호·처리 상태를 반드시 포함하라.\n\n{transcript}",
        config=types.GenerateContentConfig(temperature=1.0),
    )
    return r.text.strip()
프롬프트의 문구 왜 넣었나
"이후 상담에 필요한 사실 위주로" 이 말이 없으면 "고객이 불편을 겪었고 상담원이 친절히 안내했다" 같은 분위기 묘사가 되기 쉽습니다
"3줄 이내" 요약이 길면 줄인 보람이 없습니다. 길이는 숫자로 정합니다(3장)
"고객 요청·주문번호·처리 상태를 반드시 포함" 다음 턴에 꼭 필요한 항목을 이름으로 지정합니다
temperature=1.0 기록은 창의적일 필요가 없습니다

무엇을 남길지 정한 것은 개발자이고, 그 기준에 따라 실제로 골라 쓴 것은 모델입니다.


3. 대화 클래스 — 요약과 원문을 나눠 들고 있는다

SummaryMemoryChat은 기억을 두 칸에 나눠 담습니다.

class SummaryMemoryChat:
    def __init__(self, keep_recent: int = 4):
        self.summary = ""                  # 압축된 과거
        self.recent: list[dict] = []       # 원문 그대로 둔 최근 메시지
        self.keep_recent = keep_recent     # 요약한 뒤 원문으로 남길 메시지 수 (2 = 문답 한 쌍)

문의가 오면 두 칸을 이어 붙여 하나의 글로 만들어 보냅니다.

context = ""
if self.summary:
    context += f"[이전 상담 요약]\n{self.summary}\n\n"
for h in self.recent:
    context += f"{'고객' if h['role'] == 'user' else '하루'}: {h['text']}\n"
context += f"고객: {text}"

16장에서는 이력을 types.Content의 리스트로 보냈습니다. 여기서는 대화를 하나의 글로 정리해 보냅니다. 도구를 쓰지 않으니 이렇게 해도 충분하고, 요약문을 넣기도 쉽습니다.


4. 언제, 어떻게 요약으로 바꾸는가

답을 받은 뒤 원문이 너무 쌓였는지 확인합니다.

self.recent += [{"role": "user", "text": text},
                {"role": "model", "text": answer}]
# 원문이 keep_recent 의 두 배를 넘으면 앞쪽을 요약으로 흡수
if len(self.recent) > self.keep_recent * 2:
    old = self.recent[: -self.keep_recent]
    merged = ([{"role": "system", "text": f"(기존 요약) {self.summary}"}]
              if self.summary else []) + old
    self.summary = summarize_history(merged)
    self.recent = self.recent[-self.keep_recent:]

keep_recent=2일 때를 손으로 따라가 봅니다. 문답 한 쌍이 메시지 두 개입니다.

턴이 끝난 뒤 recent의 메시지 수 4개를 넘는가 일어나는 일
1 2 아니오 없음
2 4 아니오 없음
3 6 예 앞의 4개(1·2턴)를 요약으로 바꾸고, 3턴의 2개만 원문으로 남긴다
4 4 아니오 없음
5 6 예 기존 요약 + 앞의 4개를 다시 요약한다

다섯 번째 줄이 중요합니다. 이미 요약이 있으면 그 요약까지 재료에 넣어 다시 요약합니다. 그래서 대화가 아무리 길어져도 들고 있는 것은 "요약 하나 + 최근 원문 조금"으로 크기가 일정합니다.


5. 요약의 두 가지 비용

호출 비용. 요약 자체가 LLM 호출 한 번입니다. 다만 그 뒤의 모든 턴에서 "원문 여러 턴" 대신 "요약 몇 줄"이 나가므로, 대화가 길수록 이득이 커집니다.

정보 손실. 요약은 손실 압축(lossy compression) 입니다. 줄이는 과정에서 무언가는 빠집니다. 무엇이 중요한지를 요약하는 시점의 모델이 판단하므로, 나중에 필요해질 세부가 빠질 수 있습니다. 요약을 다시 요약할 때마다 한 번 더 걸러집니다.

trim은 오래된 것을 전부 잊고, 요약은 오래된 것 중 덜 중요해 보인 것을 잊습니다. 원문을 보존하는 방법은 아닙니다.


핵심 정리

  • 요약 메모리는 오래된 이력을 버리는 대신 요약으로 바꿉니다.
  • 요약 프롬프트에 사실 위주, 길이, 꼭 넣을 항목을 적습니다. 기준은 개발자가, 요약은 모델이 합니다.
  • 기억은 요약(summary) 과 최근 원문(recent) 두 칸에 나눠 담습니다.
  • 이미 요약이 있으면 그 요약까지 넣어 다시 요약하므로 크기가 일정하게 유지됩니다.
  • 요약에는 호출 비용과 정보 손실이 따릅니다.
← 이전 절왜 장기 기억이 필요한가 — 16장이 남긴 두 가지 숙제다음 절 →기억의 세 층과 고객 프로필 — 대화가 끝나도 남는 것
오명운 · macro@prag-ai.com