Deep Learning/NLP

[NLP/LLM] LLM 캐시의 작동 원리를 알아보고 OpenAI API로 캐시를 구현해보자

seoraroong 2025. 2. 27. 14:13

LLM Cache

LLM Application Architecture

LLM 캐시는 LLM의 응답을 저장하고 재사용하는 시스템이다.

즉, 같은 질문 (프롬프트)에 대한 답변을 매번 새로 생성하지 않고 이전에 생성된 결과를 캐싱하여 빠르게 응답할 수 있도록 하는 기술이다.

상업용 API를 사용하거나 직접 모델을 서빙해 LLM을 추론할 수 있는데, 두 가지 모두 추론을 가능한 한 줄이는 것이 자원이나 비용 측면에서 효율적이라고 할 수 있다.

 

LLM Cache를 왜 사용할까?

(1) 비용 절감

OPENAI API 같은 유료 LLM 서비스를 사용할 때, 같은 질문을 반복하면 불필요한 비용이 발생한다.

캐싱을 이용하면 중복 호출을 방지하고 API 사용량을 줄일 수 있다.

(2) 속도 향상

LLM은 복잡한 연산을 수행하기 때문에 응답 시간이 길어질 수 있다.

캐싱을 이용하면 즉시 결과를 반환할 수 있어 사용자 경험을 향상시킬 수 있다.

(3) 시스템 안정성 향상

LLM API 서버가 일시적으로 다운되거나 속도가 느려지더라고 캐시된 응답을 반환하면 서비스 지속 가능성이 높아진다.

(4) 일관된 응답 유지

같은 질문을 하더라도 LLM은 매번 조금씩 다른 답변을 생성할 수 있다.

캐싱을 이용하면 동일한 프롬프트에 대해 일관된 응답을 제공할 수 있다.

 

LLM Cache의 방식

LLM 캐시는 일치 캐시(exact cache)유사 검색 캐시(similar cache)로 구분할 수 있다.

각 방식은 프롬프트(입력 쿼리)를 어떻게 처리하는지에 따라 다르게 동작한다.

 

- 일치 캐시 (exact cache)

일치 캐시는 요청이 완전히 일치하는, 즉 프롬프트가 완전히 같아야 같은 응답을 반환한다.

문자열 그대로 동일한지 판단하기 때문에 파이썬 딕셔너리와 같은 자료 구조에 프롬프트와 그에 대한 응답을 저장하고, 새로운 요청이 들어왔을 때 딕셔너리의 key에 동일한 프롬프트가 있는지 확인한다.

 

빠르고 간단한 방식이며 정밀한 캐싱이 가능하다는 장점이 있지만, 띄어쓰기나 단어 순서가 조금만 달라도 다른 요청으로 인식된다는 단점이 있다.

 

예를 들어보자면,

"What is AI?", "What is AI", "what is ai?" 세 개 문장이 모두 다른 프롬프트로 인식된다.

 

그래서 일치 캐시를 사용하기 위해서는 소문자 변환, 공백 제거등의 입력 정규화 과정을 중요하게 고려해야한다.

 

- 유사 검색 캐시 (similar cache)

유사 검색 캐시는 이전에 유사한 요청이 있었는지 확인하는 방식으로, 문자열을 그대로 비교하지 않고 문자열을 임베딩 모델을 통해 변환한 임베딩 벡터로 변환 후 유사도를 계산한다.

코사인 유사도 또는 유클리드 거리를 사용해 가장 유사한 프롬프트를 찾게되는 방식이다.

 

예를 들어보자면,

"What is AI?" 와 "Tell me about artificial intelligence?"는 문자열 자체는 다르지만 의미적으로 유사한 질문이기 때문에 캐싱을 이용해 동일한 응답을 제공할 수 있다.

 

  일치 캐시 (exact cache) 유사 검색 캐시 (similar cache)
캐시 검색 방식 문자열 일치 비교 벡터 임베딩을 이용한 유사 검색
속도 매우 빠름 벡터 연산으로 인해 상대적으로 느림
정확도 정확히 같은 프롬프트만 캐싱 의미적으로 유사한 질문도 캐싱 가능
유연성 문자 하나만 달라져도 다른 요청으로 취급하기에 유연성이 낮음 비슷한 질문도 같은 응답을 제공하기에 유연성이 높음
사용 예시 동일한 질문을 자주 받는 챗봇 유사한 질문이 많은 검색 서비스

 

OpenAI API로 캐시를 구현해보자

실습은 "LLM을 활용한 실전  AI 애플리케이션 개발" 교재의 내용을 참고했다.

 

파이썬 딕셔너리와 Chroma를 사용해 캐시 기능을 구현하는 실습이다.

 

- OpenAI와 Chroma 클라이언트 생성하기

import os
import chromadb
from openai import OpenAI

os.environ["OPENAI_API_KEY"] = "openai api key 입력"

openai_client = OpenAI()
chroma_client = chromadb.Client()

OpenAI 클라이언트는 언어 모델과 임베딩 모델을 사용하기 위한 것이고, Chroma 클라이언트는 임베딩 벡터를 저장하고 검색하기 위한 것이다.

 

- LLM 캐시를 사용하지 않았을 때 동일한 요청에 대해 걸린 시간을 확인해보기

import time

def response_text(openai_resp):
  return openai_resp.choices[0].message.content

question = "북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?"

for _ in range(2):
  start_time = time.time()
  response = openai_client.chat.completions.create(
      model="gpt-3.5-turbo",
      messages=[
          {
              'role': 'user',
              'content': question
          }
      ],
  )
  
  response = response_text(response)
  print(f"질문: {question}")
  print("소요 시간: {:.2f}s".format(time.time() - start_time))
  print(f"답변: {response}\n")
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 2.04s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을에서 겨울 사이인 
10월부터 3월까지입니다. 이 기간 동안 한반도 지역은 추위가 깊어지며, 맑은 날씨가 많이 이어지는 
특징을 보입니다.

질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 1.97s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 겨울까지이며, 
주로 11월부터 3월까지 머무르는 것으로 알려져 있습니다. 이 기간 동안 한반도와 주변 지역은 추운 날씨와 
함께 이들 기단의 영향을 받아 한파와 눈으로 인한 날씨 변화가 발생할 수 있습니다.

동일한 질문을 두 번 요청했을 때 걸린 시간은 각각 2.04초, 1.97초이다.

 

일치 캐시를 구현하면 요청 시간에 어떤 변화가 있을까?

 

- 파이썬 딕셔너리 자료구조를 이용한 일치 캐시 구현하기

class OpenAICache:
  def __init__(self, openai_client):
    self.openai_client = openai_client
    self.cache = {} # 딕셔너리
  
  def generate(self, prompt):
    if prompt not in self.cache:
      response = self.openai_client.chat.completions.create(
          model = "gpt-3.5-turbo",
          messages = [
              {
                  'role': 'user',
                  'content': prompt
              }
          ],
      )
      self.cache[prompt] = response_text(response)
    return self.cache[prompt]

openai_cache = OpenAICache(openai_client)

question = "북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 시간은?"

for _ in range(2):
  start_time = time.time()
  response = openai_cache.generate(question)
  print(f"질문: {question}")
  print("소요 시간: {:.2f}s".format(time.time() - start_time))
  print(f"답변: {response}\n")
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 시간은?
소요 시간: 1.75s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 시간은 보통 약 3~5일 정도입니다. 
이 기단들이 만날 때는 대기가 불안정해져서 비 또는 폭우가 내릴 확률이 높아지며, 기온 변화도 크게 
일어날 수 있습니다. 이러한 기단들이 국내에 머무르는 기간 동안은 날씨가 불안정하고 변화무쌍할 수 
있으니, 주의가 필요합니다.

질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 시간은?
소요 시간: 0.00s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 시간은 보통 약 3~5일 정도입니다. 
이 기단들이 만날 때는 대기가 불안정해져서 비 또는 폭우가 내릴 확률이 높아지며, 기온 변화도 크게 
일어날 수 있습니다. 이러한 기단들이 국내에 머무르는 기간 동안은 날씨가 불안정하고 변화무쌍할 수 
있으니, 주의가 필요합니다.

첫 번째 입력으로 받은 프롬프트는 self.cache에 없기 때문에 1.75초가 소요되었다.

그러나 첫 번째 프롬프트로부터 응답을 받고, 해당 프롬프트가 self.cache에 저장되었기 때문에 다음 두 번째 요청에서는 0.00초가 걸린 것을 확인할 수 있다.

 

- 유사 검색 캐시 구현하기

위에서 파이썬 딕셔너리로 일치 캐시를 구현한 OpenAICache 클래스에 기능을 추가하면된다.

class OpenAICache:
  def __init__(self, openai_client):
    self.openai_client = openai_client
    self.cache = {} 
    self.semantic_cache = semantic_cache
  
  def generate(self, prompt):
    if prompt not in self.cache:
      similar_doc = self.semantic_cache.query(query_texts=[prompt], n_results=1)
      if len(similar_doc['distances'][0]) > 0 and similar_doc['distances'][0][0] < 0.2:
        return similar_doc['metadatas'][0][0]['response']
      else:
        response = self.openai_client.chat.completions.create(
          model = "gpt-3.5-turbo",
          messages = [
              {
                  'role': 'user',
                  'content': prompt
              }
          ],
      )
      self.cache[prompt] = response_text(response)
      self.semantic_cache.add(documents=[prompt],
                              metadatas=[{"response":response_text(response)}], ids=[prompt])
    return self.cache[prompt]

self.semantic_cache를 추가한 코드이다.

generate 메서드에서 일치 캐시를 통해 동일한 프롬프트가 있는지 확인하는 과정을 거치고, 동일한 프롬프트가 있다면 저장된 결과를 반환하고 없다면 유사 검색 캐시를 확인한다.

Chroma 벡터 데이터베이스의 query 메서드에 query_text를 입력하면 벡터 데이터베이스에 등록된 임베딩 모델을 사용해 텍스트를 임베딩 벡터로 변환하고 검색을 수행하게 된다.

검색 결과를 확인 후 검색한 텍스트와 검색 결과 텍스트 사이의 거리 조건을 만족한다면 검색된 문서를 반환하는 구조이다.

조건을 만족하지 못했다면 새롭게 결과를 생성하도록 한다.

 

from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction
import time
openai_ef = OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-ada-002"
)

semantic_cache = chroma_client.get_or_create_collection(name="semantic_cache",
                                                 embedding_function=openai_ef, metadata={"hnsw:space": "cosine"})

openai_cache = OpenAICache(openai_client, semantic_cache)

questions = ["북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?",
             "북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?",
             "북태평양 기단과 오호츠크해 기단이 만나 한반도에 머무르는 기간은?",
             "국내에 북태평양 기단과 오호츠크해 기단이 함께 머무르는 기간은?"]

for question in questions:
  start_time = time.time()
  response = openai_cache.generate(question)
  print(f"질문: {question}")
  print("소요 시간: {:.2f}s".format(time.time() - start_time))
  print(f"답변: {response}\n")
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 0.61s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 보통 3~4주 정도입니다. 
이 기간 동안 한반도 지역에 추위가 찾아와 날씨가 매우 추워지는 현상이 발생하게 됩니다.

질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 0.61s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 보통 3~4주 정도입니다. 
이 기간 동안 한반도 지역에 추위가 찾아와 날씨가 매우 추워지는 현상이 발생하게 됩니다.

질문: 북태평양 기단과 오호츠크해 기단이 만나 한반도에 머무르는 기간은?
소요 시간: 0.16s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 보통 3~4주 정도입니다. 
이 기간 동안 한반도 지역에 추위가 찾아와 날씨가 매우 추워지는 현상이 발생하게 됩니다.

질문: 국내에 북태평양 기단과 오호츠크해 기단이 함께 머무르는 기간은?
소요 시간: 0.18s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 보통 3~4주 정도입니다.
이 기간 동안 한반도 지역에 추위가 찾아와 날씨가 매우 추워지는 현상이 발생하게 됩니다.