기능별 토큰(Tokens Per Feature)을 측정하기 전까지는 LLM 비용을 해결할 수 없었습니다
요약
LLM 비용 최적화를 위해 모델 단위가 아닌 기능별 토큰(Tokens Per Feature) 측정의 중요성을 강조합니다. 애플리케이션 컨텍스트를 태깅하여 어떤 제품 기능이 비용을 유발하는지 분석하는 구체적인 방법을 제시합니다.
핵심 포인트
- 모델 단위의 총합은 비용 발생의 근본 원인을 숨길 수 있음
- 모든 LLM 호출에 기능(feature) 및 작업(operation) 컨텍스트를 태깅할 것
- 사용자 ID 대신 제품 기능이나 워크플로 단위로 그룹화하여 분석
- 기능별 토큰 측정을 통해 재시도 비용 및 비효율적인 모델 사용 식별 가능
나의 LLM 청구 금액은 계속해서 늘어났고, 그래서 나는 당연해 보이는 일을 했습니다. 바로 더 저렴한 모델을 찾는 것이었습니다.
그것은 약간의 도움이 되었지만, 왜 청구 금액이 계속 늘어나는지에 대해서는 설명해주지 못했습니다.
대시보드는 애플리케이션이 얼마나 많은 토큰을 사용했는지는 알려줄 수 있었습니다. 하지만 그 토큰들이 무엇을 하고 있는지는 알려줄 수 없었습니다.
채팅에서 발생한 것일까요?
문서 요약(Document summaries)?
백그라운드 분류(Background classification)?
에이전트(Agent)가 동일한 도구 호출(tool call)을 재시도하는 것 때문일까요?
나는 어떤 제품 기능이 그 비용을 만들어내는지 모르는 상태에서 총합을 최적화하려고 노력하고 있었습니다.
유용한 단위는 **모델당 토큰(tokens per model)**이 아니었습니다.
바로 **기능별 토큰(tokens per feature)**이었습니다.
모델 수준의 총합은 진짜 문제를 숨깁니다
제공업체(Provider)의 대시보드는 보통 사용량을 모델, API 키, 프로젝트 또는 기간별로 그룹화합니다.
이는 청구(billing)에는 유용하지만, 제품 결정에는 항상 유용한 것은 아닙니다.
LLM 기반의 네 가지 기능이 있는 애플리케이션을 상상해 보세요:
- 대화형 채팅 (interactive chat)
- 문서 요약 (document summarization)
- 지원 티켓 분류 (support-ticket classification)
- 주간 보고서를 준비하는 에이전트 (an agent that prepares weekly reports)
만약 청구 금액이 30% 증가한다면, 모델 이름만으로는 어떤 기능이 변했는지 설명할 수 없습니다.
아마도 채팅 트래픽이 늘어났을 수도 있습니다.
아마도 요약 기능이 선택된 섹션 대신 문서 전체를 보내기 시작했을 수도 있습니다.
아마도 분류기(classifier)가 훨씬 더 큰 시스템 프롬프트(system prompt)를 받았을 수도 있습니다.
아마도 보고서 에이전트가 도구 실패(tool failures) 후 재시도하면서 동일한 계획을 여러 번 생성했을 수도 있습니다.
이러한 문제들은 완전히 다른 해결책을 요구합니다.
모든 요청을 더 저렴한 모델로 전환하면 청구 금액은 줄어들겠지만, 이는 엔지니어링 실수를 숨길 수도 있습니다.
모든 요청에 기능을 태깅하세요
나는 모든 LLM 호출에 약간의 애플리케이션 컨텍스트(context)를 부여하기 시작했습니다:
const context = {
feature: "document_summary",
operation: "initial_summary",
...
모델 제공업체는 이러한 필드들이 필요하지 않습니다.
이것들은 애플리케이션의 사용 기록(usage record)에 속해야 합니다.
나는 개별 사용자 ID를 주요 그룹화 차원으로 사용하는 것을 피합니다. 비용 분석을 위해서는 제품 기능, 워크플로(workflow) 또는 작업(operation)이 일반적으로 더 유용하며 개인정보 보호 문제도 적게 발생합니다.
실제적인 기록은 다음과 같습니다:
{
"timestamp": "2026-07-22T03:12:48.201Z",
"feature": "document_summary",
...
모든 요청에 대한 기록을 확보하고 나니, 다음과 같은 질문들에 더 잘 답변할 수 있게 되었습니다:
- 어떤 기능(feature)이 가장 많은 토큰(tokens)을 사용하는가?
- 어떤 기능의 사용량이 가장 빠르게 증가하는가?
- 하나의 성공적인 작업(operation)에는 얼마나 많은 토큰이 필요한가?
- 재시도(retries)가 완료된 작업의 증가 없이 토큰 사용량만 늘리고 있는가?
- 입력(input)이 출력(output)보다 더 빠르게 증가하고 있는가?
- 어떤 기능이 필요하지 않은데도 비용이 많이 드는 모델을 사용하고 있는가?
간단한 Node.js 사용량 기록기
다음은 OpenAI 호환 채팅 완료(chat-completions) 엔드포인트를 사용하는 최소한의 구현체입니다.
이 코드는 Node.js 내장 모듈만 사용하며, Node 18 이상을 요구합니다.
llm-client.mjs를 생성합니다:
import { appendFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
...
기능(feature)은 다음과 같이 래퍼(wrapper)를 호출합니다:
import {
createChatCompletion
} from "./llm-client.mjs";
...
래퍼는 매 요청마다 llm-usage.jsonl 파일에 한 줄씩 기록합니다.
프롬프트(prompt)나 모델 응답(model response)은 저장하지 않습니다. 기능 수준의 비용 분석을 위해 제가 일반적으로 필요한 것은 사용자 콘텐츠가 아니라 사용량 메타데이터(usage metadata)이기 때문입니다.
기능별 토큰 요약
가공되지 않은 JSONL 파일은 디버깅에는 유용하지만, 제가 가장 먼저 보고 싶은 리포트는 훨씬 더 단순한 형태입니다:
기능(Feature) 요청(Requests) 입력(Input) 출력(Output) 합계(Total)
document_summary 42 182,140 21,382 203,522
interactive_chat 391 96,241 44,829 141,070
...
summarize-usage.mjs를 생성합니다:
import { readFile } from "node:fs/promises";
const file =
...
다음 명령어로 실행합니다:
node summarize-usage.mjs
전체 합계는 첫 번째 단계일 뿐입니다.
저는 또한 성공적인 작업당 토큰(tokens per successful operation)도 계산합니다:
const tokensPerRequest =
feature.total_tokens / feature.requests;
에이전트 워크플로(agent workflows)의 경우, API 요청당 토큰보다는 완료된 워크플로당 토큰을 사용하는 것을 선호합니다.
사용자의 한 번의 행동이 다섯 번의 모델 호출(model calls)을 트리거할 수 있습니다. 완료된 행동을 추적하지 않고 각 요청을 개별적으로 최적화한다면, 워크플로(workflow)는 여전히 토큰을 낭비하고 있음에도 불구하고 요청 수준의 지표(request-level metrics)는 더 좋아 보이게 만들 수 있습니다.
작업 수준의 세부 정보 추가 (Add operation-level detail)
기능 태그(feature tag)는 사용량이 어디에서 발생했는지 알려줍니다.
작업 태그(operation tag)는 해당 기능 내부에서 어떤 일이 일어났는지 알려줍니다.
예를 들어:
weekly_report_agent
├── create_plan
├── call_data_tool
...
weekly_report_agent가 100,000개의 토큰을 소비한다고 가정해 봅시다.
그 총합만으로는 많은 것을 알 수 없습니다.
만약 45,000개의 토큰이 repair_tool_arguments에서 발생했다면, 저는 아마 더 저렴한 쓰기 모델(writing model)이 필요한 것이 아닐 것입니다. 저는 왜 도구 호출(tool call)이 계속 실패하는지를 이해해야 합니다.
만약 draft_report의 입력 토큰(input tokens)이 계속 증가한다면, 너무 많은 원본 소스 자료를 보내고 있는 것일지도 모릅니다.
만약 단일 보고서를 위해 create_plan이 세 번 실행된다면, 재시도(retry) 또는 상태 관리(state-management) 로직을 점검해야 합니다.
기능(feature)은 저에게 어디를 봐야 할지 알려줍니다.
작업(operation)은 저에게 무엇을 고쳐야 할지 알려줍니다.
재시도를 별도로 측정하기 (Measure retries separately)
성공적인 응답은 정상적으로 보이기 때문에 재시도(retries)는 놓치기 쉽습니다.
저는 각 레코드에 시도 횟수(attempt number)를 추가합니다:
{
feature: "weekly_report_agent",
operation: "draft_report",
...
그런 다음 다음 항목들을 비교합니다:
- 총 요청 수 (total requests)
- 고유 작업 ID (unique operation IDs)
- 성공한 작업 (successful operations)
- 재시도 토큰 (retry tokens)
- 성공한 작업당 토큰 (tokens per successful operation)
이렇게 하면 트래픽은 안정적으로 보이지만 내부적으로 요청이 반복되어 토큰 사용량이 두 배로 늘어나는 오해의 소지가 있는 결과를 방지할 수 있습니다.
작업 ID(operation ID)는 워크플로 시작 시점에 한 번 생성될 수 있습니다:
const operationId = randomUUID();
모든 재시도는 동일한 작업 ID를 유지하지만 시도 횟수(attempt)를 증가시킵니다:
{
operation_id: operationId,
attempt: 2
...
이제 재시도로 인한 낭비를 월간 청구서에서 추론하는 대신 직접 측정할 수 있습니다.
요청 경로 외부에서 토큰을 비용으로 변환하기 (Convert tokens to cost outside the request path)
저는 API 래퍼(API wrapper) 내부에 모델 가격을 하드코딩하지 않습니다.
가격은 변동되며, 서로 다른 제공업체(providers)는 각각 다른 입력(input), 캐시된 입력(cached-input), 출력(output) 요율을 제공할 수 있습니다.
대신, 저는 별도의 요율 테이블(rate table)을 유지합니다:
const rates = {
"example-model": {
input_per_million: 1.00,
...
그런 다음 보고(reporting) 과정에서 비용을 추정합니다:
function estimateCost(record, rate) {
const inputCost =
((record.input_tokens ?? 0) / 1_000_000) *
...
위의 숫자들은 현재 가격이 아닌 플레이스홀더(placeholders)입니다.
결제 결정을 위해 보고서를 사용하기 전에, 저는 이 숫자들을 제공업체의 현재 요율로 교체하고 해당 요율 테이블의 유효 날짜를 기록합니다.
가격 책정을 요청 래퍼(request wrapper) 외부에 유지하면, 가격 변경 후에도 원래의 토큰 기록을 수정하지 않고 과거 사용량을 재계산할 수 있습니다.
누락된 사용량 또한 하나의 지표입니다
모든 API 응답이 동일한 형식으로 토큰 사용량을 포함하는 것은 아닙니다.
스트리밍 응답(Streaming responses)은 사용량을 반환하기 위해 추가 옵션이 필요할 수 있습니다. 일부 제공업체는 서로 다른 필드 이름을 노출하기도 합니다. 실패한 요청은 사용량을 전혀 반환하지 않을 수도 있습니다.
저는 누락된 사용량을 조용히 0으로 변환하지 않습니다.
0은 요청에 토큰을 전혀 사용하지 않았음을 의미합니다.
null은 알 수 없음을 의미합니다.
이 둘은 매우 다른 진술입니다.
보고서에는 각 기능(feature)별로 missing_usage 카운트가 포함됩니다. 만약 이 숫자가 증가한다면, 눈에 보이는 총계가 안정적으로 보이더라도 비용 보고서의 신뢰도는 떨어지고 있는 것입니다.
무엇을 먼저 최적화하는가
사용량이 기능(feature) 및 작업(operation)별로 그룹화되면, 저는 다음 목록에 따라 작업을 진행합니다:
- 불필요한 호출 (Unnecessary calls)
캐시된 결과(cached result), 결정론적 함수(deterministic function), 또는 데이터베이스 쿼리(database query)로 해결할 수 있는 상황에서 해당 기능이 모델을 호출하고 있지는 않습니까?
- 반복되는 컨텍스트 (Repeated context)
모든 요청이 동일한 대규모 문서, 도구 스키마(tool schema), 대화 기록(conversation history), 또는 지침(instructions)을 보내고 있지는 않습니까?
- 재시도 낭비 (Retry waste)
타임아웃(timeouts), 잘못된 도구 인자(invalid tool arguments), 또는 파싱 실패(parsing failures)로 인해 동일한 작업이 다시 실행되고 있지는 않습니까?
- 과도하게 큰 출력 (Oversized outputs)
분류(classification) 작업에 800개의 생성된 토큰이 정말 필요합니까, 아니면 작은 구조화된 응답(structured response)만으로도 충분합니까?
- **모델 선택 (Model selection)
요청 형태(request shape)와 워크플로우 동작(workflow behavior)을 수정한 후에도, 이 작업에 현재 모델이 여전히 필요합니까?
모델 선택(Model selection)은 중요합니다. 다만 그것이 항상 첫 번째 문제는 아닐 뿐입니다.
내가 놓치고 있었던 지표
월간 LLM 청구서는 결과만을 알려줍니다.
기능별 토큰(Tokens per feature)은 그 결과가 어디에서 왔는지를 알려줍니다.
성공적인 작업당 토큰(Tokens per successful operation)은 한 단계 더 나아갑니다. 이는 인프라 사용량을 제품이 실제로 달성한 성과와 연결해 줍니다.
그것이 제가 던지는 질문을 바꾸어 놓았습니다.
다음과 같은 질문 대신:
어떤 모델을 교체해야 할까?
저는 이렇게 물을 수 있습니다:
왜 문서 요약(document summarization) 입력이 40% 증가했는가?
왜 보고서 하나를 완료하는 데 9번의 모델 호출(model calls)이 필요한가?
완료된 워크플로우(workflows)는 그대로인데 왜 재시도(retry) 토큰은 증가하고 있는가?
이러한 질문들은 단순히 더 저렴한 청구서를 받는 것이 아니라, 엔지니어링 측면의 해결책으로 이어집니다.
저는 TokenBay에서 일하고 있기 때문에, OpenAI 호환 인터페이스(OpenAI-compatible interface) 뒤에 있는 여러 모델을 정기적으로 다룹니다. 모델 수준의 사용량(Model-level usage)도 여전히 유용하지만, 기능(feature) 및 작업(operation) 태그야말로 애플리케이션 내부에서 해당 사용량을 실행 가능한 정보(actionable)로 만들어 주는 요소입니다.
제가 다음에 추가하려는 것은 작은 예산 가드레일(budget guardrail)입니다. 전체적인 월간 제한이 아니라, 완료된 각 기능 작업(feature operation)에 대한 토큰 예산입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기