
LLM API 활용 실제: OpenAI, Anthropic API로 기능 만들기
LLM API를 처음 연동했을 때 토큰 비용 폭탄, 응답 지연, 할루시네이션에 당황했다. 실제로 배운 LLM API 활용법을 정리했다.

LLM API를 처음 연동했을 때 토큰 비용 폭탄, 응답 지연, 할루시네이션에 당황했다. 실제로 배운 LLM API 활용법을 정리했다.
둘 다 같은 Transformer 자식인데 왜 다를까? '빈칸 채우기'와 '이어 쓰기' 비유로 알아보는 BERT와 GPT의 결정적 차이. 프로젝트에서 겪은 시행착오와 선택 가이드.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

페이스북은 왜 REST API를 버렸을까? 원하는 데이터만 쏙쏙 골라 담는 GraphQL의 매력과 치명적인 단점 (캐싱, N+1 문제) 분석.

세 가지 AI 코딩 도구를 실제로 써보고 비교했다. 자동완성, 코드 생성, 리팩토링 각각 어디가 강한지 솔직한 후기.

첫 LLM API 연동 후 일주일 만에 토큰 비용이 50달러를 넘겼다. 사용자가 긴 문서를 올릴 때마다 GPT-4가 전체를 읽고 있었다. 프롬프트 캐싱이란 걸 몰랐고, 스트리밍도 안 써서 사용자는 10초씩 빈 화면을 보고 있었다. LLM API는 REST API처럼 쓰면 안 된다는 걸 그때 깨달았다.
OpenAI와 Anthropic API를 실제로 쓰면서 배운 것들을 정리했다. 토큰 비용 최적화, 스트리밍 응답, 구조화된 출력, function calling까지. 문서에는 안 나오는 팁들이다.
LLM API 호출은 HTTP 요청처럼 보이지만, 내부에서는 완전히 다른 일이 일어난다. 일반 REST API는 DB 쿼리 한 번이면 끝이지만, LLM은 수천억 개의 파라미터를 거쳐 토큰 하나하나를 생성한다.
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});
async function summarizeText(text: string) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '당신은 간결한 요약을 제공하는 어시스턴트입니다.'
},
{
role: 'user',
content: `다음 텍스트를 3문장으로 요약해주세요:\n\n${text}`
}
],
temperature: 0.3,
max_tokens: 200
});
return response.choices[0].message.content;
}
이 코드의 문제는 await에 있다. 사용자가 5000자 문서를 올리면 GPT-4가 200토큰을 생성하는 동안 15초를 기다려야 한다. 이게 내가 처음 만든 요약 기능이었다. 사용자는 로딩 스피너만 보다가 페이지를 떠났다.
핵심은 스트리밍이다. LLM은 토큰을 하나씩 생성하는데, 우리는 모든 토큰이 나올 때까지 기다릴 필요가 없다. 마치 영화 스트리밍처럼 생성되는 대로 보여주면 된다.
async function summarizeTextStreaming(text: string) {
const stream = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '당신은 간결한 요약을 제공하는 어시스턴트입니다.'
},
{
role: 'user',
content: `다음 텍스트를 3문장으로 요약해주세요:\n\n${text}`
}
],
temperature: 0.3,
max_tokens: 200,
stream: true
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
process.stdout.write(content); // 또는 클라이언트로 전송
}
}
}
체감 속도가 완전히 달라진다. 첫 토큰은 1-2초 안에 나오고, 사용자는 텍스트가 타이핑되는 걸 보면서 기다린다. ChatGPT가 스트리밍을 쓰는 이유다.
LLM API의 가장 큰 함정은 토큰 비용이다. GPT-4의 경우 input 토큰은 1M당 $2.5, output은 $10다. 처음엔 대수롭지 않게 생각했는데, 사용자가 늘자 비용이 기하급수적으로 올랐다.
첫 번째 깨달음: 토큰은 단어가 아니다. 영어는 1 단어 ≈ 1.3 토큰이지만, 한국어는 1 글자 ≈ 2-3 토큰이다. "안녕하세요"는 5글자지만 10토큰이 넘는다. 한국어 서비스는 영어보다 토큰 비용이 2-3배 높다.
두 번째 깨달음: 모델 선택이 비용의 80%를 결정한다. GPT-4는 강력하지만 비싸다. 간단한 분류나 요약은 GPT-4o-mini면 충분하다. 나는 모든 요청에 GPT-4를 쓰다가, 기능별로 모델을 나눠서 비용을 70% 줄였다.
const MODEL_SELECTION = {
// 복잡한 추론이 필요한 작업
complex: 'gpt-4o',
// 간단한 분류, 요약
simple: 'gpt-4o-mini',
// 대량 처리
batch: 'gpt-4o-mini'
};
async function classifyFeedback(text: string) {
// 감정 분류 같은 간단한 작업은 mini로 충분
const response = await openai.chat.completions.create({
model: MODEL_SELECTION.simple,
messages: [
{
role: 'system',
content: '사용자 피드백을 긍정/부정/중립으로 분류하세요.'
},
{
role: 'user',
content: text
}
],
temperature: 0, // 분류는 일관성이 중요
max_tokens: 10 // 한 단어면 충분
});
return response.choices[0].message.content;
}
세 번째 깨달음: Prompt Caching이 게임 체인저다. Anthropic의 Claude API는 프롬프트 캐싱을 지원한다. 긴 시스템 프롬프트나 문서를 캐싱하면, 두 번째 호출부터는 캐시된 토큰 비용이 90% 줄어든다.
import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
async function analyzeCodeWithCache(code: string) {
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
system: [
{
type: 'text',
text: '당신은 코드 리뷰 전문가입니다. 다음 가이드라인을 따르세요...',
cache_control: { type: 'ephemeral' } // 5분간 캐싱
}
],
messages: [
{
role: 'user',
content: `이 코드를 리뷰해주세요:\n\n${code}`
}
]
});
return response.content[0].text;
}
시스템 프롬프트가 1000토큰이라면, 캐싱 없이는 요청마다 1000토큰을 낸다. 캐싱을 쓰면 첫 요청만 1000토큰, 이후는 100토큰만 낸다. 같은 기능을 반복 호출하는 서비스라면 필수다.
처음엔 temperature가 뭔지 몰라서 그냥 0.7로 뒀다. 결과가 매번 달라져서 당황했다. temperature는 "창의성"이 아니라 확률 분포의 무작위성이다.
LLM은 다음 토큰을 선택할 때 확률 분포를 만든다. "안녕" 다음에 "하세요" 80%, "하십니까" 15%, "히" 5% 같은 식이다. temperature는 이 분포를 얼마나 평평하게 만들지 결정한다.
// 데이터 추출/분류 - 일관성 중요
async function extractStructuredData(text: string) {
return await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0, // 매번 같은 결과
messages: [...]
});
}
// 창작 콘텐츠 - 다양성 중요
async function generateBlogIdeas(topic: string) {
return await openai.chat.completions.create({
model: 'gpt-4o',
temperature: 0.9, // 다양한 아이디어
messages: [...]
});
}
max_tokens는 비용 폭발을 막는 안전장치다. 처음에는 안 정해줬다가, GPT-4가 5000토큰짜리 에세이를 뱉어내서 요청 하나에 5센트를 낸 적이 있다. 이제는 항상 max_tokens를 정한다.
LLM의 응답은 텍스트다. "긍정적입니다"를 파싱하는 건 악몽이다. 오타, 줄바꿈, 다른 표현... JSON으로 받고 싶었다.
첫 시도: 프롬프트에 "JSON으로 답하세요"// ❌ 신뢰할 수 없음
const badResponse = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: '이 리뷰를 분석하고 JSON 형식으로 답하세요: {"sentiment": "positive/negative", "score": 1-5}'
}
]
});
// 결과: "물론이죠! 분석 결과는 다음과 같습니다:\n```json\n{...}\n```"
GPT-4가 코드 블록으로 감싸거나, 설명을 덧붙인다. 파싱이 엉망이 됐다.
해결책: JSON Mode (OpenAI) 또는 Tool Use (Anthropic)// OpenAI JSON Mode
async function analyzeSentiment(review: string) {
const response = await openai.chat.completions.create({
model: 'gpt-4o',
response_format: { type: 'json_object' },
messages: [
{
role: 'system',
content: 'JSON 형식으로만 응답하세요.'
},
{
role: 'user',
content: `리뷰 분석 (sentiment: positive/negative/neutral, score: 1-5, summary: string):\n${review}`
}
]
});
return JSON.parse(response.choices[0].message.content);
}
// Anthropic Tool Use (더 강력)
async function extractProductInfo(description: string) {
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
tools: [
{
name: 'save_product',
description: '제품 정보 저장',
input_schema: {
type: 'object',
properties: {
name: { type: 'string', description: '제품명' },
price: { type: 'number', description: '가격' },
category: { type: 'string', description: '카테고리' },
features: { type: 'array', items: { type: 'string' } }
},
required: ['name', 'price', 'category']
}
}
],
messages: [
{
role: 'user',
content: `제품 정보 추출:\n${description}`
}
]
});
const toolUse = response.content.find(block => block.type === 'tool_use');
return toolUse?.input;
}
Tool use는 function calling의 진화판이다. LLM이 함수를 "호출"하는 형태로 구조화된 데이터를 반환한다. 스키마 검증까지 자동으로 돼서, JSON mode보다 훨씬 안정적이다.
LLM API는 실패한다. 자주. rate limit에 걸리고, 서버가 느려지고, 네트워크가 끊긴다. 처음엔 try-catch 하나로 퉁쳤는데, 프로덕션에서 계속 터졌다.
핵심 전략: Exponential Backoff + 재시도async function callLLMWithRetry<T>(
apiCall: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await apiCall();
} catch (error: any) {
const isLastRetry = i === maxRetries - 1;
// Rate limit - 대기 후 재시도
if (error?.status === 429) {
if (isLastRetry) throw error;
const waitTime = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Rate limited. Waiting ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
continue;
}
// Server error (500-599) - 재시도
if (error?.status >= 500 && error?.status < 600) {
if (isLastRetry) throw error;
await new Promise(resolve => setTimeout(resolve, 1000));
continue;
}
// Client error (400-499) - 재시도 불필요
throw error;
}
}
throw new Error('Max retries exceeded');
}
// 사용
const result = await callLLMWithRetry(() =>
openai.chat.completions.create({
model: 'gpt-4o',
messages: [...]
})
);
Timeout도 필수다. GPT-4가 가끔 30초 넘게 걸린다. 사용자는 그렇게 안 기다린다.
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
// 10초 제한
const result = await withTimeout(
openai.chat.completions.create({...}),
10000
);
두 API를 모두 써봤다. 각각 장단점이 명확하다.
OpenAI 장점:이론을 실전에 적용한 예다. 고객 문의를 모아서 자동으로 FAQ를 만드는 기능을 구현했다.
interface FAQ {
question: string;
answer: string;
category: string;
priority: number;
}
async function generateFAQ(customerQueries: string[]): Promise<FAQ[]> {
// 1단계: 쿼리 분류 (저렴한 모델 사용)
const categorized = await Promise.all(
customerQueries.map(query =>
callLLMWithRetry(() =>
openai.chat.completions.create({
model: 'gpt-4o-mini',
temperature: 0,
response_format: { type: 'json_object' },
messages: [
{
role: 'system',
content: 'JSON 형식으로 응답: {category: string, priority: number}'
},
{
role: 'user',
content: `이 문의를 분류하세요:\n${query}`
}
]
})
)
)
);
// 2단계: 카테고리별로 그룹화
const grouped = categorized.reduce((acc, item, index) => {
const data = JSON.parse(item.choices[0].message.content);
const category = data.category;
if (!acc[category]) acc[category] = [];
acc[category].push({ query: customerQueries[index], ...data });
return acc;
}, {} as Record<string, any[]>);
// 3단계: FAQ 생성 (강력한 모델 + 캐싱)
const faqs: FAQ[] = [];
for (const [category, queries] of Object.entries(grouped)) {
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 2000,
system: [
{
type: 'text',
text: '당신은 고객 문의를 분석해 명확한 FAQ를 작성하는 전문가입니다. 다음 원칙을 따르세요:\n1. 질문은 명확하고 구체적으로\n2. 답변은 2-3문장으로 간결하게\n3. 전문 용어는 쉽게 풀어쓰기',
cache_control: { type: 'ephemeral' } // 시스템 프롬프트 캐싱
}
],
messages: [
{
role: 'user',
content: `카테고리: ${category}\n문의들:\n${queries.map(q => q.query).join('\n')}\n\n상위 5개 FAQ를 JSON 배열로 생성하세요.`
}
]
});
const categoryFAQs = JSON.parse(response.content[0].text);
faqs.push(...categoryFAQs);
}
return faqs;
}
이 코드는 팁을 모두 녹였다:
처음엔 전체를 GPT-4로 돌려서 요청당 20센트가 나왔는데, 이렇게 바꾸니 5센트로 줄었다. 속도는 3배 빨라졌다.
스타트업에서 LLM API 비용은 무시 못 한다. 사용자가 늘면 기하급수적으로 증가한다. 내가 쓰는 전략들이다.
1. Input 토큰 줄이기async function cachedLLMCall(
key: string,
apiCall: () => Promise<string>
): Promise<string> {
const cached = await redis.get(key);
if (cached) return cached;
const result = await apiCall();
await redis.setex(key, 3600, result); // 1시간 캐시
return result;
}
// 사용
const sentiment = await cachedLLMCall(
`sentiment:${hashText(review)}`,
() => analyzeSentiment(review)
);
5. 사용량 모니터링
이렇게 관리하니 월 LLM 비용이 $200 아래로 유지됐다. MAU 1000명 기준으로 사용자당 20센트다.
LLM API를 일반 REST API처럼 쓰면 안 된다. 토큰마다 돈이 나가고, 응답이 느리고, 결과가 확률적이다. 하지만 제대로 쓰면 개발 시간을 10배 줄일 수 있다.
핵심 교훈:
LLM API를 처음 연동할 때 느꼈던 막막함은 결국 "이게 다른 종류의 API"라는 걸 이해하면서 사라졌다. 이제는 분류, 요약, 추출, 생성 기능을 몇 시간 만에 만든다. 코드는 50줄이면 충분하다.
LLM API는 마법이 아니라 도구다. 특성을 이해하고, 비용을 관리하고, 에러를 대비하면 스타트업의 강력한 무기가 된다.