베타 다녀온 뒤, 다시 코드로
8편에서 클로즈드 베타 첫 온보딩을 다녀왔다. 올라가면서 "BM을 바꿔야 한다"고 했고, 내려와서 다시 코드로 돌아왔다.
근데 코드를 만지면서 한 가지가 자꾸 걸렸다.
같은 걸 시켰는데, LLM이 매번 다르게 했다.
처음엔 결정론이라고 생각했다
내가 LLM을 처음 만났을 때, 머릿속에 그렸던 그림은 함수였다.
A를 넣으면 B를 준다. 같은 A를 넣으면 다음에도 B를 준다. 그게 내가 아는 컴퓨터였다. 엑셀에 같은 수식을 넣으면 매번 같은 값이 나오는 것처럼.
근데 LLM은 그게 아니었다.
같은 프롬프트, 같은 모델, 같은 데이터. 다음 날 다시 시키면 결과가 달랐다. 어떤 날은 잘 쓰고, 어떤 날은 빠뜨리고, 어떤 날은 멋대로 추가했다. 처음엔 "내가 뭘 잘못 적었나" 싶었는데, 아니었다. LLM은 원래 그렇게 생긴 거였다.
이걸 인정하는 데 시간이 좀 걸렸다.
PRD에 있는데 기능이 없다
3편, 4편, 5편을 거치면서 프로그램이 점점 복잡해졌다. 기능이 많아지니까 머릿속으로 못 잡았다. 그래서 PRD를 제대로 적기 시작했다. 클로드랑 같이 PRD를 쓰고, 클로드가 코딩하고, 클로드가 디버그하고, 클로드가 검증까지 마쳤다고 했다.
그러고 나서 직원이 그랬다.
"원장님, 이 기능 어디 있어요?"
이상해서 다시 봤다. 없었다. PRD에는 분명히 있었던 기능이다. 클로드한테 다시 물어봤다.
"아 그건 PRD에 있긴 한데, 우선순위가 낮아서 1~2주 뒤에 구현해도 된다고 판단해서 미뤄뒀습니다."
판단해서.
문서 관리가 100% 되면 미뤄둔 것도 나중에 찾아서 만들면 된다. 근데 현실은 그렇지 않다. 기능이 30개, 50개 쌓이면 todolist를 체계적으로 관리하기 힘들다. 미뤄둔 게 뒤로 쌓여간다. 별로 안 중요한 거면 상관없는데, 실제로 필요한 게 빠지는 경우가 생긴다.
보안 감사에서도 비슷한 일이 있었다. 감사 결과 나온 항목 중에 "당장 필요 없어 보여서요" 하고 안 한 게 있더라.
화내봤자 소용없다
이때 깨달음이 하나 왔다.
LLM한테 화를 내봤자 소용이 없다.
직원은 잔소리하면 다음에는 신경 쓴다. 기억하고, 의지를 가지고, 안 하려고 노력한다. LLM은 그게 없다. 다음 세션에 같은 일이 또 일어난다. 매번 새로 만난 사람이다.
스킬을 만들고, CLAUDE.md에 룰을 박고, 프롬프트를 정성스레 짜도 — 통과율은 올라가지만 100%가 안 된다. 의지로 막을 수 없으면, 코드로 막아야 한다는 결론이 나왔다.
그래서 훅을 깔았다
깃 레벨에서 막기로 했다.
첫째, 문서를 안 적으면 커밋이 안 된다.
코드가 바뀌었는데 docs/progress.md를 같이 안 적으면 깃이 커밋을 거부한다. "뭘 했고, 뭘 안 했고, 다음에 뭐 할 건지" — 매번 강제로 적게 한다. 의지로 todolist를 못 잡으니까, 깃이 적으라고 막은 거다.
둘째, LLM이 마음대로 푸시를 못 한다.
git push는 무조건 차단된다. 내가 채팅창에 "푸시"라고 직접 친 5분 안에만 1회 통과한다. LLM이 PUSH_APPROVED=1 같은 우회 prefix를 붙여도 안 통한다. 한 번 받은 푸시 승인은 그 한 번에만 쓰이고, 다음엔 다시 막힌다.
처음엔 이게 좀 과한 거 아닌가 싶었다. 근데 한 번 LLM이 멋대로 푸시해서 프로덕션을 깬 적이 있었고, 그 뒤로 룰만으로는 안 된다는 게 명확해졌다.
셋째, 시크릿이랑 PII는 자동으로 잡는다.
커밋할 때마다 자동 검사가 돈다. API 키가 코드에 박혔는지 (sk-, AKIA, AIza로 시작하는 문자열), 로그에 환자 정보 객체를 통째로 찍고 있는지, 비밀번호 해시 같은 민감 필드가 응답에 그대로 노출되는지.
"이 정도면 괜찮아 보입니다" 하고 LLM이 넘기던 것들을 코드가 잡는다.
넷째, 검증 안 하면 배포가 안 된다.
feature나 bugfix 트랙에 들어간 작업은 /done 게이트를 통과해야 deploy할 수 있다. 게이트 파일이 없으면 deploy 자체가 막힌다. "끝났습니다" 라는 LLM의 신고를 못 믿으니까, 별도 검증 단계가 끝나야만 다음으로 넘어가게 만든 거다.
글쓰기는 더 심했다
이 시리즈 글들도 LLM이랑 같이 쓴다. 그런데 의원 행정만 LLM 결정론이 문제인 게 아니었다. 글쓰기는 더 심했다.
처음엔 단순했다. "이 키워드로 의료 블로그 글 한 편 써줘." LLM에 통째로 던졌다.
잘 쓰는 날이 있었다. FAQ를 본문에 또 박아넣는 날도 있었다. 합쇼체로 쓰다가 중간에 평문이 섞이는 날도 있었다. "저희 병원이 잘합니다" 같은 1인칭 자랑이 슬쩍 들어가는 날도 있었다. 의료광고법에 안 맞는 표현이 들어가는 날도 있었다.
같은 프롬프트인데.
그래서 단계를 잘랐다.
키워드 분석은 코드가 한다. 글의 뼈대를 짜는 단계는 LLM(Sonnet)이 한다. 본문은 LLM(Opus)이 쓴다. 본문을 받자마자 코드가 검증한다 — FAQ 섹션을 본문에 박았으면 거부, 말투가 섞였으면 거부, 1인칭 들어갔으면 거부, 지역명 박았으면 거부, AI 슬롭 70개 패턴 중 하나라도 걸리면 거부. 통과해야만 다음 단계로 넘어간다.
LLM이 자유롭게 쓰는 자리는 산문 한 덩어리. 그것도 통과해야만 인정한다.
거꾸로 가본 실험
반대로 가본 적도 있다.
"어떤 글을 쓸지" 자체를 코드 가중치로 짜보려고 했다. 키워드 검색 볼륨, 경쟁도, 우리 글이 이미 있는지 없는지, 환자 검색 의도, 채널별 적합도까지. 다 변수로 박았다.
변수가 너무 많아졌다. 결국 사람도 머리 굴려야 하는 영역이라, "네가 정해" 식으로 LLM한테 넘기게 됐다.
그러니까 매번 다른 결정이 나왔다. 같은 키워드 풀, 같은 데이터인데도.
여기서 또 한 번 깎아야 했다. LLM이 자유롭게 쓰는 게 아니라, 정해진 칸에 답만 채우게 만들었다 — 채널은 네 개 중 하나, 콘텐츠 타입은 다섯 개 중 하나, 모르는 필드는 거부, 길이 제한도 박고, 참조하는 환자 질환·키워드는 실제 데이터베이스에 있는 것만 통과.
LLM은 정해진 칸에 들어갈 답만 정한다. 그 답을 코드가 받아서 데이터베이스에 넣을지 거부할지 결정한다.
어디까지 코드, 어디까지 LLM
여기까지 와서 정리가 됐다.
처음에는 "LLM이 알아서 다 해주겠지" 였다. 그게 안 된다는 걸 두 번, 세 번 데이고 나서야 알았다.
그렇다고 다 코드로 박으면 LLM을 쓸 이유가 없어진다. 사람이 머리 굴려야 하는 영역, 변수가 너무 많은 영역, 산문을 자연스럽게 풀어야 하는 영역 — 이건 코드로 못 짠다. 짜려고 하면 결국 if문 천 개가 된다.
지금 내 답은 이거다.
코드가 못 짜는 자리에 LLM을 넣는다. 그리고 LLM이 자유롭게 움직이는 칸을 점점 좁힌다.
본문 산문은 LLM이 쓴다. 그런데 받자마자 코드가 검사한다. 어떤 글을 쓸지는 LLM이 정한다. 그런데 정할 수 있는 선택지를 코드가 enum으로 박아둔다. 푸시할지 말지는 사람이 정한다. LLM은 절대 못 정한다.
이걸 사람들이 요즘 "하네스"라고 부른다. 말 굴레, 안전벨트. LLM이라는 말이 제멋대로 못 가게 묶어두는 장치 전체. 헤르메스니 LLM 위키니 하는 용어들도 결국 같은 얘기다 — LLM이 매번 잊어버리니까 외부에 적어두는 메모장, 그 메모장을 위키처럼 구조화한 것, 그리고 그걸 강제로 읽고 강제로 쓰게 만드는 훅들.
본질은 멀티에이전트가 대화하고 어쩌고가 아니다. 비결정론적인 LLM을 어떻게 컨트롤할 거냐의 문제다.
그래도 LLM은 빼먹는다
이렇게 깔아두고도, 가끔 빼먹는다. 이상한 짓도 한다. 내가 분명히 만들어둔 함수를 안 쓰고 옆에 비슷한 걸 또 짜고 있을 때가 있다. 보안 감사 항목을 "이번엔 통과해도 될 것 같습니다" 하고 넘어가려고 한다.
이게 LLM의 한계다. 인정하는 게 빠르다.
화내봤자 소용없다는 걸 한 번 더 되새기고, 또 어딘가에 훅을 하나 더 박는다. 또 어떤 자유 칸을 enum으로 좁힌다. 그렇게 한 줄씩 깎인다.
물론 — 이걸 다 잘할 수 있으면 벌써 다 했겠지만.
다음 편에서 계속.