기억 오염과 회수 신뢰성 — session_id 격리 하나로는 왜 부족한가
← 전체 목차 · 이전: 안 잊는 Agent는 왜 상용화할 수 없는가 · 다음: "쓰기 성공"과 "읽기 가능" 사이
"세션별로 기억을 나눠 저장하고, 매번 이번 세션 것만 불러온다" — 이 답은 완결돼 보이지만, session_id가 실제로 답하는 질문은 "이 기억이 누구의 어느 대화에 속하는가" 뿐이다. **"이 기억이 지금 이 질문과 관련이 있는가"**는 전혀 답하지 못한다. session_id는 컨테이너 차원이지 의미 차원이 아니며, 이 둘을 하나로 착각하는 순간 오염과 소실이라는 두 종류의 실패가 동시에 열린다.
오염(False Positive)과 소실(False Negative) — 회수가 틀리는 두 방향
| 방향 | 증상 | 왜 session_id로 못 막나 |
|---|
| 오염 — 무관한 기억이 끼어듦 | 같은 세션 안에서 A 작업의 잔여 정보가 B 작업 답변에 섞임 | A와 B가 같은 session_id 아래 있으므로 격리 대상 자체가 아님 |
| 오염 — 다른 세션 정보가 끼어듦 | 벡터 검색이 유사도만으로 판단해 다른 세션의 비슷한 문구를 끌어옴 | 유사도는 의미적으로 가깝다는 뜻이지 소속이 맞다는 뜻이 아님 |
| 소실 — 있어야 할 기억이 안 불려옴 | 대화는 매끄럽게 이어지는데, 창밖(윈도우 밖)의 사실은 조용히 사라짐 | 대화 연속성 ≠ 기억 능력 — 연속성은 단기 기억이 살아있다는 증거일 뿐, 장기 회수가 정상 작동한다는 증거가 아님 |
"오염"과 "소실"은 반대 현상처럼 보이지만 원인이 같다 — 회수 로직이 "관련성"을 판정할 정교한 수단을 갖고 있지 않다는 것.
컨테이너 차원 하나로는 안 되는 이유 — 실전에서 뚫리는 4가지 경로
- 프런트 리셋은 우회 가능 — "세션 전환 시 초기화"는 UI 동작일 뿐이다. 클라이언트가 옛
session_id를 그대로 실어 재호출하면 초기화는 형해화된다. 프런트는 신뢰 경계가 아니다.
- 동일 세션 내 다중 작업 간섭 — 사용자가 한 세션에서 작업 A를 마치고 작업 B로 넘어가면,
session_id는 완전히 정확한데도 A의 잔여 정보가 B의 추론에 섞여 들어간다. session_id가 옳은데도 발생하는 오염이라는 점에서 가장 치명적인 반례다.
- 벡터 검색의 구조적 부작용 — 유사도 검색은 "잘 작동할수록" 비슷한 다른 세션의 조각을 더 잘 찾아온다. 이것은 버그가 아니라 유사도 기반 검색이 정상 작동한 결과이므로, 필터 조건을 검색의 구조적 측(유사도 점수)이 아니라 **결정적 측(소속 태그)**에 둬야 막힌다.
- 핵심 기억 모듈을 고칠 수 없는 레거시 — 이미 배포된 Agent의 저장 로직을 건드릴 수 없을 때, "그럼 방법이 없다"는 답은 성립하지 않는다. 읽기 경로에만 개입하는 우회로가 필요하다.
기준선: 의미 분할 + 경계 상태 머신
session_id 하나짜리 필터를 **소속(session_id) + 관련성(semantic_tag)**의 이중 필터로 바꾼다.
| 필드 | 역할 |
|---|
session_id | 세션·테넌트 단위의 하드 격리 |
block_id | 기억 단위 조각의 주키 |
semantic_tag | 이 조각이 어느 작업/주제에 속하는가 — 검색 시 두 번째 필터 조건 |
weight_level | 우선순위 — 낮으면 자동 정리 대상 |
boundary_mark | 작업 경계 표기 — 상태 전이의 판정 근거 |
검색은 session_id + semantic_tag가 동시에 맞을 때만 회수한다. 동일 세션이라도 다른 작업의 조각은 semantic_tag가 달라 걸러지고, 다른 세션의 비슷한 조각도 session_id가 달라 걸러진다.
기억 조각의 생애는 경계 상태 머신으로 흐른다.
활성 → 보관 → 망각 → 격리
매 상태 전이는 현재 작업의 「의미 경계」가 바뀌었는지를 근거로 판정한다
소실 쪽 대책: 회수 이중 경로
오염이 "잘못된 것을 불러옴"이라면 소실은 "필요한 것을 못 불러옴"이다. 대책은 경로를 하나 더 두는 것이다.
핵심 정보 ──┬─→ 벡터 저장소 (의미 유사도 회수) ← 주 경로, 확률적
└─→ 구조화 DB (사용자 ID·업무 ID 정확 일치) ← 보조 경로, 결정론적
의미 회수가 실패하거나 신뢰도가 낮을 때 → 보조 경로로 전환
→ 핵심 정보만큼은 "못 찾음"이 아니라 "정확히 찾음"을 보장
의미 검색은 본질적으로 확률적이라 반드시 새어나간다. 사용자 ID·업무 식별자 같은 정확 일치 키로 우회할 수 있는 경로를 별도로 두면, 핵심 정보의 소실만큼은 구조적으로 막을 수 있다.
레거시 대응: 기억 대리 계층
핵심 저장 로직을 못 고칠 때는 읽기 경로에만 개입하는 독립 계층을 새로 얹는다. 모든 입출력을 가로채 구조화 상태를 추출하고, 필터링을 거친 뒤 프롬프트에 재조립해 주입한다 — 저장 구조는 건드리지 않는다. 원 시스템에 손대지 못해 "방법이 없다"로 끝나는 것과, 대리 계층으로 관리 능력을 확보하는 것이 실무 경험치가 갈리는 지점이다.
핵심 통찰
- "기억 오염"의 올바른 정의는 "세션이 섞였다"가 아니라 "무관한 이력 정보가 추론 사슬에 들어왔다"이다 — 정의를 컨테이너 기준으로 잡으면 해법도 컨테이너(session_id) 하나로 수렴하고, 그 즉시 동일 세션 내 다중 작업 간섭이라는 가장 흔한 반례에 무력해진다.
- session_id는 "누구 것인가"를 답하고, semantic_tag는 "지금 필요한가"를 답한다 — 서로 다른 질문에 답하는 두 축이므로 하나가 다른 하나를 대체할 수 없다. 검색 필터는 반드시 이 둘의 곱이어야 한다.
- 프런트엔드 상태 초기화는 방어가 아니라 UX다 — 신뢰 경계는 서버 측 결정적 필터에만 있을 수 있다. 클라이언트 동작에 의존한 격리는 의도적이든 아니든 반드시 우회된다.
- 유사도 검색이 잘 작동할수록 교차 오염 위험도 커진다 — 이것은 모순이 아니라 구조다. 그래서 "검색 정확도를 올린다"는 오염 문제의 해법이 아니고, 결정적 필터(소속 태그)를 별도로 두는 것만이 해법이다.
- 대화가 매끄럽다는 것은 단기 기억이 살아있다는 증거일 뿐, 장기 회수가 정상이라는 증거는 아니다 — 이 둘을 혼동하면 회수가 조용히 실패해도 사용자 체감상 "괜찮아 보이는" 구간이 길게 이어지다가 어느 날 갑자기 티가 난다.
- 회수는 이중화해야 신뢰할 수 있다 — 확률적 경로(의미 검색) 하나만 두면 핵심 정보조차 "때때로 못 찾는" 상태를 벗어날 수 없다. 결정론적 경로(정확 키 매칭)를 병행해야 핵심 정보의 소실만큼은 구조적으로 방지된다.
- 레거시 제약은 "포기의 이유"가 아니라 "설계력의 시험대"다 — 저장 로직을 못 고치는 상황은 예외가 아니라 프로덕션의 상수다. 읽기 경로만 가로채는 대리 계층을 설계할 수 있는지가 초급과 실전 경험의 경계선이다.
- 기억의 생애는 상태로 관리해야지 매번 즉흥 판단하면 안 된다 — 활성/보관/망각/격리 네 상태와 그 전이 조건을 미리 정의해 두면, "이 기억을 지금 보여줘도 되는가"가 매 요청마다 새로 판단할 문제가 아니라 조회 가능한 상태값이 된다.
엔지니어링 실전 Tips
- 필터는 언제나 이중으로:
session_id(하드 격리) + semantic_tag(관련성 필터) 둘 다 걸어야 한다. 하나만 걸면 반드시 한쪽 방향으로 뚫린다.
- 경계 표기와 기억 본문은 같은 트랜잭션에 쓸 것: 따로 쓰면 "본문은 있는데 경계 표기가 없는" 고아 조각이 생기고, 상태 머신이 그 조각을 영원히 잘못 판정한다.
- 동시 쓰기 대비 멱등을 걸어둘 것: 같은 조각이 중복 기록될 때 영향 행수가 0이 되도록 유니크 제약을 걸면, 별도 정합성 검사 없이 중복을 즉시 감지할 수 있다.
- 레거시 개조가 불가할 때는 읽기 전용 대리 계층부터: 쓰기 구조를 손대지 않고 필터링·재조립만 담당하게 하면 롤백 비용이 가장 낮다.
- 정확 매칭 경로의 키(사용자 ID·업무 ID)는 쓰기 시점에 반드시 함께 저장할 것 — 사후에는 복원 불가능하다.
- 오염률과 소실률을 별도 지표로 잡을 것: "기억 관련 사고율" 하나로 뭉치면 원인(오염인지 소실인지)을 알 수 없어 튜닝 방향을 못 잡는다.
- 의미 분할 서비스가 죽었을 때의 강등 경로를 미리 정의할 것: 슬라이딩 윈도우 강제 절단 + 키워드 필터 + 표본 점검 같은 저비용 대체 수단을 평시에 준비해 둔다.