서론: 왜 AI API 계약 테스트인가?
AI API를 활용한 시스템을 운영하다 보면 모델 버전 업그레이드, 프롬프트 변경, 응답 구조 수정이 빈번하게 발생합니다. 저도 여러 프로젝트에서 AI 응답 포맷 변경으로 인한 장애를 겪은 후, AI API 계약 테스트의 중요성을 깨달았습니다. 이번 튜토리얼에서는 HolySheep AI를 통한 AI API 계약 테스트 구현 방법을 상세히 다룹니다.
계약 테스트(Contract Testing)는 소비자 중심의 API 응답 스키마를 검증하여, 제공자와 소비자 간의 인터페이스 불일치를 조기에 발견하는 기법입니다. 특히 HolySheep AI처럼 다중 모델(GPT-4.1, Claude Sonnet, Gemini, DeepSeek)을 단일 엔드포인트로 통합 제공하는 환경에서는 모델 간 응답 구조 차이가 발생할 수 있어 계약 테스트가 필수적입니다.
계약 테스트 아키텍처 설계
1. 테스트 전략 개요
AI API 계약 테스트는 전통적인 REST API 계약 테스트와 다릅니다. 응답의 의미적 일관성과 구조적 유효성을 동시에 검증해야 합니다. HolySheep AI를 활용한 계약 테스트 아키텍처는 다음과 같이 구성됩니다:
- 스키마 정의 레이어: JSON Schema 기반 응답 구조 정의
- 동적 응답 검증 레이어: LLM 응답의 유연성 반영 검증 로직
- 모델별 적응 레이어: 각 모델 특성에 따른 검증 규칙 적용
- 모니터링 레이어: 실시간 계약 위반 감지 및 알림
2. 프로젝트 구조
ai-contract-testing/
├── contracts/
│ ├── schemas/
│ │ ├── chat-completion.schema.json
│ │ ├── structured-output.schema.json
│ │ └── embedding.schema.json
│ └── pact/
│ ├── consumer-contracts/
│ └── provider-contracts/
├── src/
│ ├── validators/
│ │ ├── schema-validator.ts
│ │ ├── semantic-validator.ts
│ │ └── cost-tracker.ts
│ ├── providers/
│ │ └── holysheep-provider.ts
│ └── runners/
│ └── contract-runner.ts
├── tests/
│ ├── unit/
│ ├── integration/
│ └── contract/
│ └── holysheep-contracts.spec.ts
├── package.json
└── tsconfig.json
HolySheep AI 클라이언트 설정
먼저 HolySheep AI 클라이언트를 설정하겠습니다. HolySheep AI는 단일 API 키로 여러 모델을 지원하므로, 계약 테스트에서 다양한 모델 응답을 효율적으로 검증할 수 있습니다. 지금 가입하여 무료 크레딧을 받고 시작하세요.
// src/providers/holysheep-provider.ts
import axios, { AxiosInstance, AxiosError } from 'axios';
interface HolySheepConfig {
apiKey: string;
baseUrl: string;
timeout: number;
maxRetries: number;
}
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
interface ChatCompletionRequest {
model: string;
messages: ChatMessage[];
temperature?: number;
max_tokens?: number;
response_format?: { type: 'json_object' | 'text' };
}
interface ChatCompletionResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: ChatMessage;
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
response_ms: number;
cost_cents: number;
}
class HolySheepProvider {
private client: AxiosInstance;
private config: HolySheepConfig;
constructor(config: HolySheepConfig) {
this.config = config;
this.client = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout,
headers: {
'Authorization': Bearer ${config.apiKey},
'Content-Type': 'application/json',
},
});
this.client.interceptors.response.use(
(response) => {
// 응답 시간 측정
const responseTime = Date.now();
response.data.response_ms = responseTime - response.config.metadata?.startTime;
return response;
},
async (error: AxiosError) => {
if (error.response?.status === 429) {
//Rate Limit 처리
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
return this.client.request(error.config!);
}
throw error;
}
);
}
async chatCompletion(request: ChatCompletionRequest): Promise {
const startTime = Date.now();
try {
const response = await this.client.post(
'/chat/completions',
request,
{
// 요청 타임스탬프 기록
}
);
// 비용 계산 (HolySheep AI 가격표 기준)
const modelPricing = {
'gpt-4.1': { input: 8, output: 8 }, // $8/MTok
'claude-sonnet-4-5': { input: 15, output: 15 }, // $15/MTok
'gemini-2.5-flash': { input: 2.5, output: 10 }, // $2.50/$10/MTok
'deepseek-v3.2': { input: 0.42, output: 1.68 }, // $0.42/$1.68/MTok
};
const pricing = modelPricing[request.model as keyof typeof modelPricing] || modelPricing['gpt-4.1'];
const usage = response.data.usage;
response.data.cost_cents =
(usage.prompt_tokens / 1000000) * pricing.input * 100 +
(usage.completion_tokens / 1000000) * pricing.output * 100;
response.data.response_ms = Date.now() - startTime;
return response.data;
} catch (error) {
console.error('HolySheep API Error:', error);
throw error;
}
}
getAvailableModels(): string[] {
return [
'gpt-4.1',
'claude-sonnet-4-5',
'gemini-2.5-flash',
'deepseek-v3.2',
];
}
}
export { HolySheepProvider, HolySheepConfig, ChatCompletionRequest, ChatCompletionResponse };
계약 스키마 정의 및 검증
1. JSON Schema 기반 스키마
AI API 응답의 구조적 일관성을 보장하기 위해 JSON Schema를 정의합니다. HolySheep AI에서 제공하는 다양한 모델들의 응답을 일관된 계약으로 검증할 수 있습니다.
// contracts/schemas/structured-output.schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "object", "created", "model", "choices", "usage"],
"properties": {
"id": {
"type": "string",
"pattern": "^chatcmpl-[a-zA-Z0-9]+$"
},
"object": {
"type": "string",
"const": "chat.completion"
},
"created": {
"type": "integer",
"minimum": 1600000000
},
"model": {
"type": "string",
"enum": ["gpt-4.1", "claude-sonnet-4-5", "gemini-2.5-flash", "deepseek-v3.2"]
},
"choices": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["index", "message", "finish_reason"],
"properties": {
"index": {
"type": "integer",
"minimum": 0
},
"message": {
"type": "object",
"required": ["role", "content"],
"properties": {
"role": {
"type": "string",
"const": "assistant"
},
"content": {
"type": "string",
"minLength": 0
}
}
},
"finish_reason": {
"type": "string",
"enum": ["stop", "length", "content_filter", "tool_calls", "function_call"]
}
}
}
},
"usage": {
"type": "object",
"required": ["prompt_tokens", "completion_tokens", "total_tokens"],
"properties": {
"prompt_tokens": {
"type": "integer",
"minimum": 0
},
"completion_tokens": {
"type": "integer",
"minimum": 0
},
"total_tokens": {
"type": "integer",
"minimum": 0
}
}
},
"response_ms": {
"type": "number",
"minimum": 0
},
"cost_cents": {
"type": "number",
"minimum": 0,
"maximum": 100
}
}
}
계약 테스트 Runner 구현
// src/runners/contract-runner.ts
import Ajv, { JSONSchemaType, ValidateFunction } from 'ajv';
import addFormats from 'ajv-formats';
import { HolySheepProvider } from '../providers/holysheep-provider';
import structuredOutputSchema from '../../contracts/schemas/structured-output.schema.json';
interface ContractTestResult {
testName: string;
model: string;
passed: boolean;
schemaValid: boolean;
semanticValid: boolean;
latencyMs: number;
costCents: number;
errors: string[];
timestamp: Date;
}
interface ContractTestCase {
name: string;
model: string;
systemPrompt: string;
userPrompt: string;
expectedSchema: object;
semanticRules?: {
minLength?: number;
maxLength?: number;
requiredKeywords?: string[];
bannedKeywords?: string[];
};
timeout: number;
}
class ContractRunner {
private provider: HolySheepProvider;
private ajv: Ajv;
private validators: Map = new Map();
constructor(provider: HolySheepProvider) {
this.provider = provider;
this.ajv = new Ajv({ allErrors: true, verbose: true });
addFormats(this.ajv);
// 기본 스키마 캐싱
this.validators.set('structured-output', this.ajv.compile(structuredOutputSchema));
}
async runContractTests(testCases: ContractTestCase[]): Promise {
const results: ContractTestResult[] = [];
for (const testCase of testCases) {
const result = await this.executeTest(testCase);
results.push(result);
// 테스트 결과 로깅
console.log([${result.passed ? 'PASS' : 'FAIL'}] ${testCase.name} (${testCase.model}));
if (result.errors.length > 0) {
console.log( Errors: ${result.errors.join(', ')});
}
console.log( Latency: ${result.latencyMs}ms | Cost: ${result.costCents.toFixed(3)}¢);
}
return results;
}
private async executeTest(testCase: ContractTestCase): Promise {
const startTime = Date.now();
const errors: string[] = [];
try {
// HolySheep AI API 호출
const response = await Promise.race([
this.provider.chatCompletion({
model: testCase.model,
messages: [
{ role: 'system', content: testCase.systemPrompt },
{ role: 'user', content: testCase.userPrompt },
],
response_format: { type: 'json_object' },
}),
this.createTimeout(testCase.timeout),
]);
const latencyMs = Date.now() - startTime;
// 1. 스키마 검증
const validator = this.validators.get('structured-output')!;
const schemaValid = validator(response);
if (!schemaValid) {
errors.push(Schema validation failed: ${JSON.stringify(validator.errors)});
}
// 2. 시맨틱 검증
let semanticValid = true;
if (testCase.semanticRules) {
const content = response.choices[0].message.content;
if (testCase.semanticRules.minLength && content.length < testCase.semanticRules.minLength) {
errors.push(Content too short: ${content.length} < ${testCase.semanticRules.minLength});
semanticValid = false;
}
if (testCase.semanticRules.maxLength && content.length > testCase.semanticRules.maxLength) {
errors.push(Content too long: ${content.length} > ${testCase.semanticRules.maxLength});
semanticValid = false;
}
if (testCase.semanticRules.requiredKeywords) {
const missingKeywords = testCase.semanticRules.requiredKeywords.filter(
(keyword) => !content.toLowerCase().includes(keyword.toLowerCase())
);
if (missingKeywords.length > 0) {
errors.push(Missing required keywords: ${missingKeywords.join(', ')});
semanticValid = false;
}
}
if (testCase.semanticRules.bannedKeywords) {
const foundBanned = testCase.semanticRules.bannedKeywords.filter(
(keyword) => content.toLowerCase().includes(keyword.toLowerCase())
);
if (foundBanned.length > 0) {
errors.push(Found banned keywords: ${foundBanned.join(', ')});
semanticValid = false;
}
}
}
return {
testName: testCase.name,
model: testCase.model,
passed: schemaValid && semanticValid,
schemaValid,
semanticValid,
latencyMs,
costCents: response.cost_cents,
errors,
timestamp: new Date(),
};
} catch (error) {
return {
testName: testCase.name,
model: testCase.model,
passed: false,
schemaValid: false,
semanticValid: false,
latencyMs: Date.now() - startTime,
costCents: 0,
errors: [error instanceof Error ? error.message : String(error)],
timestamp: new Date(),
};
}
}
private createTimeout(ms: number): Promise {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(Timeout after ${ms}ms)), ms);
});
}
generateTestReport(results: ContractTestResult[]): string {
const total = results.length;
const passed = results.filter((r) => r.passed).length;
const totalCost = results.reduce((sum, r) => sum + r.costCents, 0);
const avgLatency = results.reduce((sum, r) => sum + r.latencyMs, 0) / total;
return `
========================================
AI API Contract Test Report
========================================
Total Tests: ${total}
Passed: ${passed} (${((passed / total) * 100).toFixed(1)}%)
Failed: ${total - passed}
Performance Metrics:
- Average Latency: ${avgLatency.toFixed(0)}ms
- Total Cost: ${totalCost.toFixed(3)}¢
Model Breakdown:
${results.map((r) => ${r.model}: ${r.passed ? 'PASS' : 'FAIL'} (${r.latencyMs}ms, ${r.costCents.toFixed(3)}¢)).join('\n')}
========================================
`;
}
}
export { ContractRunner, ContractTestResult, ContractTestCase };
통합 테스트 실행 및 벤치마크
// tests/contract/holysheep-contracts.spec.ts
import { HolySheepProvider } from '../src/providers/holysheep-provider';
import { ContractRunner } from '../src/runners/contract-runner';
const HOLYSHEEP_API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
describe('HolySheep AI Contract Tests', () => {
let provider: HolySheepProvider;
let runner: ContractRunner;
beforeAll(() => {
provider = new HolySheepProvider({
apiKey: HOLYSHEEP_API_KEY,
baseUrl: 'https://api.holysheep.ai/v1',
timeout: 30000,
maxRetries: 3,
});
runner = new ContractRunner(provider);
});
const testCases = [
{
name: 'JSON 구조화 응답 - GPT-4.1',
model: 'gpt-4.1',
systemPrompt: '당신은 유용한 도우미입니다. 항상 유효한 JSON으로만 응답하세요.',
userPrompt: '사용자 이름과 이메일을 포함한 사용자 정보를 JSON으로 반환해주세요.',
semanticRules: {
requiredKeywords: ['name', 'email'],
maxLength: 500,
},
timeout: 15000,
},
{
name: 'JSON 구조화 응답 - Claude Sonnet',
model: 'claude-sonnet-4-5',
systemPrompt: 'You are a helpful assistant. Always respond with valid JSON only.',
userPrompt: 'Return user information including name and email in JSON format.',
semanticRules: {
requiredKeywords: ['name', 'email'],
maxLength: 500,
},
timeout: 15000,
},
{
name: 'JSON 구조화 응답 - Gemini 2.5 Flash',
model: 'gemini-2.5-flash',
systemPrompt: '당신은 유용한 도우미입니다. 항상 유효한 JSON으로만 응답하세요.',
userPrompt: '사용자 이름과 이메일을 포함한 사용자 정보를 JSON으로 반환해주세요.',
semanticRules: {
requiredKeywords: ['name', 'email'],
maxLength: 500,
},
timeout: 10000,
},
{
name: 'JSON 구조화 응답 - DeepSeek V3.2',
model: 'deepseek-v3.2',
systemPrompt: '당신은 유용한 도우미입니다. 항상 유효한 JSON으로만 응답하세요.',
userPrompt: '사용자 이름과 이메일을 포함한 사용자 정보를 JSON으로 반환해주세요.',
semanticRules: {
requiredKeywords: ['name', 'email'],
maxLength: 500,
},
timeout: 15000,
},
{
name: '긴 컨텍스트 처리 - GPT-4.1',
model: 'gpt-4.1',
systemPrompt: '당신은 텍스트 분석기입니다.',
userPrompt: '이 텍스트의 핵심 포인트를 3문장으로 요약해주세요: ' + 'Lorem ipsum '.repeat(100),
semanticRules: {
minLength: 50,
maxLength: 300,
},
timeout: 20000,
},
{
name: '비용 추적 검증',
model: 'deepseek-v3.2',
systemPrompt: '간단한 인사를してください.',
userPrompt: '안녕하세요, 간단히 인사해 주세요.',
semanticRules: {
maxLength: 100,
},
timeout: 10000,
},
];
it('should run all contract tests', async () => {
const results = await runner.runContractTests(testCases);
// 리포트 생성
const report = runner.generateTestReport(results);
console.log(report);
// 결과 검증
results.forEach((result) => {
expect(result.schemaValid).toBe(true);
});
});
it('should validate cost tracking accuracy', async () => {
const costTestCase = testCases.find((tc) => tc.name.includes('비용'))!;
const results = await runner.runContractTests([costTestCase]);
const result = results[0];
console.log(Cost validation: ${result.costCents.toFixed(3)}¢ for ${result.model});
// DeepSeek V3.2는 가장 저렴한 모델 ($0.42/MTok input)
expect(result.costCents).toBeLessThan(0.1); // 10¢ 미만
expect(result.latencyMs).toBeLessThan(5000); // 5초 이내
});
it('should handle rate limiting gracefully', async () => {
// 동시 요청 테스트
const concurrentRequests = Array(5).fill(null).map(() =>
provider.chatCompletion({
model: 'deepseek-v3.2',
messages: [{ role: 'user', content: 'Say "test"' }],
})
);
const startTime = Date.now();
const results = await Promise.allSettled(concurrentRequests);
const totalTime = Date.now() - startTime;
console.log(Concurrent requests completed in ${totalTime}ms);
const fulfilled = results.filter((r) => r.status === 'fulfilled').length;
console.log(Fulfilled: ${fulfilled}/${results.length});
});
});
// 벤치마크 실행용
if (require.main === module) {
const HOLYSHEEP_API_KEY = process.env.HOLYSHEEP_API_KEY || 'YOUR_HOLYSHEEP_API_KEY';
async function runBenchmarks() {
console.log('Starting HolySheep AI Contract Benchmark...\n');
const provider = new HolySheepProvider({
apiKey: HOLYSHEEP_API_KEY,
baseUrl: 'https://api.holysheep.ai/v1',
timeout: 30000,
});
const runner = new ContractRunner(provider);
const benchmarkResults = [];
const models = ['gpt-4.1', 'claude-sonnet-4-5', 'gemini-2.5-flash', 'deepseek-v3.2'];
for (const model of models) {
const latencies: number[] = [];
const costs: number[] = [];
console.log(Testing ${model}...);
for (let i = 0; i < 5; i++) {
const start = Date.now();
try {
const response = await provider.chatCompletion({
model,
messages: [{ role: 'user', content: 'What is 2+2?' }],
});
latencies.push(Date.now() - start);
costs.push(response.cost_cents);
} catch (error) {
console.error(Error on ${model}:, error);
}
}
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
const avgCost = costs.reduce((a, b) => a + b, 0) / costs.length;
benchmarkResults.push({
model,
avgLatencyMs: avgLatency.toFixed(0),
avgCostCents: avgCost.toFixed(4),
minLatencyMs: Math.min(...latencies),
maxLatencyMs: Math.max(...latencies),
});
}
console.log('\n========== BENCHMARK RESULTS ==========');
console.log('Model | Avg Latency | Min | Max | Avg Cost');
console.log('---------------------|-------------|-----|-----|---------');
benchmarkResults.forEach((r) => {
console.log(
${r.model.padEnd(20)} | ${r.avgLatencyMs.padStart(11)}ms | ${r.minLatencyMs.toString().padStart(3)}ms | ${r.maxLatencyMs.toString().padStart(3)}ms | ${r.avgCostCents.padStart(7)}¢
);
});
console.log('========================================\n');
}
runBenchmarks().catch(console.error);
}
비용 최적화 전략
저의 경험상 AI API 계약 테스트를 프로덕션 환경에서 지속 실행하려면 비용 최적화가 필수적입니다. HolySheep AI의 가격표를 기반으로 최적화 전략을 세웠습니다:
- 모델 선택 전략: 테스트용으로는 DeepSeek V3.2 ($0.42/MTok)가 가장 경제적
- 토큰 최소화: 시스템 프롬프트 압축 및 배치 처리
- 캐싱 전략: 동일한 입력에 대한 응답 재사용
- 모니터링 Dashboard: 일일 비용 임계치 설정 및 알림
// src/providers/cost-optimizer.ts
interface CostBudget {
dailyLimit: number;
monthlyLimit: number;
alertThreshold: number;
}
interface CostRecord {
timestamp: Date;
model: string;
promptTokens: number;
completionTokens: number;
costCents: number;
}
class CostOptimizer {
private budget: CostBudget;
private records: CostRecord[] = [];
private cache: Map = new Map();
constructor(budget: CostBudget) {
this.budget = budget;
}
// 단순 해시 기반 캐시 키 생성
private generateCacheKey(model: string, messages: any[]): string {
const messageStr = JSON.stringify(messages);
let hash = 0;
for (let i = 0; i < messageStr.length; i++) {
const char = messageStr.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return ${model}-${Math.abs(hash)};
}
// 캐시된 응답 반환
getCachedResponse(model: string, messages: any[]): any | null {
const key = this.generateCacheKey(model, messages);
const cached = this.cache.get(key);
if (cached) {
console.log(Cache hit for ${model}: ${key.substring(0, 20)}...);
return cached.response;
}
return null;
}
// 응답 캐싱
cacheResponse(model: string, messages: any[], response: any, costCents: number): void {
const key = this.generateCacheKey(model, messages);
this.cache.set(key, { response, costCents });
// 캐시 크기 제한 (100개)
if (this.cache.size > 100) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
// 비용 기록
recordUsage(record: CostRecord): void {
this.records.push(record);
this.checkBudget();
}
// 예산 확인
private checkBudget(): void {
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayCost = this.records
.filter((r) => r.timestamp >= today)
.reduce((sum, r) => sum + r.costCents, 0);
const dailyLimit = this.budget.dailyLimit;
if (todayCost > dailyLimit * (this.budget.alertThreshold / 100)) {
console.warn(⚠️ Daily budget alert: ${todayCost.toFixed(3)}¢ / ${dailyLimit}¢);
}
if (todayCost > dailyLimit) {
throw new Error(Daily budget exceeded: ${todayCost.toFixed(3)}¢ > ${dailyLimit}¢);
}
}
// 비용 보고서
generateCostReport(): string {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const todayRecords = this.records.filter((r) => r.timestamp >= today);
const monthRecords = this.records.filter((r) => {
const recordDate = new Date(r.timestamp);
return recordDate.getMonth() === now.getMonth() && recordDate.getFullYear() === now.getFullYear();
});
const todayCost = todayRecords.reduce((sum, r) => sum + r.costCents, 0);
const monthCost = monthRecords.reduce((sum, r) => sum + r.costCents, 0);
const modelBreakdown = todayRecords.reduce((acc, r) => {
acc[r.model] = (acc[r.model] || 0) + r.costCents;
return acc;
}, {} as Record);
return `
========================================
Cost Optimization Report
========================================
Today's Cost: ${todayCost.toFixed(3)}¢ / ${this.budget.dailyLimit}¢
Month Cost: ${monthCost.toFixed(3)}¢ / ${this.budget.monthlyLimit}¢
Model Breakdown (Today):
${Object.entries(modelBreakdown).map(([model, cost]) => ${model}: ${cost.toFixed(3)}¢).join('\n')}
Cache Hit Rate: ${((this.cache.size / (this.cache.size + todayRecords.length)) * 100).toFixed(1)}%
========================================
`;
}
}
export { CostOptimizer, CostBudget, CostRecord };
자주 발생하는 오류와 해결책
1. Rate Limit (429) 오류
// 오류 메시지 예시:
// Error: Request failed with status code 429
// Retry-After: 5
// 해결책:指數 백오프와 재시도 로직 구현
async function retryWithBackoff(
fn: () => Promise,
maxRetries: number = 5,
baseDelayMs: number = 1000
): Promise {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
if (error.response?.status === 429 && attempt < maxRetries - 1) {
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter
? parseInt(retryAfter) * 1000
: baseDelayMs * Math.pow(2, attempt);
console.log(Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries}));
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// 사용 예시
const response = await retryWithBackoff(() =>
provider.chatCompletion({
model: 'gpt-4.1',
messages: [{ role: 'user', content: 'Hello' }],
})
);
2. JSON 파싱 오류
// 오류 메시지 예시:
// JSONParseError: Unexpected token '<'
// 원인: LLM이 JSON이 아닌 텍스트 응답 반환
// 해결책: 응답 전처리 및 안전한 파싱
function safeJsonParse(response: string): any {
// 마크다운 코드 블록 제거
let cleaned = response.trim();
if (cleaned.startsWith('```json')) {
cleaned = cleaned.slice(7);
} else if (cleaned.startsWith('```')) {
cleaned = cleaned.slice(3);
}
if (cleaned.endsWith('```')) {
cleaned = cleaned.slice(0, -3);
}
cleaned = cleaned.trim();
// 이스케이프된 문자 처리
cleaned = cleaned.replace(/\\n/g, '\n').replace(/\\"/g, '"');
try {
return JSON.parse(cleaned);
} catch (parseError) {
// 유효한 JSON이 아닌 경우 부분 파싱 시도
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
if (jsonMatch) {
try {
return JSON.parse(jsonMatch[0]);
} catch {
throw new Error(Failed to parse JSON: ${cleaned.substring(0, 100)});
}
}
throw new Error(Invalid JSON response: ${cleaned.substring(0, 100)});
}
}
// 사용 예시
const content = response.choices[0].message.content;
const parsedResponse = safeJsonParse(content);
3. 토큰 초과 (400) 오류
// 오류 메시지 예시:
// Error: Request failed with status code 400
// This model's maximum context window is 128000 tokens
// 해결책: 컨텍스트 자동 트렁케이션
function truncateToTokenLimit(
messages: Array<{ role: string; content: string }>,
maxTokens: number = 120000,
systemPrompt: string = ''
): Array<{ role: string; content: string }> {
// 대략적인 토큰 계산 (한국어: 1토큰 ≈ 1.5자, 영어: 1토큰 ≈ 4자)
const estimateTokens = (text: string): number => {
const koreanChars = (text.match(/[가-힣]/g) || []).length;
const otherChars = text.length - koreanChars;
return Math.ceil(koreanChars / 1.5 + otherChars / 4);
};
const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;
const availableTokens = maxTokens - systemTokens - 500; // 안전 마진
// 가장 오래된 메시지부터 제거
const truncatedMessages = [...messages];
let totalTokens = truncatedMessages.reduce(
(sum, msg) => sum + estimateTokens(msg.content),
0
);
while (totalTokens > availableTokens && truncatedMessages.length > 1) {
const removed = truncatedMessages.shift();
if (removed) {
totalTokens -= estimateTokens(removed.content);
}
}
if (totalTokens > availableTokens) {
// 마지막 메시지 트렁케이션
const lastMsg = truncatedMessages[truncatedMessages.length - 1];
const lastTokens = estimateTokens(lastMsg.content);
const excessTokens = totalTokens - availableTokens;
const charsToRemove = Math.ceil(excessTokens * 3);
lastMsg.content = lastMsg.content.slice(0, -charsToRemove) + '...';
}
console.log(Truncated to ~${totalTokens} tokens);
return truncatedMessages;
}
// 사용 예시
const truncatedMessages = truncateToTokenLimit(
originalMessages,
120000,
systemPrompt
);
4. 응답 지연 시간 초과
// 오류 메시지 예시:
// TimeoutError: Request timeout after 30000ms
// 해결책: 적응형 타임아웃 및 폴백