RAG 파이프라인을 만들다가 문제가 하나 생겼다. 검색된 문서를 프롬프트에 붙여서 Claude에 넘기는데, 문서 개수가 들쭉날쭉하다 보니 어떤 요청은 입력이 몇 천 토큰이고 어떤 요청은 몇 만 토큰이었다. 컨텍스트 윈도우를 넘기면 그냥 에러가 나버리고, 비용도 요청마다 크게 달라졌다.
그래서 요청을 실제로 보내기 전에 "이 프롬프트가 토큰 몇 개짜리인지" 먼저 알고 싶었다. tiktoken 같은 라이브러리로 어림잡아 세는 방법도 있지만, Claude는 토크나이저가 공개돼 있지 않아서 추정값이 실제와 안 맞는다. 다행히 Anthropic은 정확한 값을 알려주는 전용 엔드포인트를 제공한다.
count_tokens는 뭘 하나
messages/count_tokens는 messages 요청과 똑같은 형식을 받아서, 응답을 생성하지 않고 입력 토큰 수만 돌려준다. 실제 추론이 안 돌기 때문에 무료고, 응답도 즉시 온다.
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const { input_tokens } = await client.messages.countTokens({
model: 'claude-sonnet-5',
system: '너는 문서 요약 도우미다.',
messages: [
{ role: 'user', content: '이 문서를 3줄로 요약해줘:\n\n' + docText },
],
});
console.log(`입력 토큰: ${input_tokens}`);
핵심은 messages.create에 넘길 파라미터를 그대로 countTokens에 넣을 수 있다는 점이다. system, 여러 턴의 messages, 심지어 tools나 이미지 블록까지 다 반영해서 세준다. 도구 정의가 붙으면 그 스키마도 토큰을 잡아먹으니, 실제 요청과 동일하게 넣어야 값이 정확하다.
보내기 전에 게이트로 쓰기
나는 이걸 요청 전 방어선으로 썼다. 컨텍스트 한도에 가까우면 문서를 잘라내는 식이다.
const LIMIT = 180_000; // 200K 윈도우에 여유 두기
async function safeAsk(system: string, prompt: string) {
const { input_tokens } = await client.messages.countTokens({
model: 'claude-sonnet-5',
system,
messages: [{ role: 'user', content: prompt }],
});
if (input_tokens > LIMIT) {
throw new Error(`입력이 너무 큼: ${input_tokens} 토큰`);
}
return client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 1024,
system,
messages: [{ role: 'user', content: prompt }],
});
}
입력 토큰 수에 모델별 100만 토큰당 단가를 곱하면 요청 하나의 예상 입력 비용도 바로 나온다. 로그에 찍어두니 어떤 요청이 비싼지 한눈에 보였다.
실전 팁
- 출력 토큰은 못 잰다. count_tokens는 입력만 센다. 총비용은
max_tokens를 상한으로 잡고 따로 계산해야 한다. - 매 요청마다 부를 필요는 없다. 입력 크기가 뻔한 경로라면 생략하고, 사용자 업로드처럼 크기가 튀는 곳에만 쓰면 된다.
- 프롬프트 캐싱과 같이 보면 좋다. 어느 부분이 토큰을 잡아먹는지 알면 캐시할 접두부를 정하기 쉬워진다.
토큰을 어림짐작하던 걸 실측으로 바꾸니, 컨텍스트 초과 에러가 사라지고 요청별 비용도 예측 가능해졌다. 호출 한 번이 공짜라는 게 제일 크다.