필터 교차 제외
이날은 하루 종일 버그와 싸운 날이었다.
프롬프트 주입 시스템에서 5개의 연쇄 버그가 터졌다.
하나를 고치면 다음이 나오는 식이었다.
마지막 버그는 Go 소스 코드까지 분석해야 했다.
그리고 밤에는 괴담 크롤러 파이프라인도 재설계했다.
첫 번째 버그는 필터의 교차 제외였다.
프롬프트 주입 시스템에는 두 개의 훅이 있다.
하나는 메시지 변환 훅이고 다른 하나는 채팅 메시지 훅이다.
두 훅이 각각 다른 필터로 프롬프트를 걸렀다.
메시지 변환 훅은 display가 true가 아닌 것만 처리했다.
채팅 메시지 훅은 regex가 없는 것만 처리했다.
근데 regex가 있으면서 display도 true인 프롬프트가 있었다.
이 프롬프트는 두 필터 모두에서 제외됐다.
어느 쪽에도 주입되지 않은 거다.
필터를 따로 설계하면 이런 교차 제외가 생긴다.
각 필터는 자기 기준에서 맞는 것 같은데, 합치면 빈틈이 생긴다.
해결은 URL 매칭된 프롬프트를 항상 주입하는 경로로 분리하는 거였다.
전역 상태 오염
두 번째 버그는 전역 상태 오염이었다.
토큰 예산 추적을 위한 변수가 전역에 있었다.
주 에이전트가 주입하면 마커가 설정된다.
그러면 서브에이전트가 “예산이 소진됐다”고 판단해서 주입을 건너뛴다.
주 에이전트의 주입이 서브에이전트에 영향을 준 거다.
해결은 세션별 상태 격리다.
세션 ID를 키로 쓰는 맵에 상태를 저장했다.
각 세션이 독립적으로 예산을 추적한다.
주 에이전트가 주입해도 서브에이전트의 예산에 영향을 안 준다.
멀티 세션 환경에서 전역 변수는 위험하다.
세션마다 독립적인 상태가 필요하면, 맵으로 격리해야 한다.
Go 코어 단일 파트 제한
다섯 번째이자 가장 깊은 버그는 Go 소스까지 파고들어야 했다.
display가 false인 프롬프트가 주입되지 않았다.
메시지 변환 훅에서 주입하는데, 안 간다.
원인을 추적하다가 Go 소스를 분석했다.
opencode의 Go 코어에서 메시지 내용을 읽는 메서드가 있었다.
이 메서드는 첫 번째 텍스트 파트만 반환한다.
내가 주입한 파트를 첫 번째가 아닌 두 번째에 넣었으니까, Go 코어가 무시한 거다.
모든 Go 프로바이더가 이 메서드를 쓴다.
OpenAI, Anthropic, Gemini 전부. 그러니까
첫 번째 파트가 아닌 건 어느 프로바이더에서든 무시된다.
해결은 첫 번째 파트의 텍스트에 직접 주입하는 거였다.
새 파트를 추가하는 게 아니라, 기존 첫 번째 파트의 텍스트 앞에 끼워 넣는다.
괴담 크롤러 재설계
밤에는 괴담 수집 파이프라인을 재설계했다.
기존 파이프라인은 품질 게이트가 없었다.
수집하면 그냥 저장했다.
근데 30%가 쓰레기였다.
그래서 파이프라인을 다시 설계했다.
새 파이프라인은 5단계다.
증분 크롤링, 해시랑 N-gram으로 중복 제거, 임베딩으로 의미 중복 제거, AI 큐레이션, 저장.
임베딩 중복 제거가 필요한 이유
해시랑 N-gram은 같은 글을 잡는다.
근데 “같은 괴담을 다른 사람이 다르게 쓴 글”은 못 잡는다.
루리웹에 있는 “자유로 귀신” 이야기랑 디씨인사이드에 있는 “자유로 귀신” 이야기는 내용은 같은데 글은 다르다.
임베딩으로 코사인 유사도 0.9 이상이면 같은 이야기로 판단한다.
이건 해시로 못 잡는다.
의미가 같은지 봐야 한다.
순서 최적화
그리고 순서를 최적화했다.
임베딩 중복 제거는 싸다.
AI 큐레이션은 비싸다.
LLM 호출이 필요하니까. 그래서 싼 단계를 먼저 돌린다.
임베딩으로 중복을 잡고 남은 것만 AI 큐레이션에 넘긴다.
이러면 LLM 호출 수가 최소화된다.
마무리
이날의 핵심은 연쇄 버그였다.
하나를 고치면 다음이 나오고 또 고치면 또 나오고. 마지막에는 Go 소스까지 파고들어야 했다.
연쇄 버그가 생기는 이유는, 시스템에 근본적인 설계 문제가 있기 때문이다.
필터를 따로 설계하면 교차 제외가 생긴다.
전역 변수를 쓰면 세션 간 오염이 생긴다.
파트 구조를 모르면 무시되는 주입이 생긴다.
각 버그를 개별적으로 고치는 것보다, 근본 설계를 바로잡는 게 더 효율적이다.
하지만 근본 설계를 바로잡으려면 시스템 전체를 이해해야 한다.
Go 코어까지 내려가서 이해해야 했던 것처럼. 그리고 괴담 크롤러 재설계에서 배운 건 “비싼 단계를 나중에”다.
싼 걸로 먼저 걸러내면, 비싼 단계에 들어가는 데이터가 줄어든다.
이건 비용 최적화의 기본이다.