본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 27. · 3 Views
Context Compression 컨텍스트 압축 전략 완벽 가이드
LLM 애플리케이션에서 컨텍스트 윈도우를 효율적으로 관리하는 압축 전략을 다룹니다. Anchored Summarization부터 프로브 기반 평가까지, 토큰 비용을 최적화하면서 정보 품질을 유지하는 핵심 기법들을 실무 관점에서 설명합니다.
목차
- Anchored Iterative Summarization
- Opaque Compression
- Regenerative Full Summary
- Tokens-per-Task 지표
- 압축 트리거 전략
- 프로브 기반 평가 프레임워크
1. Anchored Iterative Summarization
어느 날 김개발 씨는 챗봇 서비스를 운영하다가 이상한 문제를 발견했습니다. 대화가 길어질수록 AI가 처음에 사용자가 말했던 중요한 정보를 점점 잊어버리는 것이었습니다.
"분명히 30분 전에 예산은 100만원이라고 했는데, 왜 자꾸 200만원으로 추천하지?"
Anchored Iterative Summarization은 구조화된 앵커 포인트를 기준으로 반복적으로 요약하는 기법입니다. 마치 책의 목차처럼 핵심 정보를 고정된 위치에 유지하면서 나머지 내용만 압축합니다.
이를 통해 긴 대화에서도 중요한 정보가 흘러가버리는 정보 드리프트 현상을 방지할 수 있습니다.
다음 코드를 살펴봅시다.
class AnchoredSummarizer:
def __init__(self):
# 절대 잊으면 안 되는 핵심 정보를 저장하는 앵커
self.anchors = {
"user_preferences": [],
"constraints": [],
"key_decisions": []
}
self.conversation_buffer = []
def add_anchor(self, category: str, content: str):
# 중요 정보는 앵커에 고정 - 압축 대상에서 제외
if category in self.anchors:
self.anchors[category].append(content)
def summarize_with_anchors(self, new_messages: list) -> str:
# 앵커는 항상 상단에 유지
anchor_section = self._format_anchors()
# 일반 대화만 요약 대상
summary = self._compress_messages(new_messages)
return f"{anchor_section}\n---\n{summary}"
김개발 씨는 입사 6개월 차 AI 엔지니어입니다. 회사에서 운영하는 고객 상담 챗봇이 최근 이상한 행동을 보이기 시작했습니다.
고객이 대화 초반에 "예산은 100만원입니다"라고 분명히 말했는데, 대화가 길어지면 갑자기 200만원짜리 상품을 추천하는 것이었습니다. "이게 왜 이러지?" 김개발 씨가 로그를 살펴보는데, 선배 개발자 박시니어 씨가 다가왔습니다.
"아, 그거 정보 드리프트 문제야. 컨텍스트를 요약할 때 중요한 정보가 점점 희석되는 거지." 그렇다면 정보 드리프트란 정확히 무엇일까요?
쉽게 비유하자면, 전화 게임을 생각해보면 됩니다. 처음 사람이 "사과가 세 개"라고 말했는데, 열 명을 거치면 "사탕이 네 개"가 되어버리는 것처럼요.
요약을 반복할수록 원래 정보가 조금씩 변형되거나 사라지는 현상입니다. LLM 애플리케이션에서 이 문제는 심각합니다.
토큰 한도 때문에 이전 대화를 계속 요약해야 하는데, 요약할 때마다 중요한 세부사항이 조금씩 빠집니다. 열 번 요약하면 처음 정보의 절반도 남지 않을 수 있습니다.
바로 이런 문제를 해결하기 위해 Anchored Iterative Summarization이 등장했습니다. 핵심 아이디어는 간단합니다.
절대 잊으면 안 되는 정보는 요약 대상에서 제외하고 별도의 앵커 영역에 고정해두는 것입니다. 마치 책의 목차가 항상 앞쪽에 있는 것처럼요.
위의 코드를 살펴보면, anchors 딕셔너리가 핵심입니다. 여기에 사용자 선호도, 제약사항, 핵심 결정사항을 카테고리별로 저장합니다.
이 정보들은 아무리 대화가 길어져도 압축되지 않고 그대로 유지됩니다. add_anchor 메서드는 중요한 정보가 등장할 때마다 호출됩니다.
예를 들어 "예산 100만원"이라는 정보가 나오면 constraints 카테고리에 추가합니다. 한번 앵커에 들어간 정보는 절대 요약 과정에서 손실되지 않습니다.
summarize_with_anchors 메서드가 실제 압축을 수행합니다. 앵커 섹션은 항상 최상단에 배치하고, 일반 대화 내용만 요약합니다.
구분선으로 앵커와 요약을 명확히 분리하여 LLM이 혼동하지 않도록 합니다. 실무에서는 어떻게 활용할까요?
고객 상담 시스템에서 고객이 말한 예산, 선호 브랜드, 배송 주소 같은 정보는 앵커로 지정합니다. 나머지 잡담이나 질문은 요약해도 괜찮습니다.
이렇게 하면 아무리 대화가 길어져도 핵심 정보는 보존됩니다. 주의할 점도 있습니다.
모든 정보를 앵커로 지정하면 압축 효과가 없어집니다. 정말 중요한 정보만 선별해서 앵커로 지정해야 합니다.
보통 전체 정보의 10-20% 정도만 앵커로 유지하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가봅시다.
박시니어 씨의 조언을 듣고 앵커 시스템을 도입한 후, 챗봇은 더 이상 고객의 예산을 잊어버리지 않게 되었습니다. "역시 구조화가 답이었군요!"
실전 팁
💡 - 앵커 카테고리는 도메인에 맞게 설계하세요 (예: 의료 챗봇이라면 증상, 알레르기, 복용 약물 등)
- 앵커가 너무 많아지면 주기적으로 오래된 항목을 정리하는 로직을 추가하세요
2. Opaque Compression
김개발 씨는 앵커 시스템을 도입했지만 여전히 고민이 있었습니다. 토큰 비용이 너무 많이 나오는 것이었습니다.
"대화 하나에 수천 토큰이 들어가는데, 이걸 어떻게 줄이지?" 그때 박시니어 씨가 의미심장하게 말했습니다. "사람이 읽을 필요 없는 데이터라면, 사람이 읽을 수 있게 저장할 필요도 없지."
Opaque Compression은 인간이 읽을 수 없지만 LLM은 이해할 수 있는 형태로 정보를 압축하는 기법입니다. 마치 ZIP 파일처럼 원본보다 훨씬 작은 용량으로 정보를 저장하고, 필요할 때 LLM이 이를 해석합니다.
99% 이상의 놀라운 압축률을 달성할 수 있습니다.
다음 코드를 살펴봅시다.
import json
import zlib
import base64
class OpaqueCompressor:
def __init__(self, llm_client):
self.llm = llm_client
def compress(self, text: str) -> str:
# 1단계: LLM에게 핵심만 추출하도록 요청
extracted = self.llm.complete(
f"Extract only key facts as minimal tokens:\n{text}"
)
# 2단계: 바이너리 압축 후 base64 인코딩
compressed = zlib.compress(extracted.encode('utf-8'), level=9)
return base64.b64encode(compressed).decode('ascii')
def decompress_for_llm(self, opaque_data: str) -> str:
# LLM 프롬프트에 포함할 때 디코딩
decoded = zlib.decompress(base64.b64decode(opaque_data))
return decoded.decode('utf-8')
# 압축률: (1 - 압축후/압축전) * 100
def get_compression_ratio(self, original: str, compressed: str) -> float:
return (1 - len(compressed) / len(original)) * 100
김개발 씨의 챗봇 서비스가 인기를 끌면서 토큰 비용이 급증했습니다. 한 달 API 비용이 수백만 원을 넘어가자 팀장님이 미간을 찌푸렸습니다.
"이 비용 좀 줄여봐요." 박시니어 씨가 김개발 씨에게 물었습니다. "저장된 대화 내용, 누가 읽어?" 김개발 씨가 대답했습니다.
"AI가 읽죠. 이전 맥락을 파악하려고요." 박시니어 씨가 고개를 끄덕였습니다.
"그럼 사람이 읽을 수 있게 저장할 필요가 없잖아." 이것이 Opaque Compression의 핵심 발상입니다. 비유하자면, 속기사의 속기록을 생각해보세요.
일반인은 전혀 알아볼 수 없는 기호들이지만, 훈련받은 속기사는 완벽하게 해독할 수 있습니다. 마찬가지로 LLM은 인간보다 훨씬 다양한 형태의 정보를 이해할 수 있습니다.
일반적인 요약은 "사용자가 예산 100만원으로 노트북을 찾고 있으며, 게임용으로 사용할 예정입니다"처럼 완전한 문장으로 저장합니다. 하지만 이건 인간을 위한 형식입니다.
Opaque 방식은 다릅니다. "u:budget100w,laptop,gaming"처럼 극도로 압축된 형태로 저장합니다.
인간은 이해하기 어렵지만, LLM에게 적절한 프롬프트를 주면 원래 의미를 완벽히 복원할 수 있습니다. 위 코드에서 compress 메서드는 두 단계로 동작합니다.
먼저 LLM에게 핵심 정보만 최소한의 토큰으로 추출하도록 요청합니다. 그다음 zlib으로 바이너리 압축을 수행합니다.
결과는 base64로 인코딩하여 텍스트 형태로 저장합니다. 이 이중 압축의 효과는 놀랍습니다.
1000자짜리 대화가 50자 미만으로 줄어들 수 있습니다. 압축률로 따지면 95%에서 99%에 달합니다.
decompress_for_llm 메서드는 압축된 데이터를 LLM 프롬프트에 넣을 때 사용합니다. 완전한 복원이 아니라 LLM이 이해할 수 있는 수준까지만 디코딩합니다.
어차피 최종 해석은 LLM이 할 테니까요. 실무에서 주의할 점이 있습니다.
압축률이 높을수록 정보 손실 위험도 커집니다. 핵심 정보가 누락되면 아무리 압축률이 좋아도 소용없습니다.
따라서 압축 전후로 품질 검증 단계를 반드시 추가해야 합니다. 또한 모든 정보에 Opaque 압축을 적용할 필요는 없습니다.
앞서 배운 앵커 정보는 그대로 유지하고, 일반 대화 내용에만 Opaque 압축을 적용하는 하이브리드 전략이 효과적입니다. 박시니어 씨의 조언대로 Opaque 압축을 도입한 후, 김개발 씨 팀의 월간 토큰 비용은 70% 이상 감소했습니다.
팀장님의 표정도 한결 밝아졌습니다.
실전 팁
💡 - 압축률과 정보 보존율 사이의 균형점을 찾으세요. 보통 90-95% 압축률이 안전합니다
- 디버깅을 위해 원본 데이터를 일정 기간 별도 저장하는 것을 권장합니다
3. Regenerative Full Summary
김개발 씨는 새로운 고민에 빠졌습니다. Opaque 압축은 효율적이지만, 가끔 고객 상담 내역을 사람이 직접 확인해야 할 때가 있었습니다.
"이 암호 같은 데이터를 어떻게 읽지?" 박시니어 씨가 웃으며 말했습니다. "압축과 재생성, 두 마리 토끼를 다 잡는 방법이 있어."
Regenerative Full Summary는 압축된 정보를 다시 읽기 쉬운 형태로 완전히 재생성하는 방식입니다. 마치 메모를 보고 보고서를 새로 작성하는 것처럼, LLM이 압축된 데이터를 바탕으로 자연스러운 문장을 생성합니다.
저장은 압축 형태로, 표시는 읽기 쉬운 형태로 하는 전략입니다.
다음 코드를 살펴봅시다.
class RegenerativeSummarizer:
def __init__(self, llm_client):
self.llm = llm_client
self.compressed_store = {} # 압축 저장소
def store_compressed(self, session_id: str, data: str):
# 내부적으로는 압축 형태로 저장
self.compressed_store[session_id] = self._compress(data)
def regenerate_readable(self, session_id: str, style: str = "formal") -> str:
# 필요할 때 읽기 쉬운 형태로 재생성
compressed = self.compressed_store.get(session_id)
prompt = f"""
Given this compressed context: {compressed}
Regenerate a {style} summary that is easy to read.
Include all key information in natural sentences.
"""
return self.llm.complete(prompt)
def regenerate_for_context(self, session_id: str) -> str:
# LLM 컨텍스트용으로 최적화된 형태로 재생성
compressed = self.compressed_store[session_id]
return self.llm.complete(
f"Expand to minimal context: {compressed}"
)
김개발 씨 팀의 챗봇 서비스에 클레임이 들어왔습니다. 고객이 "분명히 이렇게 말했는데 왜 다르게 처리됐냐"고 항의한 것입니다.
CS팀에서 대화 기록을 확인하려고 했지만, 화면에 나타난 것은 알아볼 수 없는 압축 데이터뿐이었습니다. "이걸 어떻게 읽어요?" CS팀장의 질문에 김개발 씨는 당황했습니다.
Opaque 압축의 부작용이었습니다. 박시니어 씨가 해결책을 제시했습니다.
"저장은 압축으로, 보여줄 때는 재생성하면 되지." Regenerative Full Summary의 핵심은 용도에 따른 분리입니다. 저장 공간과 비용 절감을 위해서는 압축 형태를 유지하고, 사람이나 LLM이 읽어야 할 때만 적절한 형태로 재생성합니다.
비유하자면, 회의록을 생각해보세요. 속기사는 빠르게 기호로 받아 적지만, 나중에 공식 회의록을 작성할 때는 이를 바탕으로 완전한 문장으로 다시 씁니다.
원본 속기는 보관하고, 필요한 형태로 여러 버전을 만들어내는 것입니다. 위 코드의 store_compressed 메서드는 데이터를 압축 형태로 저장합니다.
이것이 원본입니다. 공간을 최소한으로 차지하면서 모든 정보를 담고 있습니다.
regenerate_readable 메서드가 핵심입니다. 압축된 데이터를 받아 LLM에게 "읽기 쉬운 요약을 생성해달라"고 요청합니다.
style 파라미터로 형식을 지정할 수 있습니다. 공식 보고서용이라면 "formal", 간단한 확인용이라면 "casual"처럼요.
regenerate_for_context는 다른 LLM 호출의 컨텍스트로 사용할 때 호출합니다. 이 경우에는 사람이 읽을 필요가 없으므로, 토큰을 최소화하면서 LLM이 이해할 수 있는 정도로만 확장합니다.
이 방식의 장점은 유연성입니다. 같은 데이터에서 여러 형태의 출력을 만들어낼 수 있습니다.
고객용 요약, 내부 보고서, AI 컨텍스트용 등 목적에 맞게 재생성하면 됩니다. 단점도 있습니다.
재생성할 때마다 LLM API를 호출해야 하므로 추가 비용이 발생합니다. 따라서 자주 조회되는 데이터는 캐싱 전략을 함께 적용해야 합니다.
CS팀의 요청을 받은 김개발 씨는 관리자 페이지에 "대화 내역 보기" 버튼을 추가했습니다. 버튼을 누르면 압축된 데이터를 읽기 쉬운 형태로 재생성해서 보여줍니다.
CS팀장은 만족스러운 표정을 지었습니다. "이제야 일을 할 수 있겠네요."
실전 팁
💡 - 자주 조회되는 세션의 재생성 결과는 캐싱하여 API 호출을 줄이세요
- 재생성 프롬프트에 도메인 특화 지시사항을 추가하면 품질이 향상됩니다
4. Tokens-per-Task 지표
김개발 씨는 월간 보고서를 작성하면서 고민에 빠졌습니다. "API 호출당 평균 500 토큰...
이게 좋은 건가 나쁜 건가?" 박시니어 씨가 보고서를 들여다보더니 고개를 저었습니다. "호출당 토큰은 의미 없어.
작업당 총 토큰을 봐야지."
Tokens-per-Task는 단일 API 요청이 아닌, 하나의 작업을 완료하는 데 소비된 총 토큰을 측정하는 지표입니다. 마치 여행 비용을 비행기 표값만으로 따지지 않고 숙박, 식비, 교통비까지 포함하는 것과 같습니다.
진정한 비용 최적화는 작업 단위에서 이루어져야 합니다.
다음 코드를 살펴봅시다.
from dataclasses import dataclass, field
from typing import List
import time
@dataclass
class TaskMetrics:
task_id: str
task_type: str
start_time: float = field(default_factory=time.time)
api_calls: List[dict] = field(default_factory=list)
def record_call(self, input_tokens: int, output_tokens: int):
self.api_calls.append({
"input": input_tokens,
"output": output_tokens,
"timestamp": time.time()
})
@property
def total_tokens(self) -> int:
return sum(c["input"] + c["output"] for c in self.api_calls)
@property
def tokens_per_call(self) -> float:
if not self.api_calls:
return 0
return self.total_tokens / len(self.api_calls)
def get_efficiency_score(self) -> dict:
# 작업 완료에 필요한 총 비용 분석
return {
"total_tokens": self.total_tokens,
"call_count": len(self.api_calls),
"avg_per_call": self.tokens_per_call,
"duration_sec": time.time() - self.start_time
}
김개발 씨의 챗봇은 고객 문의 하나를 처리하는 데 보통 5-7번의 API 호출이 필요했습니다. 의도 파악, 정보 검색, 답변 생성, 후처리 등 여러 단계를 거치기 때문입니다.
그런데 월간 보고서에는 "평균 요청당 500 토큰"이라고만 적혀 있었습니다. 이 숫자만 보면 꽤 효율적으로 보입니다.
하지만 박시니어 씨는 다른 관점을 제시했습니다. "문의 하나 처리하는 데 총 몇 토큰 쓰는지 알아?" 김개발 씨가 계산해보니 평균 3,500 토큰이었습니다.
호출당으로 보면 500이지만, 작업당으로 보면 7배나 됩니다. 이것이 Tokens-per-Task 지표의 핵심입니다.
비유하자면, 택시 요금을 생각해보세요. 기본요금 4,000원이 저렴해 보여도, 목적지까지 가는 데 10번을 갈아타야 한다면 총비용은 40,000원입니다.
한 번에 20,000원인 콜밴이 오히려 저렴할 수 있습니다. API 호출도 마찬가지입니다.
호출당 토큰을 줄이려고 컨텍스트를 과도하게 압축하면, 오히려 추가 호출이 필요해져서 총비용이 증가할 수 있습니다. 반대로 한 번 호출에 충분한 컨텍스트를 주면 호출 횟수가 줄어 총비용이 감소하기도 합니다.
위 코드의 TaskMetrics 클래스는 작업 단위로 모든 API 호출을 추적합니다. record_call 메서드로 각 호출의 입출력 토큰을 기록하고, total_tokens 프로퍼티로 총합을 계산합니다.
get_efficiency_score 메서드가 중요합니다. 총 토큰, 호출 횟수, 평균, 소요 시간을 종합적으로 보여줍니다.
이 데이터를 분석하면 어디서 비효율이 발생하는지 파악할 수 있습니다. 실무에서는 작업 유형별로 이 지표를 분리해서 봐야 합니다.
간단한 FAQ 응답과 복잡한 기술 지원은 당연히 토큰 소비가 다릅니다. 같은 유형의 작업끼리 비교해야 의미 있는 최적화가 가능합니다.
김개발 씨는 Tokens-per-Task 대시보드를 만들었습니다. 분석 결과 놀라운 사실을 발견했습니다.
전체 토큰의 40%가 "이전 대화 요약 재생성"에 쓰이고 있었습니다. 캐싱을 도입하자 작업당 토큰이 30% 감소했습니다.
"호출당 지표만 봤으면 영원히 몰랐을 거예요." 김개발 씨의 깨달음이었습니다.
실전 팁
💡 - 작업 유형별로 Tokens-per-Task 기준선을 설정하고, 이상치를 모니터링하세요
- 총 토큰뿐 아니라 작업 완료율도 함께 추적하여 품질과 비용의 균형을 맞추세요
5. 압축 트리거 전략
김개발 씨는 새로운 문제에 직면했습니다. "압축을 언제 해야 하지?" 너무 자주 하면 품질이 떨어지고, 너무 늦으면 토큰 한도를 초과합니다.
박시니어 씨가 세 가지 전략을 화이트보드에 그리기 시작했습니다. "상황에 따라 다른 방식을 쓰면 돼."
압축 트리거 전략은 컨텍스트 압축을 언제 실행할지 결정하는 정책입니다. 크게 고정 임계값, 슬라이딩 윈도우, 중요도 기반 세 가지 방식이 있습니다.
마치 창고 정리를 정기적으로 할지, 공간이 부족할 때 할지, 중요한 물건 위주로 할지 선택하는 것과 같습니다.
다음 코드를 살펴봅시다.
from abc import ABC, abstractmethod
from typing import List
class CompressionTrigger(ABC):
@abstractmethod
def should_compress(self, context: List[dict]) -> bool:
pass
class FixedThresholdTrigger(CompressionTrigger):
def __init__(self, max_tokens: int = 4000):
self.max_tokens = max_tokens
def should_compress(self, context: List[dict]) -> bool:
total = sum(msg.get("tokens", 0) for msg in context)
return total >= self.max_tokens
class SlidingWindowTrigger(CompressionTrigger):
def __init__(self, window_size: int = 10):
self.window_size = window_size
def should_compress(self, context: List[dict]) -> bool:
# 최근 N개만 유지, 나머지는 압축 대상
return len(context) > self.window_size
class ImportanceBasedTrigger(CompressionTrigger):
def __init__(self, importance_threshold: float = 0.3):
self.threshold = importance_threshold
def should_compress(self, context: List[dict]) -> bool:
# 중요도 낮은 메시지 비율이 임계값 초과시
low_importance = [m for m in context if m.get("importance", 1) < 0.5]
return len(low_importance) / len(context) > self.threshold
김개발 씨가 박시니어 씨에게 물었습니다. "압축 타이밍을 어떻게 정해야 할까요?" 박시니어 씨가 화이트보드에 세 개의 원을 그렸습니다.
"각각 장단점이 있어. 하나씩 설명해줄게." 첫 번째 원에 "고정 임계값"이라고 적었습니다.
"가장 단순한 방식이야. 토큰이 4000개를 넘으면 압축해.
마치 쓰레기통이 가득 차면 비우는 것처럼." 고정 임계값의 장점은 구현이 간단하고 예측 가능하다는 것입니다. 단점은 융통성이 없다는 것입니다.
중요한 대화 중간에도 임계값에 도달하면 압축이 실행됩니다. 두 번째 원에 "슬라이딩 윈도우"라고 적었습니다.
"최근 10개 메시지만 유지하고 나머지는 압축해. 창문을 밀듯이 오래된 건 밀려나가는 거지." 슬라이딩 윈도우는 항상 일정한 메시지 수를 유지합니다.
메모리 사용량이 예측 가능하고, 최근 맥락이 항상 보존됩니다. 하지만 오래된 메시지 중에 정말 중요한 것이 있어도 무조건 밀려납니다.
세 번째 원에 "중요도 기반"이라고 적었습니다. "이건 좀 똑똑해.
메시지마다 중요도를 매기고, 중요도 낮은 것들이 많아지면 그것들만 압축해." 중요도 기반 전략은 가장 지능적입니다. 핵심 정보는 보존하고 잡담만 압축할 수 있습니다.
하지만 중요도를 매기는 로직이 추가로 필요하고, 이 로직이 잘못되면 오히려 역효과가 납니다. 위 코드에서 각 트리거는 should_compress 메서드를 구현합니다.
컨텍스트를 받아서 압축할지 말지 불리언으로 반환합니다. FixedThresholdTrigger는 총 토큰 수만 확인합니다.
단순하지만 효과적입니다. SlidingWindowTrigger는 메시지 개수를 봅니다.
ImportanceBasedTrigger는 각 메시지의 importance 필드를 활용합니다. 실무에서는 이 세 가지를 조합해서 사용하는 경우가 많습니다.
예를 들어, 슬라이딩 윈도우로 기본 관리를 하되, 중요도가 높은 메시지는 윈도우 밖으로 밀려나도 앵커로 보존하는 식입니다. 김개발 씨는 고민 끝에 하이브리드 방식을 선택했습니다.
토큰 기준 임계값을 주 트리거로 사용하되, 중요도가 0.8 이상인 메시지는 압축 대상에서 제외하도록 했습니다. "이제 중요한 정보는 안 잃어버리겠네요!"
실전 팁
💡 - 처음에는 고정 임계값으로 시작하고, 데이터가 쌓이면 중요도 기반으로 발전시키세요
- 여러 트리거를 OR 조건으로 조합하면 더 유연한 정책을 만들 수 있습니다
6. 프로브 기반 평가 프레임워크
김개발 씨의 압축 시스템이 완성되었습니다. 하지만 한 가지 의문이 남았습니다.
"이 압축이 정말 잘 된 건지 어떻게 확인하지?" 박시니어 씨가 의미심장하게 말했습니다. "프로브를 심어봐.
압축 후에도 핵심 정보를 찾을 수 있는지 테스트하는 거야."
프로브 기반 평가는 압축된 컨텍스트에서 특정 정보를 정확히 추출할 수 있는지 테스트하는 방법입니다. 마치 시험 문제처럼 "이 정보가 압축 후에도 남아 있는가?"를 질문합니다.
정확성, 완전성, 일관성, 관련성, 간결성, 시의성 6가지 차원으로 품질을 측정합니다.
다음 코드를 살펴봅시다.
from dataclasses import dataclass
from typing import List, Callable
@dataclass
class Probe:
dimension: str # 평가 차원
question: str # 검증 질문
expected: str # 기대 답변
scorer: Callable[[str, str], float] # 점수 계산 함수
class ProbeEvaluator:
DIMENSIONS = [
"accuracy", # 정확성: 사실이 정확한가
"completeness", # 완전성: 빠진 정보가 없는가
"consistency", # 일관성: 모순이 없는가
"relevance", # 관련성: 불필요한 정보는 없는가
"conciseness", # 간결성: 충분히 압축되었는가
"timeliness" # 시의성: 최신 정보가 반영되었는가
]
def __init__(self, llm_client):
self.llm = llm_client
self.probes: List[Probe] = []
def add_probe(self, dimension: str, question: str, expected: str):
self.probes.append(Probe(
dimension=dimension,
question=question,
expected=expected,
scorer=self._default_scorer
))
def evaluate(self, compressed_context: str) -> dict:
results = {dim: [] for dim in self.DIMENSIONS}
for probe in self.probes:
answer = self.llm.complete(
f"Context: {compressed_context}\nQuestion: {probe.question}"
)
score = probe.scorer(answer, probe.expected)
results[probe.dimension].append(score)
# 차원별 평균 점수 계산
return {dim: sum(scores)/len(scores) if scores else 0
for dim, scores in results.items()}
김개발 씨는 압축 시스템을 구축했지만 불안했습니다. "압축이 잘 된 건지 어떻게 알지?" 토큰 수가 줄었다고 해서 품질이 좋다고 할 수는 없으니까요.
박시니어 씨가 해결책을 제시했습니다. "프로브를 심어봐.
압축 전에 알 수 있던 정보를 압축 후에도 알 수 있는지 테스트하는 거야." 프로브는 마치 시험 문제와 같습니다. "사용자의 예산은 얼마인가?"라는 질문을 압축된 컨텍스트에 던져서, 올바른 답이 나오면 그 정보는 잘 보존된 것입니다.
하지만 단순히 "맞다/틀리다"만으로는 부족합니다. 그래서 6가지 평가 차원을 도입합니다.
첫째, **정확성(Accuracy)**입니다. 추출된 정보가 사실과 일치하는가를 봅니다.
"예산이 100만원"인데 "50만원"이라고 답하면 정확성이 낮습니다. 둘째, **완전성(Completeness)**입니다.
필요한 정보가 모두 보존되었는가를 봅니다. 예산은 맞췄지만 배송지 정보가 사라졌다면 완전성이 낮습니다.
셋째, **일관성(Consistency)**입니다. 정보들 사이에 모순이 없는가를 봅니다.
한 곳에서는 "게임용"이라고 하고 다른 곳에서는 "문서 작업용"이라고 하면 일관성이 낮습니다. 넷째, **관련성(Relevance)**입니다.
불필요한 정보는 제거되었는가를 봅니다. 핵심 정보는 있는데 잡담까지 다 남아 있으면 관련성 점수가 낮습니다.
다섯째, **간결성(Conciseness)**입니다. 충분히 압축되었는가를 봅니다.
정보는 다 있지만 너무 장황하면 간결성이 낮습니다. 여섯째, **시의성(Timeliness)**입니다.
최신 정보가 반영되었는가를 봅니다. 처음에 말한 예산보다 나중에 수정한 예산이 중요합니다.
위 코드의 add_probe 메서드로 프로브를 등록합니다. 각 프로브는 평가 차원, 질문, 기대 답변을 포함합니다.
evaluate 메서드는 모든 프로브를 실행하고 차원별 평균 점수를 반환합니다. 실무에서는 도메인에 맞는 프로브 세트를 미리 준비해둡니다.
고객 상담용이라면 "예산은?", "선호 브랜드는?", "배송지는?" 같은 질문들이요. 압축할 때마다 이 프로브들을 자동으로 실행하여 품질을 모니터링합니다.
김개발 씨는 프로브 시스템을 도입한 후, 압축 품질이 80% 이하로 떨어지면 알림이 오도록 설정했습니다. 어느 날 알림이 왔습니다.
확인해보니 특정 유형의 대화에서 완전성 점수가 급격히 낮았습니다. 압축 로직을 수정하여 문제를 해결했습니다.
"프로브가 없었으면 고객 불만이 터진 후에야 알았을 거예요." 김개발 씨는 안도의 한숨을 쉬었습니다. 이제 그의 압축 시스템은 효율성과 품질을 모두 갖추게 되었습니다.
실전 팁
💡 - 프로브는 실제 사용 사례에서 추출하세요. 고객이 자주 묻는 질문이 좋은 프로브 후보입니다
- 주기적으로 프로브 세트를 업데이트하여 새로운 유형의 대화도 커버하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Memory Systems 에이전트 메모리 아키텍처 완벽 가이드
AI 에이전트가 정보를 기억하고 활용하는 메모리 시스템의 핵심 아키텍처를 다룹니다. 벡터 스토어의 한계부터 Knowledge Graph, Temporal Knowledge Graph까지 단계별로 이해할 수 있습니다.
Phase 5 취약점 발굴과 분석 완벽 가이드
보안 전문가가 되기 위한 취약점 발굴의 핵심 기법을 다룹니다. 코드 리뷰부터 퍼징, 바이너리 분석까지 실무에서 바로 활용할 수 있는 기술을 초급자 눈높이에 맞춰 설명합니다.
Multi-Agent Patterns 멀티 에이전트 아키텍처 완벽 가이드
여러 AI 에이전트가 협력하여 복잡한 작업을 수행하는 멀티 에이전트 시스템의 핵심 패턴을 다룹니다. 컨텍스트 격리부터 Supervisor, Swarm, Hierarchical 패턴까지 실무에서 바로 적용할 수 있는 아키텍처 설계 원칙을 배웁니다.
침해사고 대응 실무 완벽 가이드
보안 침해사고가 발생했을 때 초기 대응부터 디지털 포렌식, 침해 지표 추출까지 실무에서 바로 활용할 수 있는 파이썬 기반 대응 기법을 다룹니다. 초급 개발자도 이해할 수 있도록 실제 시나리오와 함께 설명합니다.
보안 인프라 구축 완벽 가이드
서버 보안의 핵심인 다층 방어 아키텍처부터 ELK Stack 연동까지, 실무에서 바로 적용할 수 있는 보안 인프라 구축 방법을 단계별로 알아봅니다. 초급 개발자도 쉽게 따라할 수 있도록 스토리텔링 방식으로 설명합니다.