본문 바로가기
AI2026년 8월 22일7분 읽기

시맨틱 캐싱 — LLM 응답을 임베딩으로 재사용해 비용 줄이기

YS
김영삼
조회 3
시맨틱 캐싱 — LLM 응답을 임베딩으로 재사용해 비용 줄이기

시맨틱 캐싱(semantic caching)은 LLM에 들어온 질문을 임베딩으로 바꿔, 과거에 의미가 충분히 비슷한 질문이 있었으면 그때의 답을 그대로 돌려주는 캐싱 기법이다. 문자열이 똑같아야만 적중하는 일반 캐시와 달리, "환불 어떻게 해요?"와 "환불 방법 알려주세요"를 같은 질문으로 보고 재사용한다.

이걸 처음 도입한 계기는 순전히 돈이었다. 고객 문의 봇을 돌리는데 로그를 보니 사람들이 사실상 똑같은 걸 표현만 바꿔서 계속 물었다. 매번 LLM을 부르니 비용도 비용이고 응답도 1~2초씩 걸렸다. 그런데 일반 캐시는 문장이 한 글자라도 다르면 미스라 적중률이 처참했다.

일반 캐시로는 왜 안 되나

전통적 캐시는 키가 완전히 일치해야 한다. 자연어 질문은 같은 의도라도 표현이 무한히 갈린다. "비밀번호 변경", "패스워드 어떻게 바꿔요", "비번 재설정 방법" — 사람에겐 같은 질문이지만 해시 키로는 전부 다른 키다. 그래서 실측 적중률이 한 자릿수 퍼센트에 그친다.

시맨틱 캐시는 키를 문자열이 아니라 임베딩 벡터로 잡는다. 새 질문이 오면 임베딩을 만들어 캐시에 저장된 벡터들과 유사도를 재고, 임계값을 넘는 가장 가까운 항목이 있으면 그 답을 반환한다.

동작 구조

class SemanticCache:
    def __init__(self, embed, store, threshold=0.92):
        self.embed = embed          # 질문 -> 벡터
        self.store = store          # 벡터 DB (cosine)
        self.threshold = threshold

    def get(self, question):
        v = self.embed(question)
        hit = self.store.search(v, top_k=1)
        if hit and hit[0].score >= self.threshold:
            return hit[0].answer     # 캐시 적중
        return None                  # 미스

    def put(self, question, answer):
        v = self.embed(question)
        self.store.upsert(v, {"answer": answer, "q": question})

# 사용
def ask(cache, llm, q):
    cached = cache.get(q)
    if cached is not None:
        return cached
    a = llm.generate(q)
    cache.put(q, a)
    return a

핵심은 threshold다. 이 숫자 하나가 캐시의 성패를 가른다. 나는 여기서 제대로 데였다.

임계값이 전부다

임계값을 낮게(예: 0.80) 잡으면 적중률은 치솟지만 엉뚱한 답이 나간다. 실제로 "환불 되나요?"에 "취소 방법은…" 답을 재사용해서 내보낸 적이 있다. 반대로 0.98처럼 빡빡하게 잡으면 거의 안 맞아서 캐시를 왜 달았나 싶다.

임계값적중률오답 위험적합 상황
0.80~0.85높음FAQ 등 정형 답변
0.90~0.93중간보통대부분의 실무 권장
0.97+낮음작음오답 절대 불가한 도메인

임계값은 감으로 정하지 말고, 실제 질문 로그에서 "같은 의도" 쌍과 "다른 의도" 쌍을 라벨링해 유사도 분포를 그린 다음 겹치지 않는 경계를 찾아 잡아야 한다. 도메인마다 이 경계가 다르다.

캐시하면 안 되는 것들

시맨틱 캐시는 답이 시간·사용자·맥락에 따라 달라지는 질문에 쓰면 큰일 난다. "내 주문 상태 알려줘", "오늘 환율은?" 같은 건 질문 표현은 비슷해도 답이 매번 달라야 한다. 나는 캐시 대상을 태그로 구분한다. 개인화·실시간 정보가 낀 요청은 아예 캐시를 건너뛴다.

시맨틱 캐시는 "질문이 비슷하면 답도 같다"는 가정 위에 선다. 이 가정이 깨지는 질문(개인화, 실시간, 상태 조회)에는 절대 걸지 마라.

프롬프트 캐싱과는 다르다

혼동하기 쉬운데, 제공사(API)의 프롬프트 캐싱과 시맨틱 캐싱은 층이 다르다. 프롬프트 캐싱은 같은 접두 토큰을 재사용해 입력 처리를 아끼는 것이고 여전히 모델은 호출된다. 시맨틱 캐싱은 모델 호출 자체를 건너뛴다. 둘은 경쟁이 아니라 같이 쓴다. 나는 프론트에 시맨틱 캐시를 두고, 미스일 때 프롬프트 캐싱이 걸린 모델 호출로 넘긴다.

운영에서 챙길 것

  • TTL: 답이 오래되면 무효화. 정책·가격 안내는 특히.
  • 무효화 훅: 원본 지식이 바뀌면 관련 캐시를 지운다.
  • 적중 로깅: 실제로 반환된 캐시 답이 질문에 맞았는지 샘플 검수. 임계값 튜닝의 근거가 된다.
  • 임베딩 비용: 미스여도 임베딩 호출은 든다. 다만 LLM 생성보다 훨씬 싸다.

자주 묻는 질문

시맨틱 캐싱과 API 프롬프트 캐싱은 뭐가 다른가요?

프롬프트 캐싱은 동일한 접두 토큰의 처리 비용을 아끼지만 모델은 여전히 호출됩니다. 시맨틱 캐싱은 의미가 비슷한 과거 질문의 답을 재사용해 모델 호출 자체를 생략합니다. 층이 다르므로 함께 쓰는 게 좋습니다. 시맨틱 캐시가 미스일 때 프롬프트 캐싱이 걸린 모델 호출로 넘기는 구성이 흔합니다.

유사도 임계값은 얼마로 잡아야 하나요?

정답은 도메인마다 다릅니다. 대개 코사인 0.90~0.93 부근에서 시작하되, 실제 질문 로그로 같은 의도·다른 의도 쌍을 라벨링해 유사도 분포를 그린 뒤 두 분포가 겹치지 않는 경계를 임계값으로 잡는 게 안전합니다. 오답이 치명적인 도메인은 더 높게 잡습니다.

어떤 질문은 캐싱하면 안 되나요?

답이 사용자·시간·상태에 따라 달라지는 질문입니다. 주문 상태 조회, 실시간 환율, 개인화된 추천 등은 표현이 비슷해도 답이 매번 달라야 하므로 캐싱하면 오답이 나갑니다. 이런 요청은 태그로 구분해 캐시를 건너뛰게 하세요.

캐시된 답이 오래되면 어떻게 하나요?

항목마다 TTL을 두어 시간이 지나면 무효화하고, 원본 지식이 갱신될 때 관련 캐시를 지우는 무효화 훅을 둡니다. 특히 가격·정책 같은 변동성 있는 답변은 TTL을 짧게 잡고, 실제 반환된 답을 주기적으로 샘플 검수해 품질을 확인하는 게 좋습니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록