공지를 전부 LLM에 넣지 않기로 했습니다 (규칙 스코어링 1차 필터와 캐시 산수)
배너 고를 공지를 AI에게 맡기려다 비용 구조부터 계산했습니다. Prompt Caching 최소 프리픽스와 모델별 캐시 때문에 Haiku 우선 전략이 절반쯤 어긋났어요.
[배경 - 배너를 사람이 눈으로 고르고 있었다]
아주이벤트 홈 화면에는 배너가 있습니다. 중요한 공지를 크게 띄우는 자리예요.
이 배너에 뭘 올릴지는 관리자가 직접 정합니다. 새로 올라온 공지를 훑어보고 눈에 띄는 걸 고르는 식이에요. 저희 서비스는 하루 평균 830건의 알림을 보내는 규모인데, 그 뒤에 있는 원본 공지도 매일 쌓입니다.
문제는 두 가지였어요. 첫째, 반복 작업입니다. 둘째, 기준이 없습니다. 오늘 제가 고른 것과 다음 주에 다른 사람이 고르는 게 다를 수 있어요.
그래서 AI에게 추천을 맡겨보기로 했습니다. 그런데 설계를 시작하자마자 비용 구조에서 막혔어요.
[문제 상황 분석 - 전부 넣으면 얼마인가]
가장 단순한 방법부터 계산해봤습니다
제일 쉬운 건 전체 공지를 프롬프트에 넣고 “이 중에 배너로 좋은 걸 골라줘” 하는 겁니다. 구현은 30분이면 끝나요.
계산을 해보니 이게 안 되는 이유가 두 개였습니다.
첫째, 입력 토큰이 선형으로 늘어납니다. 공지 하나가 T 토큰이고 하루 N건이면 매 호출마다 N×T가 들어가요. 공지가 쌓일수록 호출당 비용이 계속 올라갑니다.
둘째, 품질이 같이 떨어집니다. 관련 없는 공지가 대부분인 목록을 통째로 주면 모델이 그 안에서 신호를 찾아야 해요. 후보가 이미 좁혀진 상태로 주는 것보다 결과가 흔들립니다.
두 번째가 더 중요했어요. 비용은 돈 문제인데 품질은 기능이 되냐 마냐의 문제니까요.
세 가지 선택지를 놓고 봤습니다
| 방식 | 장점 | 단점 |
|---|---|---|
| 전체 공지를 그대로 전달 | 구현이 단순하다 | 토큰이 선형 증가, 품질 제어가 어렵다 |
| 규칙만으로 자동 선정 | API 비용이 0이다 | 시의성과 맥락을 못 읽는다 |
| 규칙으로 1차, LLM으로 2차 | 비용과 품질을 같이 잡는다 | 완전 자동화가 아니다 |
두 번째를 진지하게 검토했어요. 조회수와 좋아요만 봐도 어느 정도는 골라집니다. 그런데 이 방식은 “시험 기간이 다가오니 학사 공지가 중요하다” 같은 판단을 못 해요. 숫자에는 그 정보가 없습니다.
세 번째로 갔습니다. 규칙이 후보를 좁히고, LLM이 그중에서 고르고, 사람이 확정하는 구조예요.
[해결 방법 - 2단계 구조와 비용 장치]
1단계는 규칙으로 좁힙니다
1차 스코어링에 쓸 신호는 다섯 개로 잡았어요.
- 조회수
- 좋아요 수
- 키워드 구독 매칭 수
- 카테고리 가중치
- 일정 근접도 (마감이 임박할수록 높게)
이 다섯 개로 점수를 매겨 상위 후보만 남깁니다. 여기서 얻는 건 비용 절감만이 아니에요. 배너 적합 기준이 가중치로 문서화됩니다. 지금까지 사람 머릿속에만 있던 기준이 코드에 남는 거예요.
일정 근접도를 넣은 이유가 이겁니다. 조회수만 보면 이미 마감된 공지가 계속 상위에 남아요. 배너는 “지금 봐야 할 것”을 띄우는 자리인데 그러면 안 됩니다.
2단계는 후보만 LLM에 넘깁니다
좁혀진 후보에 대해서만 Claude API를 부릅니다. 여기서 얻는 건 두 가지예요. 배너 적합성 판단과, 왜 그렇게 판단했는지에 대한 문장입니다.
두 번째가 실제로는 더 중요했어요. 점수만 나오면 관리자가 그걸 검증할 수 없습니다. “마감이 3일 남았고 구독 키워드와 겹치는 게 많다” 같은 문장이 붙어야 사람이 판단에 참여할 수 있어요.
출력 형식은 프롬프트가 아니라 API로 고정합니다
여기서 예전 습관 하나를 버려야 했습니다.
“JSON으로만 답해” 라고 프롬프트에 쓰고, 응답 앞부분을 { 로 미리 채워넣는 방식(prefill)을 오래 썼어요. 그런데 이 방식은 최근 모델에서 400 에러가 납니다. 마지막 턴을 assistant로 채우는 게 더 이상 허용되지 않아요.
대신 output_config.format 에 JSON 스키마를 주면 API 차원에서 형식이 강제됩니다. 파싱 실패를 대비한 재시도 루프도 같이 사라져요. 프롬프트로 부탁하던 걸 계약으로 바꾸는 셈입니다.
저비용 모델 우선, 캐싱으로 반복 비용 절감
여기까지가 원래 계획이었어요. Haiku로 먼저 부르고, 결과가 미덥지 않으면 Sonnet으로 올리고, 고정 프롬프트는 Prompt Caching으로 재사용하는 구조입니다.
가격은 이렇습니다. 100만 토큰 기준이에요.
| 모델 | 입력 | 출력 |
|---|---|---|
| Claude Haiku 4.5 | $1 | $5 |
| Claude Sonnet 5 | $3 | $15 |
| Claude Opus 5 | $5 | $25 |
Haiku와 Sonnet은 3배 차이입니다. 하루 몇 번 부르는 작업이라 절대 금액은 작지만, 구조를 잡아두는 편이 낫다고 봤어요.
캐싱 쪽 산수도 정리했습니다. 캐시 쓰기는 기본 입력가의 1.25배(5분 TTL), 읽기는 약 0.1배예요. 그러니까 5분 TTL 기준으로 두 번만 읽으면 이미 이득입니다. 1.25 + 0.1 = 1.35 인데 캐시 없이 두 번 부르면 2.0 이니까요.
1시간 TTL은 쓰기가 2배라 세 번은 읽어야 손익분기를 넘습니다.
[성과 - 계산 단계에서 드러난 것 두 가지]
여기서 계획이 어긋났어요. 문서를 다시 읽다가 두 개를 발견했습니다.
캐시에는 최소 길이가 있고, 모델마다 다릅니다
프리픽스가 일정 길이를 넘지 않으면 캐시가 아예 만들어지지 않습니다. 그런데 에러가 나지 않아요. 그냥 cache_creation_input_tokens 가 0으로 조용히 지나갑니다.
문제는 이 최소 길이가 모델마다 다르고, 세대 순서대로 줄어들지도 않는다는 겁니다.
| 모델 | 최소 캐시 길이 |
|---|---|
| Claude Opus 5 | 512 토큰 |
| Claude Sonnet 5 | 1,024 토큰 |
| Claude Haiku 4.5 | 4,096 토큰 |
제가 짜려던 프롬프트는 스코어링 기준 설명과 출력 규칙 정도라 4,096 토큰에 한참 못 미쳤어요. 즉 저비용 모델을 쓰려고 Haiku를 골랐는데, 정작 그 모델에서만 캐싱이 안 되는 구성이었습니다.
Haiku가 싼 건 맞지만 캐시 읽기가 0.1배라는 걸 감안하면, 프롬프트가 짧을 때 Haiku를 고르는 게 항상 유리하지는 않아요. 비용 비교를 모델 단가만으로 하면 안 된다는 걸 여기서 배웠습니다.
캐시는 모델별이라 폴백하면 처음부터 다시 씁니다
두 번째가 더 큽니다. 캐시는 모델 단위로 저장돼요. Haiku에서 만든 캐시를 Sonnet이 읽지 못합니다.
그러니까 “Haiku로 먼저 부르고 아니면 Sonnet” 전략은 폴백이 일어날 때마다 Sonnet 쪽 캐시 쓰기 비용(1.25배)을 새로 냅니다. 폴백 비율이 높으면 두 모델 양쪽에 캐시를 쓰면서 결과는 하나만 쓰는 상태가 돼요.
정리하면 이렇게 갈립니다.
- 폴백이 드물다: Haiku 우선이 이득
- 폴백이 잦다: 처음부터 Sonnet 하나로 가는 게 단순하고 쌀 수 있다
그리고 이 갈림길을 정하려면 폴백 비율을 측정해야 합니다. 저는 그걸 안 재고 구조부터 정하고 있었어요.
[결론]
먼저 정정할 게 있습니다. 이 기능은 아직 저장소에 없습니다.
이력서에는 AI 배너 추천을 적용해 검토 범위를 상위 3~5개로 줄였다고 적었는데, 저장소를 다시 확인해보니 스코어링 코드도 Claude API 호출도 없어요. 관리자 배너는 여전히 수동으로 등록합니다. 설계하고 비용 계산까지 한 것과 만든 것을 제가 구분하지 않고 적었습니다.
그래서 이 글의 성과 항목은 “관리자 작업이 줄었다” 가 아니라 “만들기 전에 계산해서 설계가 틀린 걸 찾았다” 입니다.
배운 걸 정리하면 이렇습니다.
- LLM에 넣을 양을 줄이는 게 프롬프트를 다듬는 것보다 먼저다
- 모델 단가만 비교하면 캐싱 조건을 놓친다
- 캐시가 안 되는 건 에러가 아니라 침묵이라 지표로 확인해야 한다
남은 한계도 적어둘게요.
첫째, 스코어링 가중치에 근거가 없습니다. 다섯 개 신호를 어떤 비율로 섞을지는 결국 감으로 정하게 돼요. 과거에 관리자가 실제로 고른 배너를 정답으로 놓고 맞춰보는 방법이 있는데, 그 기록을 남기지 않았습니다.
둘째, AI 판단을 평가할 기준이 없습니다. 추천이 좋은지 나쁜지를 관리자 승인율로 볼 수 있는데, 승인 여부를 저장하는 테이블이 없어요. 사람이 최종 결정하는 구조를 택했으면 그 결정을 데이터로 남겨야 다음 개선이 가능합니다.
셋째, 완전 자동화로 갈 조건을 안 정했습니다. 지금은 사람이 승인하는 게 맞다고 봤지만, 어느 지표가 얼마가 되면 자동으로 넘길지는 못 정했어요.
만들기 전에 계산해본 게 이번엔 도움이 됐습니다. 다만 그건 안 만든 것에 대한 변명은 아니에요.