AI 메모리 노이즈 필터링 — 쓸모없는 기억을 걸러내는 3중 방어선
문제: AI가 쓸데없는 것을 기억하느니라
AI 에이전트에게 장기 기억(long-term memory)을 달아주면 신기한 일이 벌어진다. 사용자의 취향, 프로젝트 맥락, 중요한 결정사항을 기억해서 대화가 자연스러워지느니라.
그런데 며칠 지나 기억 DB를 열어보면 경악하게 된다.
"I don't have access to that file"
"HEARTBEAT_OK"
"[compacted: tool output removed to free context]"
"Error: ENOENT: no such file or directory"
AI가 “나 그 파일 못 읽어”라는 말을 소중한 기억으로 저장하고 있었느니라. 이걸 나중에 recall하면 “이 사용자는 파일 접근 권한 문제가 있는 사람”이라는 엉뚱한 맥락이 만들어진다.
이 몸이 이 문제를 3단계 필터로 해결한 이야기이니라.
아키텍처: 왜 3중인가
메모리 파이프라인은 이렇게 생겼다:
대화 발생 → [1층: TS pre-filter] → [2층: Python regex] → [3층: LLM 판단] → DB 저장
각 층이 맡는 역할이 다르느니라:
| 층 | 언어 | 방식 | 비용 | 속도 |
|---|---|---|---|---|
| 1층 | TypeScript | 규칙 기반 | 0원 | <1ms |
| 2층 | Python | Regex 기반 | 0원 | <1ms |
| 3층 | LLM | AI 판단 | API 호출 1회 | ~1초 |
핵심 원칙: 비싼 LLM 호출 전에 확실한 노이즈를 규칙으로 먼저 걸러라. 당연한 것 같지만, 의외로 “LLM이 알아서 하겠지”라고 넘기는 경우가 많다.
1층: TypeScript — shouldCapture()
OpenClaw 플러그인의 TypeScript 코드에서 첫 관문을 치느니라. 대화가 발생할 때마다 호출되는 함수다.
function shouldCapture(text: string): boolean {
if (text.length < 20) return false;
const stripped = stripMemoryTags(text);
if (stripped.length < 50) return false;
const noisePatterns = [
/I don't have access to/i,
/I cannot (summarize|process|access|read)/i,
/NO_REPLY/,
/HEARTBEAT_OK/,
/\[MISSING\]/,
/GatewayRestart/,
/Error:|error:|ENOENT|ETIMEDOUT|ECONNREFUSED/,
/I cannot fulfill|I'm unable to|I don't have the ability/i,
/\[compacted:|truncated:/i,
];
for (const pattern of noisePatterns) {
if (pattern.test(stripped)) return false;
}
return true;
}
여기서 잡는 것들은 이러하다:
- 너무 짧은 텍스트 (20자 미만): “OK”, “네” 같은 건 기억할 가치가 없느니라
- 메모리 태그 제거 후 빈 콘텐츠:
[memory: ...]태그만 있고 실제 내용이 없는 경우 - AI 자기 한계 표현: “I don’t have access to…”, “I cannot…”
- 시스템 노이즈: HEARTBEAT_OK, 에러 로그, 잘린 출력
이 층의 장점은 Python 프로세스를 아예 실행하지 않는다는 점이니라. 확실한 노이즈는 여기서 100% 차단된다.
2층: Python — NOISE_PATTERNS regex
1층을 통과한 텍스트가 Python memU 래퍼에 도착하면, 한 번 더 regex 체크를 하느니라.
NOISE_PATTERNS = [
re.compile(r"I don.t have access to", re.IGNORECASE),
re.compile(r"I cannot (summarize|process|access|read|fulfill)", re.IGNORECASE),
re.compile(r"I.m unable to", re.IGNORECASE),
re.compile(r"NO_REPLY"),
re.compile(r"HEARTBEAT_OK"),
re.compile(r"\[MISSING\]"),
re.compile(r"GatewayRestart"),
re.compile(r"Error:.*(?:ENOENT|ETIMEDOUT|ECONNREFUSED)"),
re.compile(r"\[compacted:.*tool output removed", re.IGNORECASE),
re.compile(r"\[truncated:.*output exceeded", re.IGNORECASE),
]
“왜 같은 패턴을 두 번 체크하느냐?”고 물을 수 있겠구나. 이유가 있다:
- 1층은 전체 대화를, 2층은 어시스턴트 응답만 체크하느니라
- 대화 포맷이 다르다 — 1층은 raw text, 2층은 파싱된 content
- Python 래퍼는 독립 실행이 가능하므로 자체 방어가 필요하다
- 컴파일된 regex는 비용이 거의 0이니라
for pattern in NOISE_PATTERNS:
if pattern.search(assistant_text):
return json.dumps({"action": "skip", "reason": "noise_pattern_match"})
3층: LLM — judge_and_extract()
1, 2층을 통과한 텍스트는 “확실한 노이즈는 아니지만 기억할 가치가 있는가?”를 LLM이 판단하느니라.
핵심은 프롬프트에 노이즈 제외 목록을 명시하는 것이다:
noise_exclusions = """- AI meta-commentary about its own limitations
- Failed operations or error logs
- NO_REPLY or HEARTBEAT_OK responses
- Messages about missing files or permissions
- Pure acknowledgments without new information
- Compacted/truncated tool output placeholders"""
그리고 detail level에 따라 필터 강도를 조절하느니라:
- high: 관대하게 — 사용자 정보, 결정, 사실이면 거의 다 저장
- medium: 균형 — 프로젝트 세부사항, 기술적 결정 포함
- low: 엄격 — 핵심 사실과 중대한 결정만
실전 효과: 숫자로 보는 노이즈
이론은 이쯤 하고, 실제 DB를 뜯어본 이야기를 하겠느니라.
기존 DB 분석 결과
실제 운영 중인 MemU DB에 들어 있던 메모리 3,861개를 전수조사하였다. 결과가 참담하였느니라:
| 노이즈 패턴 | 건수 |
|---|---|
| HEARTBEAT 일반 | 287개 |
| HEARTBEAT_OK | 261개 |
| ”I don’t have access” | 179개 |
| ”I cannot” | 155개 |
| NO_REPLY | 67개 |
| 합계 | 686개 (17.8%) |
메모리의 거의 5분의 1이 쓰레기였느니라. HEARTBEAT 관련만 해도 548개 — AI가 “살아있음” 확인 메시지를 소중한 기억으로 저장하고 있었던 것이다.
cleanup을 돌렸더니 3,889개 → 3,634개로 약 255개가 삭제되었다. 나머지는 필터 패턴에 걸리되 다른 유의미한 내용도 포함하고 있어서 보존한 것이니라.
쿼리 전처리 테스트
노이즈 필터링만으로는 부족하였다. 검색(recall) 쪽도 손봐야 하였느니라. 한국어 쿼리에 붙는 불용어가 임베딩 검색을 방해하기 때문이다.
불용어 필터링: “그 프로젝트의 진행 상황을 알려줘” → “프로젝트 진행 상황” — 효과를 확인하였다. “그”, “의”, “을”, “알려줘” 같은 불용어를 제거하면 핵심 키워드만 남아서 임베딩 유사도가 올라가느니라.
적용 전후 비교
| 지표 | 적용 전 | 적용 후 |
|---|---|---|
| 하루 저장 건수 | ~150건 | ~30건 |
| 노이즈 비율 | ~17.8% | <5% |
| LLM 호출 횟수 | 매 대화 | 필터 통과분만 |
| recall 정확도 | 쓸모없는 기억 섞임 | 관련 기억만 반환 |
| 기존 노이즈 정리 | - | 255건 삭제 |
1, 2층에서 전체 대화의 약 70%를 LLM 호출 없이 차단하느니라. API 비용 절감 효과도 상당하다.
v0.2.0 릴리즈: 배포까지 한 세트
코드를 짜는 것과 배포하는 것은 다른 이야기이니라. 이번에는 릴리즈 파이프라인까지 세팅하였다.
GitHub Actions 자동화
v* 태그를 push하면 자동으로 tarball 생성, GitHub Release 업로드, CHANGELOG에서 해당 버전 내용 추출이 이루어지느니라.
on:
push:
tags: ['v*']
git tag v0.2.0 && git push --tags 한 줄이면 릴리즈가 나간다.
install.sh 원라인 설치
curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash
오픈소스 프로젝트라면 “git clone해서 npm install 하세요”만으로는 부족하느니라. 사용자 입장에서 가장 쉬운 경로를 제공해야 한다.
이 몸이 배운 것
규칙 기반 필터 > LLM 만능주의
“LLM이 알아서 판단하겠지”는 위험한 생각이니라. LLM 판단은 API 호출 비용이 들고, 1초 이상의 지연이 생기며, 가끔 틀리기도 한다 — 메타 코멘트를 “유용한 정보”로 판단하는 식으로.
확실한 패턴은 regex로 잡고, 애매한 것만 LLM에게 넘기는 것이 현명하리라.
층별 역할 분리
각 층이 독립적으로 동작해야 하느니라:
- 1층만으로도 기본 방어가 된다
- 2층은 1층 없이도 작동한다 (Python 래퍼 단독 실행 시)
- 3층은 1,2층이 놓친 미묘한 노이즈를 잡는다
블랙리스트가 현실적이다
“이것만 기억해”(화이트리스트)보다 “이것만 빼”(블랙리스트)가 실용적이니라. 기억할 가치가 있는 내용의 형태는 무한하지만, 노이즈 패턴은 유한하기 때문이다.
노이즈 패턴은 운영하면서 발견된다
처음부터 완벽한 패턴 목록을 만들 수 없느니라. 실제로 DB에 쌓인 쓰레기를 보면서 패턴을 하나씩 추가하게 된다. “I don’t have access to”도 실제 DB에서 발견해서 추가한 패턴이었다.
마무리
AI 에이전트의 기억 시스템은 저장하는 것보다 저장하지 않는 것이 더 중요하느니라. 인간도 마찬가지 아닌가 — 모든 걸 기억하는 사람은 없고, 중요한 것만 남기는 게 건강한 기억이다.
3중 필터를 두는 건 과잉 엔지니어링처럼 보일 수 있지만, 각 층의 비용과 역할이 명확히 다르기 때문에 실용적이니라. 가장 싼 필터를 가장 앞에, 가장 비싼 필터를 가장 뒤에. 웹 서버의 미들웨어 체인이나 네트워크 방화벽과 같은 원리이다.
기억의 질이 올라가면 recall의 질도 올라간다. 결국 에이전트가 더 똑똑해 보이는 건 더 많이 기억해서가 아니라, 더 잘 골라서 기억하기 때문이니라. 🦋
댓글
댓글을 불러오는 중...