컨텍스트 압축

컨텍스트 압축 (Context Compaction)

컨텍스트 압축(context compaction)은 Agent의 대화를 줄여, 긴 실행이 모델의 컨텍스트 윈도우를 소진하지 않도록 해요. 이전 기록을 더 작은 표현으로 다시 쓰면서, Agent가 계속 작업하는 데 필요한 컨텍스트는 보존합니다.

출처: 문서

본문

압축은 손실이 있는(lossy) 작업입니다. 압축이 실행된 후 Agent는 대화의 더 짧은 기록으로 작업하게 됩니다. 무엇이 남고 무엇이 버려지는지는 압축 전략에 따라 달라져요.

How context compaction works

컨텍스트 압축은 세 가지 책임을 분리합니다:

Responsibility Abstraction Purpose
언제 압축할지 결정 CompactionHook LLM 호출 전에 Agent의 컨텍스트를 모니터링하고, 설정한 임계값에 도달하면 compactor를 호출해요.
어떻게 압축할지 결정 Compactor 프로토콜 대화가 어떻게 다시 쓰이는지 정의해요. Haystack에는 SlidingWindowCompactor, SummarizationCompactor, ToolResultPruningCompactor가 포함됩니다.
대화 측정 TokenCounter 프로토콜 모델로 보내기 전에 메시지와 도구 스키마의 크기를 추정해요.

이 분리를 통해 표준 트리거에 서로 다른 압축 전략과 토큰 카운터를 결합할 수 있어요. 예를 들어, 로컬 슬라이딩 윈도우는 추가 모델 호출 없이 오래된 기록을 제거할 수 있고, 요약 compactor는 같은 기록을 LLM으로 요약할 수 있습니다.

Basic setup

다음 예시는 Agent의 before_llm 후크 지점에 CompactionHook을 등록해요. 모델 컨텍스트 윈도우의 70%에서 압축을 시작하고, compactor가 컨텍스트를 약 40%로 줄이도록 합니다.

from typing import Annotated

from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIResponsesChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.hooks.compaction import CompactionHook, SlidingWindowCompactor
from haystack.tools import tool


@tool
def fetch_page(url: Annotated[str, "The URL to fetch"]) -> str:
    """Fetch a web page and return its text."""
    return "Fusion startups reported net-energy-gain milestones this year. " * 500


compaction_hook = CompactionHook(
    compactor=SlidingWindowCompactor(),
    context_window=400_000,  # gpt-5.4-nano's context window
    compact_at=0.7,
    compact_to=0.4,
)

agent = Agent(
    chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4-nano"),
    tools=[fetch_page],
    system_prompt="You are a research assistant. Fetch pages as needed and cite what you used.",
    hooks={"before_llm": [compaction_hook]},
)

result = agent.run(
    messages=[ChatMessage.from_user("Summarize recent fusion energy milestones.")],
)
print(result["last_message"].text)

임계값 구성, 컨텍스트 측정, 수명주기, 직렬화에 대해서는 CompactionHook 문서를 참고하세요.

Compaction strategies

Compactor는 현재 메시지, 목표 토큰 수, 그리고 컨텍스트 측정에 쓰인 것과 같은 토큰 카운터를 받아요. 더 짧은 교체 대화를 반환하거나, 바꿀 유용한 것이 없으면 None을 반환합니다.

Compactor Strategy Trade-off
SlidingWindowCompactor Agent의 지시와 최신 사용자 작업을 보존하고, 맞는 동안 완전한 이력 턴을 유지하며, 그것으로 충분하지 않을 때만 현재 작업의 Agent 단계를 자름 빠르고 로컬에서 작동하지만, 버려진 정보는 요약되지 않음
SummarizationCompactor 이력 턴과 현재 작업 단계를 LLM 생성 요약으로 점진적으로 교체하고, 필요할 때만 더 오래된 요약을 결합 이전 작업의 압축된 기록을 보존하지만 모델 비용·지연이 추가되고 여전히 손실이 있음
ToolResultPruningCompactor 오래된 대형 도구 결과를 짧은 자리표시자로 교체하면서 도구 호출/결과 구조는 보존 실행의 형태와 최근 결과는 유지하지만, 정리된 결과의 내용은 제거됨

Combining compaction strategies

before_llm에 여러 CompactionHook 인스턴스를 등록하면 점점 더 공격적인 전략을 적용할 수 있어요. 후크는 리스트 순서대로 같은 Agent 상태에 대해 실행되므로, 각 후크는 이전 후크가 남긴 메시지를 측정합니다.

예를 들어, 먼저 큰 도구 결과를 정리하고 슬라이딩 윈도우를 백업으로 사용하세요:

from haystack.hooks.compaction import (
    CompactionHook,
    SlidingWindowCompactor,
    ToolResultPruningCompactor,
)

prune_tool_results = CompactionHook(
    compactor=ToolResultPruningCompactor(),
    context_window=400_000,
    compact_at=0.7,
    compact_to=0.4,
)
drop_old_steps = CompactionHook(
    compactor=SlidingWindowCompactor(),
    context_window=400_000,
    compact_at=0.7,
    compact_to=0.4,
)

agent = Agent(
    chat_generator=OpenAIResponsesChatGenerator(model="gpt-5.4-nano"),
    tools=[fetch_page],
    hooks={"before_llm": [prune_tool_results, drop_old_steps]},
)

정리(pruning)가 업데이트된 컨텍스트를 compact_at 아래로 내리면 슬라이딩 윈도우 후크는 아무것도 하지 않아요. 정리가 적격 결과가 남지 않아 None을 반환하거나, 컨텍스트를 트리거 아래로 내리지 않고 줄인다면, 슬라이딩 윈도우는 이력 턴을 제거한 뒤 필요하면 현재 작업의 가장 오래된 Agent 단계를 제거합니다. 정리 compactor가 이미 자리표시자로 교체한 결과는 그것이 속한 이력 턴과 함께 남아, 그 도구 호출은 계속 답을 갖게 됩니다. 두 후크를 같은 모델 컨텍스트 윈도우와 호환되는 토큰 카운터로 구성해 비슷한 추정치에서 결정을 내리게 하세요.

Creating a custom compactor

애플리케이션 특정 메시지를 보존하거나 커스텀 메타데이터로 내용을 선택적으로 줄이는 등 다른 전략이 필요하면 Compactor 프로토콜을 구현하세요.

from typing import Any

from haystack.core.serialization import default_to_dict
from haystack.dataclasses import ChatMessage
from haystack.hooks.compaction import Compactor
from haystack.token_counters import TokenCounter


class CustomCompactor(Compactor):
    def compact(
        self,
        messages: list[ChatMessage],
        target_tokens: int,
        token_counter: TokenCounter,
    ) -> list[ChatMessage] | None:
        # Return a shorter, valid conversation or None when nothing should change.
        ...

    def to_dict(self) -> dict[str, Any]:
        return default_to_dict(self)

Compactor는 다음 규칙을 따라야 합니다:

  • 대화가 실제로 작아지지 않으면 None을 반환하세요.
  • 입력 messages 리스트를 수정하지 않고 새 리스트를 반환하세요.
  • 도구 호출을 모든 결과 메시지와 함께 유지하세요. Chat-completion API는 불완전한 도구 호출 교환을 거부합니다.

target_tokens 값은 보장이 아니라 목표예요. 목표가 Agent가 반드시 유지해야 하는 컨텍스트와 충돌하면, 필요한 컨텍스트를 보존하고 목표에 최대한 가깝게 가세요.

compact_async()는 기본적으로 compact()를 호출합니다. LLM 호출처럼 압축이 I/O를 수행할 때는 이를 오버라이드해 비동기 Agent 실행이 막히지 않게 하세요. 생성자 설정을 직렬화하려면 to_dict()를 사용하세요. 프로토콜의 기본 from_dict()는 일반 생성자 값을 처리하며, Secret이나 중첩 컴포넌트처럼 직렬화 값이 먼저 재구성돼야 할 때는 이를 오버라이드하세요.

Token counters

기본 ApproximateTokenCounter는 텍스트 길이로 토큰을 추정하며 추가 의존성이 필요 없어요. 모델 또는 제공 업체 특정 추정이 필요하면 다른 내장 또는 커스텀 TokenCounter를 구성할 수 있습니다.

토큰 카운터는 추정에 도구 스키마와 비텍스트 내용도 포함할 수 있어요. 사용 중인 카운터의 페이지를 참고해 이미지와 파일을 어떻게 처리하는지 이해하세요.

Context compaction and tool result offloading

도구 결과 오프로딩(tool result offloading)은 인접한 문제를 해결해요. 큰 도구 결과를 저장소에 쓰고 대화에는 포인터를 남깁니다. 두 접근 방식은 함께 잘 작동합니다. 오프로딩은 도착하는 개별 결과를 작게 유지하고, 압축은 대화 전체의 크기를 제한하니까요.

오프로딩된 결과는 저장된 내용에 대한 참조로 표현됩니다. compactor가 그 메시지를 제거하거나 다시 쓰면, 모델은 내용을 다시 읽을 때 필요한 참조를 잃어버려요.

ToolResultPruningCompactor는 기본적으로 오프로딩으로 표시된 결과를 건너뛰어, 저장된 내용 참조를 보존합니다.

더 알아보기 (Learn more)