만들고 있는 Obsidian 플러그인은 기사를 넣으면 LLM이 기사마다 개념·인물·회사별 노트를 만든다. 노트를 만든 직후에는 자동 정리 단계가 돌아 비슷한 노트 쌍을 찾고, LLM이 두 노트의 본문을 대조해 “합치자”는 제안 카드를 띄운다. 실제 병합은 사람이 카드의 실행 버튼을 눌러야 일어난다. 이번 글은 그 병합 제안을 버그로 보고 AI 코딩 에이전트와 함께 반나절을 쓴 이야기다. 결론부터 말하면 제안은 맞았고, 틀린 건 제안을 막은 감사 쪽이었다.
노트를 나눈 단계와 합치자는 단계가 반대로 말했다 — LLM 노트 병합 제안
테스트 실행 중에 자동 정리가 두 번 병합을 제안했다. 하나는 에이전트 하네스를 다룬 기사에서 나온 일반 정의 노트와, 같은 기사의 “코딩 버그 하나를 고치는 과정” 예시를 따로 뗀 노트였다. 다른 하나는 보상 해킹(reward hacking) 기사에서 나온 노트 세 개였다. 상위 개념 노트, 강화학습 편, LLM 에이전트 편이다.
실행 결과를 감사하던 에이전트는 둘 다 “범위가 다른 노트”라며 실행하지 않았다고 기록했다. 나중에 이 제안들을 다시 보자고 하자, 에이전트는 질문을 이렇게 세웠다. 노트를 만드는 단계가 일부러 나눈 걸 바로 다음 단계가 왜 합치자고 하나. 지난 2주 기록을 뒤지니 같은 기사에서 나온 형제 노트를 합치자는 제안이 다섯 번 있었고, 그중 두 번은 그때 실제로 병합까지 했다.
프롬프트를 세 번 고쳤고, 매번 반대쪽이 터졌다 — LLM 프롬프트 테스트
에이전트가 찾은 원인은 그럴듯했다. 노트를 합칠지 정하는 자동 정리에는 같은 대상을 다른 맥락으로 설명한 노트는 같은 개념이라는 규칙이 있는데(이전 글에서 정한 기준이다), 노트를 만드는 단계의 나누기 규칙에는 이 정의가 없었다. 두 단계가 다른 기준을 쓰니 반대 결론이 나온다는 것이다.
그래서 노트를 만드는 프롬프트를 고쳐 가며 쟀다. 기사 하나씩 실제 플러그인 코드로 LLM 요청을 만들고, 고치기 전과 후의 프롬프트로 기사마다 세 번씩 보냈다. 문제가 됐던 기사 5개 말고도, 노트가 따로 나뉘어야 맞는 기사들을 회귀 확인용으로 같이 돌렸다.
| 회차 | 바꾼 것 | 문제의 형제 노트가 생긴 횟수 | 부작용 |
|---|---|---|---|
| 지금 프롬프트 | — | 15번 중 6번 | — |
| 1차 | 같은 개념 정의를 노트 만드는 규칙에도 넣음 | 15번 중 3번 | 남은 건 다른 모양 |
| 2차 | + “원문 전체를 대표하는 노트를 따로 만들라”는 규칙 완화 | 15번 중 0번 | 노트가 굵어짐(4개 → 1 |
| 3차 | + “다른 기사가 와서 갱신할 대상이면 나눠라” | 15번 중 4번 | 한두 문장짜리 노트가 늘어남 |
2차에서 완화한 규칙은 원래 기사 첫머리의 일화가 어느 노트에도 안 들어가고 사라지던 문제를 막으려고 넣은 것이었다. 그 규칙이 원문 전체를 대표하는 노트를 따로 만들게 해서, 기사가 사실상 한 주제일 때는 그 노트가 주제 노트와 겹쳤다. 완화하자 문제의 모양은 사라졌지만, 이번에는 “불안형 리더”, “회피형 리더”처럼 이름 있는 하위 유형까지 상위 노트의 절로 묻혔다. 이 플러그인이 따르는 카파시의 LLM Wiki는 새 원문이 오면 “integrates it into the existing wiki — updating entity pages”라고 하고, “A single source might touch 10-15 wiki pages”라고 쓴다. 다음 기사가 갱신할 개념 페이지가 있어야 위키가 쌓인다. 그래서 3차에 그 물음을 넣었는데, 이번에는 나누는 쪽 전체가 다시 밀렸다. 한쪽을 고치면 다른 쪽이 기준을 넘었다.
“rca 보다는 계속 주변부만 만지작거리고 있는 듯 한데” (근본 원인 분석)
세 번째 결과 뒤 에이전트는 같은 기사에서 나온 노트끼리는 병합 후보에서 코드로 빼자는 안을 냈다. 그 안이 무엇을 해결하지 못하는지 듣고 나서 에이전트에게 그렇게 말했다. 근본 원인 분석(RCA)을 하라고 해 놓고 증상 주변만 고치고 있다는 뜻이었다. 에이전트는 더 깊은 원인을 내놓았다. 노트를 만드는 단계는 기존 위키 노트를 전혀 보지 못한다. 7월에 기존 노트를 갱신하는 경로를 빼고 병합을 정리 단계에 몰아줬기 때문이다. 그러니 노트 경계를 두 단계가 따로 정한다는 설명이었다.
코드로 보면 사실이었다. 하지만 다시 따져 보라고 하자 에이전트가 스스로 뒤집었다. 세 번의 측정은 모두 빈 보관함에서 기사 하나만 넣고 돌렸다. 기존 노트가 하나도 없는데도 같은 기사 안의 쪼개기가 그대로 나왔다. 기존 노트를 보여 줘도 기사 하나를 어떻게 자를지는 바뀌지 않는다. 이 원인은 다른 기사끼리 생기는 중복에는 맞지만, 이번 문제에는 맞지 않았다.
설계를 다시 보니, 그건 버그가 아니었다 — 노트 병합 기준
에이전트는 원인을 다시 규칙 차이로 정리하고, 측정 횟수를 늘리자고 했다. 그 말을 듣고 내가 기억하던 설계를 말했다. 노트를 만드는 단계는 나누기만 하고, 병합은 그다음 자동 정리가 제안하고, 사람이 확인해서 실행한다. 7월에 일부러 그렇게 만들었다. 그렇다면 “만드는 단계가 나눈 걸 정리 단계가 합치자고 한다”는 모순이 아니라 설계된 경로다.
물었어야 할 질문은 하나였다. 그 제안이 맞았나. 그 규칙에는 “같은 개념을 특정 상황에 적용해 설명했을 뿐인 노트는 그 개념의 하위 개념이 아닙니다”라는 문장이 있다. 일반 정의와 그 적용 예시, 보상 해킹의 강화학습 편과 LLM 에이전트 편은 이 규칙으로 같은 개념이다. 두 제안 모두 맞았다. “범위가 다르다”는 규칙이 아니라 감사하던 에이전트의 감이었고, 나도 그 판정을 규칙에 대 보지 않았다.
실제로 바로잡은 것
노트를 만드는 프롬프트는 하나도 바꾸지 않았다. 세 번의 측정(LLM 호출 123번, 약 0.48달러)은 결론과 함께 기록만 남기고 접었다. 대신 이렇게 했다.
- 감사 기록의 두 판정을 “제안이 맞았다”로 정정했다. 앞으로 병합 제안을 감사할 때는 감으로 범위를 보지 않고 규칙 문장에 대 본다.
- 진짜 남은 문제를 두 가지로 좁혔다. 보상 해킹 세 노트를 합친 노트 이름이 상위 개념 노트가 아니라 “Reward Hacking in LLM Agents”로 잡힌 것, 그리고 실행하지 않은 병합 제안이 스캔할 때마다 똑같이 다시 뜨는 것이다. 앞의 것은 코드를 보니 합칠 노트 목록의 첫 노트 이름을 그대로 쓰고, 그 목록 순서가 정해져 있지 않았다. 이건 다음 편에서 고쳤다. 뒤의 것은 나중에 원인을 확인했다. 두 노트가 같은 개념이라는 판정은 캐시에 남아 다음 스캔에서도 그대로 쓰이는데, 사용자가 그 제안을 실행하지 않고 넘겼다는 기록은 어디에도 남지 않는다. 그래서 두 노트가 그대로면 스캔마다 같은 제안이 다시 만들어진다. 처음 설계할 때 알고 받아들인 동작이었고, 합치지 않기로 한 제안을 숨기는 방법을 넣을지는 아직 정하지 않았다.
남는 생각
측정 자체는 꼼꼼했다. 실제 코드로 요청을 만들고, 기준을 미리 정하고, 회귀용 기사까지 같이 돌렸다. 그래도 질문이 틀리면 정교한 측정은 엉뚱한 곳을 정확히 잴 뿐이다. 이번에 빠진 건 맨 앞의 한 단계였다. 이 동작이 설계와 어긋나는가. 그걸 먼저 물었다면 첫 측정 전에 끝났을 일이다. 원인 후보를 내놓을 때마다 이미 모은 증거의 조건(이번에는 “빈 보관함”)과 맞는지 대 보는 것도 같은 이야기다.