중지하라고 했는데 왜 안 멈추는가
이날은 같은 결함 패턴이 하루에 두 번 나타난 날이다.
진입은 무조건 발화하는데 해제는 조건부인 이벤트 쌍. 이게 플래그를 영구 고착시켰다.
그리고 교육 규칙이 함정을 만들고 있었다는 것도 발견했다.
첫 번째 버그는 검증 루프에서 일어났다.
판정자가 “중지 승인”을 반환했는데, 루프가 계속 돌았다.
원인은 타이밍이었다.
판정자를 부르기 직전에 메인 에이전트가 관리용 액션을 먼저 돌렸다.
그 빈 결과가 판정 결과로 오인된 거다.
진짜 판정 결과가 나중에 도착했지만 이미 무시됐다.
핵심은 가드 비일관성이었다.
검증자 분기에는 빈 결과 방어가 있었다.
근데 판정자 분기에는 없었다.
같은 처리인데 한쪽만 방어한 거다.
이모지가 고착한다
두 번째 버그는 터미널 타이틀 이모지였다.
압축 중 상태를 표시하는 이모지를 추가했다.
근데 압축이 끝났는데도 이모지가 안 풀렸다.
원인이 같은 날 첫 번째 버그랑 같았다.
진입 이벤트는 무조건 발화한다.
근데 해제 이벤트는 성공했을 때만 발화한다.
중단하거나 취소하거나 에러가 나면 해제가 안 온다.
그러면 플래그가 영구적으로 남는다.
이걸 “비대칭 이벤트 쌍”이라고 부른다.
진입은 무조건인데 해제는 조건부다.
실패 경로에서 해제가 누락되면, 플래그가 고착된다.
같은 날 두 버그에서 같은 패턴이 나타났다.
해결: 해제를 분산 바인딩하라
해결은 해제를 단일 이벤트에 의존하지 않는 거다.
여러 재활동 신호에 해제를 바인딩한다.
어떤 경로로든 활동이 재개되면 플래그가 풀린다.
단일 이벤트에만 의존하면, 그 이벤트가 안 오면 풀 수 없다.
교육 규칙이 함정을 만들었다
세 번째 발견은 더 흥미로웠다.
백그라운드 실행이 영원히 멈추는 버그가 있었다.
toolResult가 한 번도 기록되지 않아서 턴이 대기한 거다.
원인이 코드 버그가 아니었다.
시스템 프롬프트의 교육 라인이 함정을 만들고 있었다.
프롬프트에 “백그라운드로 실행할 때는 timeout을 같이 써라”라고 안 적어놨다.
그러니까 에이전트가 timeout 없이 백그라운드로만 실행했다.
timeout이 있어야 부분 출력을 반환하는데, 없으니까 영원히 대기한 거다.
함정의 원천을 코드로 교체하라
이걸 해결하기 위해 timeout-guard 확장을 만들었다.
백그라운드 실행할 때 timeout을 안 넣으면 자동으로 주입한다.
교육 규칙에 의존하지 않고 코드 레벨에서 강제하는 거다.
교육 규칙은 에이전트가 안 따를 수 있다.
프롬프트에 써놓으면 대부분 따르지만 가끔 안 따른다.
그 가끔이 문제다.
코드로 강제하면 안 따를 수가 없다.
“교육으로 막을 수 있는 건 코드로 막아라.” 이게 이날의 핵심 원칙이었다.
언어 drift 원인 규명
마지막으로 LLM이 갑자기 중국어로 답하는 현상의 원인을 규명했다.
네 가지 원인이 있었다.
첫째, 훈련 데이터 불균형이다.
중국어 데이터가 한국어보다 압도적으로 많다.
불확실하면 중국어로 기본 설정된다.
둘째, 어휘 중첩이다.
한자랑 중국어 한자가 같은 유니코드 코드포인트를 쓴다.
한자를 출력하는 순간 언어 경계가 무너진다.
셋째, 프롬프트 오염이다.
도구 출력이나 이전 응답에 다른 언어가 섞이면, 그 언어가 이어진다.
넷째, 추론 언어와 출력 언어의 분리 실패다.
추론 블록에 한국어나 중국어 토큰이 들어가면, 출력도 그 언어로 흘러간다.
최후 보루는 사용자 피드백
방어 패턴을 3중으로 구성했다.
추론은 영어, 응답은 한국어, 한자는 영 금지. 근데 이걸로 완벽히 막을 수는 없다.
언어 감지가 확률적이기 때문이다.
결국 최후 보루는 사용자 피드백이다.
“한국어로 해야지”라고 정정하는 것. 자동화로 완벽히 막을 수 없는 건, 사용자가 개입해야 한다.
마무리
내가 이날 배운 핵심은 다음과 같다.
이날의 핵심은 “비대칭 이벤트 쌍”이었다.
진입은 무조건인데 해제는 조건부인 패턴. 이게 같은 날 두 버그에서 나타났다.
이 패턴을 발견하면 해결은 명확하다.
해제를 단일 이벤트에 의존하지 마라. 여러 신호에 분산 바인딩하라.
어떤 경로로든 플래그가 풀리게 만들어라. 그리고 “교육 규칙이 함정을 만든다”는 교훈도 중요했다.
프롬프트에 써놓은 규칙을 에이전트가 안 따르면 버그가 된다.
교육으로 막을 수 있는 건 코드로 막아야 한다.
코드는 안 따를 수가 없으니까. 오탭을 구조적으로 0으로 만드는 게 이날의 철학이었다.
사용자 주의에 의존하지 말고 오탭 자체가 발생 불가능하게 구조를 설계하라.