엔지니어링LLM 비용을 줄인 캐시와 아침 배치 설계
· 핀치
- #llm
- #cache
- #cost
- #performance
핀치의 AI 기능은 종목 분석, 데일리 브리핑, 포트폴리오 진단, 채팅입니다. 전부 LLM을 부르고, 쓸 수 있는 LLM 크레딧은 정해져 있었습니다. 9월 중순, 인프라 파트가 남은 크레딧을 집계해 보니 52,745 크레딧이었습니다. 당시 쓰는 속도로는 분석 24회분, 1.6일치였고, 데모는 9월 28일이었습니다.
이 글은 그 크레딧으로 데모까지 가기 위해 호출 수와 토큰을 줄인 과정입니다. 줄이는 과정에서 캐시가 의도와 다르게 동작한 일도 두 번 있었습니다.
토큰이 어디서 나가는지부터
같은 집계에서 토큰의 68%가 재생성에서 나가고 있었습니다. 핀치는 LLM 출력을 자동 검사하고, 걸리면 이유를 붙여 다시 생성합니다. 재생성 상한과 캐시 유지 시간이 코드에 박혀 있어서, 조이려면 배포를 해야 했습니다. 두 값을 설정으로 빼고 바로 조였습니다.
- 재생성 상한: 3회에서 2회(첫 생성 1회 + 재생성 1회)
- 종목 분석 공통 섹션 캐시: 6시간에서 24시간
상한을 1회로 내리자는 요청도 있었지만 2회로 뒀습니다. 그 통계는 검사 오탐 하나를 고치기 전 데이터라 반려율이 이미 내려갔을 가능성이 컸고, 재시도를 없애면 반려되는 순간 섹션이 비어서 나갑니다. 설정으로 뺐으니 반려율을 다시 재 본 뒤 배포 없이 내릴 수 있습니다.
추론 토큰 줄이기
다음으로 본 건 추론 토큰이었습니다. 추론 모델은 답을 쓰기 전에 생각하는 데 토큰을 쓰는데, 그 양을 effort 값으로 조절합니다. 같은 프롬프트로 재 보니 high 28초, medium 16초, low 4초였고 출력은 같았습니다. 답 90토큰에 추론 4,480토큰이 붙던 구조였습니다.
28초
effort high
16초
effort medium
4초
effort low
모든 경로의 기본값을 low로 내리고, 출력 상한도 16,000에서 4,000토큰으로 줄였습니다. 같은 사용자·종목·기준일이면 개인화 섹션도 이전 응답을 재사용하게 했고, 브리핑 배치 대상은 최근 30일 활성 사용자에서 7일로 좁혔습니다.
캐시가 의도대로 동작하지 않은 두 가지
같은 날 운영 기록을 보다가 캐시 자체의 문제도 두 개 찾았습니다.
섹션 하나가 실패하면 전체가 사라졌습니다. 종목 분석은 섹션 일곱 개를 병렬로 만드는데, 하나가 타임아웃되면 요청 전체가 504로 끝나서 이미 만든 섹션까지 버려졌습니다. 또 한 섹션이 검사에 걸려 비어 있으면 캐시 조회가 그 행을 통째로 버려서, 그 종목은 요청마다 일곱 섹션을 다시 만들었습니다. 실패한 섹션은 빈 칸으로 돌려주고, 캐시는 섹션마다 가장 최근의 성공 결과를 가져오게 바꿨습니다.
캐시가 만료되지 않았습니다. 캐시 히트도 응답 기록에 남는데, 다음 조회가 그 기록을 "최근 24시간 안의 응답"으로 다시 집어서 만료 시간이 계속 늘어났습니다. 운영에서 삼성전자 분석은 9월 14일에 근거 0건으로 만들어진 것이 이틀째 복제되고 있었고, 그사이 새로 들어온 문서 94개는 쓰이지 않았습니다. 캐시 원본은 실제로 생성한 기록(cached=false)만 치도록 고쳤습니다.
사용자 요청 대신 아침 배치에서
요청이 들어올 때 생성하면 사용자가 기다리고, 같은 걸 여러 번 만들기도 합니다. 그래서 무거운 생성은 매일 아침 9시 20분 배치로 옮겼습니다.
포트폴리오 진단
진단은 첫 요청 때 만들어서 사용자가 기다렸고, 캐시 키(지문)에 현재가가 들어 있어 장중 시세가 바뀔 때마다 다시 만들었습니다. 운영 7일 캐시 히트율이 26%였습니다. 지문에서 현재가를 빼서 하루 한 번이 되게 하고, 아침 배치가 브리핑과 함께 진단도 만들도록 했습니다. 요청 때는 저장된 진단을 그대로 돌려주고, 저장된 게 없는 첫 사용자만 한 번 만듭니다.
실패한 섹션 재시도
배치가 만든 종목 분석에서 검사에 걸린 섹션은 비어 있고, 그 종목을 처음 연 사람이 재시도를 기다렸습니다. 섹션 두 개가 두 번씩 재시도하면 십수 초가 걸렸어요. 요청 경로에서는 다시 만들지 않고, 재시도는 다음 날 아침 배치가 맡게 했습니다.
배치 하루 토큰 상한
종목 30개를 한 바퀴 도는 데 약 60만 토큰이 듭니다. 9월 16일에는 프롬프트를 바꿔 캐시가 비는 바람에 하루 상한 200만 토큰이 소진됐고, 배치가 30종목 중 2종목에서 멈췄습니다. low 기준 500만 토큰이 약 1달러라 상한을 500만으로 올렸습니다.
캐시가 측정을 가린 일
9월 하순에 성과 요인 요약이 검사에 반려되는 문제를 조사하면서 effort와 재생성 상한을 바꿔 가며 3번씩 돌려 봤습니다. 그런데 2·3회차가 캐시에 걸려 LLM을 부르지 않았습니다. 0/3으로 읽은 결과가 실제로는 0/1이었습니다. 요약이 반려된 응답도 10분간 캐시하게 해 둔 터라, 실패를 다시 재현하려는 시도가 바로 캐시에 막혔습니다.
그때는 운영 파드의 내부 함수를 실행 중에 덮어써서 우회했는데, 정상적인 확인 방법은 아니었습니다. 그래서 캐시를 건너뛰는 옵션을 함수에 추가했습니다. 다만 HTTP로는 열지 않았습니다. 사용자가 재생성을 강제할 수 있으면 토큰 예산을 우회하는 통로가 되기 때문입니다.
병목은 모델이 아니라 호출 수
데모 당일 아침에는 채팅 반응 속도를 개선해 달라는 요청이 있어서 운영에서 직접 재 봤습니다. "내 포트폴리오 어때?"는 13.8초에 LLM 호출 2번, "현대차 왜 떨어졌어?"는 27.3초에 호출 3번이었고 결국 차단됐습니다. 시간은 모델 크기보다 호출 횟수와 출력 길이에서 나오고 있었습니다.
그래서 더 큰 모델이 아니라 호출당 시간이 짧은 모델(gpt-6-luna)로 바꾸고, 도구를 고르는 턴은 추론을 껐습니다. 이 모델은 도구와 추론을 함께 쓰면 400 오류를 내기 때문에, 모델 이름만 바꿨다면 채팅이 바로 죽었을 겁니다. 답을 쓰는 마지막 호출에는 low를 그대로 둡니다.
13.8 → 8.2초
내 포트폴리오 어때?
27.3 → 8.4초
현대차 왜 떨어졌어? (차단 → 정상)
정리
비용을 줄인 방법은 단순했습니다. 재생성을 줄이고, 추론을 덜 쓰고, 같은 걸 다시 만들지 않고, 무거운 생성은 사용자가 없는 시간에 합니다. 시간이 더 걸린 건 캐시가 실제로 그렇게 동작하는지 확인하는 쪽이었습니다. 부분 실패, 만료되지 않는 캐시, 측정을 가리는 캐시 모두 숫자를 직접 세어 보고 나서야 보였습니다.