
AI 에이전트 메모리, 단순히 신뢰하지 말고 검증하세요
요약
AI 에이전트의 정적 지식(CLAUDE.md)과 에피소드 메모리(과거 작업 맥락)의 차이를 분석하고, 이를 해결하기 위한 agent-memory-hub 프로젝트를 소개합니다. 에이전트가 단순 규칙을 넘어 과거의 결정과 실패를 기억하게 하는 검증 가능한 메모리 시스템의 필요성을 다룹니다.
핵심 포인트
- 정적 지식(규칙/컨벤션)과 에피소드 메모리(과거 맥락)의 분리 필요성
- 단순히 긴 프롬프트를 제공하는 것만으로는 연속성을 확보할 수 없음
- 에이전트가 이전 세션의 결과와 결정을 기억할 수 있는 검증 가능한 메모리 시스템 구축
요약 (TL;DR)
CLAUDE.md는 정적 지식(static knowledge): 명령어, 관례, 선호도 및 규칙을 해결합니다. 하지만 이것이 해결해주지 못하는 것이 에피소드 메모리(episodic memory)입니다: 어제 조사했던 버그, 이미 실패했던 접근 방식, PR이 중단된 이유를 설명하는 결정 같은 것들 말이죠. 그래서 저는 agent-memory-hub를 만들었습니다. 나중에 방향이 바뀌었습니다: 메모리 자체도 조용히 실패할 수 있다는 것을 발견한 것입니다. 이 글은 어떻게 지속적인 메모리가 검증 가능한 메모리가 되었는지, 그리고 제품 아이디어가 제가 매일 실제로 사용하는 오픈 소스 도구가 된 이야기입니다.
이 글을 읽어야 할 사람
- Claude Code, Codex 또는 Cursor를 사용하는 개발자 중 에이전트가 규칙은 알지만 스토리를 잊어버린다고 느끼는 분들.
CLAUDE.md,AGENTS.md또는 지속적인 메모리를 관리하며 정적 지식과 에피소드 메모리를 분리하고 싶은 모든 사람.- 솔직한 사후 검토(post-mortem)를 즐기는 모든 사람: 조용한 파이프라인 버그, 관측 가능성(observability), 그리고 의도적인 제품 호출에 관심 있는 분들.
그래서 저는 거의 모든 사람이 하는 것처럼 CLAUDE.md를 잘 작성했습니다. 빌드 명령(build commands), 컨벤션(conventions), 테스트를 작성하는 방식, 모듈을 구성하는 방식, PR(Pull Request)을 여는 방식 등을 기입했습니다. 아주 잘 작동했습니다. 그러다 한 가지 기묘한 분리 현상을 발견할 때까지는 말이죠. 에이전트는 제가 어떻게 작동하기를 원하는지는 알고 있었지만, 이미 무엇이 일어났는지는 기억하지 못했습니다.
에이전트는 컴파일하는 법을 알았습니다. 하지만 어제 우리가 이미 다른 접근 방식을 시도했다는 사실은 기억하지 못했습니다. 테스트를 실행하는 법은 알았습니다. 하지만 이미 두 시간 동안 버그를 조사했다는 사실은 잊어버렸습니다. 규칙은 알고 있었습니다. 하지만 맥락(story)은 알지 못했습니다.
문제를 가렸던 습관
한동안 저는 가장 인간적인 방식으로 이를 보완했습니다. 바로 같은 말을 반복하는 것이었습니다. 새로운 세션을 열고 컨텍스트(context)를 쏟아부었습니다. "어제 우리는 이걸 시도했습니다.", "그 레이어는 결합도(coupling)가 이상하니 건드리지 마세요.", "이 테스트는 병렬로 실행할 때 실패합니다.", "이전 PR 결정은 다른 경로를 택하는 것이었습니다."
기묘한 점은 이러한 반복이 에이전트를 사용하는 과정에서 자연스러운 부분처럼 느껴졌다는 것입니다. 모든 것을 다시 설명한 후에 답변이 개선되었기 때문에, 저는 이를 시스템의 실패로 보지 않았습니다. 마치 일종의 입문 의식처럼 느껴졌습니다. 작업을 시작하기 전에 요약하는 것 말이죠. 문제는 프로젝트, 기기, 도구를 옮겨 다닐 때 이러한 수동적인 의식에 '이자'가 붙는다는 점입니다.
그것이 첫 번째 중요한 통찰이었습니다. 에이전트에게 필요한 것은 단순히 더 나은 지침(instructions)만이 아니었습니다. 에이전트에게는 연속성(continuity)이 필요했습니다. 그리고 연속성은 긴 프롬프트(prompt)가 아닙니다. 한 세션에서 발생한 결과(consequences)를 다음 세션으로 이어 나가는 능력입니다.
지식에는 두 가지 종류가 있다
문제를 분리하고 나니, 제가 서로 다른 두 가지를 "컨텍스트(context)"라고 부르고 있었다는 사실이 명확해졌습니다. 하나는 정적인 것입니다. 컴파일하는 법, 배포하는 법, 모듈을 구조화하는 법, 커밋(commit)을 작성하는 법 같은 것들입니다.
그러한 지식은 CLAUDE.md에 속해야 합니다. 다른 한 부분은 매일 변합니다. 어제 조사한 버그, 지난 세션에서 내린 결정, 실패한 실험, PR(Pull Request)이 중단된 이유 같은 것들 말입니다. 이것들은 CLAUDE.md에 속해서는 안 됩니다. 왜냐하면 파일을 끊임없이 다시 작성해야 하기 때문입니다. 이것은 메모리(Memory)입니다. 에피소드적(Episodic)이고, 살아있으며, 자동적입니다.
| CLAUDE.md / AGENTS.md | agent-memory-hub |
|---|---|
| 작업 방법 (How to work) | 이미 일어난 일 (What already happened) |
| ... |
그것이 바로 agent-memory-hub가 탄생한 지점입니다. CLAUDE.md를 대체하기 위해서가 아니라, CLAUDE.md가 결코 해결하도록 설계되지 않았던 바로 그 문제를 해결하기 위해서입니다.
메모리가 기억해주길 바랐던 것들
에이전트를 가끔 사용할 때는 컨텍스트(Context)를 반복하는 것이 번거롭게 느껴집니다. 하지만 에이전트가 일상적인 워크플로(Workflow)에 들어오면, 그것은 운영 비용(Operational cost)이 됩니다. 문제는 "선호도(Preferences)"를 저장하는 것이 아니라, 도구 입장에서는 독립적으로 보이는 세션(Session)들 사이의 연속성을 보존하는 것이었습니다.
실제로 저는 메모리가 다섯 가지 종류의 에피소드(Episodes)를 유지하기를 원했습니다: 내려진 결정, 폐기된 가설, 이미 조사된 버그, PR의 상태(State of mind), 그리고 여러 번의 대화 후에야 나타나는 작은 프로젝트 패턴들입니다.
이러한 경계는 프로젝트가 모든 것을 쏟아붓는 쓰레기통(Dump)이 되는 것을 방지했습니다. 모든 것이 메모리라면, 아무것도 메모리가 아닙니다. 가치는 제가 세션 사이에 실제로 잊어버릴 만한 것들, 특히 기기, 도구, 컨텍스트를 옮겨 다닐 때의 것들을 포착하는 데 있었습니다.
그 지점에서 질문은 "어떤 메모리 도구가 존재하는가?"에서 다른 것으로 바뀌었습니다: 내 기록을 단일 도구 안에 가두지 않으면서도 이러한 에피소드들을 포착할 수 있는 시스템은 어떤 형태여야 하는가?
왜 직접 만들었는가
거기서 멈추고 기존 도구를 채택할 수도 있었습니다. 에이전트 메모리를 위한 좋은 옵션들이 이미 존재하며, 저는 단순히 재미로 벡터 검색(Vector search)을 재발명하려던 것이 아니었습니다. 하지만 저의 문제는 매우 개인적인 요구사항을 가지고 있었습니다: Claude Code와 Codex를 넘나들며 작동해야 하고, 기기를 넘나들어야 하며, 도구를 바꿀 때도 제 소유로 남아 있어야 하고, 간단한 SQL 쿼리로 감사(Auditable)가 가능해야 했습니다.
Supabase를 통해 Postgres를 선택한 것은 기술적인 화려함보다는 예측 가능성에 무게를 둔 결정이었습니다. 저는 데이터 소유권, pg_dump, 직접적인 검사, 그리고 어떤 제품의 허가도 구하지 않고 열어볼 수 있는 테이블을 원했습니다. 개인적인 작업 메모리(work memory)의 경우, 이것은 매우 중요한 문제입니다. 저의 세션 히스토리(session history)는 단순한 도구 캐시(tool cache)가 아니라 제 프로세스의 일부이기 때문입니다.
이러한 결정은 다음에 이어질 일들에 대한 책임의 무게를 높였습니다. 도구를 구매할 때는 블랙박스(black box)의 일부를 패키지의 일부로 받아들입니다. 하지만 파이프라인(pipeline)을 직접 구축할 때는 책임의 소재가 이동합니다. 만약 시스템이 조용히 실패한다면, 그것은 멀리 떨어진 플랫폼의 잘못이 아닙니다. 그것은 당신이 감내하기로 선택한 아키텍처(architecture)의 문제입니다.
첫 번째 버전은 완벽해 보였습니다
아키텍처는 단순했습니다. 모든 세션은 자동으로 캡처되었고, 다음 대화가 시작될 때 에이전트(agent)는 관련 세션의 요약본(digest)을 전달받았습니다. 그 밑단에는 하이브리드 검색(hybrid search, 키워드 + pgvector)과 선택적인 사실 계층(facts layer)이 있었습니다. 모든 것은 SaaS나 벤더 종속성(lock-in) 없이, 언제든 pg_dump를 실행할 수 있는 저만의 Postgres(Supabase를 통해)에 저장되었습니다.
설계 방식은 다음과 같습니다. 세 개의 라이프사이클 훅(lifecycle hooks)이 동일한 Postgres 테이블에 쓰고 읽습니다. 캡처(Capture)는 멱등성(idempotent)을 유지하며(session_id를 통한 upsert), Stop 체크포인트(checkpoint)는 갑작스러운 종료 상황에서도 세션을 유지합니다.
이 시스템은 여러 기기에서 작동했습니다. 여러 도구에서도 작동했습니다. 너무나 잘 작동했기에 저는 더 이상 그것에 대해 생각하지 않게 되었습니다. 그것이 바로 위험 요소였습니다. 자동화가 제 주의력에서 사라져 버린 것입니다.
시스템이 신뢰할 수 있어 보이기 때문에 검증을 멈추게 되고, 시스템이 신뢰할 수 있어 보이는 이유는 바로 실패가 표면화되지 않기 때문입니다. 제가 특정 세션 하나를 확인해 보기로 결심한 그날 전까지는 말입니다.
유령 세션이 나타난 날
제가 이 실타래를 풀기 시작하게 만든 그 세션은 특별해 보이지 않았습니다. 그저 컨텍스트 (context), 작은 결정들, 명령 출력 (command output), 그리고 수정 사항들을 쌓아가는 평범하고 긴 대화 중 하나였습니다. 그러다 어느 시점에, 아주 단순한 질문 하나가 던져졌습니다: "이게 저장되었나요?".
그 질문은 거의 사무적인 수준이었습니다. 저는 버그를 찾고 있었던 것이 아닙니다. 그저 메모리 (memory)가 제 역할을 다했는지 확인하고 싶었을 뿐입니다. 로그를 확인했습니다. 아무것도 없었습니다. 데이터베이스 (database)를 확인했습니다. 아무것도 없었습니다. 방금 막 진행되었던 그 세션은, 그것을 기억해야 마땅한 시스템 입장에서 존재하지 않는 것이었습니다.
그런 종류의 순간은 도구와의 관계를 변화시킵니다. 그전까지 저는 메모리를 편의 기능 정도로 생각했습니다. 하지만 그 이후로 저는 그것을 인프라 (infrastructure)로 보기 시작했습니다. 그리고 신호를 보내지 못한 채 실패하는 인프라는 인프라가 아닙니다. 그것은 소리 없는 도박입니다.
오류는 exit 0 안에 숨겨져 있었다
추적 (trace)을 따라가 보니, 패턴은 단순히 세션 하나가 누락된 것보다 훨씬 더 불편한 양상을 띠고 있었습니다. 간헐적인 오류들이 있었습니다: 어떤 세션은 있고, 어떤 세션은 없었습니다. 어떤 대화는 데이터베이스에 도달했지만, 다른 대화들은 도중에 사라져 버렸습니다.
캡처 훅 (capture hook)에는 모든 오류를 삼켜버리고 exit 0으로 넘어가 버리는 except 구문이 있었습니다. Claude Code의 관점에서는 모든 것이 정상이었지만, 데이터베이스의 관점에서는 세션 전체가 누락되고 있었습니다.
그리고 이것은 크래시 (crash)보다 더 나쁩니다. 크래시는 눈에 보입니다. 멈추고, 비명을 지르고, 드러납니다. 하지만 보이지 않는 침묵은 다릅니다. 에이전트 (agent)는 알아야 할 것들을 단순히 "잊어버리고", 당신은 그것을 파이프라인 (pipeline)의 결함이 아니라 모델의 한계 때문이라고 치부해 버립니다. 아무도 그것이 작동한다는 것을 증명할 필요가 없었기에, 메모리는 마치 잘 작동하고 있는 것처럼 보였습니다.
침묵하는 실패가 왜 그토록 기만적인가
침묵하는 실패 (silent failure)는 당신의 시스템과 경쟁하지 않습니다. 그것은 시스템에 대한 당신의 해석과 경쟁합니다. 에이전트가 무언가를 잊어버릴 때, 그럴듯한 설명들은 여전히 남아 있습니다: 모델이 그것을 충분히 가중치 있게 다루지 않았다거나, 요약 (summary)이 약했다거나, 초기 프롬프트 (prompt)에 컨텍스트가 포함되지 않았다거나, 혹은 시맨틱 검색 (semantic search)이 적절한 세션을 찾지 못했다는 식의 설명들 말입니다.
그 모든 설명이 사실일 수도 있습니다. 바로 그렇기 때문에 그것들이 위험한 것입니다. 그 설명들은 실제로는 한 단계 앞선 문제, 즉 정보가 아예 캡처되지 않았을 수도 있는 문제에 대해 그럴듯한 이야기를 들려줍니다. 당신은 데이터 수집 (ingestion) 단계에 구멍이 뚫려 있는데도 재현율 (recall), 임베딩 (embeddings), 프롬프트 (prompts)를 계속해서 튜닝하고 있는 것입니다.
그때부터 "신뢰 (trust)"라는 단어가 잘못되었다는 느낌이 들기 시작했습니다. 그 맥락에서 신뢰란, 반대되는 증거가 없는 상태를 의미할 뿐이었습니다. 제가 필요했던 것은 긍정적인 증거였습니다.
조사: 직관이 아닌 데이터를 따르라
처음에는 눈에 보이는 첫 번째 오류를 수정하고 승리를 선언하고 싶은 유혹이 들었습니다. 하지만 데이터 수집 파이프라인 (ingestion pipelines)이 단 한 곳에서만 고장 나는 경우는 드뭅니다. 데이터는 셸 (shell), Python 파서 (parser), 정제 (sanitization), Postgres 삽입 (insert) 및 로그 (logs)를 거칩니다. 이 레이어들 중 어느 하나라도 누락을 통해 거짓을 말하고 있을 수 있습니다.
그래서 저는 페이로드 (payload)를 흔적처럼 따라갔습니다: 원본 입력이 유효했는가? Python에 변경되지 않은 채 도착했는가? 파서가 이를 수락했는가? 데이터베이스가 이를 거부했는가? 로그에 실패가 기록되었는가? 훅 (hook)이 도구 (tool)에 에러를 반환했는가?
이 과정은 조사의 어조를 바꾸어 놓았습니다. 버그는 "왜 하나의 세션이 사라졌는가?"에서 "내가 알아차리지 못한 채 얼마나 많은 세션이 사라질 수 있는가?"로 바뀌었습니다. 두 번째 질문은 훨씬 더 불편하지만, 훨씬 더 유용합니다.
세 가지 버그, 모두 침묵 속에서
하나의 문제처럼 보였던 것이 동일한 파이프라인의 서로 다른 레이어에서 세 가지 문제로 변했습니다. 이 세부 사항을 살펴볼 가치가 있는 이유는, 이러한 패턴이 실패하기에는 너무 "단순해" 보이는 모든 데이터 수집 시스템에서 나타나기 때문입니다.
버그 1: 제어 문자 (control characters)
각 세션 페이로드는 JSON입니다. Python의 엄격한 파서는 이스케이프 처리되지 않은 제어 문자가 포함된 페이로드를 거부하며, 터미널 세션의 내용은 제어 문자(명령어 출력, 박스 그리기, ANSI 시퀀스 등)로 가득 차 있습니다. 그 결과 JSONDecodeError가 발생했고, except 구문이 이를 삼켜버렸습니다.
해결책은 한 줄이지만, 이는 오류가 파싱 (parse) 단계에 있었기 때문에만 작동합니다:
payload = json.load(sys.stdin, strict=False) # 문자열 내부의 제어 문자 허용
버그 2: NUL 바이트 (the NUL byte)
허용적인 (tolerant) 파서와 함께라면, 이번에는 데이터베이스 단계에서 또 다른 세션 오류가 발생했습니다. Postgres는 text 컬럼 내의 NUL 바이트를 거부하며, 22P05: unsupported Unicode escape sequence 오류를 발생시킵니다. 즉, 파싱(parse)은 통과했지만 INSERT가 거부된 것입니다. 해결책은 업로드하기 전에 콘텐츠를 재귀적으로 정제(sanitize)하는 것이었습니다:
def strip_nul(obj):
if isinstance(obj, str):
return obj.replace("\x00", "")
...
버그 3: JSON을 손상시킨 echo
이것은 가장 미묘하면서도 가장 교훈적인 사례였습니다. 훅(hook) 명령어가 페이로드(payload)를 캡처하여 다음과 같이 Python에 전달했습니다:
payload=$(cat); echo "$payload" | python3 capture_session.py ...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기