
AI 에이전트의 기억을 git으로 관리했더니 스테이터스 칸만 부패했다
요약
AI 에이전트의 기억을 Git 기반 Markdown으로 관리할 때 발생하는 데이터 부패 문제를 다룹니다. 정보의 변화 속도에 따라 정본(Source of Truth)을 분리하여, 정적인 지식은 Git에, 동적인 상태 정보는 태스크 관리 툴에 두는 설계 전략을 제안합니다.
핵심 포인트
- 데이터의 변화 속도(부패 속도)에 따라 정본을 분리해야 함
- 변하기 쉬운 정보(상태, 기한)를 Git에 강제하면 반드시 부패함
- System of Record(기록)와 System of Engagement(참여)의 괴리 해결
- 규율로 인간의 행동 패턴을 강제하는 설계는 실패할 가능성이 높음
정본(Source of Truth)을 하나로 유지하는 것을 그만두고, 「부패 속도」로 나눈 이야기
사내용 AI 에이전트를 운용하고 있습니다. 역할은 질문에 답하는 것이 아니라, 눈앞의 태스크가 어떤 시책의 어떤 목적으로 이어지는지를 거슬러 올라갈 수 있게 하는 것입니다.
이를 위해 에이전트의 기억(이하 「뇌」)을 git 관리되는 Markdown으로 갖추게 했습니다. 수개월간 운용해 보니, 스테이터스(Status) 칸만 부패했습니다. 이 기사는 왜 부패했는지, 정본(source of truth)을 어떻게 나누었는지, 그리고 그 과정에서 밟았던 지뢰에 관한 이야기입니다.
먼저 말씀드리자면, 이 구조 자체에 새로움은 없습니다.
목적: 왜 하는가.
└ 영역: 목표를 짊어지는 단위.
└ 시책: 구체적인 노력.
...
계층의 명칭은 조직에 따라 바꿔 읽어주세요. 중요한 것은 단계의 이름이 아니라, 부모가 하나로 결정된다는 점입니다. 노드는 1개당 1파일(Markdown + frontmatter)이며, parent에 부모를 적습니다(=어느 노드에서도 목적까지 일직선으로 거슬러 올라갈 수 있음).
---
node: initiative
id: project-a
...
frontmatter + 1파일 1노드는 정적 사이트 생성기(Static Site Generator)의 표준이며, 부모 링크로 계층을 만드는 것은 Dendron 등의 PKM 툴이 하고 있는 일이고, 목적에서 시책으로의 누적은 목표 관리 툴이 전부 하고 있습니다.
이 기사에서 쓰고 싶은 것은, 이 평범한 구조를 선택한 결과로서 무엇이 망가졌고, 어디에 선을 그었는가입니다.
운용을 시작하자마자 깨닫습니다. 구조·목적·결정·지식은 건전하게 자라나는데, 스테이터스와 기한과 Next(다음 단계)만 언제 봐도 오래되어 있습니다.
원인은 기술이 아닙니다. 팀은 매일 태스크 관리 툴을 만집니다. 거기서 스테이터스를 움직이고, 기한을 다시 설정하고, 다음 수를 적습니다. git의 Markdown을 열어 status:를 고쳐 쓰는 사람은 단 한 명도 없었습니다. 저 자신도 고쳐 쓰지 않았습니다.
여기서 말할 수 있는 것은 하나입니다.
사람이 매일 작업하는 면이, 선언된 정본을 무시하고 사실상의 정본이 된다.
Geoffrey Moore가 말하는 System of Record(기록의 정본)와 System of Engagement(매일 관여하는 면)의 괴리 그 자체입니다.
그리고 중요한 것은, 이 상황에서 취할 수 있는 최악의 수가 「규율」이라는 것입니다. "뇌가 정본이므로 Markdown도 업데이트해 주세요"라는 말은, 설령 밀어붙일 수 있다 하더라도 밀어붙이는 비용을 영구적으로 지불하게 합니다. 게다가 문서 측이 부패해 있는 동안, 에이전트는 오래된 정보로 추론합니다. 규율로 인간의 중력에 저항하는 설계는 패배합니다.
일원화를 포기했습니다. 다만 포기하는 방식을 정했습니다. 필드의 변화 속도로 정본을 나눕니다.
| 필드군 | 정본 | 방향 |
|---|---|---|
| 구조·Why·목적·결정·지식 | 뇌 (변하기 어려움 / 에이전트의 추론 기반 / git 관리가 효과적) | 뇌 → 하류 |
| 스테이터스·기한·Next | 태스크 관리 툴 (변화가 빠름 / 현장이 매일 만짐) | 하류 → 뇌 |
선을 그을 때의 판단 기준이, 이 기사에서 가장 가져가셨으면 하는 부분입니다.
「어느 쪽이 유서 깊은가」가 아니라 「어느 쪽이 먼저 부패하는가」로 결정한다.
변하기 어려운 것을 git에 두면, 차분 리뷰(diff review)와 이력과 CI가 효과적입니다. 변하기 쉬운 것을 git에 두면 반드시 부패합니다. 반대로, 변하기 어려운 것을 태스크 관리 툴에 두면, 왜 그 판단을 했는지에 대한 문맥(Why)이 상실됩니다. 각각의 면이 잘하는 부패 속도가 따로 있다는 관점입니다.
「정본은 하나여야 한다」는 규범은, 아마 변화 속도가 균일한 데이터를 전제로 하고 있을 것입니다. 하나의 노드 안에 「년 단위로 변하지 않는 목적」과 「매일 변하는 Next」가 공존하고 있는 시점에서, 그 전제는 무너져 있습니다.
정본을 나누기만 하면, 이번에는 뇌가 에이전트에게 오래된 것이 됩니다. 괴리의 방향이 반전되었을 뿐입니다. 에이전트가 1개 파일을 읽으면 「의도」와 「현재 상황」 모두를 알 수 있다는 성질은 버리고 싶지 않습니다.
그래서 뇌의 각 노드에 마커로 둘러싸인 기계 소유 블록(Machine-owned block)을 두고, 동기화 스크립트가 그 안만을 덮어씁니다.
<!-- exec:begin (하류 툴 유래・자동 업데이트・직접 쓰지 않음) -->
## 실행 스냅샷 (하류 툴 유래・자동 업데이트)
> 실행계 (스테이터스 / 기한 / Next)의 정본은 태스크 관리 툴. `sync-down`이 덮어쓰므로 **직접 편집하지 마세요** (다음 동기화 시 삭제됨). 의도·성과물·구조·지식의 정본은 뇌 (이 블록의 외부).
...
이것 또한 빌려온 것입니다. Ansible의 # BEGIN ANSIBLE MANAGED BLOCK이나, 생성된 코드의 DO NOT EDIT 헤더와 같은 발상입니다. 새로운 점은 이것을 「LLM 기억의 정본 분계 (Source of Truth Boundary)」로 사용했다는 것뿐입니다.
마커 문자열 자체는 무엇이든 상관없지만, 한 번 정하면 바꾸지 말아야 합니다. 블록의 동일성을 이 문자열로 판정하기 때문에, 변경하면 기존 블록을 감지하지 못해 동일한 노드에 신구(新舊) 두 개의 블록이 나란히 놓이게 됩니다 (그리고 오래된 쪽은 아무도 업데이트하지 않으므로, 조용히 부패합니다).
구현에서 효과를 본 것은 세 가지였습니다.
동기는 마커 사이만 치환합니다. 사람과 에이전트가 작성하는 의도·구조·지식은 대상에서 제외합니다.
def upsert_execution_block(text, snapshot):
"""반환값 (새 텍스트, 변경 여부). **블록 외부는 일절 건드리지 않는다**."""
block = render_execution_block(snapshot)
...
이것은 페일 세이프 (fail-safe) 설계입니다. 동기가 깨지더라도 잃는 것은 자동 생성 부분뿐입니다. 사람이 작성한 문장이 기계에 의해 파괴되는 경로를 만들지 않습니다.
생성 블록에는 「최종 동기: 2026-07-30 20:03」과 같은 내용을 넣고 싶어집니다. 넣어서는 안 됩니다.
넣게 되면 내용이 바뀌지 않아도 매번 차분 (diff)이 발생합니다. git의 이력이 노이즈로 가득 차서 「의미 있는 변경」을 볼 수 없게 됩니다. 그것만으로도 충분한 이유지만, 더 큰 대가가 있습니다. 멱등성 (idempotency)이 보장되지 않으면, 드리프트 검사 (drift check)를 원리적으로 작성할 수 없습니다.
현재 동기에는 --check 옵션이 있어, 「커밋된 내용 == 지금 하류(downstream)에서 생성한 내용」을 비교하여 어긋나 있으면 종료 코드 1을 반환합니다. CI 게이트로 사용할 수 있습니다. 만약 시각 정보가 들어있다면, 이 비교는 항상 불일치하게 되어 검사 자체가 성립하지 않습니다.
기계가 쓰는 파일은 동일한 입력이라면 항상 동일한 출력을 내놓아야 합니다. 그렇게 해두어야 「차분이 발생했다」는 사실이 곧 「실제로 무언가 변했다」는 시그널이 됩니다.
여기가 본론입니다.
「Next (다음 할 일)」는 팀의 누구라도 쓸 수 있는 자유 기술 항목입니다. 블록 추출은 마커 사이의 비탐욕 매칭 (non-greedy match)으로 수행하고 있었습니다.
_EXEC_RE = re.compile(re.escape(EXEC_BEGIN) + r".*?" + re.escape(EXEC_END), flags=re.DOTALL)
만약 누군가 Next란에 -->를 포함한 문자열을 쓴다면 어떻게 될까요? 비탐욕 매칭이 주입된 가짜 종료 마커에 의해 끊겨버립니다. 결과적으로 그 이후의 텍스트가 블록 외부, 즉 사람과 에이전트가 작성하는 영역에 남게 됩니다. 게다가 다음 동기 시 블록 내부만 덮어쓰기 때문에, 밖으로 나간 텍스트는 아무도 지우지 않습니다.
정리하자면, 이런 경로가 열려 있었습니다.
태스크 관리 도구의 자유 기술란에 글을 쓸 수 있는 사람이 AI 에이전트의 기억에 영구적인 텍스트를 남길 수 있다.
에이전트는 그 파일을 읽고 판단합니다. 영속화되는 프롬프트 인젝션 (prompt injection)입니다. 악의를 가정하지 않더라도, --> 할 일이라고 쓴 사람이 있다면 같은 일이 일어납니다.
대처법은 세 줄이었습니다. 구분자를 형성할 수 없도록 파괴하는 것입니다.
def _neutralize_markers(value: str) -> str:
"""기계 소유 블록의 마커를 외부 유래 텍스트로부터 무해화한다 (fail-safe의 핵심)."""
return value.replace("<!--", "<!- -").replace("-->", "-- >")
이스케이프 (escape)가 아니라 파괴입니다. 표시가 조금 깨질 수는 있지만, 외부 입력이 구조를 만들 수 없다는 것이 보장됩니다. 동기에서 다루는 외부 유래 값(상태·기한·Next) 모두에 이 처리를 적용했습니다.
테스트는 동작 그 자체를 고정했습니다.
test_render_block_neutralizes_injected_end_marker
test_upsert_injected_marker_leaves_nothing_outside_block
test_upsert_remains_idempotent_with_marker_bearing_next
...
두 번째 (leaves_nothing_outside_block)가 본질입니다. 「무해화되었다」는 것이 아니라 「블록 외부에 아무것도 새어 나오지 않았다」는 것을 검사합니다. 세 번째는 무해화 처리를 넣어도 4.2의 멱등성이 깨지지 않음을 확인하는 것입니다.
일반화하면 다음과 같습니다.
기계 소유 영역(Machine-owned domain)에 외부 입력을 흘려넣는다면, 영역의 구분자(delimiter)를 반드시 파괴해야 합니다.
LLM에 읽힐 파일을 기계 생성(machine-generated)하는 사람들은 모두 이 경로를 가지고 있습니다. 생성 대상이 Markdown이라면 HTML 주석을, YAML이라면 ---를, JSON에 임베딩하는 Markdown이라면 코드 펜스(code fence)를 사용합니다. "구조를 만드는 문자열"이 외부 입력에 섞여 들어갈 수 있는지 한 번 확인해 보시기 바랍니다.
- 정본(Source of truth)은 하나일 필요가 없습니다. 필드의 변동 폭(variance)으로 나눕니다. 판단 기준은 "어느 쪽이 옳은가"가 아니라 "어느 쪽이 먼저 부패하는가"입니다. 사람이 매일 만지는 면은 선언을 무시하고 사실상의 정본이 됩니다. 규율로 이를 거스르는 설계는 패배합니다.
- 정본을 나누었다면, 기계 소유 블록(machine-owned block)으로 다시 꿰매어 붙입니다. 블록 내부만 기계가 덮어쓰고, 외부는 사람의 영역으로 둡니다. 페일 세이프(fail-safe)의 방향을 처음에 결정해야 합니다.
- 기계가 쓰는 파일은 멱등성(idempotency)을 유지해야 합니다. 시간을 기록하지 않는 것만으로도, "커밋됨 == 지금 생성한 내용"이라는 드리프트(drift) 검사를 CI 게이트로 작성할 수 있게 됩니다.
- 외부 유래 텍스트로부터는 구분자를 파괴합니다. 이스케이프(escape)가 아니라 파괴입니다. 이것이 없으면, 하류(downstream) 툴에 문자를 쓸 수 있는 사람이 에이전트의 기억에 영구적인 지시를 남길 수 있습니다.
계층을 어떻게 설계할지가 가장 화려해 보이지만, 실제로 효과가 있었던 것은 2~4번의 수수한 부분이었습니다. AI 에이전트에게 구조를 부여하는 작업의 대부분은 의미를 부여하는 것이 아니라, "누가 어디를 쓰는가"의 경계 긋기였다는 것이 몇 달간 작업하며 느낀 실감입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기