← Notes

큐는 비우고, raw는 지우지 않는다

AI 에이전트 생태계 구축기· 8 / 10
  1. 1 — 완료는 실측으로만 말한다
  2. 2 — 같은 함정, 세 번째는 다른 얼굴로
  3. 3 — 테스트가 통과했다고 완료는 아니다
  4. 4 — 속 빈 강정 — 함수는 있는데 왜 안 불렸나
  5. 5 — 하루 만에 지은 나의 두 번째 뇌
  6. 6 — 조직보다 자산 먼저 — 두 번 실패하고 남은 3원칙
  7. 7 — AI의 판단력은 복리로 자라지 않는다
  8. 8 — 큐는 비우고, raw는 지우지 않는다
  9. 9 — 하나의 비서를 세 개의 뇌로 나눈 이유
  10. 10 — 허위 적격 0 — 애매하면 통과시키는 필터

받은편지함과 서고, 비우는 규칙이 정반대인 두 저장소

개인 지식 파이프라인 second를 몇 달째 손보며 가장 자주 걸려 넘어졌던 지점은 “처리 대기 중인 것”과 “이미 확정된 것”을 같은 창고에 두려는 습관이었다.

(queue)는 처리되면 비워지는 저장소, raw는 절대 지우지 않는 저장소다.

second는 AI 개발 세션의 대화 전문을 훅이 감지해 큐에 적재하고(수확), 사람이 부르면 큐의 전문에서 알맹이만 뽑아 raw에 쌓고(증류), raw 전체를 다시 위키로 엮는다(컴파일). 전 과정은 수확(harvest) → 증류(distill) → 컴파일(compile) → 린트(lint) → 질의(query) 5단계지만, 실제로 사람이 외우는 명령은 “정리해”(증류+컴파일이 한 번에 일어남)와 “점검해”(린트) 둘뿐이다. 큐와 raw가 서로 다른 규칙으로 움직이기 때문에, 이 두 단어만으로 나머지 단계를 손대지 않고도 파이프라인 전체가 굴러간다.

수확은 사건에 반응할 뿐 내용을 못 가린다

세션이 컴팩트되거나 종료되는 이벤트가 발생하면 무조건 훅이 큐에 한 줄을 적는다. “유의미한 작업을 했는가”를 판단할 능력은 훅에 없다. 그래도 문제가 되지 않는 이유는 큐의 성격에 있다 — 큐는 전문의 ‘위치’만 기록하는 포인터라, 증류 단계에서 내용이 없다고 판단되면 그 줄은 자동으로 버려진다. 즉, 큐의 노이즈가 위키의 노이즈로 이어지지 않는다.

그래도 헛수고를 줄이려고 입구에서 세 가지를 제외하고, 한 가지는 일부러 거르지 않는다.

  • second 자기 세션 제외: second를 다루는 세션이 second 큐에 쌓이면 무한루프가 되므로 순환을 막는다.
  • 홈 디렉토리 자체 제외: $HOME 판정으로 걸러내되 이름을 하드코딩하지 않아 다른 PC로 옮겨도 안전하다. 단 하위 프로젝트 폴더는 대상에서 빼지 않는다.
  • 다른 도구의 dot-dir 제외: ~/.hermes 같은 다른 도구의 내부 폴더는 그 도구를 건드리지 않기 위해 제외한다.
  • 크기 필터는 채택하지 않음: 짧은 대화에도 좋은 통찰이 있을 수 있다는 이유로, 세션 길이로 거르는 방식은 의도적으로 배제했다.

큐는 오프셋으로, raw는 불변으로

큐와 raw는 계층 명사로 보면 인접해 보이지만, “비우는 규칙”이 정확히 반대다.

지우나?재처리?추적 장치
(받은편지함)소화분은 큐에서 빠짐미소화분만.queue/digested.json(세션별 소화 지점 offset)
raw(서고)절대 안 지움(불변)이미 반영된 건 재컴파일 안 함위키 sources: 필드(반영 여부)

세션이 살아있어 컴팩트를 반복하며 큐에 여러 번 찍혀도, 다음 증류는 그 새 대화분만 처리한다 — 같은 세션이 중복 증류되지 않는다. raw 쪽은 지우는 대신 “이미 위키에 반영됐는가”만 추적하므로, 원본 자체에서는 재처리도 유실도 일어나지 않는다.

왜 하필 정반대여야 하는가

만약 큐도 raw처럼 지우지 않는다면, 이미 소화한 세션의 옛 대화분이 계속 큐에 남아 매번 재검토 대상이 된다. 반대로 raw를 큐처럼 비운다면, 한 번 위키에 반영됐다고 지운 원본을 나중에 다른 관점으로 다시 참조할 방법이 없어진다. 처리 대기열과 불변 로그를 같은 저장소로 섞으면 재처리 오염(같은 걸 두 번 반영)이나 유실(처리했다고 착각하고 지움) 중 하나가 반드시 생긴다. 큐와 raw를 물리적으로 분리한 것은 취향이 아니라 이 두 실패 모드를 동시에 막기 위한 최소 조건이다.

컴파일은 raw 전체를 매번 훑지 않는다 — 집합 차집합으로 신규분만

raw가 쌓일수록 컴파일이 매번 전체를 다시 읽는다면 시간이 raw 크기에 비례해 늘어난다. second는 이 문제를 순수 집합 연산으로 피한다.

  1. 위키 페이지의 frontmatter sources: 필드에 그 페이지가 어느 raw 경로에서 나왔는지 기록해둔다.
  2. 컴파일 시점에 raw 경로 전체 목록에서 위키 sources:에 이미 박힌 경로 목록을 뺀다. 그 차집합이 “미반영 신규 raw”다.
  3. 신규 raw만 위키에 엮는다. 기존에 이미 반영된 나머지는 건드리지 않는다.
  4. 새 raw가 기존 페이지와 같은 주제라면 새 페이지를 만들지 않고 기존 페이지를 갱신한다(새 사실 추가, sources에 경로 추가, 변경이력 1줄 추가) — 한 주제가 여러 페이지로 흩어지지 않고 한 페이지로 자란다.

이 방식은 연습용 second에서 이미 검증된 것이기도 하다. “ingest” 로직의 골자가 find raw 결과에서 grep raw/ wiki/ 결과를 빼는 3줄짜리 집합 연산으로 충분했다.

확장성 — 10만 장이어도 버티는가

raw가 10만 장까지 쌓인다고 가정하고 시간·토큰·시점·대응을 따로 따져보면 지금 걱정할 문제가 아니라는 결론이 나온다.

  • 시간: 신규분을 골라내는 연산은 파일시스템·문자열 연산(find + grep + 집합 차집합)뿐이라 AI 호출이 0이다. 경로가 10만 개여도 1~2초 수준 — 규모와 무관하다.
  • 토큰: 신규 raw를 읽는 비용도, 갱신할 기존 페이지를 읽는 비용도 raw 총량이 아니라 이번 신규분·관련 몇 장에만 비례한다. 진짜 급소는 index.md(전체 카탈로그) 통독이다 — raw가 늘면 위키·인덱스도 커져 매 컴파일마다 그 큰 카탈로그를 읽는 비용이 쌓인다.
  • 규모별 판단: 수십수천 장(지금수년 치)은 문제가 없다. 하루 20장씩 쌓아도 13년이 걸리는 1만~10만 장 구간에 이르러서야 index가 아파지기 시작한다.
  • 최종 판단: 그 시점의 해법(index를 카테고리별로 분할하거나 로컬 검색 도구로 통독 자체를 없애는 것, 카파시(Karpathy)가 제시한 방식)은 이미 알려져 있다. 지금 코드로 미리 넣는 것은 과설계다 — 13년 뒤에나 닥칠 문제이고 그때는 도구 지형 자체가 바뀌어 있을 것이기 때문이다. 그래서 로드맵에 “raw가 수천 장에 도달하면 index 분할·검색도구 검토”라는 게이트 메모만 남겨두었다.

ETL 파이프라인에 이식하기

이 설계는 개인 지식 관리를 넘어 일반적인 ETL 파이프라인에도 그대로 옮겨 붙는다.

  • 오프셋 기반 증분 처리: 소스가 계속 갱신되는 스트림이라면(로그 파일, 이벤트 큐, 살아있는 세션 등) “어디까지 처리했는지”를 별도로 기록해두고 그 지점 이후만 처리한다. 소스 자체를 지우거나 표시하지 않아도 되니, 같은 소스를 다른 목적으로 다시 읽는 다른 소비자와 충돌하지 않는다.
  • 집합 차집합 기반 신규분 판별: 전체 목록에서 “이미 반영됨”으로 표시된 목록을 빼는 것만으로 신규분을 골라낼 수 있다면, 매번 전체를 다시 처리할 필요가 없다. AI 호출이나 복잡한 판단 로직 없이 파일시스템 연산만으로 끝나므로, 규모가 커져도 이 단계의 시간 비용은 거의 늘지 않는다.

즉, 이 둘을 합치면 멱등성(같은 입력을 다시 처리해도 결과가 같음)을 값싸게 확보하는 패턴이 된다. 받은편지함을 서고처럼 채워두면 처리 여부를 매번 다시 확인해야 하는 잡무가 쌓이고, 서고를 받은편지함처럼 비우면 나중에 필요한 원본을 다시 꺼낼 수 없다 — 두 저장소에 서로 다른 규칙을 주는 설계는 각 저장소가 맡은 역할에 정확히 맞는 수명 주기를 부여하는 일이다.

확장성 병목을 미리 정확히 짚어두면 대비책 없이도 안심하고 미룰 수 있다는 판단도 같은 맥락이다. 지금 당장 문제가 안 되는 걸 알면서 “언젠가 아플 지점”과 “그때의 해법”을 게이트로만 남겨두는 것은, 과설계와 무대책 사이의 중간 지대를 의도적으로 선택하는 태도다.

목차