기술

LLM 토큰 계측표의 0은 '안 썼다'가 아니라 '안 알려준다'였습니다

2026.09.2610분 읽기

계측표를 붙인 다음 날 아침, 표를 열었습니다. 전날 같이 만든 한도 폴백 경로가 탄 기록을 찾았는데 0건이었습니다. 한도가 차면 대체 모델로 넘기는 경로입니다. '폴백이 안 탔다'인지 '탔는데 안 세졌다'인지부터 갈라야 했습니다. 코드를 보니 후자였습니다.

사내 제안서 지식검색 도구의 문서 엔진에 LLM 소비량 계측 층을 붙인 이유는 둘이었습니다. 며칠 전 제공자 크레딧이 말라 문서 색인이 조용히 멈춘 일이 있었습니다. 그 뒤로도 '왜 크레딧이 줄었나'를 매번 추정으로 답하고 있었습니다.

추정을 끝내려고 만든 층인데, 다음 날 열어 보니 빠진 경로가 둘이었습니다. 하나는 그 폴백이 타는 스트리밍 경로였습니다. 다른 하나는 문서마다 도는 임베딩이었고, 크레딧을 실제로 태우는 바로 그 비용이었습니다.


같은 날 만든 계측과 경로는 서로를 모릅니다

계측은 단발 생성 함수에만 붙어 있었습니다. 채팅 답변은 스트리밍 엔드포인트로 나가고, 한도 폴백도 그 경로를 탑니다. 계측 작업과 폴백 작업은 같은 날 서로 다른 작업으로 진행됐습니다. 폴백이 타는 스트리밍 생성 함수에 계측이 없다는 것을 그날은 아무도 보지 못했습니다.

한도를 재려고 만든 층에 한도 폴백이 안 잡히면 층 자체가 뜻을 잃습니다. 대체 모델로 답한 호출이 표에 없으면 '왜 그 크레딧이 줄었나'를 또 추정하게 됩니다. 폴백 같은 예외 경로는 평소에 안 타서, 빠진 것이 오래 안 드러납니다.


스트리밍 사용량은 누계로 옵니다

스트리밍은 단발 호출과 구조가 다릅니다. 사용량(usage)이 마지막 청크에 누계로 실려 옵니다. 청크가 올 때마다 더하면 같은 값을 여러 번 더해 부풀립니다. 그래서 청크가 올 때마다 덮어 두고, 스트림이 끝난 뒤 한 번만 기록하는 쪽을 골랐습니다.

기록은 finally 에서 합니다. 사람이 화면을 닫아 스트림이 끊겨도, 그때까지 간 토큰은 이미 과금됐기 때문입니다. 정상 완료·중간 끊김·누계를 한 번만 쓰는가, 세 가지를 테스트로 잠갔습니다.

누계로 온다는 것은 한 제공자 창구의 실측입니다. 증분으로 주는 창구도 있을 수 있습니다. 창구마다 첫 청크와 마지막 청크를 직접 열어 보고 정합니다.

스트리밍 LLM 사용량은 마지막 청크에 누계로 오므로 청크마다 더하면 부풀고, 덮어쓰다 끝난 뒤 한 번만 기록한다


임베딩 창구는 토큰 수를 아예 안 줍니다

두 번째 구멍은 임베딩이었습니다. 붙이려고 보니 임베딩 창구는 토큰 수를 아예 안 줬습니다. SDK 와 REST 를 둘 다 직접 쳐서 확인했습니다. 응답에는 벡터 값 배열뿐이고, 사용량 메타데이터는 비어 있거나 없었습니다.

글자 수는 우리가 보낸 것이라 정확히 압니다. 그 값을 어디에 둘지, 선택지가 셋이었습니다.

선택지

표에 남는 것

판단

A. 글자 수를 토큰 칸에 넣는다

글자→토큰 환산값

추정을 없애려고 만든 표에 추정이 들어갑니다

B. 토큰 칸을 0 으로 둔다

0

「안 썼다」로 읽힙니다

C. 글자 칸을 따로 판다

토큰 미제공 · 글자 600

채택

C 를 골랐습니다. 토큰이 안 오는 줄의 '호출당 입력'은 0 이 아니라 '토큰 미제공'이라 씁니다. 0 은 '안 썼다'로 읽히는데 실제로는 '안 알려준다'입니다. 그 표를 읽는 사람은 0 이 찍힌 경로를 비용에서 지웁니다.

DB 에는 null 로 두고 화면에는 '미제공'이라 적습니다. 화면 문구까지가 설계입니다. 색인 임베딩과 질문 임베딩도 갈랐습니다. 같은 모델이지만 색인은 배치로 문서마다 돌고, 질문은 사람이 건마다 보냅니다. 합치면 '색인을 돌렸더니 질문이 비싸졌다'처럼 읽힙니다.

LLM 계측표에서 토큰 0 과 토큰 미제공은 다른 값 — 원천이 안 주는 값은 null 로 두고 화면에는 미제공이라 쓴다


평균의 분모는 값이 온 호출만입니다

호출당 평균도 같이 고쳤습니다. 분모가 '전체 호출'이었는데, 임베딩 호출은 토큰이 없는 채로 분모에 들어갑니다. 색인이 돌 때마다 호출당 입력이 뚝 떨어져 답변이 가벼워진 것처럼 보입니다. 실제로는 아무것도 안 바뀐 것입니다.

분모를 '토큰이 온 호출'로 바꾸고, 그 호출 수를 따로 집계했습니다. 무엇을 분모로 잡느냐가 숫자의 뜻을 정합니다.


쓰기를 삼키는 층은 스키마가 어긋나도 조용합니다

새 칸 둘을 더하면서 한 번 더 걸렸습니다. CREATE TABLE IF NOT EXISTS 는 기존 표에 새 칸을 안 더합니다. 운영 서버에는 이미 표가 있으니, 새 칸이 없는 채로 INSERT 가 매번 실패할 판이었습니다.

계측은 쓰기 실패를 삼키도록 만들어 뒀습니다. 그래서 에러 없이 숫자만 안 쌓입니다. 가장 안 보이는 고장입니다.

추가 칸 목록을 두고 ALTER 를 걸었습니다. 칸이 없는 실제 DB 를 열어, ALTER 가 도는 것과 기존 행이 남는 것을 확인했습니다. 이걸 놓쳤으면 계측은 배포 뒤에도 에러 없이 빈 표를 유지했을 겁니다.


목이 아니라 실제 창구로 태웠습니다

검증은 가짜 응답이 아니라 실제 제공자 창구로 했습니다. 네 경로를 다 태워 표를 받았습니다. '토큰을 안 준다' 같은 사실은 문서가 아니라 응답을 직접 쳐서 안 것입니다.

경로

모델

토큰

글자

답변(기본)

gpt-6-astra

4,704

0

답변(한도 폴백)

gemini-3.8-flash

13

0

색인 임베딩

gemini-embedding-001

미제공

600

질문 임베딩

gemini-embedding-001

미제공

8

전체 호출 4건, 토큰이 온 호출 2건, 호출당 입력 2,358 입니다. 임베딩 2건까지 나눴다면 1,179 로 절반이 됐을 것이고, 그 숫자는 아무 사건도 뜻하지 않았을 겁니다. 한도 폴백이 탄 호출은 대체 모델 13 토큰으로 표에 나옵니다. 이제 '왜 그 크레딧이 줄었나'는 표가 답합니다.

의도적으로 안 세는 경로도 목록으로 남겼습니다. 로컬에 올린 모델은 0원이라 안 셉니다. LiteLLM 프록시·에이전트 경유·코덱스 CLI 는 지금 안 쓰는 경로라 안 셉니다. 다시 켜는 날 같은 구멍이 다시 생기므로, 켜는 작업의 체크리스트에 계측이 들어갈 자리입니다.

호출당 평균의 분모를 전체 호출로 잡으면 토큰 없는 임베딩 호출 때문에 2,358 이 1,179 로 보이고, 그 하락은 아무 사건도 아니다


운영자가 가져갈 것

계측을 붙인 다음 날, 그날 새로 만든 경로부터 봅니다. 폴백·예외 경로가 특히 그렇습니다

원천이 안 주는 값은 0 이 아니라 null 로 저장하고, 화면에는 '미제공'이라 씁니다

단위가 다른 값은 칸을 새로 팝니다. 글자→토큰 환산은 추정이고, 추정은 사실 칸에 넣지 않습니다

평균의 분모는 값이 온 호출만입니다. 값 없는 호출이 늘수록 평균이 내려가는데, 그 하락은 아무 사건도 아닙니다

스트리밍 사용량은 누계입니다. 마지막 값을 한 번 쓰고, 끊김은 finally 로 받습니다

CREATE TABLE IF NOT EXISTS 는 마이그레이션이 아닙니다. 새 칸은 ALTER 목록으로 관리하고, 칸 없는 실제 DB 로 확인합니다

반례도 있습니다. 토큰 미제공 창구가 하나뿐이면 글자 칸이 과합니다. 그 줄에 '미제공'만 찍어도 됩니다. 여기서 칸을 판 이유는 임베딩이 크레딧을 가장 많이 태우는 경로라 크기를 봐야 했기 때문입니다.

글자→토큰 환산이 충분히 정확한 도메인이면 '추정'이라 명시하고 같은 칸에 넣는 선택도 있습니다. 논점은 '추정을 하지 마라'가 아니라 '추정을 사실 칸에 넣지 마라'입니다.

계측표에서 0 을 읽을 때, 이 0 이 '안 썼다'인지 '안 알려준다'인지를 먼저 묻습니다. 그 둘을 한 칸에 두는 순간, 추정을 없애려고 만든 표가 다시 추정이 됩니다.


연작 '토큰은 어디로 갔나'

이 글은 다섯 편 중 2편입니다. 구독 LLM 을 운영하며 한도·토큰·인증이 어디서 새고 어디서 안 보였는지를 이어서 다룹니다.

#LLM계측#토큰#임베딩#스트리밍#관측가능성#LLM비용#스키마드리프트#한도폴백