LLM 앱에 Persistent Memory 추가하는 방법 (Fine-Tuning 없이) — 실용적인 아키텍처 가이드
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (or the portion you want translated) here? I’ll keep the source line unchanged and preserve all formatting, code blocks, and URLs as requested.
대부분의 LLM 앱은 데모에서 완벽하게 작동합니다
프롬프트를 보냅니다.
똑똑한 응답을 받습니다.
모두가 감탄합니다.
그런데 사용자가 다음 날 다시 찾아오면 — 시스템이 모든 것을 잊어버립니다.
그것은 모델 문제가 아닙니다.
아키텍처 문제입니다.
이 가이드에서는 파인튜닝 없이 LLM 앱에 영구 메모리를 추가하는 방법을 실용적이고 프로덕션에 바로 적용 가능한 방식으로 살펴봅니다. 사용 기술은 다음과 같습니다:
- Node.js
- OpenAI API
- Redis (구조화된 메모리를 위해)
- 의미 검색을 위한 벡터 스토어
이 패턴은 SaaS 도구, AI 어시스턴트, 혹은 도메인‑특화 LLM 앱을 구축할 때 모두 적용할 수 있습니다.
Why LLMs Are Stateless by Default
Large Language Models (LLMs) are stateless.
They only know what you send them inside the current prompt. Once the request is complete, that context is gone unless you store it somewhere.
Common mistakes I see
- Stuffing the entire chat history into every prompt
- Relying purely on RAG (Retrieval‑Augmented Generation)
- Assuming embeddings = memory
They’re not the same thing. Persistent memory requires architecture, not just prompt engineering.
왜 LLM은 기본적으로 무상태인가
대형 언어 모델(LLM)은 무상태입니다.
현재 프롬프트 안에 전달한 내용만을 알며, 요청이 완료되면 그 컨텍스트는 사라집니다(별도로 저장하지 않는 한).
내가 자주 보는 실수
- 모든 프롬프트에 전체 채팅 기록을 넣는 것
- RAG(검색 기반 생성)에만 의존하는 것
- 임베딩 = 메모리 라고 가정하는 것
이들은 동일하지 않습니다. 지속적인 메모리는 아키텍처가 필요하며, 단순히 프롬프트 엔지니어링만으로는 부족합니다.
What “Persistent Memory” Actually Means
우리가 LLM 시스템에서 persistent memory 라고 말할 때 보통 의미하는 것은:
- 시스템이 세션 간 과거 상호작용을 기억한다
- 장기적인 사용자 목표를 이해한다
- 관련된 과거 컨텍스트를 검색할 수 있다
- 시간이 지남에 따라 메모리를 지능적으로 업데이트한다
이를 위해 파인튜닝이 필요하지 않다. 대신 다음이 필요하다:
- 대화 저장소 (데이터베이스)
- 시맨틱 메모리 저장소 (벡터 DB)
- 컨텍스트 빌더 레이어
- 구조화된 아이덴티티 모델
단계별로 구축해 보자.
고수준 아키텍처
User Request
↓
API Layer (Node.js)
↓
Memory Layer
├── Redis (structured memory)
└── Vector DB (semantic retrieval)
↓
Context Builder
↓
LLM (OpenAI API)
↓
Response
↓
Memory Update
핵심 아이디어
- 👉 메모리는 LLM 외부에 존재합니다.
- 👉 LLM은 추론 엔진이 되며, 저장 엔진은 아닙니다.
Source: …
1단계 — 구조화된 메모리 저장 (Redis)
우리는 Redis를 사용해 장기 구조화 사용자 상태를 저장합니다.
의존성 설치
npm install openai redis uuid
기본 Redis 설정 (memory.js)
// memory.js
import { createClient } from "redis";
const redis = createClient({
url: process.env.REDIS_URL
});
await redis.connect();
export async function getUserMemory(userId) {
const data = await redis.get(`user:${userId}:memory`);
return data ? JSON.parse(data) : {};
}
export async function updateUserMemory(userId, memory) {
await redis.set(`user:${userId}:memory`, JSON.stringify(memory));
}
예시 구조화 메모리 객체
{
"goals": ["launch AI SaaS"],
"preferences": ["technical explanations"],
"pastMistakes": ["over‑engineered MVP"],
"summary": "User building an LLM‑based SaaS product."
}
이 접근 방식은 가볍고 빠릅니다.
Step 2 — 의미 메모리 추가 (Vector Store)
구조화된 메모리만으로는 충분하지 않습니다; 다음과 같은 항목에 대해 시맨틱 리콜도 필요합니다:
- 이전 대화
- 중요한 결정
- 장기 메모
Pinecone, Weaviate, Supabase 또는 기타 벡터 DB를 사용할 수 있습니다. 아래는 OpenAI 임베딩을 사용한 간단한 예시입니다.
Embedding helper
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function embedText(text) {
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: text
});
return response.data[0].embedding;
}
Store the embedding with metadata
{
"userId": "123",
"type": "conversation",
"content": "User decided to pivot to B2B SaaS."
}
나중에 프롬프트를 만들 때 상위 k개의 유사한 메모리를 검색합니다.
Note: 많은 LLM 앱이 RAG vs. memory를 혼동합니다.
- RAG는 문서를 검색합니다.
- Memory는 사용자 변화를 검색합니다.
단계 3 — 컨텍스트 어셈블러 구축
사용자가 요청을 보낼 때:
- Redis에서 구조화된 메모리를 로드합니다.
- 벡터 DB에서 관련 의미 메모리를 검색합니다.
- 현재 메시지와 모든 정보를 결합합니다.
- 깔끔한 시스템 프롬프트를 구성합니다.
프롬프트 빌더 예시
function buildPrompt(userMemory, semanticMemories, userInput) {
return `
You are a domain‑specific AI assistant.
User Profile:
${JSON.stringify(userMemory, null, 2)}
Relevant Past Context:
${semanticMemories.join("\n")}
Current Question:
${userInput}
Provide a consistent and context‑aware response.
`;
}
LLM 호출
const systemPrompt = buildPrompt(userMemory, semanticMemories, userInput);
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "system", content: systemPrompt }]
});
이제 LLM이 연속성을 유지합니다.
단계 4 — 메모리를 지능적으로 업데이트하기
응답을 생성한 후, 메모리를 업데이트합니다.
중요 규칙: 모든 것을 저장하지 마세요. 의미 있는 변화를 요약하세요.
간단한 업데이트 도우미
function updateMemoryFromConversation(memory, userInput, response) {
if (userInput.toLowerCase().includes("pivot")) {
memory.summary = "User pivoted business direction.";
}
// Add more heuristics as needed
return memory;
}
업데이트된 메모리 저장
const updatedMemory = updateMemoryFromConversation(
userMemory,
userInput,
completion.choices[0].message.content
);
await updateUserMemory(userId, updatedMemory);
메모리는 진화해야 하며, 단순히 잡음만 쌓여서는 안 됩니다.
실제 시스템에서 발생하는 문제
1. 메모리 드리프트
오래된 목표가 영원히 남는다. 사용자는 방향을 바꾸지만 시스템은 적응하지 않는다.
Solution:
- 오래된 항목에 time‑weight 혹은 decay factor를 적용한다.
- 주기적으로 오래된 데이터를 정리하거나 요약한다.
2. 무한 성장
모든 상호작용을 저장하면 비용이 급격히 증가한다.
Solution:
- most recent N 항목만 보관하거나 top‑k 가장 관련성 높은 임베딩만 유지한다.
- 긴 대화를 간결한 핵심 문장으로 요약한다.
3. 일관성 없는 컨텍스트 포맷팅
프롬프트가 어수선해지면 LLM의 출력 품질이 떨어진다.
Solution:
- template engine(예: Mustache, Handlebars)을 사용해 안정적인 구조를 강제한다.
- API에 보내기 전에 조립된 프롬프트를 검증한다.
4. 지연 시간 오버헤드
Redis + 벡터 DB에서 데이터를 가져오면 눈에 띄는 지연이 발생할 수 있다.
Solution:
- 가장 자주 접근하는 의미 벡터를 메모리에 캐시한다.
- Redis와 벡터‑DB 호출을 병렬화한다.
TL;DR
- Store structured state (Redis). → 구조화된 상태 저장 (Redis).
- Store semantic snippets (vector DB). → 시맨틱 스니펫 저장 (vector DB).
- Assemble a clean prompt from both sources plus the new user message. → 두 소스와 새로운 사용자 메시지를 결합하여 깔끔한 프롬프트를 구성한다.
- Call the LLM (OpenAI). → LLM 호출 (OpenAI).
- Summarize & update memory intelligently. → 메모리를 요약하고 지능적으로 업데이트한다.
With this architecture, your LLM app gains true, persistent memory without ever fine‑tuning the model. Happy building!
# Memory
1. 주기적으로 요약하기
(이 항목에 대한 추가 내용이 제공되지 않았습니다.)
2. 컨텍스트 과부하
검색된 컨텍스트가 너무 많으면 토큰 비용이 증가하고 정확도가 감소합니다.
해결책:
- 의미 검색 제한
- 요약 레이어 사용
3. 정체성 붕괴
시스템 프롬프트가 너무 자주 바뀌면 응답이 일관성을 잃습니다.
해결책:
- 안정적인 정체성 시스템 프롬프트 유지
- 메모리를 대체가 아니라 보강으로 활용
왜 파인‑튜닝이 필요하지 않은가
- 파인‑튜닝은 비용이 많이 들고 경직되어 있다.
- 대부분의 LLM 애플리케이션에서는 구조화된 메모리 + 검색만으로 충분하다.
- 모델의 지능을 바꾸는 것이 아니라 연속성을 향상시키는 것이다.
- 그것은 아키텍처 레이어이며, 모델 레이어가 아니다.
최종 생각
대부분의 개발자는 LLM 메모리를 다음과 같이 해결하려 합니다:
- 더 큰 프롬프트
- 더 나은 프롬프트 엔지니어링
- 더 많은 임베딩
하지만 지속 가능한 AI 시스템은 아키텍처를 통해 구축되며, 해킹이 아닙니다.
데모에서는 똑똑해 보이지만 프로덕션에서는 신뢰성이 떨어지는 AI 앱이라면, 다음 질문부터 시작하세요:
메모리는 어디에 존재하나요?
LLM 내부에 있지 않습니다. 외부에 있습니다.
토론 요청
LLM 앱을 위한 지속 메모리 시스템을 구축하셨다면, 듣고 싶습니다:
- 어떤 스택을 사용했나요?
- 메모리 드리프트 문제를 겪었나요?
- 컨텍스트 스케일링을 어떻게 처리했나요?
함께 논의해요!