Obsidian 플러그인이 긴 원문 하나를 LLM으로 여러 노트에 나눠 정리하면, 같은 원문에서 나온 노트들(이 글에서는 “형제 노트”라고 부른다)이 같은 사실을 서로 반복해 적는 중복이 생긴다. 그 중복을 잡으려고 문단을 임베딩 벡터로 바꿔 비슷한 쌍만 추리고, 추린 쌍마다 LLM에게 진짜 중복인지 묻는 필터를 만들어 두었다.
5편은 이렇게 끝났다 — 임베딩은 후보를 좁혀주는 필터로는 훌륭하지만, 아무리 좋은 모델을 써도 결국 걸러내지 못하는 사례가 남는다고. 그 문장을 쓰고 며칠 뒤, 정확히 그 한계에 다시 부딪혔다.
3번 중 2번, 같은 잔여 사례가 재현됐다 — 임베딩 유사도가 패러프레이즈를 못 잡는다
노트를 쪼개는 기능(split)의 버그를 검증하다가, 같은 원문을 세 번 연속으로 노트로 정리(ingest)한 적이 있었다. 우연히 저장해둔 그 세 라운드 기록을 나중에 다시 열어보니, 5편에서 얘기했던 그 “걸러내지 못하는 잔여 사례”가 3번 중 2번 꼴로 재현되고 있었다. 한 번은 “의자 회사가 고객 맞춤 항목을 절반으로 줄였다”는 문장이 두 노트에 거의 그대로 반복됐고, 한 번은 같은 성과 서사가 표현만 바꿔 반복됐다. 우연한 회귀가 아니라 반복해서 드러나는 진짜 한계였다.
문단을 더 잘게 쪼개서 문장 단위로 다시 유사도를 계산해봐도 마찬가지였다. 그 두 잔여 사례는 문장 단위로도 0.62~0.66에 머물렀다 — 어떤 임계값을 잡아도 후보로 못 들어오는 점수대였다. 문제는 모델의 등급이 아니라 접근 자체의 구조였다. 패러프레이즈되거나 주변 문장에 희석된 중복은, 수학적 유사도로는 애초에 “가깝다”고 안 나온다.
후보를 거르지 않고, 통째로 주기로 했다 — LLM으로 중복 문단 찾기
그래서 구조를 아예 바꿔보기로 했다. 지금까지는 문단을 벡터로 바꿔 후보를 추린 다음, 후보 쌍 하나하나를 LLM에게 따로따로 물어보는 2단계였다. 대신 한 번의 정리 작업이 만든 형제 노트 전체를 한 번에 LLM에게 주고, “이 안에서 같은 사실을 중복 서술하는 곳을 찾아서, 그 자리에서 바로 고쳐서 돌려줘”라고 시키는 단일 호출로 바꿨다. 후보를 미리 좁히지 않으니 패러프레이즈든 희석이든 결국 노트 전체를 읽는 눈이 직접 판단하게 된다.
예전에 실제로 정리해 둔 노트 5개 묶음(13~19개 노트, 한글·영어 혼합, 공학·리더십· 심리학·로보틱스로 도메인도 제각각)으로 이 방식을 테스트했다.
허브 노트를 오탐으로 잡았다 — LLM 중복 탐지 오탐
첫 결과는 실망스러웠다. 형제 노트 중 하나가 다른 노트들을 소개하는 색인/허브 역할을 겸하고 있는 묶음에서는 정밀도가 55% 수준까지 떨어졌다. 원인은 단순했다 — 허브 노트가 자기 하위 주제를 한두 문장으로 짧게 미리 요약하는 건 정상적인 설계인데, 이 방식은 그것마저 “다른 노트와 겹친다”며 중복으로 잡아냈다. 15개 노트짜리 묶음 하나는 이 조항 없이는 진짜 위반이 11건 중 6건 수준으로만 맞았다.
예외 조항 한 줄로 정밀도가 달라졌다 — 프롬프트로 오탐 줄이기
허브/색인 노트가 하위 주제를 짧게 압축 요약하는 건 중복이 아니라고 명시하는 조항 하나를 프롬프트에 추가했다. 조건은 세 가지였다 — 언급이 짧을 것, 하위 노트의 구체적 수치·인용·서사를 그대로 반복하지 않을 것, 하위 노트로의 링크나 안내가 함께 있을 것. 이 세 조건을 모두 만족할 때만 정상으로 인정하고, 하나라도 어긋나면(예: 짧아 보여도 똑같은 수치를 그대로 옮겨 적었다면) 여전히 중복으로 잡도록 했다.
결과가 달라졌다. 앞서 6/11 수준이었던 허브 묶음은 조항을 넣은 뒤 5/5로 뒤집혔다. 평면 구조 묶음 두 개(10/10, 8/8)와 인물 노트 하나가 전체를 요약하는 묶음(17건 중 사실상 전부)도 높은 정밀도를 유지했고, 노트 19개짜리 마지막 묶음은 애매한 압축 언급을 정확히 걸러내는 쪽으로 나아졌다.
기존 방식이 “깨끗하다”고 기록해둔 것도 사실 아니었다
여기서 예상 못 한 부수 효과가 하나 나왔다. 이번 방식이 잡아낸 위반이, 기존 2단계 방식으로는 “이 묶음은 깨끗하다”고 기록해뒀던 두 묶음(13개, 15개 노트)에서도 새로 나왔다. 수작업 표본 점검으로는 13~15개 노트 사이 모든 쌍을 다 대조할 수 없었을 뿐, 애초에 그 묶음들이 진짜로 깨끗했던 적이 없었다는 뜻이다. 후보를 좁히는 필터가 놓친 건 잔여 사례 두 건 정도가 아니라, 그동안 한 번도 제대로 검증하지 못한 채 “괜찮다”고 믿어온 영역 전체였던 셈이다.
임베딩 인프라를 통째로 들어냈다 — text-embedding-3-large 배선까지
정밀도가 확인된 뒤에는 코드를 바꿨다. 문단 추출, 임베딩 배치 호출,
코사인 유사도 계산, 후보 쌍마다 도는 검증 호출까지 — 5편에서 자세히
설명했던 그 임베딩 후보 필터 파이프라인 전체를 들어내고, 위 방식대로
노트 전체를 한 번에 주는 단일 호출로 교체했다. 덩달아 이 파이프라인
전용으로 따로 써왔던 text-embedding-3-large 배선(5편에서 순서가
뒤집히는 걸 확인했던 그 모델 전환 자체)도 이제 쓸 곳이 없어져서 같이
제거했다.
LLM 호출 횟수로 보면 정리 작업 한 번당 “생성 1회 + 검증 K회”(K는 형제 노트가 많은 묶음에서 20회를 넘기기도 했다)였던 게 “생성 1회 + 리뷰 1회”로 고정됐다. 후보를 거르지 않는 방식인데 오히려 호출 수는 줄어든 셈이다.
실제로 붙여서 일곱 번 돌려봤다
직접 실행하는 개발용 Obsidian 창에, 이번에 테스트했던 다섯 묶음의 실제 원본을 하나씩 다시 넣고 노트 정리(ingest)를 돌렸다. 까다로웠던 두 묶음(허브 구조)은 두 번씩, 나머지는 한 번씩 — 일곱 번의 실제 실행. 매번 로그만 보고 넘어가지 않고 실제로 쓰여진 노트를 직접 열어서 대조했다. 그중 한 라운드는 이렇게 확인됐다 — 원래 세부 단계별 서술을 담고 있던 노트는 그대로 남아 있었고, 그 내용을 다시 서술하던 형제 노트 쪽은 “자세한 내용은 [[그 노트]]에서 다룬다”는 한 문장으로 정확히 압축돼 있었다. 일곱 번 전부 같은 결과였다.
남는 생각
임베딩 필터를 없애면 속도나 정밀도 둘 중 하나는 희생할 거라고 생각했다. 실제로는 둘 다 나아졌다 — 호출 수는 줄었고, 그동안 필터 때문에 아예 후보로도 못 들어왔던 위반들이 새로 걸렸다. 필터라는 개념 자체가 “값비싼 판단을 아끼기 위한 절충”이라는 전제 위에 서 있는데, 그 절충이 실은 판단 정확도까지 깎아 먹고 있었던 셈이다. 다만 공짜는 아니었다 — 허브 노트 예외처럼, 노트 전체를 보는 눈에게는 후보 필터에는 없던 새로운 판단 기준(무엇이 정상적인 요약이고 무엇이 금지된 중복인가)을 직접 가르쳐야 했다. 필터를 걷어낸 대가는 속도가 아니라, 그 판단을 더 정교하게 가르치는 몫으로 넘어온 것이었다.