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
}
더 알아보기
- Observability — 사용량 지표와 트레이싱
- ChatClient — 대화 클라이언트 API