Chat/Embedding 응답 사용량 다루기

Chat/Embedding 응답 사용량 다루기

한 번의 모델 호출이 토큰을 얼마나 썼는지, 특히 캐시를 얼마나 활용했는지 꼭 확인해야 할 때가 있어요. 비용 관리나 디버깅 모두 결국 사용량(usage) 데이터에서 시작하니까요. Spring AI는 모델마다 제각각이던 사용량 조회 방식을 한 곳으로 모아, 표준 지표와 모델 고유의 상세 지표를 모두 손쉽게 볼 수 있게 했습니다.

출처: 공식문서

개요

Spring AI는 Usage 인터페이스에 getNativeUsage() 메서드를 추가하고 DefaultUsage 구현을 제공하면서 모델 사용량 처리를 개선했어요. 이 변화는 서로 다른 AI 모델들이 사용량 지표를 일관되게 추적·보고할 수 있게 하면서도, 프레임워크 전반의 일관성은 유지하도록 돕습니다.

주요 변경 사항

Usage 인터페이스 강화

Usage 인터페이스에 새 메서드가 생겼습니다.

Object getNativeUsage();

이 메서드는 모델 고유의 네이티브 사용량 데이터에 접근할 수 있게 해 줘요. 필요할 때 더 세밀한 사용량 추적이 가능해집니다.

ChatModel에서 사용하기

OpenAI의 ChatModel로 사용량을 추적하는 전체 예시를 볼게요.

@SpringBootConfiguration
public class Configuration {

        @Bean
        public OpenAiChatModel openAiChatModel() {
            return OpenAiChatModel.builder()
                .options(OpenAiChatOptions.builder()
                    .apiKey(System.getenv("OPENAI_API_KEY"))
                    .build())
                .build();
        }

    }

@Service
public class ChatService {

    private final OpenAiChatModel chatModel;

    public ChatService(OpenAiChatModel chatModel) {
        this.chatModel = chatModel;
    }

    public void demonstrateUsage() {
        // Create a chat prompt
        Prompt prompt = new Prompt("What is the weather like today?");

        // Get the chat response
        ChatResponse response = this.chatModel.call(prompt);

        // Access the usage information
        Usage usage = response.getMetadata().getUsage();

        // Get standard usage metrics
        System.out.println("Prompt Tokens: " + usage.getPromptTokens());
        System.out.println("Completion Tokens: " + usage.getCompletionTokens());
        System.out.println("Total Tokens: " + usage.getTotalTokens());

        // Access native OpenAI usage data with detailed token information
        if (usage.getNativeUsage() instanceof com.openai.models.completions.CompletionUsage) {
            com.openai.models.completions.CompletionUsage nativeUsage =
                (com.openai.models.completions.CompletionUsage) usage.getNativeUsage();

            // Detailed prompt token information
            nativeUsage.promptTokensDetails().ifPresent(details -> {
                System.out.println("Prompt Tokens Details:");
                details.audioTokens().ifPresent(tokens -> System.out.println("- Audio Tokens: " + tokens));
                details.cachedTokens().ifPresent(tokens -> System.out.println("- Cached Tokens: " + tokens));
            });

            // Detailed completion token information
            nativeUsage.completionTokensDetails().ifPresent(details -> {
                System.out.println("Completion Tokens Details:");
                details.reasoningTokens().ifPresent(tokens -> System.out.println("- Reasoning Tokens: " + tokens));
                details.acceptedPredictionTokens().ifPresent(tokens -> System.out.println("- Accepted Prediction Tokens: " + tokens));
                details.audioTokens().ifPresent(tokens -> System.out.println("- Audio Tokens: " + tokens));
                details.rejectedPredictionTokens().ifPresent(tokens -> System.out.println("- Rejected Prediction Tokens: " + tokens));
            });
        }
    }
}

ChatClient에서 사용하기

ChatClient를 쓴다면 ChatResponse 객체로 사용량 정보에 접근할 수 있어요.

// Create a chat prompt
Prompt prompt = new Prompt("What is the weather like today?");

// Create a chat client
ChatClient chatClient = ChatClient.create(chatModel);

// Get the chat response
ChatResponse response = chatClient.prompt(prompt)
        .call()
        .chatResponse();

// Access the usage information
Usage usage = response.getMetadata().getUsage();

Prompt Cache 사용량 지표

프롬프트 캐싱을 지원하는 프로바이더라면, Usage 인터페이스가 프로바이더별 캐스팅 없이도 캐시 지표에 통일된 방법으로 접근하게 해 줘요.

Usage usage = response.getMetadata().getUsage();

// Unified cache metrics — works across all providers
Long cacheReadTokens = usage.getCacheReadInputTokens();
Long cacheWriteTokens = usage.getCacheWriteInputTokens();

if (cacheReadTokens != null && cacheReadTokens > 0) {
    System.out.println("Cache hit: " + cacheReadTokens + " tokens read from cache");
}
if (cacheWriteTokens != null && cacheWriteTokens > 0) {
    System.out.println("Cache write: " + cacheWriteTokens + " tokens written to cache");
}

이 메서드들은 프롬프트 캐싱을 지원하지 않는 프로바이더에서는 null을 반환합니다.

프로바이더별 프롬프트 캐시 지표 지원 여부는 다음과 같아요.

Provider Cache Read Tokens Cache Write Tokens
Anthropic Yes Yes (cacheCreationInputTokens)
AWS Bedrock Yes Yes
OpenAI Yes (cachedTokens) No
Google Gemini Yes (cachedContentTokenCount) No
DeepSeek No No
Mistral No No
Ollama No No

NOTE: 프로바이더 고유의 세부 캐시 지표(예: Gemini의 모달리티별 캐시 내역)가 필요하면 getNativeUsage()로 프로바이더 네이티브 사용량 객체에 접근하세요.

다단계 흐름에서의 누적 사용량

도구 호출 루프 같은 다단계 흐름을 거쳐 응답이 만들어지면, getUsage()는 그 교환에서 일어난 모든 모델 호출의 누적 토큰 사용량을 보고합니다. 마지막 호출만이 아닙니다. 예를 들어 도구 호출을 한 번 유발하는 ChatClient 대화는 모델 호출이 적어도 두 번 일어나는데, 돌아오는 getUsage()는 두 호출의 합을 반영해요.

ChatResponse response = chatClient.prompt("What is the weather in Paris?")
        .tools(new WeatherTools())
        .call()
        .chatResponse();

// Cumulative across all model calls in the tool-calling loop
Usage usage = response.getMetadata().getUsage();
int totalTokens = usage.getTotalTokens();

누적 합계는 org.springframework.ai.support.UsageCalculator로 계산됩니다. 표준 토큰 수치와 통합 캐시 지표(getCacheReadInputTokens() / getCacheWriteInputTokens())를 모두 더해요.

WARNING: 프로바이더 고유의 네이티브 사용량 객체는 응답 간에 병합할 수 없습니다. 그래서 사용량이 모델 호출 두 번 이상에 걸쳐 합산되면(도구 호출 루프 후 등) getNativeUsage()null을 반환해요. 네이티브 사용량 객체는 단일 호출 응답에서만 보존됩니다. 프로바이더 네이티브 사용량 객체가 필요하다면 다단계 ChatClient 교환보다는 개별 ChatModel 호출에서 읽으세요.

장점

표준화: 서로 다른 AI 모델에서도 일관된 사용량 처리 방법을 제공합니다. 유연성: 네이티브 사용량 기능으로 모델 고유의 사용량 데이터를 지원해요. 단순화: 기본 구현 덕분에 보일러플레이트 코드가 줄어듭니다. 확장성: 호환성을 유지하면서 특정 모델 요구에 맞게 확장하기 쉽습니다.

타입 안전성 고려사항

네이티브 사용량 데이터를 다룰 때는 타입 캐스팅을 신중히 해야 해요.

// Safe way to access native usage
if (usage.getNativeUsage() instanceof com.openai.models.completions.CompletionUsage) {
    com.openai.models.completions.CompletionUsage nativeUsage =
        (com.openai.models.completions.CompletionUsage) usage.getNativeUsage();
    // Work with native usage data
}

더 알아보기