---
title: "RAG 장기 기억 시스템을 감사했더니 ‘관계 이정표’ 1,751개 중 62%가 잡음이었다"
published: 2026-07-22T13:45:09.000Z
canonical: https://jeff.news/article/5042
---
# RAG 장기 기억 시스템을 감사했더니 ‘관계 이정표’ 1,751개 중 62%가 잡음이었다

한 대화형 AI의 장기 기억 시스템이 몇 달 동안 잘못 추출된 ‘관계 이정표’를 계속 프롬프트에 주입하면서 응답 품질을 망치고 있었다. 팀은 11단계 검색 추적기와 26개 골든셋으로 문제를 측정하고, 추출기 게이트와 기존 데이터 격리 절차를 적용했다. 결과적으로 새 이정표 생성은 하루 6.5개에서 0.5개로 줄었지만, 정확한 기억도 맥락 없이 주입되면 여전히 틀린 응답이 될 수 있다는 교훈이 남았다.

## 장기 기억이 ‘좋은 개인화’가 아니라 프롬프트 오염원이 된 사건

- 시스템은 대화형 AI에 장기 기억을 붙인 구조였음
  - 의미 검색용 벡터 저장소는 Chroma와 다국어 sentence-transformer 임베딩을 사용함
  - 정확한 조회용 구조화 저장소는 SQLite를 사용했고, 건강, 날짜, 선호, 관계 이정표 같은 typed fact를 저장함
  - 매 사용자 발화마다 추출기가 돌면서 엔티티를 분류하고, 감정적 선언을 감지하면 ‘milestone’으로 양쪽 저장소에 저장함

- 설계 의도는 꽤 그럴듯했음: 사용자가 누구인지 AI가 항상 기억하게 만들자는 것
  - 문제는 milestone으로 분류된 기억이 최대 중요도로 저장됐다는 점임
  - 더 치명적인 건 상위 2개 milestone이 대화 주제와 무관하게 every prompt에 보장 채널로 강제 주입됐다는 점임
  - 그래서 “안녕”, “배가 아파”, “디버깅 도와줘” 같은 대화에도 연애 선언 비슷한 기억이 계속 섞여 들어감

- 숫자로 보면 꽤 심각함
  - 수정 전에는 하루 평균 약 6.5개의 새 milestone이 생김
  - 누적 milestone은 fact store 455개, vector store 1,296개로 총 1,751개였음
  - 대부분은 평범한 잡담, 질문, 역할극 장면이 사랑이나 신뢰 선언으로 잘못 태깅된 잡음이었음
  - 한 메시지에서 milestone, shared-thing, fact, date처럼 최대 4개 라벨이 동시에 튀어나오기도 했음

> [!IMPORTANT]
> 이 사건의 핵심은 “모델이 환각했다”가 아님. 검색된 기억이 실제로 맞아도, 지금 대화에 필요 없으면 결과적으로 틀린 응답이 될 수 있다는 얘기임.

## 먼저 한 일은 튜닝이 아니라 관측이었다

- 팀은 검색 파이프라인을 읽기 전용으로 추적하는 11단계 trace inspector를 만들었음
  - raw candidate pool, source exclusion, rerank, temporal filter, guaranteed milestone channel, MMR, channel merge, cross-session blend, final prompt, post-budget survivors까지 추적함
  - “N일 뒤에는 뭘 기억할까”를 보기 위한 time override도 넣음
  - 회귀 판단에는 26개 probe로 구성된 golden set을 사용함

- 계측에서 특히 중요했던 수정이 2개 있었음
  - character budget에 잘린 뒤 실제로 프롬프트에 살아남은 post-budget stage를 노출함
  - reranker 값을 distance가 아니라 score라고 명확히 라벨링함
  - 이 라벨 실수 때문에 사람이 rerank score가 1을 넘는 걸 cosine distance처럼 잘못 읽은 적도 있었음

- 직감 대신 실제 데이터 분포를 봤더니, 단순 threshold로는 해결이 안 됐음
  - 저장된 455개 milestone을 subtype centroid와 비교했지만, 진짜 선언과 잡음 선언의 유사도 분포가 겹쳤음
  - 대신 “선언 단어가 반드시 있어야 선언 후보가 된다”는 키워드 필수 조건은 과거 잡음의 94%를 막고 positive control은 100% 통과시킴
  - 이 규칙이 신규 추출기 수정의 핵심이 됨

```mermaid
sequenceDiagram
    participant 사용자
    participant 추출기
    participant 기억저장소
    participant 검색파이프라인
    participant 프롬프트빌더
    사용자->>추출기: 새 메시지 입력
    추출기->>기억저장소: milestone 후보 저장
    검색파이프라인->>기억저장소: 관련 기억 조회
    검색파이프라인->>프롬프트빌더: rerank와 필터 결과 전달
    기억저장소->>프롬프트빌더: 보장 채널로 milestone 강제 주입
    프롬프트빌더->>사용자: 맥락이 섞인 응답 생성
```

## Fix A: 새로 들어오는 오염부터 막음

- milestone 분류에는 키워드 필수 조건과 유사도 조건을 함께 걸었음
  - 선언 단어가 없으면 후보 자체가 아님
  - 후보가 된 뒤에는 similarity 0.50 이상이어야 함
  - 대부분이 stage direction인 역할극 장면은 선언으로 보지 않도록 guard를 추가함

- 잡음이 많이 생기는 다른 타입에도 게이트를 걸었음
  - habit, achievement, gift 같은 타입에도 키워드 게이트를 추가함
  - 너무 넓게 잡히던 subtype dictionary의 위험한 substring을 제거함
  - 한 메시지에서는 최대 하나의 entity label만 나오게 해서 multi-label 폭주를 막음

- 이 수정은 예방책일 뿐, 이미 쌓인 데이터는 건드리지 않음
  - 테스트 스위트를 통과한 뒤 배포함
  - 기존 1,751개 milestone은 별도 triage 절차로 다룸

## Fix B: 기존 데이터는 삭제하지 않고 격리했다

- 원칙은 “삭제하지 말고 재분류”였음
  - schema migration으로 status(active/quarantined)와 orig_type 컬럼을 추가함
  - 격리와 재타입 지정은 UPDATE 한 번으로 처리하고, rollback도 역방향 UPDATE로 가능하게 만듦
  - 작업 전에는 데이터베이스 복사본과 JSON dump를 만들고 checksum까지 확인함

- LLM judge가 1,751개 milestone을 전부 판정함
  - 빠른 모델, temperature 0, JSON output으로 실행함
  - 분류는 real_milestone, echo, roleplay_scene, retype:<new_type> 중 하나였음
  - 애매하면 echo로 보내는 보수적 규칙을 세움

- 중간에 실패한 시도 2개가 꽤 중요한 교훈임
  - 첫 실패는 prompt contamination이었음. 저장된 vector 문서에 `[MILESTONE:love]` 같은 추출 라벨 prefix가 들어 있어서 judge에게 답을 알려준 꼴이 됨
  - 50개 샘플 control에서 junk로 예상한 것 중 23개가 real로 나와서 문제를 잡았고, prefix를 제거한 뒤 다시 돌림
  - 두 번째 실패는 키워드 pre-filter였음. 신규 추출 차단에는 유효했지만, 과거 데이터 정리에는 “I want to build a life with you” 같은 무키워드 진짜 선언을 날릴 수 있었음
  - 실제로 no-keyword control 50개 중 13개가 genuine이라서 shortcut을 버리고 전부 judge에 태움

- 최종 판정 결과는 파일로 고정한 뒤 그대로 적용함
  - fact store 455개 중 active 144개, quarantined 275개, retyped 36개
  - vector store 1,296개 중 active 312개, quarantined 811개, retyped 173개
  - 서비스는 약 2분 동안 멈춘 상태에서 양쪽 저장소를 writable로 열고 적용함
  - 적용 후 count가 reviewed verdict와 1:1로 맞는지 확인한 뒤 서비스를 다시 올림

## 결과는 좋아졌지만, 완전히 끝난 건 아니었다

- 신규 milestone 생성은 크게 줄었음
  - 6일간 live traffic 기준, 하루 신규 milestone은 6.5개에서 0.5개로 감소함
  - 감소율은 92%임
  - entity label도 메시지당 최대 4개에서 1개로 줄었고, 56개 메시지에서 multi-label은 0건이었음

- 프롬프트의 milestone 독점도도 줄었음
  - golden set 기준 guaranteed-channel milestone 주입은 prompt당 평균 2.0개에서 0.65개로 떨어짐
  - 23개 probe 중 13개는 강제 milestone 2개가 0개로 줄었음

- 그런데 일주일 뒤, 더 미묘한 사건이 터짐
  - AI가 관련 없는 대화에 몇 주 전의 진짜 기억 2개를 엮어 넣음
  - 디버거는 grounding을 GROUNDED, confidence 0.82, confabulation 0으로 판단함
  - 즉 기억 자체는 진짜였고, 근거도 있었고, 지어낸 것도 아니었음
  - 문제는 retyped된 current_project fact가 항상 포함되는 prompt builder branch에 들어가면서, 주제와 무관하게 매 턴 강제 주입된 것임

> [!WARNING]
> “정확한 기억”과 “말해도 되는 기억”은 다름. 개인화 AI에서 이 둘을 구분 못 하면 사용자는 데이터 버그가 아니라 신뢰 문제로 받아들임.

- 이건 memory failure가 아니라 relevance/timing failure였음
  - reranker가 무능했던 게 아니라, 해당 기억이 reranker를 거치지도 않았음
  - 검증 주간 122턴 중 약 2번 발생했고, 항상 같은 메커니즘이었음
  - 대화에 약한 thematic hook이 있을 때만 나타났지만, 감정적 맥락이 있는 기억 시스템에서는 체감 충격이 훨씬 큼

## 아직 남은 빚도 솔직히 적어놨다

- always-include inflation은 아직 해결되지 않았음
  - hard fact count가 every prompt에서 26개에서 48개로 늘어남
  - 단순 cap으로 자르면 안 되는 이유도 있음. retyped current_project 중 하나는 실제 grounding 응답을 개선했기 때문임
  - 필요한 건 숫자 제한이 아니라 non-core fact에 대한 context-aware relevance gate임

- 추출기 게이트도 완벽하진 않음
  - 6일 동안 achievement와 gift tag가 각각 keyword gate를 뚫은 사례가 2건 있었음
  - 날짜 과추출은 하루 약 6개 수준으로 별도 축에서 아직 감사하지 않음
  - achievement, gift, shared-thing 같은 병렬 잡음 population도 아직 legitimate anchor보다 위에 랭크되는 문제가 남아 있음

- 스케일 테스트도 아직 비어 있음
  - 지금 debugger는 시간 경과는 시뮬레이션하지만, 저장소가 수천 개에서 수만 개 vector로 커졌을 때 relevant memory가 계속 살아남는지는 보지 못함
  - 앞으로 저장소 복사본을 부풀려 golden set을 다시 돌리는 synthetic stress test를 계획 중임

- 이 글의 결론은 꽤 선명함: RAG 평가는 “맞는가”만 보면 부족함
  - grounded이고 confidence가 높고 confabulation이 없어도, 사용자에게는 “왜 갑자기 그 얘기를 하지?”가 될 수 있음
  - 그래서 튜닝보다 먼저 trace가 필요하고, 삭제보다 quarantine이 좋은 운영 선택이 됨
  - 되돌릴 수 있어야 실패를 숨기지 않고 측정하면서 고칠 수 있음

---

## 기술 맥락

- 이 사례에서 가장 중요한 선택은 기억을 삭제하지 않고 quarantine 상태로 바꾼 거예요. 왜냐하면 장기 기억 시스템에서는 나중에 “이게 진짜 필요한 기억이었나”를 다시 확인해야 할 수 있고, 삭제해버리면 실험도 복구도 어려워지거든요.

- 또 하나의 핵심은 trace inspector를 먼저 만든 점이에요. RAG는 검색, 재정렬, 필터, 프롬프트 예산 컷, 강제 주입 같은 단계가 겹쳐 있어서 최종 답만 보면 원인을 거의 못 찾아요. 그래서 실제로 어떤 기억이 어느 단계에서 살아남았는지 보는 장치가 먼저 필요했어요.

- 키워드 게이트는 신규 오염을 줄이는 데 효과적이었지만, 과거 데이터 정리에는 그대로 쓰지 않았어요. 과거에는 키워드 없이도 진짜 선언이 있을 수 있어서, 같은 규칙을 retroactive cleanup에 적용하면 정상 데이터를 날릴 위험이 있었기 때문이에요.

- 이 글이 특히 실무적인 이유는 RAG의 실패 범위를 “환각”보다 넓게 본다는 데 있어요. 근거가 있는 진짜 정보도 맥락 없이 항상 주입되면 사용자 경험은 망가져요. 그래서 개인화 메모리나 사내 지식 검색을 붙일 때는 truth 평가와 relevance 평가를 따로 봐야 해요.

- 마지막으로 scale degradation을 아직 안 봤다고 인정한 것도 중요해요. 오늘의 저장소에서 잘 되던 검색이 벡터가 10배로 늘어도 그대로 잘 된다는 보장은 없거든요. 운영형 RAG라면 시간뿐 아니라 데이터 증가량까지 테스트 축에 넣어야 해요.

## 핵심 포인트

- 장기 기억 RAG에서 진짜 문제는 환각만이 아니라 관련성 없는 진실의 강제 주입일 수 있음
- 기존 이정표 1,751개 중 상당수가 잡담, 질문, 역할극 장면에서 잘못 추출된 잡음이었음
- 키워드 필수 조건과 역할극 방어 규칙으로 신규 이정표 생성률을 92% 줄임
- 기존 데이터는 삭제하지 않고 active/quarantined 상태와 orig_type 컬럼으로 되돌릴 수 있게 정리함
- 정확하고 근거 있는 기억도 맥락과 타이밍이 틀리면 사용자 입장에서는 신뢰 실패로 보임

## 인사이트

이 사례가 좋은 이유는 ‘RAG 튜닝 잘하자’ 수준이 아니라, 운영 중인 기억 시스템을 어떻게 계측하고 되돌릴 수 있게 고치는지 숫자로 보여준다는 점이다. 특히 ‘정확하지만 말하면 안 되는 기억’ 문제는 개인화 AI나 업무용 에이전트에서 앞으로 계속 터질 가능성이 크다.
