FastApi ํ๋ ์์ํฌ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ LLM ์๋น์ค๋ฅผ ๊ตฌํํ๋ ๋์ค, LLM์ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ด๋ ๊ณผ์ ์ ์์ด,
์ปจํ ์คํธ(๋ํ ๋ฌธ๋งฅ, ํ๋ฆ ~ ์ฌ๊ธฐ์๋ LLM์ด ๊ธฐ์กด์ ์ฌ์ฉ์์ ๋ํํ ๋ด์ฉ์ ๊ธฐ์ตํ๊ธฐ ์ํด ํ์ํ๋ค) ๊ด๋ฆฌ์ ํ์์ฑ์ ๋๋ผ๊ฒ ๋์๋ค
* ์ Redis๋ฅผ ์ฌ์ฉํด์ผ ํ ๊น?
1. ๋น ๋ฅธ ์กฐํ ์๋ : Redis๋ ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก์, ms ๋จ์ ์๋ต์ ๋ณด์ฅํ๊ธฐ ๋๋ฌธ์, ๊ฐ๋ณ๊ณ ๋น ๋ฅด๊ฒ ์กฐํ๊ฐ ๊ฐ๋ฅํ๋ค
2. TTL : Redis ์์ฒด์ ์ผ๋ก ์ ๊ณตํ๋ TTL ๊ธฐ๋ฅ์ ํตํด, ์ด๋ ฅ์ ๋ํด ์ผ์ ์ฃผ๊ธฐ๋ก ์ญ์ ๋ฅผ ์งํํด, ์ด๋ ฅ์ ๋ํ ์ฉ๋ ๋ถ๋ด์ ๋ ์ ์๋ค
3. (์ถํ ๊ตฌํ ํด๋ณผ ์์ ) ๋ฒกํฐ DB ์ญํ ์ ๋ณด๋ค ํนํ๋์ด, RAG๋ฅผ ์ํ ์๋งจํฑ ์บ์ฑ์ ํ์ฉ๋๋ค
* ๋น๊ตํด๋ณผ๋งํ ๋ค๋ฅธ DB
1) PostgreSQL : ์ด๋ ฅ ๋ฐ์ดํฐ์ ์๊ตฌ ์ ์ฅ๊ณผ ๋ฒกํฐ DB ํ์ฉ์ด ๊ฐ๋ฅ. ํธ๋์ ์ ์ ์์์ฑ์ ๋ณด์ฅํ๋ค. Redis๋ณด๋ค ์ฝ๊ธฐ/ ์ฐ๊ธฐ๊ฐ ๋๋ฆฌ๋ค
2) MongoDB : JSON ๊ตฌ์กฐ์ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋๋ก ์ ์ฅํ ๋ ์ ์ฐํ๊ฒ ์ ์ฅ ๊ฐ๋ฅ. Redis์ ๋น๊ตํด ์ง์ฐ์๊ฐ์ด ๋ฐ์ํ๋ค
FAST API ๊ธฐ๋ฐ ์๋น์ค์์ ๊ตฌํํ๊ธฐ
0. main.py
@asynccontextmanager
async def lifespan(app: FastAPI):
pool = aioredis.ConnectionPool.from_url(REDIS_URL, decode_responses=True)
_redis = aioredis.Redis(connection_pool=pool)
app.state.redis_client = _redis
try:
prompt_res = await api_client.get_prompt_list()
if prompt_res:
for prompt in prompt_res:
role_key = f"{prompt['serviceType']}:role"
await _redis.set(role_key, prompt['promptRole'])
text_key = f"{prompt['serviceType']}:text"
await _redis.set(text_key, prompt['promptText'])
yield
finally:
await _redis.close()
await pool.disconnect()
๋ค๋ฅธ DB์ ์ ์ฅ๋์ด ์๋ ํ๋กฌํํธ์ ๊ฐ์ ๊ฐ์ ธ์ redis์ ์ ์ฅํ ํ, ์ฌ์ฉ์ ์์ฒญ ์์ redis์ ํ๋กฌํํธ๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๊ธฐ ์ํ ์ด๊ธฐ ์ธํ ์ฝ๋๋ค
@asynccontextmanager
์๋น์ค์ ์๋ช ์ฃผ๊ธฐ์ ๋๊ธฐํ๋ ๋ก์ง์ ์ ์ํ๊ธฐ ์ํ ์ด๋ ธํ ์ด์ ์ด๋ค. ์ด๋ฅผ ํตํด ์๋น์ค ๊ธฐ๋์ ํด๋น ๋ก์ง์ ์คํํ๊ณ , ์ข ๋ฃ์ ์คํํ ์ฝ๋๋ ์ถ๊ฐํ ์ ์๋ค
app = FastAPI(
lifespan= lifespan
)
์ด๋ ๊ฒ ์ฌ์ฉ ๊ฐ๋ฅํ๋ค
1. context์ ํ์ฉ
class class:
def __init__(self, redis_client, api_client):
self.redis_client = redis_client
self.ttl = SESSION_TTL
self.max_history = MAX_HISTORY
1-0) ํ๋กฌํํธ
async def get_prompt(self, service_type: str, prompt_key: str) -> str:
return await self.redis_client.get(f"{service_type}:{prompt_key}") or ""
์์ ๊ฐ์ด ํด๋์ค์ redis๋ฅผ ์ฐ๊ฒฐํด๋ ๋ค์, ๋ฐ์์ ํ์ํ ๋๋ง๋ค ํธ์ถํ๋ ๋ฐฉ์์ด๋ค
1-1) ์ปจํ ์คํธ ๊ด๋ฆฌ
async def get_context_history(self, conversation_id: str) -> List[Any]:
await self._ensure_redis_loaded(conversation_id)
chat_key = f"{conversation_id}"
raw_data = await self.redis_client.lrange(chat_key, 0, -1)
if not raw_data:
return []
messages = self._parse_history(raw_data)
return messages
sync def _ensure_redis_loaded(self, conversation_id: str) -> bool:
"""Redis์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด RDB์์ ๋ก๋ + ์บ์ฑ. ๋ฐ์ดํฐ ์กด์ฌ ์ฌ๋ถ ๋ฐํ"""
chat_key = f"{conversation_id}"
exists = await self.redis_client.llen(chat_key) > 0
if exists:
return True
rdb_history = await self._fetch_history_from_rdb(conversation_id)
if not rdb_history:
return False
await self._cache_history_to_redis(chat_key, rdb_history)
return True
์ธ๋ถ์์ ์ปจํ ์คํธ๋ฅผ ์กฐํํ๊ณ ๋ด๋ถ์์ ์ปจํ ์คํธ ์กฐํ ๋ฐ ์บ์ฑํ๋ ๋ก์ง์ผ๋ก, _ensure_redis_loaded์ ๊ฐ ๋ํ์ ID๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ํด redis์ ์ปจํ ์คํธ๊ฐ ์ ์ฅ๋์ด ์๋์ง๋ฅผ ์กฐํํ๊ณ , ์๋ ๊ฒฝ์ฐ์ ์ฅ๊ธฐ ์ ์ฅ๋ DB์์ ๋ถ๋ฌ์ history์ ์ฐ์์ฑ์ ๊ฐ์ ธ์ฌ ์ ์๊ฒ ๊ตฌํํ๋ค
def build_user_prompt(self, context_history: List[dict], query: str) -> str:
if not context_history:
return query
context_lines = []
for msg in context_history:
role = msg.get("role", "user")
content = msg.get("content", "")
label = "์ฌ์ฉ์" if role == "user" else "์ด์์คํดํธ"
context_lines.append(f"{label}: {content}")
context_text = "\n".join(context_lines)
return f"[์ด์ ๋ํ]\n{context_text}\n\n[ํ์ฌ ์
๋ ฅ]\n{query}"
๊ทธ๋ ๊ฒ ๊ฐ์ ธ์จ ์ปจํ ์คํธ์ ์๋ก ์ ๋ ฅ๋ query๋ฅผ ๊ฐ์ง๊ณ ์ฌ์ฉ์์ ์ฐ์๋ ๋ํ๋ฅผ ์ด์ด๊ฐ ์ ์๋๋ก ๋ง๋ค์ด์ฃผ๋ ๋ก์ง์ด๋ค
2. ๋ง์น๋ฉฐ
์ด ๊ตฌ์กฐ๋ฅผ ํตํด ์ปจํ ์คํธ ๊ด๋ฆฌ๋ฅผ ๋น ๋ฅด๊ฒ ์ฒ๋ฆฌํ ์ ์๋ค