Skip to content
게으른 엔지니어의 기술 블로그
Go back

정교한 해결책을 설계하다가, 이미 있던 걸 뒤늦게 알아챘다

Obsidian 플러그인이 Gemini한테 보내는 프롬프트에서 뺄 게 있는지 체크해보기로 했다. 디버그 모드 작업하면서 코드를 워낙 많이 훑었으니, 이참에 프롬프트 쪽도 한번 보자는 가벼운 마음이었다.

첫 번째 수확 — 죽은 코드

src/prompts/ 폴더에 system-prompt.ts라는 파일이 있었다. 27줄짜리 한국어/영어 이중 시스템 프롬프트. 근데 리포 전체를 grep해봐도 이걸 import하는 곳이 정의 자체 말고 없었다. 빌드된 결과물(main.js)에서 이 파일의 고유 문구를 검색해도 0건 — esbuild의 tree-shaking이 이미 죽은 코드로 판단해서 최종 산출물에도 안 들어가 있었다.

git 히스토리를 보니 prompt-composer.ts, task-templates.ts랑 같은 커밋에서 만들어졌는데, 정작 PromptComposer.compose()는 처음부터 이 파일을 안 쓰고 “task 전용 프롬프트를 그대로 반환한다(일반 시스템 프롬프트로 인한 희석 방지)“는 자체 주석과 함께 설계돼 있었다. 리팩터링 도중 설계 방향이 바뀌면서, 만들어놓은 파일 하나를 지우는 걸 깜빡한 경우였다. 삭제. 여기까진 쉬웠다.

두 번째 발견 — log.md가 매번 통째로 실린다

/ingest가 Gemini한테 보내는 프롬프트에 wiki/log.md(위키 변경 이력을 담은 append-only 파일) 전체 내용이 잘리지 않고 그대로 들어가고 있었다. 이 파일은 보관함을 오래 쓸수록 계속 커지는데, 청크 하나 처리할 때마다 매번 통째로 재전송되고 있었다 — 같은 배치 안에서 청크가 5개면 5번 반복 전송되는 셈이다.

바로 “최근 N개만 잘라 보내자”로 방향을 잡으려다가, 이 플러그인의 설계 원본 문서 (LLM Wiki.md)를 다시 찾아봤다. 거기엔 log.md의 목적이 이렇게 적혀 있었다.

The log gives you a timeline of the wiki’s evolution and helps the LLM understand what’s been done recently.

즉 보여주는 것 자체는 의도된 설계였다. 그냥 지우기보다는 “최근 것만” 보내는 쪽으로 다듬는 게 맞아 보였다.

설계가 점점 커졌다

“최근 몇 개”를 정하려다 보니 새로운 문제가 보였다 — 관련된 파일 두 개가 서로 다른 청크에 흩어지면, log.md의 항목 자체가 파일 경로만 있고 주제 정보(분류, 태그)가 전혀 없어서, AI가 “이 파일이 방금 처리된 그 파일이랑 관련 있다”는 걸 알아채기가 구조적으로 어려웠다.

그래서 아예 청크를 구성하는 방식 자체를 바꾸는 쪽으로 설계가 커졌다. 관련된 파일들을 애초에 같은 청크에 묶어서 보내면, log.md의 부실한 신호에 의존할 필요가 없어지지 않을까 — 파일이 클리핑되면서 쌓이는 폴더 구조(예: raw/2026/Q3/특정 프로젝트 폴더/)를 1차 신호로, 제목·본문 유사도를 2차 신호로 써서 관련 파일끼리 그리디하게 클러스터링하는 안을 짰다.

이 과정에서 두 번 정정이 들어왔다. 처음엔 “폴더가 같으면 관련 있다”고 생각했는데, 실제로는 raw/2026/Q2/, raw/2026/Q3/ 같은 분기 폴더가 그냥 시간 버킷이라 거의 모든 파일이 같은 분기에 몰려 있다는 지적을 받고, “연도/분기처럼 생긴 세그먼트는 걸러내고 남는 폴더만 신호로 쓴다”로 고쳤다. 그다음엔 raw/MathWorks/처럼 연도/분기가 아예 없는 경로도 있다는 지적에, “특정 위치의 접두사를 떼는” 방식 대신 “경로 세그먼트를 하나씩 보면서 연도/분기 패턴만 걸러낸다”는 더 일반적인 규칙으로 다시 고쳤다.

잠깐, 이게 진짜 필요한 문제인가

여기까지 설계하고 나서 멈칫했다. 지금까지 확인한 버그들(#14, #16, #17)은 전부 실사용 중에 실제로 걸린 문제였다. 근데 이번 건 — AI가 log.md 정보 부족 때문에 실제로 문서를 잘못 쪼개거나 중복 생성한 사례를 한 번도 목격한 적이 없었다. 순전히 코드 체크에서 나온 가설 하나 때문에, 새 알고리즘과 새 테스트가 필요한 설계를 밀어붙이고 있었던 거다.

더 싼 대안도 검토했다 — 청킹 구조는 그대로 두고, log.md 항목 자체에 분류/태그 정보만 살짝 보강하는 방식. 마침 로그 항목에는 이미 isDeleted 필드가 있어서, 삭제되는 파일(병합의 소스, 분할의 원본)은 보강 대상에서 자연스럽게 제외할 수 있었다. 이게 클러스터링 설계보다는 훨씬 쌌다.

결정적인 발견

근데 이 대안을 검토하다가, 코드를 한 번 더 확인하는 과정에서 진짜 답이 나왔다. log.md를 읽는 코드 바로 옆에, 위키 카탈로그(buildWikiCatalog)를 읽는 코드가 있는데 — 이 카탈로그도 log.md랑 정확히 같은 위치(청크 루프 안)에서 매번 새로 읽히고 있었다.

즉 청크 1이 새 문서를 만들면, 청크 2가 시작할 때 카탈로그를 다시 스캔해서 그 새 문서가 이미 반영된 상태로 나온다. 게다가 카탈로그 항목은 log.md보다 훨씬 풍부하다 — 경로, 제목, 분류, 태그, 목차까지 다 들어 있다. log.md가 풀려고 했던 “청크끼리 서로의 결과를 아는” 문제를, 카탈로그가 이미 실시간으로, 그것도 더 풍부한 정보로 풀고 있었던 거다.

log.md가 카탈로그 대비 추가로 주는 건 “이게 방금 생겼다”는 시간적 뉘앙스 하나뿐이었고, 그건 병합 판단에 결정적인 정보도 아니었다. 그래서 결론은 처음 생각했던 것과 정반대로 바뀌었다 — 자르는 것도, 보강하는 것도, 청크를 재구성하는 것도 아니라 그냥 지운다.

덤으로 나온 진짜 버그

log.md를 지우면서 프롬프트 마지막 줄도 다시 봤는데, 거기엔 “index.md와 log.md를 누적 업데이트하십시오”라는 지시문이 있었다. 근데 같은 프롬프트의 다른 규칙(6번)은 “AI는 절대로 index.md나 log.md를 직접 수정하는 도구 명령을 내리지 마십시오”라고 정반대로 말하고 있었다 — 같은 프롬프트 안에 정면으로 모순되는 지시문 두 개가 같이 들어가 있었던 거다. 이건 설계 방향이랑 무관하게 그냥 명백한 버그라 같이 고쳤다.

남는 생각

이번에 시간을 제일 많이 쓴 건 정교한 해결책(청크 유사도 그루핑)을 설계하는 과정이었는데, 정작 그게 채택되지 않았다. 대신 채택된 건 “이 문제, 사실 이미 다른 코드가 풀고 있는 거 아닌가?”라는 질문 하나였다.

복잡한 해결책을 잘 설계하는 능력보다, 그 해결책이 애초에 필요한지 계속 의심하는 태도가 더 중요했다. 특히 “이게 실제로 겪은 문제인가, 아니면 코드를 읽다가 나온 가설일 뿐인가”를 한 번 더 물어본 순간이 결정적이었다 — 그 질문이 없었다면 필요 없는 클러스터링 알고리즘을 그대로 만들고 있었을 거다.


Share this post:

Next Part
프롬프트에서 뺄 거 찾다가 2편 — 데이터를 두 번 보내고 있었다