유대선
프로젝트로
·기술 회고·18

닮은꼴 ≠ 모순: 기억 충돌 탐지를 코사인에서 LLM 심판까지 (하루치 상세 정리)

AI 에이전트가 읽는 '운영 기억'이 서로 모순되는 걸 어떻게 막을까. 코사인 유사도로 시작해 그게 모순을 판정 못 한다는 걸 실데이터로 깨닫고, claude -p 'LLM 심판'으로 넘어간 하루. 4겹 방어, 각 층이 언제 작동하는지, 그리고 '모순을 확실히 0으로 만드는 건 불가능하다'는 정직한 결론까지.

이 글은 "나중에 봐도 처음부터 이해되게" 쓰는 게 목적이다. 개념 기초 → 만든 것 → 핵심 깨달음 → 한계 → 다음 순서. 바쁘면 바로 아래 ⭐ 최중요만 읽어라.


⭐ 최중요 (이거 4개만 기억하면 됨)

🔑 1. "코사인 유사도"는 닮은꼴만 찾지 모순을 판정 못 한다. "가격은 월 20달러" vs "가격은 월 40달러"는 텍스트가 92% 비슷(코사인 0.92)해서 시스템이 "충돌!"이라 찍었다. 근데 이건 다른 제품 두 개의 가격일 수도 있다. 코사인은 "비슷하게 들린다"만 알지 "같은 걸 두고 서로 배타적 주장을 한다"를 모른다.

🔑 2. 그래서 진짜 판정은 LLM이 한다 (cosine = 싼 사전필터, LLM = 진짜 판사). claude를 불러 두 기록을 읽히고 "같은 대상에 대한 진짜 모순이냐?"를 물었다. 위 20/40 케이스에 claude는 "상품명이 없어 같은 대상의 가격 변동인지 다른 상품의 가격인지 판단 불가능" 이라 답했다 — 코사인의 오탐을 정직하게 걸러냈다.

🔑 3. 어떤 방법으로도 "모순 0"은 못 만든다. 줄일 뿐이다. 탐지가 불완전하고(말이 다르면 못 잡음), 사람/에이전트가 옳게 정리한단 보장도 없고, "모순"은 끝없는 의미 문제다. 그래서 목표는 "왜곡 0"이 아니라 "근거 있고·라벨되고·인용되고·근거 없으면 거부하고·고칠 수 있는" 기억이다.

🔑 4. 방어는 한 겹이 아니라 4겹이다 (이른 순서대로).쓸 때(write-시점, 누적 예방) → ② 답할 때(read-반응형, 보험) → ③ 사람이 검토(수동 패널) → 그리고 ④ 후보를 LLM 심판이 거른다. 한 겹이 놓치면 다음 겹이 잡는다.


1. 왜 이걸 했나 — 문제부터

딸깍의 핵심은 "AI가 일하고, 너는 통제한다"이고, 그러려면 AI 에이전트가 과거 맥락(운영 기억)을 계속 들고 가야 한다. 그래서 우리는:

문제는 — 기억이 틀어지면(왜곡되면) 에이전트가 확신에 차서 틀린 답을 한다. 이게 제품을 죽이는 시나리오다. 예: 기억에 "가격 $20"(옛날, 뒤집은 결정)과 "가격 $29"(현재)가 둘 다 있으면, 에이전트가 조용히 $20을 골라 "가격은 $20입니다"라고 단정한다.

이런 모순은 시간이 지나면 반드시 쌓인다 — 결정이 번복되고(Fly→Vercel 이전), 용어가 바뀌고(Project→Service), 계획이 보류된다. 그래서 "충돌 탐지"가 필요했다.


2. 개념 기초 (이거 알아야 나머지가 읽힘)

임베딩(embedding): 문장을 숫자 벡터(여기선 384차원)로 바꾸는 것. 비슷한 의미의 문장은 비슷한 벡터가 된다. 우리는 네트워크 없는 로컬 모델(MiniLM)을 써서 무료·즉시 돌린다.

코사인 유사도(cosine): 두 벡터가 얼마나 같은 방향인지 0~1로 잰 값. 1에 가까울수록 "텍스트가 비슷"하다. 여기서의 함정: 이건 "주제/표현이 비슷"이지 "사실이 모순"이 아니다. (이게 오늘의 핵심 교훈.)

클러스터링(union-find): 비슷한 것들끼리 묶는 것. "A와 B가 비슷, B와 C가 비슷" → A·B·C 한 묶음. 임계값(threshold)을 낮추면 더 느슨하게(많이) 묶고, 높이면 빡빡하게(적게) 묶는다.

supersede(낡음 처리): 옛 기억을 삭제하지 않고 "낡음" 엣지를 달아 앞으로의 검색·답변에서 빠지게 하는 것. 되돌릴 수 있다(원본 그대로). 이게 우리의 교정(correction) 방식.

provenance(출처/신뢰): 각 기억이 어디서 왔는지 — git(커밋), user(사람), agent(에이전트가 기록), 외부 커넥터(gmail 등). 외부는 신뢰하지 않는다(주입 방어).


3. 만든 것 — 모순 방어 4겹

각 층이 언제 작동하는지가 핵심이다.

③ 수동 패널 (가장 먼저 만든 것, memory_conflicts)

사람이 ⌘⇧C로 열면, 활성 서비스의 기억을 코사인으로 클러스터링해서 "거의 같은 말 하는 묶음"을 보여준다. 사람이 "현재 것"을 고르면 나머지는 supersede.

🐛 첫 dogfood가 잡은 버그: 실데이터로 열어보니 묶음이 전부 Gmail에서 가져온 광고/알림 메일이었다. 충돌 탐지가 운영 기억이 아니라 받은편지함을 dedup하고 있었다. 원인: 메일이 node_type: issue로 들어왔고 필터가 외부 노드를 안 걸렀음. 또 append 로그에 같은 node_id가 중복돼 한 묶음에 같은 메일이 두 번 떴다. 수정(46ff1cd): 후보에서 is_external_untrusted 노드(gmail 등) 제외 + node_id 중복 제거. → 진짜 운영 기억만 스캔.

② read-반응형 (reactive_conflict_note)

에이전트(또는 인앱 라이브러리언)가 기억으로 답을 만들 때, 끌어온 기억들끼리 충돌이 있으면 답변 프롬프트에 ⚠️ 경고를 끼워 넣는다. "이 둘 안 맞음, 단정하지 말고 밝혀라."

① write-시점 (resolve_write_conflict)

기억이 기록되는 순간 기존과 비교해서:

🔧 곁가지 교훈: conflicts_with 엣지를 추가하려는데 secret-guard(시크릿 탐지 훅)가 막았다. 알고 보니 가드가 자기 자신의 시크릿 탐지 코드(token_len = ... 변수, 이름에 'token' 포함)를 시크릿으로 오탐한 것. --no-verify 우회는 auto-classifier가 막아서(맞는 통제), 가드의 정식 allowlist 마커(dalkkak-secret-guard: allow)로 그 라인을 처리했다. 보안 통제를 끄지 말고, 그 통제가 제공하는 정식 예외 메커니즘을 써라.

④ LLM 심판 (memory_judge_cluster) ← 오늘의 하이라이트

위 ①②③은 전부 코사인으로 충돌을 찾는다. 근데 코사인은 모순을 판정 못 한다(아래 §4). 그래서 후보가 잡히면 claude -p(BYO)에게 두 기록을 읽혀서 진짜 판단을 시킨다.


4. 핵심 깨달음 — "코사인은 모순을 판정 못 한다" (실증)

이게 오늘 제일 중요하게 배운 것이다. 테스트로 직접 확인했다.

테스트: Memory Lab에 가격은 월 20달러, 가격은 월 40달러 두 노드를 넣었다.

LLM 심판에 같은 걸 물었더니:

🟡 판단 불가 — 대상 불명 · different_subject "상품명이 없어 같은 대상의 가격 변동인지 다른 상품의 가격인지 판단 불가능"

→ claude가 **"이건 진짜 모순인지 내가 못 정해"**라고, 코사인의 오탐을 정직하게 걸러냈다. 이게 LLM 심판의 전부다:

코사인 (싸다·즉시·멍청)  = 1000개 → 후보 몇 개로 좁힘

LLM 심판 (느림·똑똑)     = "같은 대상 진짜 모순?" → 진짜만 통과, 모르면 '모름'

(진짜 모순의 예: "백엔드는 Fly에 배포" vs "Fly 접고 Vercel로 이전" → LLM: 같은 대상·reversal·최신=Vercel이 현재.)


5. 정직한 한계 (LLM 심판도 만능 아님)

결론: 어떤 기계도 모순을 없애지(0) 못한다. 크게 줄일 뿐. 그래서 진짜 방어선은 충돌 탐지가 아니라 — grounding(근거)+인용+근거없으면 "근거 없음"으로 거부+외부=불신 펜스+supersede(교정). 이게 본체고, 충돌 탐지 4겹은 그 위의 보강이다.


6. 안 한 것 (의도적) + 다음

남은 고려사항 6개를 짚었고, 대부분 "진짜 모순이 쌓였을 때만" 아프다. 지금 메모리는 깨끗하다(높은 임계값에서 충돌 0건). 그래서 일부러 안 만들었다:

그리고 "에이전트가 모순 쓰면 그 자리서 묻기"(가칭 C-write): 지금 에이전트는 <dk-node> 블록을 일방적으로 뱉고 백그라운드 워커(poll_once)가 나중에 줍는 구조다. 그래서 쓰는 순간 동기적으로 묻는 접점이 없다. 진짜로 그러려면 에이전트가 MCP 도구로 써서 호출이 충돌을 즉시 리턴하게 구조를 바꿔야 한다(큰 변경). 이것도 보류 — 모순을 확실히 없애주지도 않고, 아직 그 문제가 안 생겼다.

➡️ 다음 = 코드가 아니라 dogfood. 며칠 실제로 쓰면서 **"에이전트가 옛 기억 때문에 답을 틀리게 한 사례"**가 한 번이라도 나오나 본다. 나오면 → 그게 위 A~F / C-write 중 그 사례가 가리키는 것이 필요하다는 증거. 안 나오면 → 지금 걸로 충분.


7. 메타 교훈 (과정에서 배운 것)


이번 세션 커밋 지도