AI가 대화를 기억하는 법 — 세션 태그와 토픽 기반 맥락 유지 알고리즘


LLM은 대화가 끝나면 전부 잊습니다. 다음에 만나면 처음부터 다시. 니콜이라는 AI 에이전트를 만들면서 이걸 어떻게든 해결해야 했고, 세 가지 방법을 만들었습니다.
문제: "저번에 뭐 하기로 했었지?"
사용자가 어젯밤에 이런 대화를 나눴습니다.
K: React 프로젝트 Next.js로 마이그레이션하려고 해. 니콜: Next.js 14의 App Router를 추천해요! K: 매일 진행 상황 공유할게. 잘 기억해줘.
그리고 오늘 아침:
K: 어제 뭐 하기로 했었지? 니콜: ... (모름)
세션이 만료되면 이전 맥락이 날아갑니다. DB에 기록은 있는데, "마이그레이션"으로 검색해도 딱 맞는 대화를 못 찾아요. LLM이 어떤 키워드로 검색해야 하는지를 모르거든요.
알고리즘 1: 세션 태그 가중치
세션이 끝날 때 핵심 주제를 태그로 뽑아 저장하고, 다음 세션이 시작될 때 관련 태그의 요약을 프롬프트에 넣어줍니다.
흐름
[세션 종료] → 경량 LLM에게 대화 전체를 보여줌 → "이 대화의 주제를 명사 1단어로 3~5개 뽑아라" → 예: ["마이그레이션", "Next.js", "React"] → DB 저장: sessions 테이블 (tags, summary) → 태그 가중치 +1.0 [새 세션 시작] → 사용자 첫 메시지: "어제 뭐 하기로 했었지?" → 경량 LLM에게 가중치 상위 20개 태그 목록을 보여줌 → "이 메시지와 관련된 태그를 골라라" → "마이그레이션" 매칭 → "마이그레이션" 태그가 있는 이전 세션 요약을 프롬프트에 주입
태그 가중치 감쇠
매일 weight에 0.95를 곱합니다. 안 나오는 주제는 점점 잊혀지고, 자주 나오는 주제는 살아남습니다.
UPDATE session_tag_weights SET weight = weight * 0.95 WHERE weight > 0.1; DELETE FROM session_tag_weights WHERE weight < 0.1;
2주 동안 안 나온 태그는 weight가 0.1 아래로 떨어져서 삭제됩니다. 하지만 한 번이라도 다시 언급되면 +0.5가 되어 다시 살아납니다.
범용 태그 필터링
"대화", "질문", "답변" 같은 태그는 모든 세션에 해당되니까 쓸모가 없습니다. 추출 단계에서 걸러냅니다.
const GENERIC_TAGS = new Set([ "대화", "질문", "답변", "인사", "채팅", "소통", "응답", "요청", "확인", "문의" ]); const filteredTags = result.tags.filter(t => !GENERIC_TAGS.has(t));
비용
세션당 경량 LLM 2회 호출. 하루 20세션 기준 약 $0.004입니다.
알고리즘 2: 토픽 기반 대화 묶음
세션 태그만으로는 부족합니다. 30분짜리 세션을 하나의 주제로 묶는 건 너무 단순해요. 사람은 한 대화 안에서도 주제를 왔다 갔다 하거든요.
14:55 "Next.js App Router 설정했어" ← 마이그레이션 14:56 "빌드 에러 해결했어" ← 마이그레이션 14:57 "삼성전자 주가 어때?" ← 주식 (주제 전환!) 14:58 "다시 마이그레이션인데, 어디까지?" ← 마이그레이션 (복귀!)
주제 전환 감지
매 메시지마다 경량 LLM에게 묻습니다.
A와 B가 같은 화제인가? A: Next.js App Router 설정했어 / 빌드 에러 해결했어 B: 삼성전자 주가 어때? 같으면 Y, 다르면 N. 한글자만:
LLM이 "N"을 반환하면 주제가 바뀐 것입니다.
여기서 삽질을 좀 했는데, "같음/다름"으로 한글 답변을 요구하면 모델마다 응답이 제각각입니다. Gemini는 "다"만 찍고, GPT는 "다름입니다"라고 길게 쓰고. Y/N 영문 한 글자가 가장 안정적이었습니다.
주제 전환 시 처리
전환이 감지되면 세 가지 작업이 연쇄적으로 일어납니다.
1단계: 이전 토픽 저장
지금까지의 대화 묶음에서 주제 키워드를 추출합니다.
대화 주제를 나타내는 단어를 추출하라. 반드시 띄어쓰기 없는 한 단어. 3~5개. 쉼표 구분. 예시: 다이어트, 체중, 운동, 주식, 날씨
결과: ["마이그레이션", "Next.js"] → DB에 대화 내용과 함께 저장.
2단계: 새 메시지의 키워드 추출
"삼성전자 주가 어때?"에서 키워드를 뽑습니다: ["주식", "주가", "삼성전자"]
3단계: 관련 이전 토픽 검색
새 키워드로 이전 토픽을 검색합니다. 키워드 매칭 점수가 높은 순으로 정렬합니다.
SELECT *, (CASE WHEN '마이그레이션' = ANY(keywords) THEN 1 ELSE 0 END + CASE WHEN 'Next.js' = ANY(keywords) THEN 1 ELSE 0 END) AS score FROM conversation_topics WHERE score > 0 ORDER BY score DESC, started_at DESC LIMIT 3
4단계: 프롬프트에 주입
관련 토픽의 실제 대화 내용을 프롬프트에 넣습니다.
## 이전에 나눈 관련 대화 [주제: 마이그레이션, Next.js] [4/3 21:31] K: React 프로젝트 Next.js로 마이그레이션하려고 해. [4/3 21:31] 니콜: Next.js 14의 App Router를 추천해요! [4/3 21:32] K: pages/ 구조에서 app/으로 어떻게 바꿔? [4/3 21:32] 니콜: 점진적 마이그레이션이 좋아요. pages/는 그대로 두고 app/에 새 라우트부터.
이제 니콜은 "Next.js App Router로 마이그레이션하기로 했었어요"라고 대답할 수 있습니다.
토픽 저장 구조
CREATE TABLE conversation_topics ( id SERIAL PRIMARY KEY, session_id TEXT NOT NULL, keywords TEXT[] NOT NULL, -- ["마이그레이션", "Next.js"] messages JSONB NOT NULL, -- 실제 대화 내용 started_at TIMESTAMPTZ, ended_at TIMESTAMPTZ, message_count INTEGER ); CREATE INDEX idx_topics_keywords ON conversation_topics USING GIN (keywords);
GIN 인덱스로 배열 안의 키워드를 빠르게 검색합니다.
알고리즘 3: 매칭 점수 기반 기억 검색
키워드 검색에는 근본적인 한계가 있습니다. "마이그레이션"으로 검색해도 "App Router 설정 완료"라는 대화가 안 잡혀요. 그 대화에 "마이그레이션"이라는 글자가 없으니까요.
IN 방식 검색
그래서 OR 검색 대신 매칭 점수를 도입했습니다.
SELECT *, (CASE WHEN user_message ILIKE '%다이어트%' THEN 1 ELSE 0 END + CASE WHEN user_message ILIKE '%체중%' THEN 1 ELSE 0 END + CASE WHEN response_text ILIKE '%체중%' THEN 1 ELSE 0 END) AS match_score FROM activities WHERE match_score > 0 ORDER BY match_score DESC, created_at DESC
키워드 3개가 모두 일치하는 레코드가 먼저 올라옵니다. 검색 범위도 user_message만이 아니라 response_text, inner_monologue까지 넓혔고요. 니콜이 응답에서 쓴 단어로도 찾을 수 있게 된 거죠.
토픽 태그 검색과의 결합
그래도 키워드만으로 부족할 때가 있어서, 토픽 태그 검색을 같이 돌립니다.
1. 키워드 "마이그레이션 Next.js"로 activities 검색 → 직접 매칭되는 대화 N개 2. 키워드를 토픽 태그로 검색 → "마이그레이션" 태그가 있는 토픽의 대화 M개 3. 두 결과를 합침 (중복 제거)
2번에서 "마이그레이션" 태그가 있는 토픽에는 "App Router 설정 완료" 대화가 들어있습니다. 이 대화의 텍스트에 "마이그레이션"이 없어도 토픽 태그를 통해 찾을 수 있습니다.
전체 흐름 정리
[매 메시지] 1. 세션 시작인가? → 예: 세션 태그로 이전 세션 요약 주입 2. 주제 전환인가? (이전 2개 대화와 비교) → 예: 이전 토픽 저장 → 새 키워드로 관련 토픽 검색 → 프롬프트 주입 → 아니오: 현재 토픽에 메시지 추가 3. 니콜이 기억을 찾아야 하면? → memory_search 도구 호출 → 매칭 점수 기반 검색 + 토픽 태그 검색 결합 [세션 종료] 4. 마지막 토픽 저장 (fire-and-forget) 5. 세션 태그 + 요약 추출 → DB 저장 6. 태그 가중치 +1.0
비용
| 이벤트 | LLM | 비용 |
|---|---|---|
| 세션 태그 추출 (세션 종료) | nano | ~$0.0001 |
| 세션 태그 매칭 (세션 시작) | nano | ~$0.0001 |
| 주제 전환 감지 (매 메시지) | nano | ~$0.0001 |
| 토픽 키워드 추출 (전환 시) | nano | ~$0.0001 |
| 일 50메시지 기준 | ~$0.006/일 |
하루에 6원 정도. 이 정도면 대화 맥락 유지 비용으로 나쁘지 않습니다.
실제 동작 검증
실제 대화에서 검증한 결과입니다.
[세션 1 - 어젯밤] K: Next.js App Router 설정했어. K: 빌드 에러는 해결했어. → 토픽 저장: [마이그레이션, Next.js] 4개 메시지 K: 삼성전자 주가 어때? → 주제 전환 감지! → 토픽 저장: [주식, 주가, 삼성] 2개 메시지 K: 다시 마이그레이션인데, 어디까지 했었지? → 주제 전환 감지! → 관련 토픽 3개 주입 [마이그레이션, Next.js] → 니콜이 이전 대화 맥락을 알고 응답
주제 전환 감지 정확도는 Y/N 프롬프트 기준으로 테스트 시 10/10이었습니다. 실제 서비스에서도 잘 동작하고 있습니다.
한계와 개선 방향
아직 완벽하진 않습니다.
키워드 추출 품질이 들쭉날쭉합니다. LLM이 "체중감량"처럼 복합어를 만들거나, "내가" 같은 비명사를 뽑는 경우가 있어요. 프롬프트에 "1단어 명사만"이라고 예시까지 넣으면 나아지는데 100%는 아닙니다.
긴 세션에서 짧은 주제를 놓칩니다. 메시지가 65개인 세션에서 주제가 12번 바뀌면, 중간에 끼어든 날씨 질문 1건 같은 건 전환 감지를 못 할 때가 있어요.
동의어를 못 찾습니다. 키워드 매칭이라 "프레임워크"로 검색하면 "라이브러리"는 안 잡힙니다. pgvector가 이미 설치되어 있으니 임베딩 검색으로 확장하면 해결할 수 있을 것 같습니다.
마무리
결국 AI가 기억한다는 건 "타이밍 맞춰서 꺼내오는 것"이지, LLM이 뭔가를 진짜 기억하는 건 아닙니다. 주변 시스템이 맥락을 관리하고 프롬프트에 슬쩍 넣어주는 거죠.
세션 태그는 "뭘 자주 이야기하는지", 토픽은 "어떤 내용이었는지", 매칭 점수는 "정확히 찾기". 이 세 가지가 맞물리면 AI가 대화를 자연스럽게 이어가는 것처럼 느껴집니다. 그렇게 느껴지게 만드는 것이 핵심입니다.





댓글
댓글을 작성하려면 이 필요합니다.