
프롬프트만으로는 안 되는 순간, 문서 메모리는 왜 무너지는가 (1편)
요약: 문서를 많이 쌓아도 AI 가 저절로 더 잘 기억하지는 않습니다. 같은 사실이 여러 버전으로 남고 오래된 맥락이 새 결정과 같은 무게로 읽히면, 답변은 더 정확해지기는커녕 오히려 오락가락합니다. 이 글은 그런 혼선이 사이드 프로젝트에서 어떻게 터졌는지 (하루 만에 14개의 문서 충돌) 본인 팀 사례로 짚고, 마크다운 파일 더미와 운영되는 메모리 시스템이 왜 다른지 정리합니다.
시리즈 2/4 · 본편 1편
4편 시리즈, 지금은 어디
이 시리즈는 프롤로그와 본편 3편, 총 4편으로 묶입니다. 독자는 Claude Code, Cursor, Codex CLI 같은 에이전트형 개발 도구를 이미 조금 써 본 개발자로 잡았습니다.
"프롬프트를 잘 쓰면 된다" 는 감각에서 한 걸음 더 나아가, repo 안 문서를 어떻게 운영해야 세션이 바뀌어도 맥락이 일관적이고, 안정적인 출력을 제시하도록 설계했는지에 대한 글입니다.
| 프롤로그 | 메모리 아키텍처 업그레이드는 왜 이제 선택이 아닌가 | 시리즈 문제의식, 4편 안내 |
| 본편 1편 (지금 이 글) | 프롬프트만으로는 안 되는 순간, 문서 메모리는 왜 무너지는가 | 답이 오락가락하는 원인 진단 |
| 본편 2편 (다음) | 위키 관리도 자동화하고 사람의 실수를 막자! karpathy llm wiki 위에 얹은 자동화 시스템 | 폴더 구조와 frontmatter 로 마찰을 정리한 과정 |
| 본편 3편 | AI 에이전트 메모리, 파일 더미로는 왜 무너지는가 | git 위에서 오래 가는 운영 구조 |
본편 1편은 그중 왜 별도 층이 필요한가 를 풀어 봅니다. 프롬프트만 잘 써서는 풀리지 않는 종류의 문제가 있다는 것, 그리고 그게 왜 하네스 (에이전트가 일하는 작업대) 를 잘 깔자는 이야기와도 다른 차원인지까지.
이 글에서 다룰 것
프롬프트를 아무리 정교하게 다듬어도 잡히지 않는 종류의 혼선이 있습니다. 에이전트가 매 세션 무엇을 어떤 순서로 읽을지 정해져 있지 않은 환경에서, 같은 질문에도 답이 자꾸 다른 방향으로 새어 나가는 현상입니다.
본편 1편에서는 다음 세 가지를 봅니다.
- 그 혼선이 실제로 어떻게 일어나는지. 가입과 온보딩 정책 한 사례를 발견부터 처방까지 따라갑니다.
- 프롬프트만 다듬어서는 왜 안 풀렸는지. 메커니즘 세 가지로 짚어 봅니다.
- 하네스 (
.claude/안의 슬래시 커맨드, 훅, MCP 같은 작업대) 를 잘 깔아도 안 풀리는 이유. 작업대와 재료, 그리고 운영 규칙의 차이로 풀어 봅니다.
자동화와 검증 루틴 (훅, 스크립트, 정기 점검) 같은 운영 디테일은 본편 3편으로 미룹니다. 본편 1편의 두 축은 「메모리라는 단어 안에 사실 세 가지가 섞여 있다」 는 개념과 「에이전트는 stateless, 프로젝트는 stateful」 이라는 한 줄 원리입니다. 여기서 말하는 실패는 "마크다운을 써서 실패했다" 가 아닙니다. 마크다운을 매번 프롬프트 RAM 처럼 밀어 넣어서 실패한 쪽에 가깝습니다.
이 글은 "AI 에게 문서를 많이 주면 더 똑똑해진다" 는 직관이 어디서 깨지는지 보는 글입니다. 문서를 많이 주면 정보량은 늘어납니다. 정리되지 않은 정보량은 곧 소음이 됩니다. 오래된 초안, 최근 결정, 회의 메모, 임시 가설이 같은 폴더 안에서 같은 무게로 읽히면 에이전트는 그중 무엇이 정답인지 알 수 없습니다.
프롤로그 리마인드
프롤로그의 결론은 한 줄입니다. 마크다운 파일 더미 자체는 기억이 아닙니다.
마크다운 파일을 아무리 많이 쌓아 두어도, 그 자체로 에이전트의 장기 기억이 되지는 않는다는 이야기였습니다. 본편 1편에서는 이 말이 실제로 프로젝트에서 어떻게 나쁜 영향을 주는지 풀어 봅니다.
「분명 관련 문서와 지침을 던져줬는데 왜 답이 자꾸 다른 방향으로 가지?」 라는 의문이 처음 떠오르는 그 지점에서부터 시작합니다.
메모리 개념 확실하게 짚고 가기: 세 가지 개념
Claude 의 메모리 라는 말 안에는 보통 세 가지 개념이 같은 단어로 묶여 있습니다.
| 모델 컨텍스트 윈도우 | LLM 호출 단위 | 매 호출마다 새로 채워짐 | 다루지 않음 |
| 앱 자체 메모리 | Claude / ChatGPT 앱의 Settings → Memory | 사용자 개인, 앱이 자동 정리 | 가볍게만 (Layer 1, repo 밖) |
| 프로젝트 문서 메모리 | repo (저장소: 코드와 문서를 모아 둔 폴더) 안 마크다운 | 사용자 + 팀 + 에이전트 | 본편 (Layer 2, 3) |
이 시리즈에서 「메모리가 무너졌다」 라고 말할 때는 거의 항상 세 번째를 다룹니다. 첫 번째는 매 호출마다 새로 채워지므로 잃어버릴 것이 없고, 두 번째는 개인 비서에 가까운 영역이라 팀 작업과는 결이 다릅니다. 진짜 페인포인트는 프로젝트 사실이 repo 안 여기저기 마크다운에 흩어져 있고, 에이전트가 매 세션마다 처음 보는 사람처럼 굴 때 옵니다.
여기서 repo 는 Git 저장소를 뜻합니다. 개발자에게는 너무 익숙한 단어지만, AI 도구를 막 쓰기 시작한 사람에게는 이 구분이 중요합니다. Claude 앱의 개인 메모리는 내 계정 안에 있고, repo 안 문서는 팀원과 공유됩니다. 개인 메모리에 "나는 짧은 답변을 좋아한다" 를 저장하는 것과, repo 안에 "이 제품은 로그인 없이 탐색 가능하다" 를 저장하는 것은 완전히 다른 문제입니다.
세 가지를 같은 것으로 묶을 수 없는 이유는 Claude Code 공식 docs 한 줄에 잘 정리되어 있습니다.
"Each Claude Code session begins with a fresh context window. Two mechanisms carry knowledge across sessions: CLAUDE.md files (instructions you write) and Auto memory (notes Claude writes itself)."
(번역: Claude Code 의 매 세션은 빈 컨텍스트 윈도우로 시작한다. 세션 사이에 지식을 이어 주는 장치는 두 가지로, 사용자가 직접 쓰는
CLAUDE.md와 Claude 가 스스로 누적하는 auto memory 다.)
여기서 놓치면 안 되는 건 매 세션이 백지에서 시작한다는 사실입니다. 이 한 문장이 이 시리즈의 출발점입니다. 인정하지 않으면 「내가 어제 말했잖아」 가 통한다고 착각하게 됩니다.
여기에 한 가지 더 짚어 두면 좋습니다. 매 세션 시작에 자동으로 로드되는 CLAUDE.md 같은 지시문은 컨텍스트 윈도우에 들어가기는 합니다. 다만 강제 설정이 아니라 문맥 (context) 으로만 전달됩니다.
"CLAUDE.md files are loaded into the context window at the start of every session, consuming tokens alongside your conversation... Because they're context rather than enforced configuration, how you write instructions affects how reliably Claude follows them."
(번역:
CLAUDE.md는 매 세션 시작에 컨텍스트 윈도우로 로드되며 대화와 함께 토큰을 소비한다. 강제 설정이 아니라 문맥일 뿐이라, 어떻게 쓰느냐에 따라 Claude 가 얼마나 일관되게 따르는지가 달라진다.)
지시문은 「부탁」 에 가깝고, 강제는 다른 층 (훅 같은 자동화 차단) 에 있습니다. 부탁만으로 안 풀리는 영역이 본편 1편의 주제이고, 강제 layer 는 본편 3편의 주제입니다.
에이전트는 stateless, 프로젝트는 stateful
이 시리즈의 한 줄 원리이기도 합니다. 에이전트는 stateless, 프로젝트는 stateful 입니다.
새 세션은 매번 텅 빈 상태로 시작하지만, 프로젝트는 그렇지 않습니다. 어제도 결정이 있었고, 지난주에도 가설이 바뀌었고, 한 달 전 인터뷰에서 누구를 인용할지도 정해 뒀습니다. 사람은 그 누적된 맥락을 머릿속에 들고 다니지만, 에이전트는 매번 새 백지 위에서 일을 시작합니다.
이 간극을 메우는 방법은 사실상 두 가지뿐입니다.
- 매번 긴 브리핑을 채팅창에 붙인다. 매번 손으로 컨텍스트를 다시 만든다.
- 그 브리핑을 파일로 영구화해서 자동으로 붙게 만든다. 한 번 정리해 두면 다음부터 자동으로 따라온다.
이 시리즈에서 「작업 메모리 (working memory)」 라고 부르는 게 두 번째에 해당합니다. 새 협업자가 합류했을 때 첫날 무엇을 보여 주는지 떠올려 보면 빠릅니다. 제품을 한 줄로 설명한 글, 팀 구성, 진행 중인 작업, 안 하기로 정한 것, 최신 결정이 어디 있는지를 공유합니다. 사실상 온보딩 가이드입니다.
이런 정보가 한 폴더에 모여 있고 매 세션 시작 때 자동으로 에이전트의 컨텍스트에 들어간다면, 작업 메모리가 팀 단위로 공유되어 협업이 더 편해집니다.
이때 작업 메모리는 "모든 문서를 매번 다 넣는 것" 이 아닙니다. 새 팀원에게 첫날 회사의 모든 회의록을 zip 으로 보내지 않는 것과 같습니다. 먼저 현재 제품 요약, 진행 중인 일, 중요한 결정, 금지된 방향만 보여 줍니다. 더 자세한 문서는 필요할 때 찾아보게 합니다. 에이전트에게도 같은 원칙이 필요했습니다.
어떤 프로젝트에서 일어난 일인가, 짧은 배경
3명이 협업하는 사이드 프로젝트입니다. 메인 도구는 Claude Code (터미널에서 돌아가는 에이전트), 그리고 pm-skills 라는 오픈소스 PM 에이전트 (PRD, 인터뷰, 지표 같은 product 문서를 묶음 단위로 관리하게 해 주는 슬래시 커맨드 모음) 를 깔아 product 문서를 정리해 두고, 그 문서들을 근거로 의사결정을 해 왔습니다.
지금 정리된 폴더는 대략 이런 모양입니다. 처음에는 이보다 더 느슨했지만, 최종적으로는 역할이 이 정도로 갈라졌습니다.
docs/
├── README.md # vault 전체 지도
├── _claude/ # Layer 2 작업 메모리·운영 규칙
├── strategy/ # PRD, 가설, 계획 문서, 결정 로그
├── handbook/ # 한장 요약, 온보딩, 용어 사전
├── mobile-design/ # 화면 흐름, UX 정책, 디자인 토큰
├── spec/ # 에러 케이스, QA, 인터뷰 운영 스펙
├── data-model/ # ERD, DBML, Supabase 스키마 안내
├── .obsidian/ # Obsidian vault 설정
└── dataview-queryboard.md # stale, draft, canonical 점검판
폴더 이름이 핵심은 아닙니다. 전략, 스펙, 디자인, 데이터 모델, 에이전트 작업 메모리를 같은 docs/ 안에 두되 역할을 나눴다는 점이 중요합니다. Notion 으로도 비슷하게 만들 수 있지만 (MCP 사용- 토큰 아까움ㅋ), 이 프로젝트에서는 Markdown + git 이 더 싸고 Claude Code 가 바로 읽고 고칠 수 있었습니다. 본편 2편에서는 이 폴더가 어떻게 다시 읽히는 자료로 정리됐는지 다룹니다.
실제로는 읽는 순서에 따라 다른 답이 나왔습니다
처음에는 단순한 문서 정리 문제처럼 보였습니다. 그런데 문서가 쌓일수록, 직접 점검해 보니 하루 만에 14개의 문서 정합성 충돌이 잡혔습니다. 일부는 단순한 표현 차이였지만, 일부는 실제 구현 판단을 다른 방향으로 끌고 갈 수 있는 수준이었습니다.
위험했던 이유는 「문장이 틀려서」 가 아니었습니다. 어떤 문서를 먼저 읽었느냐에 따라 에이전트가 서로 다른 제품을 상상할 수 있었다는 점에서 위험했습니다.
같은 LLM 인데 어제까지는 맞게 답하던 것이 오늘은 다른 방향으로 갑니다. 모델이 갑자기 바뀐 게 아니라, 그날 컨텍스트 창에 들어간 문서가 달랐기 때문입니다.
조금 더 정밀하게 보면 두 갈래입니다.
- attention 분산. 컨텍스트가 차오를수록 long-context LLM 의 attention 이 분산됩니다. 잘 알려진 lost-in-the-middle 패턴, 즉 중간 위치 정보가 양 끝보다 약하게 읽히는 현상입니다. Claude Code 가 SessionStart hook 으로 세션 시작에 깔아 둔
Context.md도 대화가 길어지면 중간으로 밀려나면서 영향력이 줄어듭니다. - 자동 compact 발동. 컨텍스트가 한도에 가까워지면 Claude Code 는
/compact슬래시 커맨드 또는 임계 도달 시 자동 요약으로 이전 대화, 파일 내용, 도구 출력을 압축합니다. 그 과정에서 초반 지시, 세션 시작 hook 출력, 일부 인용은 요약되거나 생략됩니다. 사용자 입장에서는 「갑자기 어제 한 말을 까먹는다」 처럼 느껴지지만, 실제로는 컨텍스트가 compact 를 거치면서 그 부분이 단순 요약 형태로만 남은 상황입니다.
그러면 이벤트 이름 충돌, 미결정 사항을 확정처럼 읽는 문제, 폐기된 코드와 정책 문서가 현재 맥락처럼 남는 문제가 한꺼번에 튀어나옵니다.
대표 사례를 발견부터 처방까지 따라가 보겠습니다. 가입과 온보딩 정책에 대한 이야기입니다.
사용자가 처음 들어올 때의 흐름은 인증 모듈, DB user 모델, 첫 화면 라우팅, 온보딩 분기를 한꺼번에 결정하는 자리입니다. 이 한 결정이 갈라지면 제품 한 부분이 통째로 다른 모양이 됩니다.
사례 1, 가입과 온보딩 정책
같은 결정 (사용자가 처음 들어올 때 어떤 흐름을 거치는가) 에 대한 문서가 두 개 있었는데, 두 문서가 정반대 정책을 적어 두고 있었습니다.
- 3주 전 PRD 초안 (PRD = 기획 문서, Product Requirements Document). 「이메일 + 비밀번호 가입. 첫 로그인 시 온보딩 5단계 필수.」
- 1주 전 결정 로그 (회의에서 결정한 내용을 모아 둔 문서). 「게스트 진입 가능. OAuth 만 허용. 온보딩은 스킵 가능.」 진입 장벽을 낮추기로 합의하면서 결정 로그에 작성했고, PRD 초안 정합성 체크를 빼먹은 상태였습니다.
둘 다 문장만 보면 그럴듯합니다. 둘 다 비슷한 권위 같은 위치에 있었고, 둘 다 「최근에 수정된 마크다운 파일」 이었습니다.
사람의 머릿속에서는 PRD 초안 → 결정 로그 순서로 결정 로그가 더 최신이라고 자연스럽게 떠오릅니다. 그러나 마크다운 파일 자체에는 누가 누구를 대체했는지가 한 글자도 적혀 있지 않았습니다. 그래서 에이전트는 두 문서를 같은 비중으로 읽고, 둘을 적당히 종합해서 그럴듯한 답을 만들어 냈습니다.
이 흐름을 그림 한 장으로 보면 더 분명합니다.

이 그림에서 두 가지를 짚어 두겠습니다.
첫째, 에이전트가 거짓말을 한 게 아닙니다. 두 문서 모두 실제로 저장소에 살아 있었고, 둘 다 "현재 시점에서 유효한 마크다운 파일" 처럼 보였습니다. 에이전트가 추가한 환각은 없습니다. 입력으로 들어간 두 단서를 적당히 합쳤을 뿐입니다.
둘째, 합성된 답은 두 원본 어느 쪽과도 같지 않습니다. PRD 는 "이메일 비번 + 온보딩 필수", 결정 로그는 "게스트 + OAuth + 온보딩 스킵" 인데, 답은 "이메일 비번 + OAuth + 온보딩 일부" 처럼 양쪽을 섞은 형태로 나옵니다. 사람이 봐도 어디서 틀렸는지 한 번에 안 보입니다. 두 원본이 모두 그럴듯하기 때문입니다.
사람은 "아, 저건 옛날 얘기고 이게 최신 결정이지" 라고 배경지식으로 보정합니다. 에이전트는 그런 배경지식이 없습니다. 파일 이름, 본문 표현, 현재 컨텍스트에 들어온 순서 같은 단서만 보고 판단합니다. 그래서 문서에 상태가 없으면 에이전트는 낡은 초안도 꽤 자신 있게 현재 사실처럼 씁니다.
이렇게 만들어진 답을 그대로 코드로 옮기면 결과는 완전히 다른 제품이 됩니다. 이메일 + 비번으로 짠 가입 흐름과 게스트 + OAuth 로 짠 흐름은 인증 모듈, DB user 모델, 첫 화면 라우팅, 온보딩 분기까지 전부 다릅니다. 작은 문서 충돌 하나가 제품의 한 부분을 완전히 바꿔 버립니다.
사례 2, 제품 포지셔닝
이런 일이 한 번이었으면 운이 없었다고 넘어갔을 텐데, 같은 패턴이 다른 부분에서도 반복됐습니다.
product positioning (제품의 핵심 차별점을 어떻게 정의하느냐) 을 두고 환각이 일어난 적도 있습니다. 가상의 광고 에이전시용 카피 작성 보조 툴이 있다고 가정해 봅니다.
핵심 기능 중 하나가 레퍼런스 카피 추천 입니다. 새 캠페인 카피를 쓸 때 과거 캠페인이나 경쟁사의 비슷한 표현을 옆에 히스토리로 띄워 주는 기능입니다.
이 기능의 공식 정의는 결정 로그에 확실하게 적혀 있었습니다.
레퍼런스 카피 추천은
표절 방지기능이 아니다. 카피라이터는 비슷한 표현을 알면서도 의도적으로 참조한다. 패러디, 오마주, 밈, 반박 카피 같은 작법으로. 이 기능은 비슷한 카피들을 옆에 놓고 비교하며 차별점을 발견하는 레퍼런스 도구 다.
그런데 에이전트에게 「이 기능의 UI 를 어떻게 짜는 게 좋을까」 라고 자문을 구했더니 답이 이상하게 돌아왔습니다.
사용자가 같은 표현을 또 쓰는 실수를 줄이려면, 유사 카피 발견 시
이미 비슷한 표현이 있습니다라는 표절 경고 모달을 띄우는 기능을 구현할까요?
제품의 핵심 명제와 정반대로 기울어진 답이었습니다. Google NoteLLM 에도 같은 문서를 먹여 봤는데, 비슷하게 문제 정의 자체를 잘못 잡고 정보를 처리했습니다.
문서를 뒤져 보니 결정 로그에는 「레퍼런스 추천은 표절 경고 기능이 아니다」 가 적혀 있었지만, 같은 폴더의 오래된 글에는 중복 카피 작성 방지를 위함, 클라이언트가 같은 톤 또 시키는 실수 막기, 과거 캠페인 또 베끼는 비효율 줄이기 같은 표현이 여기저기 남아 있었습니다 (가설 설정 단계에서 적었던 옛 표현이었습니다).
에이전트는 두 종류 글을 같은 무게로 읽고 더 강한 표현 (방지, 실수, 막기) 쪽으로 기울었습니다. 가입 정책 사례와 정확히 같은 메커니즘이었습니다. 결정 로그가 더 최신이라는 표시가 어디에도 없었기 때문에, 결정 로그의 정의가 옛 표현에 묻혀 사라진 것입니다.
이건 환각이 아닙니다. 저장소 안에 레퍼런스 추천 = 막아야 할 행동 과 레퍼런스 추천 = 탐색·차별화 도구 라는 두 프레임이 공존하고 있었고, 에이전트는 그중 더 자주 등장하는 옛 프레임을 골랐을 뿐입니다. 가입 정책 사례에서 두 정책이 공존했던 것과 똑같이, 여기서는 두 포지셔닝 프레임이 공존했습니다.
모델 탓으로 돌리기 어려웠습니다
문제는 모델이 근거 없이 환각을 만들어 낸 게 아니었습니다. 저장소 안에 이미 서로 다른 전제가 공존했고, 에이전트는 그중 하나를 골라 그럴듯한 답을 합성했습니다. 가입 정책에서도, 제품 포지셔닝에서도 같은 메커니즘이 작동했습니다.
그래서 저는 이 현상을 "환각" 보다는 "컨텍스트 오염" 에 가깝다고 봅니다. 모델이 없는 사실을 만든 게 아니라, 저장소 안에 있던 낡은 사실과 최신 사실을 같은 무게로 섞은 것입니다. 원인은 모델 밖, 즉 문서 구조 안에 있었습니다.
프롬프트만 다듬어서는 안 풀렸던 이유 세 가지
사실 메모리 시스템을 만들기 시작한 출발점은 한 점검 결과였습니다. 처음에는 docs 폴더를 던지고 「@docs/strategy/14_decision_log.md 를 기준으로 정합성 체크해줘」 라고만 했습니다. Claude 로 돌렸을 때는 한두 개 정도만 잡혔습니다. 그런데 같은 docs 를 Codex 로 다시 점검하니 14개의 충돌이 잡혔습니다. Claude 만 믿고 있다가 Codex 가 14개를 잡아내는 걸 보고, 「프롬프트로 막을 게 아니라 문서 구조를 바꿔야 한다」 는 쪽으로 결론이 기울었습니다.
그다음에 시도한 게 프롬프트 처방이었습니다. 시스템 프롬프트에 이런 식으로 넣었습니다.
- 「항상 최신 결정만 따르세요.」
- 「충돌이 있으면 결정 로그를 우선하세요.」
- 「오래된 PRD 는 무시하세요.」
며칠은 효과가 있는 것 같았지만, 곧 같은 부분에서 다시 헷갈리기 시작했습니다. 프롬프트만으로는 풀리지 않는 이유가 세 갈래로 정리됐습니다.
첫째, 「최신」 의 기준이 무엇인지 문서 자체에 적혀 있지 않습니다.
파일의 mtime (파일이 마지막으로 수정된 시각) 만 보고 결정 로그가 PRD 초안보다 최신이라고 단정할 수 없습니다. 사람이 PRD 를 한 번 더 손대면 그 파일의 mtime 이 결정 로그보다 늦어집니다. 에이전트는 시간만 가지고 우선순위를 정할 방법이 없습니다.
둘째, 어느 문서가 정본인지 문서 자체에 명시되어 있지 않으면 에이전트마다 비교 기준점이 달라집니다.
위에서 본 Claude 와 Codex 결과가 갈린 진짜 이유가 여기에 있습니다. 「14_decision_log.md 를 기준으로 봐 줘」 라는 한 줄을 프롬프트에 넣어도, 그 문서를 다른 문서들과 어떻게 비교할지 (어느 부분이 정본이고 어느 부분이 옛 초안인지) 의 판단은 결국 에이전트가 합니다. 그 판단이 LLM 마다 다릅니다. 사람은 14_decision_log.md 의 어느 D 번호가 가장 최신 결정인지 머릿속으로 알지만, 에이전트는 본문을 읽고 그때그때 추정해야 합니다.
한 단계 더 들어가면, Claude Code 의 CLAUDE.md 로딩 메커니즘 자체가 이 문제를 더 키웁니다.
"Claude Code reads CLAUDE.md files by walking up the directory tree from your current working directory... All discovered files are concatenated into context rather than overriding each other."
(번역: Claude Code 는 현재 디렉토리에서 위로 걸어 올라가며 발견되는 모든
CLAUDE.md를 컨텍스트에 이어 붙인다. 덮어쓰지 않는다.)
여러 CLAUDE.md 가 발견되면 모두 합쳐져 컨텍스트에 들어갑니다. 어느 게 더 「우위」 인지 frontmatter 같은 명시적 표시가 없으면, 어느 지시가 마지막에 읽히고 더 큰 영향을 줄지 예측 불가능합니다. 일반 마크다운 문서들 사이의 충돌도 같은 메커니즘입니다. 「누가 누구를 대체하는지」 가 문서 자체에 적혀 있어야 합니다.
셋째, 컨텍스트를 더 많이 넣을수록 답이 더 오락가락합니다.
두 충돌 버전이 같이 컨텍스트에 들어가면 에이전트가 적당히 종합한 답을 내놓게 됩니다. 이 점은 Claude Code 공식 best-practices 가 모든 권고의 출발점으로 짚는 한 줄과 정확히 맞닿아 있습니다.
"Most best practices are based on one constraint: Claude's context window fills up fast, and performance degrades as it fills."
(번역: 거의 모든 best practice 는 한 가지 제약에서 출발한다. Claude 의 컨텍스트 윈도우는 빠르게 차오르고, 차오를수록 성능이 떨어진다.)
같은 페이지의 다른 단락에서 그 메커니즘을 더 구체적으로 확인할 수 있습니다.
"Claude's context window holds your entire conversation, including every message, every file Claude reads, and every command output. However, this can fill up fast... LLM performance degrades as context fills. When the context window is getting full, Claude may start 'forgetting' earlier instructions or making more mistakes."
(번역: 컨텍스트 윈도우는 대화, 파일 내용, 명령 출력 모두를 담고 금방 찬다. 차오를수록 성능이 떨어지고, 윈도우가 가득 차면 Claude 가 초반 지시를 잊거나 실수가 늘어난다.)
「최신 결정만 따르세요」 라고 시스템 프롬프트에 넣어 둬도, 컨텍스트가 차오르면 그 초반 지시 자체가 점점 약해집니다.
충돌하는 후속 정보가 나중에 들어와 더 큰 영향을 줍니다. 많이 넣으면 잘 답한다는 직관과 정반대입니다.
@AYi_AInotes 의 트윗 ("폭론 하나 던져보자, 지금 90%의 AI 에이전트 메모리, 전부 가짜야") 이 저에게 충격을 준 이유가 여기 있습니다.
충돌을 컨텍스트 안에서 풀려고 하지 말고, 컨텍스트에 들어가기 전 단계인 문서 계층에서 미리 제거해야 한다는 결론으로 이어졌습니다.
답은 프롬프트 안이 아니라 프롬프트 밖에 있어야 했습니다.
사람이 매번 「이게 최신이야」 를 손가락으로 가리키지 않아도 에이전트가 자연스럽게 판단하려면 문서 자체에 우선순위가 표시되어 있어야 합니다.
너무 당연해 보이는 말입니다. 사실 처음에는 데이터 정합성 체크를 그냥 프롬프트로 ("/docs 에서 충돌 좀 봐 줘") 처리하면서 원문을 덮어쓰고, 변경된 결정은 decision_log.md 에서 히스토리를 관리하는 방식으로 시도했는데도 역부족이었습니다.
context 메모리 시스템 구축 이후 다시 확인해 보니 14건이 탐지되더군요.
하네스를 잘 깔아도 안 풀리는 자리, 작업대와 재료의 차이
여기서 한 번 짚고 가야 할 게 있습니다. 「프롬프트 다음에 다듬을 곳」 이라고 하면 보통 떠오르는 게 하네스 (harness) 입니다.
하네스는 에이전트가 일하는 작업대입니다. Claude Code 에서는
.claude/폴더 안의 슬래시 커맨드 (/명령어식으로 호출하는 미리 만든 프롬프트)- 훅 (특정 시점에 자동 실행되는 작은 스크립트)
- MCP 서버 설정 (외부 도구 연결용)
- 격리된 컨텍스트로 실행되는 subagent (메인 에이전트가 일부 작업을 맡기는 보조 에이전트)
같은 도구들을 한 단어로 묶은 표현입니다. 슬래시 커맨드를 잘 만들면, 훅으로 자동화 걸면, 외부 도구를 MCP 로 더 연결하면 풀릴 것 같은 자리입니다. Claude Code 공식 docs 의 features-overview 에 이 도구들이 한 페이지에 잘 정리되어 있습니다.
저도 한참 그렇게 생각했습니다. 그런데 위 가입 정책 사건에서 깨달은 건 이거였습니다. 하네스를 아무리 잘 깔아도, 그 작업대 위에 올린 문서들이 서로 모순이면 결과는 똑같이 오락가락합니다.
작업대가 좋다고 해서 그 위에 올린 재료가 자동으로 일관되지는 않습니다. 슬래시 커맨드가 정교해도, 그 커맨드가 읽는 문서들이 14개씩 충돌하고 있으면 의미가 없습니다. 훅으로 자동화를 정밀하게 걸어 두어도, 이해하고 처리하는 데이터 (여기서는 /docs/*) 가 서로 모순이면 정밀하게 잘못된 결과가 나올 뿐입니다.
세 층으로 나누면 이렇습니다.
- 프롬프트 엔지니어링. 한 번의 대화에서 질문을 잘 던지는 기술
- 하네스 엔지니어링. 에이전트가 일하는 작업대를 꾸리는 기술 (
.claude/의 도구들) - 메모리 아키텍처. 에이전트가 매 세션 무엇을 어떤 순서로 읽고 신뢰할지 설계하는 기술
세 층이 서로 다른 영역이고, 한 층이 좋아도 다른 층이 비어 있으면 결과는 오락가락합니다. 위 사건에서 비어 있던 건 메모리 아키텍처였습니다. 프롬프트와 하네스를 아무리 잘 짜도 지시서가 어긋나 있으면 말짱 꽝입니다.
사람이 손코딩을 한다고 쳐도, 문서 기준을 먼저 확인하고 신뢰도를 교차검증하고 다른 팀원과 합을 맞춘 뒤 시작합니다. 정합성을 위한 검증 비용은 어쨌거나 비쌉니다. 그 비용을 매번 사람이 치를지, 문서 구조에 넣어 둘지의 선택이 메모리 아키텍처입니다.
해결 방안, 시스템을 결정하고 적용한다
모든 문서에 상태 라벨 강제
모든 마크다운 파일 맨 위에 상태 라벨을 다는 규칙을 정했습니다. 라벨은 frontmatter 로 표현됩니다. 마크다운 파일 맨 위에 --- 로 감싸 YAML 형식 (들여쓰기로 정보를 구조화하는 형식) 으로 메타데이터를 적는 블록입니다. 모양은 이렇습니다.
---
status: active # active / draft / pending / archived / superseded
type: decision
updated: 2026-04-29
---
status 값 하나만 표시되어 있어도, 사람과 에이전트 모두 이 문서를 지금 기준으로 봐도 되는지를 한 줄에 판단할 수 있습니다.
active. 현재 기준 문서.draft. 작성 중. 기준으로 쓰면 안 됨.pending. 결정 보류 중. 확정된 사실처럼 읽으면 안 된다는 신호.archived. 더 이상 활성은 아니지만 기록용 보존.superseded. 다른 문서로 대체됨. 어느 문서로 대체됐는지는 별도 필드 (superseded_by) 로 가리킴.
같은 사실의 두 버전이 갈라져도, 어느 쪽이 살아 있고 어느 쪽이 죽었는지가 frontmatter 한 줄에 명시됩니다.
frontmatter 는 사람에게는 작은 라벨이고, 에이전트에게는 판단 단서입니다. "이 문서가 어떤 종류인지", "현재 유효한지", "언제 갱신됐는지", "무엇과 관련 있는지" 를 본문을 읽기 전에 먼저 알려 줍니다. 이 한 줄이 없으면 에이전트는 모든 .md 파일을 비슷한 문서로 봅니다. 이 한 줄이 있으면 최소한 draft 와 active 를 구분할 수 있습니다.
상태 라벨이 정답을 자동으로 만들어 주지는 않습니다. 어떤 문서가 active 인지는 사람이 결정해야 합니다. 다만 한 번 결정한 뒤에는 그 결정이 파일 안에 남습니다. 다음 세션의 에이전트와 다음 팀원이 같은 기준으로 출발할 수 있습니다.
솔직히 코드는 거의 안 썼습니다
여기까지 읽으면 시스템 깐다는 게 결국 다 손으로 frontmatter 노가다 해야 하는 일 아니냐 싶을 수 있는데, 제가 직접 코드를 쓴 부분은 거의 없습니다. 한 일은 결정과 지시 정도가 전부였습니다.
대충 이런 내용을 정해 두었습니다.
**status값은 다섯 가지* (active/draft/pending/archived/superseded) *만 쓴다.\\- supersede 표시는 별도 필드
superseded_by로 가리킨다. 어느 문서가 어느 문서를 대체했는지 한 줄로 추적 가능하게 함.
이건 개발자에게 익숙한 schema 설계와 비슷합니다. 데이터베이스 컬럼을 먼저 정해 두면 그 뒤의 입력이 덜 흔들리는 것처럼, 문서에도 최소 schema 를 둔 것입니다. status, type, updated, related, superseded_by 같은 필드가 그 역할을 합니다. 문서가 자유롭게 흘러가게 두지 않고, 최소한의 타입을 부여한 셈입니다.
가져다 쓰실 풀버전 프롬프트 (범용)
본인 프로젝트에 같은 워크플로우를 그대로 옮길 수 있게, Codex, Claude Code, Grok, Cursor 어디서든 동일하게 동작하는 풀버전 프롬프트를 둡니다. 새 세션에 그대로 붙이고 docs/ (또는 본인 문서 폴더 경로) 만 본인 프로젝트 경로로 바꿔 시작하시면 됩니다.
전제 조건: 이 프롬프트는 이미 어느 정도 누적된 docs/ (또는 동등한 문서 폴더) 가 있는 프로젝트를 가정합니다. 빈 repo 거나 문서가 거의 없는 단계라면 흐름 자체가 다릅니다. 가령 raw 노트 더미를 자동으로 wiki 로 빌드하고 진화시키는 Karpathy 의 LLM Knowledge Base 시스템 프롬프트 같은 도구가 더 맞습니다. 본편 3편의 비교 블록에서 다시 짚습니다.
사전 준비 (선택): 코드 그래프 분석 MCP (예: code-review-graph, repo 의 호출관계, import, 테스트 연결을 미리 파싱해 두는 도구) 가 깔려 있으면, 아래 프롬프트의 「A. 스캔」 단계가 더 정확해집니다. 폴더 구조와 파일 간 의존성을 그래프로 미리 파악해 두니 충돌 탐지 정확도가 올라갑니다. 문서 정합성만 볼 때는 grep, find, Read 같은 기본 도구로도 충분히 시작할 수 있습니다.
역할: 프로젝트 문서 정합성 점검 에이전트.
대상: docs/ 폴더 (또는 사용자가 지정한 경로) 의 모든 .md 파일. 재귀적으로 스캔.
미리 정해진 컨벤션 (그대로 적용, 변경 금지):
1. status 값은 다섯 가지만 사용
- active: 현재 유효한 기준 문서
- draft: 작성 중. 기준으로 쓰지 말 것
- pending: 결정 보류. 확정 사실처럼 읽지 말 것
- archived: 활성 아니지만 기록용 보존
- superseded: 다른 문서로 대체됨
2. status 는 frontmatter (마크다운 맨 위 --- YAML 블록) 에 작성한다. 형식:
---
status: active
type: decision
updated: 2026-04-29
---
3. superseded 라벨이면 별도 필드 superseded_by: <대체 문서의 상대 경로> 도 추가.
작업 단계:
A. 스캔. docs/ 전체를 재귀적으로 훑고, 각 .md 파일의 첫 30줄과 기존 frontmatter (있으면) 를 기록.
- [code-review-graph](https://github.com/tirth8205/code-review-graph/blob/main/README.ko-KR.md) 같은 코드 그래프 분석 MCP 가 깔려 있으면 우선 사용 (호출관계, import, 테스트 연결까지 좁힘).
- 없으면 사용자에게 설치 의향을 한 번 물어보고, 거절하면 grep, find, Read 로 진행.
- 빈 repo 거나 .md 파일이 거의 없으면 진행 중단하고 사용자에게 보고: 「문서가 부족합니다. 정합성 점검 단계로 넘어가기 전에 먼저 문서 누적이 필요합니다」.
B. 충돌 탐지. 다음 4가지 패턴 위주로 찾는다:
- 같은 주제·기능에 두 문서가 정반대 결정을 적은 경우
- 같은 식별자 (이벤트 이름, API 이름, 화면 이름 등) 가 다르게 명명된 경우
- 미결정 사항인데 한쪽 문서가 확정 사실처럼 적은 경우
- 폐기된 결정·표현이 일부 문서에 옛 모습 그대로 남은 경우
C. 충돌 표로 정리. 다음 컬럼으로 한 번에 출력:
| # | 주제 | 문서 A (경로 · 핵심 한 줄) | 문서 B (경로 · 핵심 한 줄) | 차이 요약 |
추측에 의한 단정 금지. 사용자 결정 받기 전에 frontmatter 를 넣지 말 것.
D. 한 건씩 사용자에게 질문. 충돌 #1 부터 차례로:
> 충돌 #1, [주제]. 문서 A 와 B 중 어느 쪽이 active 입니까? 다른 쪽은 archived / superseded / pending 중 무엇입니까? superseded 라면 superseded_by 는 어느 파일입니까?
사용자 답변 받은 뒤 다음 건으로 진행. 한 번에 모두 묻지 말 것.
E. frontmatter 적용. 사용자가 결정한 status (필요 시 superseded_by) 를 각 문서 frontmatter 에 추가·갱신. 기존 frontmatter 가 있으면 필드만 머지, 없으면 새 블록 생성. updated 도 오늘 날짜로 갱신.
F. 변경 전 dry-run. 실제 파일 수정 전, 변경 예정 diff 를 보여주고 사용자 최종 확인을 받는다.
G. 요약 보고. 변경된 파일 목록과 각 파일의 새 status 를 표 한 개로 출력.
원칙:
- 사용자가 결정해야 할 것 (어느 게 active 인지) 은 절대 추측하지 말고 묻는다.
- 비밀값·고객 식별 정보가 frontmatter 에 들어가지 않게 한다.
- 이미 status 가 있는 문서는 사용자 확인 없이 덮어쓰지 않는다 (충돌 없으면 그대로 둘 것).
- 변경 전 git diff (또는 동등한 dry-run) 를 사용자에게 보여 검토 가능하게 한다.
이 프롬프트를 새 세션에 붙이면, 에이전트가 (1) docs/ 전체를 훑고, (2) 충돌 후보를 표로 정리하고, (3) 한 건씩 사용자에게 결정을 묻고, (4) 그 결정대로 frontmatter 를 채우는 흐름이 자동으로 굴러갑니다. 제가 직접 한 일은 충돌 #1 부터 차례로 「이건 superseded, 이건 active, 이건 pending」 식으로 한 줄씩 답하는 것뿐이었습니다. 폴더 전체 훑기, 표 정리, frontmatter 작성은 다 에이전트가 합니다.
사람은 판단 기준을 정하고, 에이전트가 그 기준을 적용한다. 메모리 아키텍처가 실제로 굴러가는 모양입니다. 코드 한 줄 직접 안 써도, 판단 기준 (status 값, 필드 형식, workflow) 만 분명하면 시스템은 굴러갑니다. 메모리 아키텍처는 많은 코드를 쓰는 일이 아니라 판단 기준을 분명히 정해 두는 일에 가깝습니다. 코드 안 쓰는 바이브코더에게도, AI 에 관심 많은 비개발자에게도 그대로 적용됩니다.
자동화는 본편 3편에서
위 결정과 실행 흐름을 강제하는 자동화는 본편 3편에서 한 글에 묶어 다룹니다. 훅으로 frontmatter 누락을 commit 시점에 막거나, 정기 검증 루틴으로 stale 문서를 발견하거나, Memory.md 가 잘못 덮어쓰였을 때 복구하는 자동 백업 같은 장치들입니다.
정작 좋아진 건 답 품질이 아니라 검토 가능성이었습니다
이 처방을 깐 직후의 가장 큰 변화는, 답변 품질이 좋아졌다기보다 검토 가능성이 좋아졌다는 점이었습니다.
예전에는 에이전트가 이상한 답을 하면 첫 반응이 「모델이 또 왜 이러지」 였습니다. 모델을 의심하고, 프롬프트를 다시 다듬고, 컨텍스트를 더 넣어 보고. 답이 어디서 왔는지 추적할 수 없으니 모든 게 추측이었습니다.
상태 라벨을 달기 시작한 뒤로는 첫 질문이 바뀌었습니다. 에이전트가 어떤 문서를 어떤 상태로 읽었는가? 입니다.
active 문서만 읽었는가, archived 문서가 함께 들어갔는가, superseded 표시가 어디 있었는가. 답이 틀려도 어디서 어떻게 틀렸는지를 한 번에 추적할 수 있습니다. 답 자체가 좋아졌다기보다 답의 근거를 짚을 수 있게 된 쪽이 훨씬 큰 변화였습니다.
LLM 의 답이 오락가락할 때 모델을 의심하던 데서, 저장소의 상태를 의심하는 자리로 옮겨 가는 것. 이게 메모리 아키텍처라는 층을 인정한다는 것의 실제 의미였습니다.
남는 것
결론은 단순합니다. 에이전트의 답이 흔들리기 시작하면 가장 먼저 의심해야 할 건 모델이 아니라 우리 저장소입니다.
프롬프트를 더 잘 쓰자와 작업대 (하네스) 를 더 잘 꾸리자 사이에, 에이전트가 무엇을 어느 순서로 읽을지를 정하는 단계가 따로 있습니다. 이 시리즈에서 메모리 아키텍처라고 부르는 층입니다.
본인 프로젝트에서 어느 날부터 에이전트가 같은 질문에 다른 답을 내놓기 시작한다면, 모델 의심 전에 같은 사실이 두 군데에 다른 모양으로 적혀 있지 않은지 한 번 검색해 보세요. 14개까지 한꺼번에 잡히지는 않더라도, 한두 개는 거의 항상 발견됩니다.
발견된 것에 status 라벨 한 줄씩 다는 것. 그게 메모리 아키텍처의 첫 단추입니다.
다음 글 (본편 2편) 에서는 그 충돌들을 어디다 어떻게 묶어서 혼선이 생기지 않도록 했는지를 봅니다. 위에서 본 평범한 docs/ 폴더가 어떻게 정리됐는지, 폴더 트리와 명명 규칙으로 들어가보겠습니다.
'AI 메모리 관리 시스템 (연재 중)' 카테고리의 다른 글
| [AX 사내도입까지? 시즌 2-01] Dorito's AI Agent Memory System (진행 중) (1) | 2026.06.24 |
|---|---|
| [AI 메모리 시스템 적용기 회고:03편] AI 메모리는 왜 다시 무너지는가 — 자동화와 복구 루틴 (5) | 2026.05.03 |
| [AI 메모리 시스템 적용기 회고:02편] 위키 관리도 자동화하고 사람의 실수를 막자! karpathy llm wiki 위에 얹은 자동화 시스템 (1) | 2026.05.02 |
| [AI 메모리 시스템 적용기 회고:프롤로그] AI를 잘 쓰려면, 프롬프트보다 먼저 메모리를 설계해야 한다 (4) | 2026.04.26 |