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

설계까지 다 거치고도, 실행해봐야 보였다 4편 — 없는 줄도 몰랐던 안전망을 찾아냈다

설계까지 다 거치고도, 실행해봐야 보였다 · 4/6편

3편에서 웨이브 병렬화를 배포했다. 그런데 얼마 지나지 않아 실사용 중에 다시 물음이 돌아왔다 — “왜 다리오 아모데이는 여전히 그대로야?”

디버그 로그가 알려준 건 없었다

wiki/ 폴더를 지우고 raw 원본 60개로 처음부터 다시 인제스트를 돌렸는데도, 다리오 아모데이 기사에서 다니엘라 아모데이가 여전히 따로 쪼개지지 않았다. 디버그 로그를 켜서 받아봤는데, 위키링크의 .md 확장자를 정리하는 로그와 존재하지 않는 링크를 일반 텍스트로 강등시키는 로그가 잔뜩 있었다. 처음엔 “뭔가 빠진 신호”처럼 보였는데, 확인해보니 둘 다 정상 동작이었다 — 60개 노트를 한꺼번에 새로 만들면서 서로 링크가 많이 걸린 것뿐이었다. 여기엔 문제가 없었다.

원본 자체를 다시 봤다. 예전에 실측 테스트용으로 스크래치 스크립트에 저장해뒀던 실제 기사 전문이 남아있었는데, 다니엘라 아모데이 부분은 “실리콘밸리 대표 남매… ‘문과’ 여동생 다니엘라”라는 별도 섹션으로, UC 샌타크루즈 영문학 전공부터 하원의원 보좌관 경력, 스트라이프 근무 이력, 오픈AI 안전정책 부사장 경력까지 담긴 꽤 풍부한 내용이었다. 정보가 얇아서 판단하기 애매한 케이스가 아니었다 — 분할할 근거는 충분했다.

배치 크기가 아니라 카탈로그였다

ingestChunkSize를 의심했지만, 이미 이전 편에서 병합을 뺀 덕에 배치 크기 문제는 해결된 상태였다. 남은 후보는 위키 카탈로그였다 — /ingest가 매 청크마다 이미 만들어진 노트 목록을 프롬프트에 같이 넣어주는 부분이다.

빈 카탈로그와 실제 vault 규모(58개 노트)를 놓고 A/B 테스트를 했다. 처음 나온 결과는 5/5 대 0/5로 깔끔했다. 카탈로그를 압축해서(제목만 남기고) 토큰을 절반 가까이 줄여도 여전히 0/5였다 — 그러니 원인은 토큰 길이가 아니라 카탈로그 항목의 “개수” 자체라는 결론을 얻었다.

내가 낸 첫 결과가 틀려 있었다

여기서 다시 짚어봤다. “빈 카탈로그” 조건에 쓴 프롬프트 문구를 다시 읽어보니, “카탈로그: (비어 있음 - 신규 보관함)“이라는 문장을 내가 직접 손으로 써넣은 것이었다. 그런데 실제 운영 코드는 카탈로그가 비어 있으면 그 섹션 자체를 통째로 안 넣는다. 내가 쓴 테스트가 실제 코드가 절대 만들지 않는 프롬프트를 던지고 있었던 셈이다.

정확히 같은 방식으로 다시 프롬프트를 만들어 재현하니 결과가 달라졌다(2/5). 더 이상해진 건, 두 프롬프트 문자열이 정말 한 글자도 다르지 않다는 걸 해시로 직접 확인했는데도(md5가 동일했다) 한 번은 2/5, 한 번은 10/10이 나왔다는 것이었다. 이건 표본이 너무 작다는 신호였다. N을 5에서 15로 늘려 다시 쟀다.

결과는 카탈로그 0개일 때 93%, 10개일 때 73%, 20개일 때 13%로 급락, 30개에서 7%, 58개에서는 0%. 압축해도 여전히 0%였다. “토큰이 아니라 개수가 원인”이라는 결론 자체는 그대로 살아남았지만, 이번엔 훨씬 튼튼한 근거로 다시 섰다. 첫 결과가 틀렸다는 걸 그냥 덮지 않고 다시 짚어본 게, 결과적으로 훨씬 명확한 절벽(10개에서 20개 사이)을 보여준 셈이다.

비교 대상도 같이 재봤다

같은 방식으로 /refactor의 제안 단계도 카탈로그 0개부터 58개까지 실측했다. 결과는 전 구간에서 15/15로 흔들림이 없었다. /ingest만 무너지고 /refactor는 멀쩡하다는 게 이번엔 추측이 아니라 숫자로 확인됐다 — 그러니 카탈로그가 필요한 판단은 /ingest에서 완전히 빼고 /refactor로 넘기는 방향이 타당하다는 근거가 하나 더 생겼다.

그럼 refactor를 매번 돌려야 하나

카탈로그를 /ingest에서 완전히 빼면, 병합·분할 판단은 전부 /refactor의 몫이 된다. 그런데 /refactor는 사용자가 손으로 따로 실행해야 하는 명령이다 — “그러면 매번 잊지 않고 /refactor를 돌려야겠네”라는 지적이 바로 나왔다.

한 걸음 더 들어간 질문도 나왔다 — 애초에 왜 두 명령이 따로 있어야 하나. 코드를 다시 확인해보니 이유가 있었다. /refactor는 원래부터 “AI가 병합·분할을 제안만 하고, 사람이 체크박스로 골라서 승인해야 실제로 실행되는” 2단계 구조로 설계돼 있었다. /ingest는 새 노트를 만드는 것뿐이라 잘못돼도 기존 걸 안 건드리는 저위험 작업이라 바로 쓰지만, /refactor는 이미 사람이 다듬어둔 기존 노트를 병합하거나 쪼개는 고위험 작업이라 일부러 리뷰 게이트를 걸어둔 것이었다.

이 설계 의도는 건드리지 않기로 했다. 대신 “완전 자동 실행”이 아니라 “제안 단계만 자동으로 이어서 보여주기”로 타협점을 찾았다 — 아무것도 건드리지 않는 부분만 자동화하고, 실제로 파일을 바꾸는 부분은 그대로 사람이 승인해야 하게 남겨뒀다. 여기에 조건 하나가 더 붙었다 — 인제스트가 취소돼서 노트가 하나도 안 만들어졌으면 굳이 refactor를 권할 필요가 없고, 취소됐어도 하나라도 만들어졌으면 권해야 한다는 것. 정확한 조건이라 그대로 반영했다.

이 논의 도중 질문 하나가 더 나왔다 — “지금 refactor에 링크만 넣는 기능은 없는 거 아니냐”는 것. 다시 코드를 봤다. /refactor의 프롬프트엔 “Link & Enrich”라는 세 번째 카테고리가 설명돼 있었다 — 병합도 분할도 아니고, 서로 다른 두 노트를 그냥 위키링크로 연결만 해주는 것. 그런데 실제로 AI가 출력해야 할 형식에는 이 카테고리에 대응하는 태그 자체가 없었고, 그걸 읽어서 실행하는 코드도 어디에도 없었다. 설명은 있는데 실행은 안 되는, 죽어있는 카테고리였다.

ingest 안에 “관련 노트 몇 개만 검색해서 넣어주는” 방식도 잠깐 검토했다. 매 청크마다 검색 호출이 하나 더 붙는 비용, 그리고 “소수만 넣어도 안전한지”조차 검증이 안 됐다는 점(카탈로그 10개도 이미 정확도가 꺾이는 걸 방금 확인했으니) 때문에 채택하지 않고 별도로 미뤄뒀다.

근거를 스스로 다시 썼다

/ingest 뒤에 /refactor 제안을 자동으로 보여주자는 아이디어를 처음 냈을 때 근거는 “카탈로그가 없어지며 사라진 연결 기능을 /refactor가 대신 채워줄 것”이었다. 그런데 Link & Enrich가 죽어있는 기능이라는 걸 알고 나니, “굳이 권장할 필요가 있냐”는 질문에 이 근거가 답이 안 됐다.

다시 생각했다. 카탈로그가 없어지면 /ingest는 이제 같은 인물이나 개념을 다루는 노트가 이미 있는지조차 전혀 모르게 된다. 그러니 서로 다른 인제스트 실행에 걸쳐 같은 개체를 다루는 중복 노트가 생길 위험이 예전보다 커진다 — 그리고 이 중복은 /refactor의 병합(Merge) 제안이 실제로 잡아준다. “링크가 아쉬워서”가 아니라 “카탈로그 없이 돌린 결과 늘어난 중복을 청소하기 위해”로 근거를 바꿔 썼다.

남는 생각

이번엔 검증을 몇 겹으로 거쳤는데도 첫 결론이 틀려 있었다 — 실측을 하고, 그 실측 자체를 다시 의심하고, 표본을 늘려서야 진짜 그림이 나왔다. “동작하는 것처럼 보이는 결과”와 “실제로 맞는 결과”는 역시 다른 것이었고, 이번엔 그 차이를 감추지 않고 정정하는 데서 오히려 더 명확한 근거(10개와 20개 사이의 절벽)를 얻었다.

“이게 왜 필요하지?”라는 질문이 두 번 나왔고, 두 번 다 처음 세웠던 근거를 무너뜨리고 다시 세우게 만들었다. 한 번은 refactor를 아예 필수 단계로 만드는 대신 “제안만 자동화”하는 절충안으로, 다른 한 번은 “링크 보완”이라는 틀린 근거를 “중복 청소”라는 맞는 근거로. 그리고 있는 줄 알았던 안전망(Link & Enrich)이 사실 없었다는 걸 발견한 건, 설계 문서에 적힌 대로 다 되고 있으리라는 가정 자체를 의심해야 한다는 걸 다시 확인시켜준 셈이다.


Share this post:

설계까지 다 거치고도, 실행해봐야 보였다

  1. 1. 설계까지 다 거치고도, 실행해봐야 보였다 1편 — 테스트가 원천적으로 못 잡는 자리에 버그를 심어놓고 있었다
  2. 2. 설계까지 다 거치고도, 실행해봐야 보였다 2편 — 병합을 빼자, 분할이 살아났다
  3. 3. 설계까지 다 거치고도, 실행해봐야 보였다 3편 — 레이스를 막을 방법이 아니라, 없앨 이유를 찾았다
  4. 4. 설계까지 다 거치고도, 실행해봐야 보였다 4편 — 없는 줄도 몰랐던 안전망을 찾아냈다
  5. 5. 설계까지 다 거치고도, 실행해봐야 보였다 5편 — 되돌리기 버튼 하나에, 버그가 세 겹으로 숨어 있었다
  6. 6. 설계까지 다 거치고도, 실행해봐야 보였다 6편 — 자정을 넘긴 순간에만 걸리는 레이스였다