LLM 을 부르는 기능이 하나 있습니다. 음식 이름을 적으면 장기 열 곳의 점수를 돌려줍니다. 호출 하나가 그대로 청구서에 찍히는데, 사람이 먹는 것은 크게 반복됩니다. 김치찌개를 백 명이 적으면 백 번 부르고 있었습니다.
캐시를 붙이기로 하고 조건을 따져보다가, 제가 처음에 본 근거가 틀렸다는 것을 알았습니다.
LLM 호출에서 temperature 0 은 캐시의 근거가 아닙니다
LLM 응답을 저장해도 되는 근거로 제가 처음 든 것은 temperature: 0 이었습니다.
같은 입력에 같은 출력이 나오니 저장해 두면 된다는 논리입니다.
그런데 그 프롬프트는 음식 목록을 통째로 받고 있었습니다.
["김치찌개", "공기밥"] 을 넣은 결과와 ["김치찌개"] 를 넣은 결과가 같다는 보장이 어디에도 없습니다.
temperature 가 0이어도 입력이 다르면 출력이 다릅니다.
캐시의 키는 음식 하나인데 호출의 단위는 목록이니, 둘이 어긋나 있었던 것입니다.
성립하는지는 출력 스키마를 읽어야 알 수 있었습니다.
items: [{ name, category, portion, scores, reason }]
totalScores: <items 의 합>
각 항목이 자기 이름 하나로 닫혀 있고, 총합은 그 항목들을 더한 값일 뿐입니다. 즉 이 프롬프트는 목록을 받지만 실제로는 항목마다 독립적으로 판정합니다. 그래서 이름 하나를 키로 저장해도 결과가 바뀌지 않습니다.
정리하면 조건은 두 개입니다. 출력이 입력 항목마다 독립일 것, 그리고 그 판정이 결정적일 것. temperature 는 둘째 조건의 일부일 뿐이고, 첫째 조건이 없으면 아무 소용이 없습니다.
절반만 캐시하면 합계가 조용히 틀립니다
세 개를 적었는데 둘은 사전에 있고 하나는 없는 경우가 실제로 가장 흔합니다. 없는 하나만 모델에 보내고, 돌아온 결과를 사전에서 찾은 둘과 합치면 됩니다.
여기서 한 번 틀렸습니다.
모델이 돌려준 응답에는 totalScores 가 같이 들어 있는데, 그건 모델이 받은 한 개만 더한 값입니다.
그 값을 그대로 내보내면 화면의 장기 점수가 사전 적중분만큼 작아집니다.
이 오류는 아무 데서도 에러를 내지 않습니다. 스키마 검증도 통과합니다. 숫자가 그럴듯하게 작을 뿐입니다. 그리고 사전이 채워질수록, 즉 캐시가 잘 들을수록 더 크게 틀립니다.
합계는 서버가 다시 더하게 고쳤습니다. 부분 캐시를 붙일 때는 응답 안에 들어 있는 집계값을 전부 의심해야 합니다. 그건 캐시가 없던 시절에 계산된 값입니다.
비싼 쪽을 짐작으로 정하면 상한이 근거를 잃습니다
같은 작업에서 무료와 유료의 한도를 정해야 했습니다. 처음에 저장 용량으로 그으려 했습니다. 영상과 사진이 계속 쌓이니까요.
재보니 반대였습니다.
| 항목 | 단위 원가 | 정상 사용 |
|---|---|---|
| 영상 저장 | 개당 월 0.07원 | 무시할 수준 |
| 모델 호출 | 회당 약 10원 | 하루 세 번이면 월 900원 |
영상 10초가 3.33MB 였고, 오브젝트 스토리지가 GB 당 월 0.015달러입니다. 영상 삼만 개를 저장해야 구독료에 닿습니다. 반면 모델 호출은 하루 세 끼를 적는 평범한 사용만으로 구독료의 삼분의 일을 먹습니다.
이 숫자를 보기 전에 저는 영상 상한을 월 300개로 적어뒀습니다. 어디서 나온 숫자냐면, 아무 데서도 안 나왔습니다. 막아야 할 것 같은 기분으로 적은 값이고, 실제로는 원가의 백분의 일도 안 되는 자리에 벽을 세운 셈입니다.
무제한이라고 적으면 안 되는 것은 저장이 아니라 모델 호출 쪽이었습니다.
캐시가 맞은 요청은 LLM 한도에서 빼야 합니다
한도를 세는 코드를 모델 호출 앞에 두었더니, 사전에서 찾아 답한 요청도 한 번으로 세고 있었습니다.
그러면 자주 먹는 것만 적는 사람이 손해를 봅니다. 그 사람의 요청은 비용이 0인데 남은 횟수만 줄어듭니다. 한도는 비용을 다듬으려고 있는 것이지 요청 수를 세려고 있는 게 아닙니다.
세는 자리를 모델을 실제로 부르기 직전으로 옮겼습니다. 전부 사전에서 찾으면 그 요청은 한도를 건드리지 않고 끝납니다.
개수는 카운터 대신 기록에서 셌습니다
월 상한을 세는 방법으로 사용자 문서에 숫자를 올려두는 방식을 먼저 생각했습니다. 같은 저장소에서 이틀 전에 그 방식이 실제로 어긋난 적이 있습니다. 올리는 코드와 지우는 코드가 갈리면서, 실제 기록이 열일곱 개인데 카운터는 0인 계정이 생겼습니다. 그 값을 다른 기능이 읽고 있어서 그 계정은 아무리 완료해도 화면이 안 움직였습니다.
그래서 영상 개수는 카운터를 두지 않고 기록에서 직접 셉니다. 집계 쿼리는 문서를 읽지 않고 개수만 돌려주므로 값이 싸고, 무엇보다 실제 데이터에서 나온 값이라 어긋날 수가 없습니다.
모델 호출 횟수만 카운터로 뒀습니다. 셀 컬렉션이 애초에 없어서입니다. 대신 그 값은 서버만 쓰고 화면의 다른 숫자에 영향을 주지 않습니다. 카운터를 쓸 수밖에 없다면, 최소한 그 값이 틀렸을 때 무엇이 같이 틀리는지는 좁혀둘 수 있습니다.
아직 모르는 것
모델 호출 원가 10원은 토큰 수로 추정한 값이고 청구서로 확인하지 않았습니다. 영상 크기 3.33MB 도 표본이 한 건입니다. 파일 크기를 기록하기 시작한 지 얼마 안 됐습니다. 자릿수는 맞을 것으로 보지만 한 자릿수는 믿을 값이 아닙니다.
사전이 실제로 얼마나 맞는지도 아직 모릅니다. 음식 이름은 사람마다 다르게 적으므로 적중률이 낮을 수 있고, 그건 며칠 쌓아본 뒤에야 셀 수 있습니다. 지금 확실한 것은 캐시가 결과를 바꾸지 않는다는 것까지입니다.