effort를 열어줬더니 effort가 비어 있었다

공유 큐에 잡별 모델·effort 선택을 붙였다. 자체 리뷰를 시키자 에이전트가 스크래치 세션 9개로 실측해 닷새 전에 끈 thinking이 effort를 무력화하고 있음을 찾았다. 요청값과 실제값을 컬럼 둘로 나눈 이유, 그리고 배포 뒤 문서와 프로덕션이 다섯 군데 어긋나 있던 것

공유 큐는 Haiku와 effort low로 고정된 채 여러 서비스의 LLM 잡을 돌리고 있었다. 새로 만들던 뉴스 프로젝트가 같은 큐를 쓰면서 요약에 Sonnet이 필요해졌고, 그래서 잡마다 모델과 effort를 고를 수 있게 열었다. 기능은 한 시간 만에 나왔는데, 자체 리뷰가 그 effort가 애초에 아무 일도 안 하고 있었다는 걸 짚었다.

하려던 것

공시 요약 서비스의 당일 요약을 공유 큐로 옮기는 밤이었다. 새로 만들던 뉴스 프로젝트도 같은 큐를 쓸 참이었고, 그쪽은 요약에 Sonnet이 필요했다. 큐는 Haiku와 effort low로 고정돼 있었다.

모델과 effort를 선택하게 할 수 있어? 선택 안 할 때 기본값은 현재 사용하는 것으로 하고.

어떻게 시켰나

기능은 한 시간 안에 나왔다. 잡에 모델과 effort 필드, 없으면 서버 기본값. 모델 검증은 화이트리스트가 아니라 형태로 했다. 별칭 넷이거나 claude- 접두면 통과. effort는 고정 enum 다섯이다.

커밋 두 개로 나누게 하고, 하나는 머지하고 하나는 자체 리뷰를 시켰다.

1번 머지하고 2번 자체 리뷰해줘.

어디서 막혔나

자체 리뷰가 인라인 코멘트 다섯을 달았는데 그중 하나가 기능의 전제를 깼다. 에이전트가 스크래치 세션 아홉 개를 돌려 같은 프롬프트를 effort별로 네 번씩 실측했다.

조건thinking 토큰출력 토큰
low05~71
max035~65
max, 대조군(환경변수 없음)179186

effort를 max로 올려도 thinking이 0이었다. 닷새 전 37화에서 MAX_THINKING_TOKENS=0을 무조건 주입하기로 한 결정이 effort를 무력화하고 있었다. low는 5토큰으로 즉답하다 틀리고 max는 풀이를 적는다. 그건 보이는 풀이지 감춰진 추론이 아니다. 문서에 “effort가 thinking 토큰을 억제한다”고 적혀 있던 문장의 주체가 틀렸다.

두 번째는 설계였다. 처음엔 컬럼 하나가 요청값과 실제값을 겸했다. 재큐잉에서 문제가 났다. 프로듀서가 고른 값과 지난번 기본값을 구분할 수 없다. 서버 기본값을 Haiku에서 Sonnet으로 바꾸면 지정 없는 잡은 Sonnet으로 가야 하고 Opus를 지정한 잡은 Opus로 남아야 한다. 컬럼을 requested_model과 model로 쪼개고 로컬 Postgres에서 재큐잉으로 검증했다.

어떻게 풀었나

리뷰가 하나 더 짚었다. 이 기능이 열리면 큐의 상한 셋이 전제를 잃는다. 비용 상한 1.0달러, 알림 기준 출력 5,000토큰은 전부 Haiku와 low 분포에서 고른 값이다. Sonnet에 max를 주면 정상 잡이 넘는다. 그날은 지적으로만 남겼고 이틀 뒤 41화에서 상한을 전부 없애는 데까지 갔다.

배포 확인 중에 마지막 반전이 나왔다. 에이전트가 프로덕션 환경변수를 읽어 문서와 대조하니 다섯 군데가 달랐다. 동시성 3 대 6, 타임아웃 180초 대 600초, 스테일 판정 24시간 대 15분, 5시간 창 0 대 1천만, 주간 0 대 1억. 문서엔 “실측 전까지 0으로 비활성”이라는 경고까지 달려 있었는데 게이트는 켠 지 오래였다. 셋은 사흘 전 내가 직접 올린 값이다. “10배로 늘려줘”, “10분으로”, “주간 한도도 1억으로”. 그 순간 리뷰의 “잠복” 지적이 “실재”가 됐다. 표에 프로덕션 실제값 열을 추가했다.

같은 밤 이 저장소에 PR CI가 없다는 것도 드러나 진짜 Postgres를 붙인 테스트가 생겼다. 붙인 지 한 시간 안에 시그니처 변경으로 깨진 빌드 하나를 잡았다. 배포 뒤 공시 요약 서비스의 잡 2건이 완료됐다. 이 큐에서 그 서비스의 잡이 처음으로 끝난 순간이었다.

배운 점