NoSQL๐Ÿ—‚๏ธ

LLM ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ w. redis

wannaDevelopIt 2026. 2. 11. 19:58

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. ๋งˆ์น˜๋ฉฐ

์ด ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค