같은 버그를 다섯 번 고쳤는데 네 번은 틀렸다

봇 하나가 아침 8시에 시작해서 밤 11시까지 안 끝나 있었다.
트위터에서 글을 긁어와 번역하고, 로컬 모델한테 요약을 시켜서 노트로 남기는 놈이다. 원래 한 시간이면 끝난다. 그날은 열다섯 시간을 돌고도 57개 중 20개에 머물러 있었다.
로그는 이랬다.
로컬 모델 재시도 1/2 — timed out
로컬 모델 재시도 2/2 — timed out
로컬 모델 재시도 1/2 — timed out
로컬 모델 재시도 2/2 — timed out
번역 20/57
첫 번째: 네트워크 탓인 줄 알았다
그 전날 로그를 보니 이런 게 있었다.
RSS 접속 실패: urlopen error timed out
RSS 접속 실패: urlopen error timed out
RSSHub 붙음 (3번째 시도)
아침에 노트북 네트워크가 불안정했다. 같은 날 다른 봇도 같은 이유로 죽어 있었다. 원인이 명확해 보였다.
그래서 재시도를 붙였다. 실패하면 45초 기다렸다가 세 번까지.
그게 열다섯 시간짜리를 만들었다.
번역은 트윗 하나당 모델을 한 번씩 부른다. 57개면 57번이다. 거기에 "세 번까지 기다린다"를 붙이면 한 개당 최대 16분이 된다. 16분 곱하기 57.
같은 날 다른 봇에도 똑같은 재시도를 넣었는데 거긴 멀쩡했다. 그 봇은 호출이 두 번뿐이었다. 같은 약이 한쪽에선 치료고 한쪽에선 독이었다.
두 번째: 출력이 길어서인 줄 알았다
모델이 뱉는 걸 들여다봤다. 한 줄짜리 트윗을 번역해달라고 했는데 이런 게 나왔다.
Okay, let's tackle this translation request. The user wants the tweet
translated into natural Korean without any explanation...
영어로 혼잣말을 하고 있었다. 출력 길이 설정이 8192로 잡혀 있었으니, 저렇게 주절대기 시작하면 8천 토큰까지 갈 수 있다. 초당 30토큰이면 270초. 타임아웃이 300초였다. 딱 맞아떨어졌다.
그래서 출력 길이를 512로 줄였다. 시간은 32초에서 6.7초로 떨어졌다.
그런데 번역이 안 나왔다.
512토큰을 전부 혼잣말에 쓰고 끝나버렸다. 빠르고 쓸모없어졌다.
세 번째: 재시도가 많아서인 줄 알았다
재시도를 세 번에서 두 번으로 줄였다. 다음 날 실행은 여섯 시간 만에 여전히 막혀 있었다.
네 번째: 혼잣말을 잘라내려다
전체 출력을 처음부터 끝까지 봤다. 8065자였다. 그리고 맨 끝에 이게 있었다.
...(혼잣말 8천 자)...
</think>
비트코인은 오늘 76,000 지지선 위에서 유지되고 있습니다.
번역은 처음부터 거기 있었다. 혼잣말이 끝나는 자리에 </think> 태그가 붙고, 그 뒤에 답이 나온다. 그리고 코드에는 이미 그 태그 뒤를 잘라내는 방어가 들어 있었다.
즉 원래 코드가 맞았고, 내가 출력 길이를 줄여서 그 태그에 도달하기 전에 잘라버린 것이다. 두 번째 수정이 답을 죽였다.
되돌리고, 이번엔 "제목이 나오는 자리부터 취한다"는 규칙으로 앞부분을 잘라냈다. 그런데 모델이 출력 앞에서 규칙을 복창하면서 제목을 언급하는 바람에, 그 언급을 진짜 제목으로 잡았다. 멀쩡한 본문 12,000자가 날아갔다.
다섯 번째: 그제서야 다른 걸 봤다
네 번을 틀리고 나서 처음으로 한 일은, 고칠 자리를 찾는 게 아니라 이 루프가 몇 번 도는지 세는 것이었다.
57번이었다.
혼잣말 8천 자는 모델의 성격이다. 못 고친다. 그런데 그 비용을 트윗마다 낼 필요는 없었다. 열 개씩 묶어서 한 번에 보내면 4번만 내면 된다.
번역 묶음 1/4 — 10/10개
번역 묶음 2/4 — 10/10개
번역 묶음 3/4 — 9/9개
번역 묶음 4/4 — 9/9개
번역 완료 38/39개
여섯 시간에서 25분이 됐다.
그리고 코드 원래 주석에 이렇게 적혀 있었다.
# 2) 번역 (벌크 = 로컬)
벌크. 묶어서 하라는 게 원래 설계였는데, 구현은 한 개씩 돌고 있었다. 내가 언제 그렇게 만들었는지는 기억나지 않는다.
네 번의 공통점
나중에 적어놓고 보니 앞의 네 번은 전부 같은 실수였다.
한 번의 호출만 보고 고쳤다. 이 호출이 왜 느린가, 왜 실패하는가, 어떻게 하면 빨라지는가. 네 번 다 현미경을 들이댔고 네 번 다 초점이 맞았다. 느린 이유도 맞았고, 출력이 긴 것도 맞았고, 혼잣말을 하는 것도 맞았다.
전부 맞는데 다 틀렸다. 이 호출이 57번 돈다는 걸 한 번도 세지 않았기 때문이다.
같은 날 다른 봇에 넣은 재시도가 멀쩡했던 이유도 같다. 거긴 두 번이었다. 처방이 맞았는지 틀렸는지를 가른 건 약이 아니라 횟수였다.
남긴 것
다음에 이 파일을 열었을 때 되돌리지 않도록 주석을 달았다.
# 한 트윗씩 부르던 걸 10개씩 묶는다.
# 이 모델은 트윗 한 줄을 번역하는 데도 혼잣말을 8,000자쯤 하고
# 그 끝 뒤에야 답을 내놓는다(실측). 그 비용을 트윗마다 내면
# 42~57번이라 6~15시간이 걸린다. 실제로 이틀 연속 그랬다.
# 묶으면 그 비용을 4번만 내면 된다.
그리고 하나 더.
# ⚠️ num_predict를 줄이면 안 된다. 혼잣말 끝의 </think> 뒤에
# 진짜 번역이 붙어 나오기 때문에, 잘라내면 답 자체가 사라진다.
두 번째가 더 중요하다고 생각한다. 저건 내가 이미 한 번 시도했다가 실패한 길이고, 주석이 없으면 나는 분명히 또 갈 것이다. 빨라 보이니까.
고친 내용보다 왜 그 방향이 아닌지를 적어두는 게 나중에 더 쓸모 있었다. 지금까지 세 번 그랬다.