만들고 있는 Obsidian 플러그인은 원문 자료를 LLM으로 읽어 개념별 노트로 정리하고, /refactor 명령으로
같은 대상을 다룬 노트를 하나로 합친다. 이 병합(merge) 기능이 두 노트를 하나로 합칠
때, 가끔 한쪽 소스에만 있던 내용이 통째로 사라지는 버그가 있었다.
처음 발견된 사례는 단순했다 — 두 노트를 합쳤더니, 한쪽에만 언급됐던
실명 도구 하나가 결과물 어디에도 남지 않았다. 재현 사례를 몇 건 더
모아보니 스페이스X의 사업부문별 재무 수치, 세 개 기사를 합친 결과에서
사라진 사용 사례 목록, 국민연금 노트의 대안 수치 같은 것들이 같은
패턴으로 빠져 있었다. 병합 자체의 결정(“이 둘은 같은 대상이다”)은
매번 옳았는데, 정작 합치는 과정에서 한쪽의 고유한 내용이 새어 나가고
있었다.
프롬프트를 나란히 놓고 봤다 — 정보 밀도 보존 규칙만 빠져 있었다
원인을 찾으려고 병합 실행 프롬프트와, 같은 파이프라인의 다른 프롬프트 (ingest, split)를 나란히 열어봤다. 세 프롬프트 다 “언어를 원문과 일치시켜라”, “원문에 없는 내용을 지어내지 마라” 같은 공통 규칙을 쓰고 있었는데, 딱 하나 — “원문에서 한두 문장으로 짧게만 언급된 수치·통계·실명 사례도 생략하지 말고 보존하라”는 정보 밀도 보존 규칙만 ingest와 split 프롬프트에는 있고, 병합 프롬프트에는 없었다. 이 규칙은 원래 ingest 단계의 비슷한 누락 버그를 잡으려고 예전에 따로 만들어진 것이었다. 병합 프롬프트는 “각 문서의 정보가 누락되지 않도록”이라는 느슨한 문장만 갖고 있었고, 그 구체적인 규칙은 한 번도 옮겨진 적이 없었다.
그럴듯한 원인처럼 보였다. 같은 병으로 다른 프롬프트를 두 번이나 고쳤던 전례가 있는데, 정작 병합 프롬프트만 그 처방을 못 받은 셈이니까.
실제 노트로 검증해보기로 했다 (프롬프트 A/B 테스트)
프롬프트만 보고 바로 고치는 대신, 그 규칙을 병합 프롬프트에 추가한 버전과 원래 버전을 실제로 비교해보기로 했다. 가상의 예시 대신, 과거 버그 재현에 실제로 쓰였던 원본 저장소의 커밋 히스토리를 뒤져서 병합 직전 상태의 진짜 노트를 두 세트 찾아냈다.
첫 번째는 한 문서를 세 조각(사업 구조, 기업공개, 자본조달)으로 나눴던 결과물 — 서로 겹치지 않고 각자 고유한 수치를 담은 노트들이다. 두 번째는 심리적 안전감을 다룬 서로 다른 두 원문 기사에서 각각 파생된 노트 — 같은 개념을 각자 다른 연구와 사례로 설명하는, 훨씬 많이 겹치는 구조다. 두 세트 다 실제 병합 대상이 될 법한, 조작하지 않은 진짜 콘텐츠였다.
8번을 돌렸는데, 차이가 없었다
규칙을 뺀 원래 프롬프트(OLD)와 규칙을 추가한 프롬프트(NEW)로 각 세트를 4번씩, 총 8번을 병합 실행에 태웠다. 모델은 플러그인과 같은 gpt-5.6-luna, 추론 강도(reasoning effort)는 medium이었다. 결과를 채점하기 위해 각 세트마다 “반드시 살아남아야 할 구체 정보” 목록을 미리 만들어뒀다 — 매출 비중과 영업이익률 같은 숫자, 연구자 이름과 발표 연도, 특정 사건의 수치 같은 것들.
스페이스X 세트(항목 26개 만점)는 OLD가 26, 26, 25, 25점, NEW가 26, 25, 25, 25점이었다. 심리적 안전감 세트(항목 15개 만점)는 OLD가 15, 15, 14, 15점, NEW가 15, 14, 15, 15점이었다. 어느 쪽이 더 나은지조차 가려지지 않을 만큼 똑같았다. 가끔 하나씩 빠진 항목도 실제 수치나 고유명사가 아니라 섹션 제목 수준의 표현 차이였다.
그럴듯함과 증명은 다른 물건이었다
프롬프트 텍스트만 비교했을 땐 명백한 결함처럼 보였던 게, 실제 실행 결과로는 전혀 차이를 만들지 못했다. 두 가지로 해석할 수 있다 — 이 모델은 이미 그 정도 규모(노트 2~3개, 200줄 이내)에서는 규칙이 있든 없든 밀도를 잘 지키고 있거나, 원래 재현됐던 심각한 사례들(4개 노트가 얽힌 병합, 별도 심층 보고서가 병합 소스 목록에도 안 들어간 경우)은 애초에 프롬프트 문구가 아니라 훨씬 큰 규모나 병합 대상 자체를 잘못 고르는 다른 단계의 문제였을 수 있다.
추가했던 규칙은 코드에서 되돌렸다. 효과가 증명되지 않은 채로 남겨두지 않기로 했다. 대신 결정은 이렇게 정리했다 — 매 병합 실행마다 계속 원본과 대조하는 점검은 이어가되, 다음 코드 수정은 정확한 재현 조건(어떤 노트, 어떤 설정에서, 무엇이 빠졌는지)을 다시 잡은 뒤에만 시도하기로.
그날 밤 새 사례가 나왔고, 9번을 더 돌렸다
이 결정을 내린 그날 밤 자정을 넘겨, 실제 노트에서 새 사례가 나왔다. 서로 다른 두 기사에서 만들어진
Graphify라는 도구의 노트 두 개가 합쳐졌는데, 한쪽 기사가 “권장” 설치법으로 소개한 uv tool install
방법이 통째로 빠지고 다른 쪽의 pip install 방법만 남았다. 같은 대상이라는 판단은 맞았고, 빠진 건 한쪽에만
있던 고유 정보였다. 이번 버그와 정확히 같은 모양이었다.
이번엔 실제 상황에 더 가깝게 재현했다. 같은 두 기사로 노트를 각각 새로 만들고, 그 둘을 OLD와 NEW로 합쳤다.
medium으로 5번을 돌린 뒤에야, 실제로 쓰는 플러그인 설정이 medium이 아니라 high라는 걸 알았다. 앞의 8번도
실제와 다른 설정으로 돌린 셈이었다. high로 4번을 더 돌렸다. 9번 모두, OLD도 NEW도 uv tool install 권장
문구를 그대로 남겼다. 규칙이 없는 원래 프롬프트조차 실제로 일어난 누락을 한 번도 재현하지 못했다.
세 번째 소재로도 같은 결과였다. 따로 떼어 낸 두 노트로는 이 누락이 일어나는 조건 자체를 만들지 못하는 것으로 보였다. 규칙은 다시 되돌렸고, 다음엔 실제로 누락이 일어난 그 실행에서 바로, 노트를 정리하기 전에 원인을 파 보기로 했다.
남는 생각
코드 diff 하나를 보고 “이거였구나” 싶은 순간과, 그 diff가 실제로 결과를 바꾸는 걸 확인하는 순간은 다른 두 가지 일이었다. 둘 다 논리적 추론처럼 느껴지지만, 앞의 것은 그저 설명이 매끄럽다는 뜻일 뿐이고 뒤의 것만 증거다. 재현되지 않는 버그를 고치겠다고 코드부터 바꾸면, 고쳤다는 확신만 남고 실제로 무엇이 바뀌었는지는 아무도 모르게 된다. 지금은 그 확신 대신 재현을 기다리는 쪽을 택했다.