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

LLM 언어 혼입을 잡으려 정성껏 만든 코드가, 실측해보니 한 일이 없었다

Read in English →

고치라는 지시부터, 다시 봐야 했다 · 7/8편

만들고 있는 Obsidian 플러그인은 원문 자료를 LLM으로 읽어 개념별 노트 여러 개로 정리한다. 이번 문제는 영어 원문에서 뽑은 노트인데, 본문 중간에 한국어 문장이 뜬금없이 섞여 나오는 언어 혼입이었다. 6편과는 원인도, 고치는 방식도 전혀 다른 길을 걸었다.

세 번의 프롬프트 수정, 전부 제자리걸음

처음엔 전역 규칙에 “원문 언어를 따르라”고 못을 박았다. 재현율은 그대로였다. 그 규칙을 본문 바로 옆으로 옮겨 다시 강조했다. 역시 그대로였다. 세 번째로 아예 “지금부터 쓰는 문장은 예외 없이 원문 언어를 따라야 한다”는 문장을 본문 작성 지시 바로 위에 박아 넣었다. 70개 노트 기준 5~7개꼴로 한국어가 섞여 들어오는 비율은 세 번 다 거의 그대로였다.

그래서 코드로 만들었다 — 그리고 버그 두 개를 고쳤다 (한글 비율 정규식)

프롬프트만으로 안 잡히니 결정론적 탐지 코드를 짜기로 했다. 문단마다 한글·영문 비율을 세서, 노트의 지배 언어와 다른 쪽으로 치우친 문단만 골라 그 문단 하나만 다시 쓰게 하는 방식이었다.

만들면서 실제로 버그 두 개를 직접 잡았다. 첫 번째는 노트 전체를 하나로 이어 붙여 비율을 재던 방식이 문제였다 — 한 문단만 길게 새면 나머지 짧은 문단들을 숫자로 눌러버렸다. 문단 단위로 각자 투표하게 바꾸자 해결됐다. 두 번째는 위키링크였다 — 한국어 문장 안에 영어로 된 노트 경로가 섞여 있으면 그 경로의 글자 수가 비율을 깎아 먹었다. 링크 표시 텍스트만 남기고 경로는 세지 않도록 고쳤다. 세 번째 버그(링크의 표시 텍스트 자체가 긴 영어 구절인 경우)는 못 잡은 채로 남았지만, 뒤에 나올 이유로 더 이상 쫓지 않았다.

실측해보니, 그 코드가 한 일은 없었다 (dedup 프롬프트)

코드와 프롬프트 수정을 다 넣고 실제 Obsidian 볼트에서 실측했다. 결과는 65개 노트 중 한국어 혼입 0건 — 완전히 클린했다. 코드가 이겼다고 결론 내릴 뻔했다.

그런데 디버그 로그를 다시 보니 이상했다. 이 플러그인은 원문 하나에서 함께 만들어진 노트들(형제 노트) 사이에 같은 문단이 겹치면 한쪽을 지우고 링크로 바꾸는 중복 제거(dedup) 단계를 거친다. 언어 탐지 코드는 그 단계 앞뒤로 두 번 돌게 되어 있었는데, 두 번 다 “고친 건수 0건”으로 찍혀 있었다. 코드는 실행됐지만 아무것도 하지 않았다는 뜻이었다.

그럼 무엇이 65개를 클린하게 만들었을까. 같은 라운드에 딱 하나 더 바꾼 게 있었다 — dedup이 중복 문단을 형제 노트 중 한쪽에서 지우고 다른 쪽에 다시 써넣는 재작성 프롬프트에, 언어 규칙을 하나 추가한 것. 그 프롬프트는 처음 만들어진 이후로 단 한 번도 언어에 대한 지시를 받은 적이 없었다. 재작성되는 문단이 영어 노트 안에 있어도, 그 프롬프트 입장에선 어떤 언어로 써야 하는지 알려준 사람이 없었던 셈이다. 거기에 규칙 한 줄을 넣은 것만으로 65개 전부가 클린해졌다 — 정교하게 만든 탐지+수정 코드는 처음부터 필요가 없었다.

코드를 지우고, 규칙 하나로 합쳤다 (출력 언어 규칙)

코드가 기여한 게 0건이라는 걸 확인한 이상, 남겨둘 이유가 없었다. 탐지 로직, 버그 두 개를 고쳐가며 완성한 재작성 함수, 그걸 부르는 배선까지 전부 지웠다. 대신 “출력은 원본 자료의 언어를 따라야 한다”는 규칙 하나를 만들어 실제 생성이 일어나는 지점 바로 옆에 심었다 — ingest 본문, 병합, 분할, 그리고 그 dedup 재작성까지 네 곳 전부에 똑같이. 병합과 분할 쪽은 애초에 이런 언어 규칙 자체가 없었다는 것도 이번에 알았다.

지운 지 몇 시간 만에, 다른 자리에서 같은 질문이 돌아왔다

“코드까지 안 가도 프롬프트로 충분하다”는 결론을 막 확정한 참이었다. 그런데 전혀 다른 버그를 다시 열어보다가, 정확히 반대 방향의 결론이 필요한 상황과 마주쳤다.

이 플러그인에는 노트를 병합하거나 분할하자고 제안하고 실행하는 명령(/refactor)도 있다. 그 분할 실행에서, “이 조각은 원본 자리를 그대로 대체한다”는 뜻으로 번호 대신 “0”을 쓰는 규칙이 있다. 이 규칙 자체는 예전에도 한 번 고친 적이 있었는데, 그런데도 “0.md”라는 이름의 고아 파일이나, 파일명과 실제 내용이 한 칸씩 밀리는 증상이 며칠째 간헐적으로 재현되고 있었다. “모델이 ‘0’을 첫 번째 조각 번호로 착각해서 한 칸씩 밀리는 게 아니냐”는 가설이 나왔다.

로그 두 줄로 잡은 진짜 원인

기존 디버그 로그로는 정황만 잡혔지, 정확히 어느 단계에서 “0”이 오염되는지는 알 수 없었다. 디버그 전용 로그 두 줄을 추가했다 — 하나는 제안 단계가 만드는 원본 배열 그대로, 하나는 그 배열이 실제 파일 경로로 매핑된 결과.

라이브에서 재현하자 바로 잡혔다. 제안 단계가 “0” 대신 “wiki/2026/09/WeekOf14/0.md”라는 풀 경로를 내고 있었다. 이 노트를 보여주는 카탈로그는 기존 노트를 예외 없이 “경로: wiki/…/파일명.md” 형식으로 보여주고, 같은 프롬프트 안의 다른 지시도 “기존 노트를 언급할 땐 항상 전체 경로를 쓰라”고 반복해서 강조하고 있었다. “0” 규칙은 그 강한 반복 패턴에 대한 유일한 예외였는데, 그 예외라는 사실이 프롬프트 어디에도 명시돼 있지 않았다. 같은 소스로 스캔을 다시 돌려도 두 번 다 똑같이 오염됐다.

이번엔 프롬프트만으론 안 될 걸 알아서, 코드도 같이 고쳤다

프롬프트에 “이 값은 파일명이 아니라 특수 신호값이니 절대 확장자나 경로를 붙이지 말라”는 문장과, 실제로 오염됐던 정확한 사례를 반례로 박아 넣었다. 그런데 몇 시간 전 배운 게 있었다 — 프롬프트 수정만으론 100% 안 잡힐 수 있다는 것. 그래서 코드에도 방어를 같이 넣었다. “0”, “0.md”, 경로+0.md처럼 생긴 값을 전부 같은 신호로 인식하게 만들었다.

배포하고 같은 소스로 다시 스캔했다. 제안 단계는 이번에도 똑같이 “wiki/…/0.md”를 냈다 — 프롬프트 수정이 이 경향 자체를 없애지는 못한 것이다. 하지만 이번엔 그 오염된 값이 실행 단계에서 정상적으로 원본 자리로 흡수됐다. 고아 파일도, 밀린 파일명도 나오지 않았다.

덧붙임: 한 달 뒤 같은 방식의 테스트에서, 전부 영어인 노트에 한국어 태그 하나가 붙은 일이 다시 있었다. 그 노트의 본문은 전부 영어였다.

남는 생각

같은 세션 안에서 “코드는 필요 없다”와 “코드가 필요하다”는 결론을 둘 다 내렸다. 둘 다 맞았다. 언어 혼입은 원문 언어를 따르라는 지시를 얼마나 정확히, 얼마나 가까운 자리에 배치하느냐의 문제였고, 한 군데 빠진 지시를 채우자 코드 없이도 완전히 풀렸다. 반면 “0”이라는 특수 신호값이 오염되는 문제는, 그 값이 될 수 있는 잘못된 형태가 몇 가지로 정해져 있는 문제였다 — 이런 문제는 프롬프트로 계속 다듬기보다 코드로 그 몇 가지 형태를 전부 정규화해버리는 쪽이 싸고 확실했다.

“코드로 풀어야 하는가, 프롬프트로 풀어야 하는가”는 한 번 정해두고 다음에도 써먹을 수 있는 답이 아니었다. 문제가 열린 자연어 판단(언어가 뭔지, 어디까지 써야 하는지)인지, 아니면 닫힌 몇 가지 형태(신호값이 어떤 문자열로 새는지)인지 매번 다시 구분해야 하는 질문이었다. 지운 지 몇 시간 만에 다시 코드를 심으면서, 그 구분을 다시 배웠다.


Share this post:

고치라는 지시부터, 다시 봐야 했다

  1. 1. 고치라는 지시부터, 다시 봐야 했다 1편 — 죽은 주석을 고치다가, 또 틀렸다
  2. 2. 고치라는 지시부터, 다시 봐야 했다 2편 — 가설은 반은 맞고 반은 틀렸다
  3. 3. 고치라는 지시부터, 다시 봐야 했다 3편 — 고치기 전에 한 번 더 물었다
  4. 4. 고치라는 지시부터, 다시 봐야 했다 4편 — 지우라는 지시와, 어떻게 지울지는 다른 질문이었다
  5. 5. 정답은 이미 있었고, 안 쓴 쪽이 문제였다
  6. 6. LLM 프롬프트 규칙을 두 번 고쳤는데, 정작 노트를 나누지 말았어야 했다
  7. 7. LLM 언어 혼입을 잡으려 정성껏 만든 코드가, 실측해보니 한 일이 없었다
  8. 8. LLM 노트 과분할을 막을 장치를 설계하다가, 이미 있는 장치를 못 보고 있었다