너무 많은 것을 기억하게 되자 나의 로컬 AI 어시스턴트가 더 나빠졌다
요약
로컬 소형 모델을 활용한 AI 어시스턴트 구축 시 발생하는 메모리 오염 문제를 해결하는 방법을 다룹니다. 대화 기록을 단기 상태와 정제된 장기 사실로 분리하여 모델의 성능 저하를 방지하는 전략을 제시합니다.
핵심 포인트
- 대화 기록 누적으로 인한 소형 모델의 성능 저하 및 반복 현상 해결
- 메모리를 단기 대화 상태와 정제된 장기 사용자 사실로 이원화
- 장기 기억은 채팅 로그가 아닌 선호도 등 정제된 사실(facts)로 관리
- 사실 추출 작업은 백그라운드 스레드와 고성능 모델을 사용하여 효율화
너무 많은 것을 기억하게 되자 나의 로컬 AI 어시스턴트가 더 나빠졌다
지난주에 개인용 AI 어시스턴트를 작은 로컬 모델(local model)로 옮겼는데, 즉시 지루한 문제에 직면했습니다. 모델은 괜찮았지만, 저의 메모리 레이어(memory layer)가 문제였습니다.
이전 버전은 가공되지 않은 대화 기록(raw conversation history)을 유지하고 이를 프롬프트(prompt)에 다시 재생했습니다. 호스팅된 모델(hosted models)을 사용할 때는 이 방식이 충분히 잘 작동했습니다. 그러다 저는 동일한 앱을 Mac mini에서 Swama를 통해 Qwen3-4B-4bit를 실행하는 로컬 OpenAI 호환 서버(OpenAI-compatible server)로 연결했습니다.
196개의 메시지가 누적된 후, 어시스턴트는 전형적인 소형 모델의 실패 모드(failure mode)를 보이기 시작했습니다. 자신의 이전 답변을 그대로 따라 하거나(parroting), 오래된 컨텍스트(stale context)에 과도한 가중치를 두었으며, 제가 더 많은 "기억"을 줄수록 유용성이 떨어지는 것처럼 들렸습니다.
해결책은 벡터 데이터베이스(vector database)가 아니었습니다. 대부분의 기억을 삭제하는 것이었습니다.
저는 기억을 두 가지 서로 다른 것으로 나누었습니다:
- 단기 대화 상태 (short-term conversation state)
- 장기 사용자 사실 (long-term user facts)
단기 기록은 이제 RAM에만 유지됩니다. 유휴 시간(idle gap) 이후에는 초기화되며, 마라톤 세션이 향후 모든 턴(turn)을 오염시키지 않도록 엄격한 제한(hard cap)을 두었습니다.
self.session_memory: Dict[str, ConversationBufferMemory] = {}
self._last_activity: Dict[str, float] = {}
...
장기 기억은 채팅 로그가 아닙니다. 그것은 선호도, 인물, 장치, 반복되는 활동 등과 같이 정제된 사실(distilled facts)의 작은 목록입니다. 사용자당 최대 30개의 사실만 유지합니다.
_FACT_EXTRACTION_PROMPT = """
Update the fact list. Add only stable, personal facts worth remembering across
conversations: preferences, interests, people, pets, places, devices, recurring
...
사실 추출(fact extraction)은 매 교환 후에 백그라운드 스레드(background thread)에서 실행됩니다. 채팅 경로(chat path)가 메모리 정리(memory housekeeping)를 기다려서는 안 되기 때문입니다.
threading.Thread(
target=self._extract_facts,
args=(user_id, msgs[-2].content, msgs[-1].content),
...
또한 저는 증류 (distillation) 단계에서 의도적으로 호스팅된 모델 (hosted model)을 사용합니다. 로컬 4B 모델은 빠른 상호작용을 하기에 충분히 훌륭하지만, 장기 기억 정리 (long-term memory cleanup)는 지연 시간 (latency)보다 품질이 더 중요한 영역 중 하나입니다. 어차피 응답 경로 (response path) 밖에 있는 작업이기도 합니다.
또 다른 로컬 모델의 미세 조정 (tweak) 사항은 도구 편향 (tool bias)이었습니다. 작은 모델들은 도구가 존재하더라도, 특히 시스템 프롬프트에 "먼저 당신의 지식을 사용하세요"와 같은 내용이 포함되어 있다면 오래된 가중치 (stale weights)로부터 답변할 가능성이 훨씬 높습니다. 그래서 Swama 핸들러는 실시간 데이터를 위해 투박한 오버라이드 (override)를 추가합니다:
_TOOL_BIEND = (
" IMPORTANT OVERRIDE: 현재 발생하고 있는 모든 일에 대해 - 날씨, 바다 또는"
" 카이트서핑 조건, 기기/홈 상태, 가격, 뉴스, 실시간 데이터"
...```
Qwen3는 요청하지 않았음에도 불구하고 도구 호출 (tool calls) 이후 중간 스트림에서 `<think>` 블록을 출력하기도 하므로, 스트리밍 핸들러가 이러한 태그들을 점진적으로 제거합니다. 화려하지는 않지만, 음성/채팅 UI에 가공되지 않은 추론 마크업 (raw reasoning markup)이 유출되는 것을 원치 않는다면 반드시 필요한 작업입니다.
유용한 교훈은 이것이었습니다:
기억은 "더 많은 이전 토큰 (previous tokens)"이 아닙니다.
개인용 어시스턴트에게 있어, 가공되지 않은 전사 기록 (raw transcript)을 재생하는 것은 구축하기에는 가장 저렴하지만 시스템을 악화시키는 가장 쉬운 방법 중 하나입니다. 어시스턴트에게는 현재 대화를 유지하기 위한 충분한 최근 컨텍스트 (context)와 더불어, 세션 전반에 걸쳐 유지되는 소수의 안정적인 사실 (stable facts) 세트가 필요합니다.
그 외의 모든 것은 그럴듯한 이름을 붙인 프롬프트 오염 (prompt pollution)일 뿐입니다.
**출처:** 최근 개인용 어시스턴트 백엔드 작업: Swama 로컬 모델 지원, OpenAI 호환 엔드포인트를 통한 Qwen3-4B-4bit, RAM 전용 세션 기록, 2시간 유휴 초기화, 200개 메시지 제한, 백그라운드 사실 추출, 30개의 저장된 사용자 사실.
**태그:** ai, python, llm, devops
**상태:** 게시됨
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기