지난 시간에는 벡터 데이터베이스를 통해 사용자의 질문과 관련된 정보를 찾아 프롬프트에 통합하는 RAG에 대해 알아보았다. 이제 만들어진 프롬프트를 바탕으로 LLM 모델에 결과를 요청해야 한다. 그런데 여기선 두 가지 상황에서 문제가 발생한다. 먼저 OpenAI 같은 상업용 API를 사용할 경우, 프롬프트의 토큰 수만큼 비용이 발생한다. 그렇다고 직접 LLM을 서빙하는 경우도 녹녹지 않다. GPU를 많이 사용해야 하기 때문이다.
다양한 상황에서 최대한 LLM 추론을 줄여야 할 텐데, 어떤 상황에서 줄일 수 있을까? 그건 바로 이미 이전 상황에서 생성된 답변이 있는 경우를 활용할 때다! 똑같은 질문을 다시 하거나, 비슷한 질문을 했을 때 같은 답변을 내놓는다면 LLM 추론 횟수를 줄일 수 있을 것이다. 이번 시간에는 이를 위한 LLM 캐시를 사용해 보자.
LLM 캐시 작동 원리
캐시는 저장 공간이라고 생각하면 된다. 요청에 대한 정보가 이미 캐시에 있다면, 요청에 해당하는 답변을 그대로 캐시에서 가져와서 사용하면 된다. 혹은 요청이 캐시에 저장된 요청과 유사하다면, 특정 유사도를 넘을 경우에 가져와서 사용하면 된다. 전자를 우리는 일치 캐시라고 하고, 후자를 유사 검색 캐시라고 한다.
LLM 캐시가 있을 때 vs 없을 때
일단 기본적으로 캐시가 있을 경우 동일한 요청 처리에 대해 얼마만큼의 시간이 소요되는지를 알아보자. 비교를 위해 있을 때와 없을 때를 모두 해본다. 먼저 일치 캐시를 구현해 보자.
일치 캐시가 없을 때
from openai import OpenAI
import time
import os
client = OpenAI(api_key= os.getenv("OPENAI_API_KEY"))
# 답변에서 content만 가져오는 메서드
def response_text(openai_resp):
return openai_resp.choices[0].message.content
question = "북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?"
# 같은 요청 처리를 두번 연속 처리
for _ in range(2):
start_time = time.time()
response = 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')
위 코드는 같은 요청을 두 번 하는 경우이다. 캐시를 구현하지 않았으니 요청에 대한 처리를 두 번 하게 된다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 3.86s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 가을과 겨울에 해당됩니다. 일반적으로 10월부터 4월까지 한국을 방문하며, 특히 11월부터 2월 사이에 기상 조건이 가장 좋아지기 때문에 많은 조류들이 이 기간에 국내에 머무르게 됩니다. 이 기간에는 여러 종류의 조류들이 국내에 도착하여 겨울철을 보내기 때문에 새로운 색다른 조류들을 관찰할 수 있는 좋은 기회가 됩니다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 2.41s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 보통 봄과 가을에 겹치는 시기에 해당합니다. 이러한 기간은 주로 4월부터 5월 중순, 그리고 9월부터 10월 중순까지입니다. 이 기간 동안 국내에는 기온이 급격히 변하고 바람이 강하게 부는 등 비교적 불안정한 날씨가 나타날 수 있습니다.
두 번째 요청에 대한 처리가 금방 끝난 걸로 보이지만 답변의 길이가 작다. 이게 영향이 있었을 것이다.
일치 캐시가 있을 때
일치 캐시는 말 그대로 요청이 똑같아야 한다. 요청을 키로 하는 딕셔너리에 그저 저장하는 것이기 때문이다. 그래서 구현이 간단하다. 딕셔너리로 구현한 캐시에 해당 요청이 있으면 그걸 쓰고, 아닌 경우에는 새롭게 답변을 생성한다. 이 방식은 구현이 간단하지만 한 글자라도 틀리면 전혀 의미가 없다.
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(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.59s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 봄까지인 10월부터 4월까지입니다. 이 기간 동안 한반도와 일본, 중국 등 주변 지역은 추운 날씨와 바람이 강하게 불면서 추위를 겪게 됩니다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 0.00s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 봄까지인 10월부터 4월까지입니다. 이 기간 동안 한반도와 일본, 중국 등 주변 지역은 추운 날씨와 바람이 강하게 불면서 추위를 겪게 됩니다.
유사 검색 캐시가 있을 때
이제 비슷한 요청이 들어왔을 때에도 캐시를 활용할 수 있는 유사 검색 캐시를 사용해 보자.
class OpenAICache:
def __init__(self, openai_client, semantic_cache):
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)
# 검색 결과가 존재하고, 검색한 문서와 검색 결과 문서 사이의 유사도가 충분히 작은지(<0.2) 고려한다
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]
작동 로직은 다음과 같다. 먼저 요청이 들어왔을 때 일치 캐시에서 찾아본다. 만약 없다면 이어서 유사 검색 캐시 검색을 위해 질문을 임베딩 벡터로 변환하여 검색한다. 만약 있다면 반환하고, 없다면 새롭게 생성하여 일치 캐시와 유사 검색 캐시에 저장한다.
from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction
# 임베딩으로는 OpenAI의 embedding function을 이용한다
openai_ef = OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-ada-002"
)
# 벡터 데이터베이스로 chroma DB를 이용한다
semantic_cache = chroma_client.create_collection(name="semantic_cache",
embedding_function=openai_ef, metadata={"hnsw:space": "cosine"})
openai_cache = OpenAICache(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')
그렇게 구현한 유사 검색 캐시를 테스트해 보기 위해 코드를 작성해 본다. 첫 번째 질문과 두 번째 질문은 같으니 일치 캐시가 작동할 것이고, 나머지 두 질문에 대해서는 유사 검색 캐시가 작동할 것을 예측해 본다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 4.04s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 겨울까지이며, 보통 10월부터 4월까지의 기간 동안 국내에 영향을 미칩니다. 특히 11월부터 2월 사이에 기단의 영향을 가장 크게 받는 것으로 알려져 있습니다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은?
소요 시간: 0.00s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 겨울까지이며, 보통 10월부터 4월까지의 기간 동안 국내에 영향을 미칩니다. 특히 11월부터 2월 사이에 기단의 영향을 가장 크게 받는 것으로 알려져 있습니다.
질문: 북태평양 기단과 오호츠크해 기단이 만나 한반도에 머무르는 기간은?
소요 시간: 0.42s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 겨울까지이며, 보통 10월부터 4월까지의 기간 동안 국내에 영향을 미칩니다. 특히 11월부터 2월 사이에 기단의 영향을 가장 크게 받는 것으로 알려져 있습니다.
질문: 국내에 북태평양 기단과 오호츠크해 기단이 함께 머무리는 기간은?
소요 시간: 0.60s
답변: 북태평양 기단과 오호츠크해 기단이 만나 국내에 머무르는 기간은 주로 가을부터 겨울까지이며, 보통 10월부터 4월까지의 기간 동안 국내에 영향을 미칩니다. 특히 11월부터 2월 사이에 기단의 영향을 가장 크게 받는 것으로 알려져 있습니다.
예측대로 새로운 생성은 맨 처음 요청에만 발생하였다. 이어서 일치 캐시, 그리고 나머지 두 번은 유사 검색 캐시에 의해 찾은 답변을 반환함을 확인할 수 있다.
'AI' 카테고리의 다른 글
[LLM] 임베딩 모델로 데이터 의미 압축하기 (1) - 텍스트 임베딩 이해하기 (0) | 2024.11.16 |
---|---|
[LLM] RAG로 Hallucination 방지하기 with Llama-index (7) | 2024.11.12 |
[LLM] 트랜스포머 구조 파헤치기 (4) - 인코더와 디코더 (0) | 2024.11.08 |
[LLM] 트랜스포머 구조 파헤치기 (3) - 정규화와 피드 포워드 층 (0) | 2024.11.07 |
[LLM] 트랜스포머 구조 파헤치기 (2) - 어텐션 (Attention) 이해하기 (2) | 2024.11.04 |