황현동 블로그 개발, 인생, 유우머

251221 텍스트 위주 LLM 서비스 개발 솔루션 가이드

Tags:

대화형 AI와 텍스트 생성 서비스가 일상화되면서, 텍스트 중심의 LLM(Large Language Model) 서비스 개발이 핵심 기술로 자리잡고 있습니다. 본 가이드는 실무에서 바로 활용 가능한 LLM 서비스 개발 방법론을 제시합니다.

LLM 서비스 아키텍처 개요

기본 아키텍처 패턴

┌────────────── LLM 서비스 아키텍처 ──────────────┐
│                                               │
│  ┌─────────────┐    ┌─────────────────────┐   │
│  │   Client    │    │   Application       │   │
│  │             │◄──►│   Gateway          │   │
│  │ Web/Mobile  │    │                     │   │
│  │   App       │    │ • Authentication    │   │
│  └─────────────┘    │ • Rate Limiting     │   │
│                     │ • Request Routing   │   │
│                     └─────────────────────┘   │
│                               │               │
│                               ▼               │
│  ┌─────────────────────────────────────────┐   │
│  │         LLM Processing Layer            │   │
│  │                                         │   │
│  │  ┌─────────────┐  ┌─────────────────┐  │   │
│  │  │   Prompt    │  │   LLM Provider  │  │   │
│  │  │ Engineering │◄─┤   Integration   │  │   │
│  │  │             │  │                 │  │   │
│  │  │ • Templates │  │ • OpenAI        │  │   │
│  │  │ • Context   │  │ • Claude        │  │   │
│  │  │ • Validation│  │ • Gemini        │  │   │
│  │  └─────────────┘  │ • Local Models  │  │   │
│  │                   └─────────────────┘  │   │
│  └─────────────────────────────────────────┘   │
│                               │               │
│                               ▼               │
│  ┌─────────────────────────────────────────┐   │
│  │          Data Layer                     │   │
│  │                                         │   │
│  │ ┌─────────────┐ ┌─────────────────────┐ │   │
│  │ │  Vector     │ │    Conversation     │ │   │
│  │ │ Database    │ │     History         │ │   │
│  │ │             │ │                     │ │   │
│  │ │ • Embeddings│ │ • Session Mgmt      │ │   │
│  │ │ • RAG       │ │ • Message Store     │ │   │
│  │ │ • Knowledge │ │ • Context Memory    │ │   │
│  │ └─────────────┘ └─────────────────────┘ │   │
│  └─────────────────────────────────────────┘   │
└───────────────────────────────────────────────┘

핵심 구성 요소별 상세 가이드

1. LLM 프로바이더 통합

멀티 프로바이더 지원 패턴

// LLM 프로바이더 추상화 인터페이스
interface LLMProvider {
  generateText(prompt: string, options?: GenerationOptions): Promise<string>;
  generateStream(prompt: string, options?: GenerationOptions): AsyncGenerator<string>;
  getTokenCount(text: string): number;
  getMaxTokens(): number;
}

// OpenAI 구현
class OpenAIProvider implements LLMProvider {
  private client: OpenAI;
  
  constructor(apiKey: string, model: string = 'gpt-4') {
    this.client = new OpenAI({ apiKey });
    this.model = model;
  }
  
  async generateText(prompt: string, options: GenerationOptions = {}): Promise<string> {
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages: [{ role: 'user', content: prompt }],
      max_tokens: options.maxTokens || 2000,
      temperature: options.temperature || 0.7,
      stream: false
    });
    
    return response.choices[0]?.message?.content || '';
  }
  
  async* generateStream(prompt: string, options: GenerationOptions = {}): AsyncGenerator<string> {
    const stream = await this.client.chat.completions.create({
      model: this.model,
      messages: [{ role: 'user', content: prompt }],
      max_tokens: options.maxTokens || 2000,
      temperature: options.temperature || 0.7,
      stream: true
    });
    
    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content;
      if (content) yield content;
    }
  }
}

// Claude 구현
class ClaudeProvider implements LLMProvider {
  private client: Anthropic;
  
  constructor(apiKey: string, model: string = 'claude-3-sonnet-20240229') {
    this.client = new Anthropic({ apiKey });
    this.model = model;
  }
  
  async generateText(prompt: string, options: GenerationOptions = {}): Promise<string> {
    const response = await this.client.messages.create({
      model: this.model,
      max_tokens: options.maxTokens || 2000,
      temperature: options.temperature || 0.7,
      messages: [{ role: 'user', content: prompt }]
    });
    
    return response.content[0]?.text || '';
  }
}

프로바이더 팩토리 패턴

class LLMProviderFactory {
  static create(provider: string, config: ProviderConfig): LLMProvider {
    switch (provider) {
      case 'openai':
        return new OpenAIProvider(config.apiKey, config.model);
      case 'claude':
        return new ClaudeProvider(config.apiKey, config.model);
      case 'gemini':
        return new GeminiProvider(config.apiKey, config.model);
      default:
        throw new Error(`Unknown provider: ${provider}`);
    }
  }
}

// 사용 예시
const provider = LLMProviderFactory.create('openai', {
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-4'
});

2. 프롬프트 엔지니어링 시스템

템플릿 기반 프롬프트 관리

// 프롬프트 템플릿 시스템
class PromptTemplate {
  private template: string;
  private variables: Set<string>;
  
  constructor(template: string) {
    this.template = template;
    this.variables = new Set(
      template.match(/\{\{(\w+)\}\}/g)?.map(match => 
        match.replace(/\{\{|\}\}/g, '')
      ) || []
    );
  }
  
  render(data: Record<string, any>): string {
    let result = this.template;
    
    for (const variable of this.variables) {
      if (!(variable in data)) {
        throw new Error(`Missing template variable: ${variable}`);
      }
      
      const regex = new RegExp(`\\{\\{${variable}\\}\\}`, 'g');
      result = result.replace(regex, String(data[variable]));
    }
    
    return result;
  }
  
  getVariables(): string[] {
    return Array.from(this.variables);
  }
}

// 프롬프트 라이브러리
class PromptLibrary {
  private templates: Map<string, PromptTemplate> = new Map();
  
  register(name: string, template: string): void {
    this.templates.set(name, new PromptTemplate(template));
  }
  
  render(name: string, data: Record<string, any>): string {
    const template = this.templates.get(name);
    if (!template) {
      throw new Error(`Template not found: ${name}`);
    }
    return template.render(data);
  }
  
  load(templates: Record<string, string>): void {
    for (const [name, template] of Object.entries(templates)) {
      this.register(name, template);
    }
  }
}

// 사용 예시
const promptLib = new PromptLibrary();
promptLib.load({
  summarize: `
다음 텍스트를 {{length}} 단어로 요약해주세요:

텍스트: {{content}}

요약:`,
  
  translate: `
다음 텍스트를 {{target_language}}로 번역해주세요:

원문: {{content}}

번역:`,
  
  codeReview: `
다음 {{language}} 코드를 리뷰하고 개선 사항을 제안해주세요:

\`\`\`{{language}}
{{code}}
\`\`\`

리뷰:
1. 코드 품질:
2. 성능 개선:
3. 보안 고려사항:
4. 개선된 코드:
`
});

3. 대화 컨텍스트 관리

세션 기반 컨텍스트 시스템

// 메시지 인터페이스
interface Message {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp: Date;
  metadata?: Record<string, any>;
}

// 대화 세션 관리
class ConversationSession {
  private messages: Message[] = [];
  private maxMessages: number = 50;
  private maxTokens: number = 4000;
  
  constructor(
    private sessionId: string,
    private tokenCounter: (text: string) => number
  ) {}
  
  addMessage(role: Message['role'], content: string, metadata?: Record<string, any>): Message {
    const message: Message = {
      id: crypto.randomUUID(),
      role,
      content,
      timestamp: new Date(),
      metadata
    };
    
    this.messages.push(message);
    this.trimMessages();
    
    return message;
  }
  
  getMessages(): Message[] {
    return [...this.messages];
  }
  
  getContext(): string {
    return this.messages
      .map(msg => `${msg.role}: ${msg.content}`)
      .join('\n');
  }
  
  private trimMessages(): void {
    // 메시지 수 제한
    if (this.messages.length > this.maxMessages) {
      this.messages = this.messages.slice(-this.maxMessages);
    }
    
    // 토큰 수 제한 (대략적)
    let totalTokens = 0;
    const trimmedMessages: Message[] = [];
    
    for (let i = this.messages.length - 1; i >= 0; i--) {
      const messageTokens = this.tokenCounter(this.messages[i].content);
      if (totalTokens + messageTokens > this.maxTokens) break;
      
      totalTokens += messageTokens;
      trimmedMessages.unshift(this.messages[i]);
    }
    
    this.messages = trimmedMessages;
  }
  
  clear(): void {
    this.messages = [];
  }
}

// 대화 관리자
class ConversationManager {
  private sessions: Map<string, ConversationSession> = new Map();
  
  getSession(sessionId: string): ConversationSession {
    if (!this.sessions.has(sessionId)) {
      this.sessions.set(sessionId, new ConversationSession(
        sessionId,
        (text: string) => Math.ceil(text.length / 4) // 간단한 토큰 추정
      ));
    }
    return this.sessions.get(sessionId)!;
  }
  
  deleteSession(sessionId: string): void {
    this.sessions.delete(sessionId);
  }
  
  getSessions(): string[] {
    return Array.from(this.sessions.keys());
  }
}

4. RAG (Retrieval-Augmented Generation) 시스템

벡터 데이터베이스 통합

// 벡터 저장소 인터페이스
interface VectorStore {
  addDocument(id: string, text: string, metadata?: Record<string, any>): Promise<void>;
  search(query: string, limit?: number): Promise<SearchResult[]>;
  delete(id: string): Promise<void>;
}

interface SearchResult {
  id: string;
  content: string;
  score: number;
  metadata: Record<string, any>;
}

// Pinecone 구현 예시
class PineconeVectorStore implements VectorStore {
  private pinecone: Pinecone;
  private index: Index;
  private embeddings: EmbeddingsProvider;
  
  constructor(apiKey: string, indexName: string, embeddings: EmbeddingsProvider) {
    this.pinecone = new Pinecone({ apiKey });
    this.index = this.pinecone.Index(indexName);
    this.embeddings = embeddings;
  }
  
  async addDocument(id: string, text: string, metadata: Record<string, any> = {}): Promise<void> {
    const embedding = await this.embeddings.embed(text);
    
    await this.index.upsert([{
      id,
      values: embedding,
      metadata: { text, ...metadata }
    }]);
  }
  
  async search(query: string, limit: number = 5): Promise<SearchResult[]> {
    const queryEmbedding = await this.embeddings.embed(query);
    
    const results = await this.index.query({
      vector: queryEmbedding,
      topK: limit,
      includeMetadata: true
    });
    
    return results.matches?.map(match => ({
      id: match.id,
      content: match.metadata?.text as string,
      score: match.score || 0,
      metadata: match.metadata || {}
    })) || [];
  }
  
  async delete(id: string): Promise<void> {
    await this.index.deleteOne(id);
  }
}

// RAG 서비스
class RAGService {
  constructor(
    private vectorStore: VectorStore,
    private llmProvider: LLMProvider
  ) {}
  
  async query(question: string, options: RAGOptions = {}): Promise<string> {
    // 1. 관련 문서 검색
    const searchResults = await this.vectorStore.search(
      question, 
      options.contextLimit || 3
    );
    
    // 2. 컨텍스트 구성
    const context = searchResults
      .map((result, index) => `[문서 ${index + 1}]\n${result.content}`)
      .join('\n\n');
    
    // 3. 프롬프트 생성
    const prompt = `
다음 문서들을 참고하여 질문에 답변해주세요:

${context}

질문: ${question}

답변:`;
    
    // 4. LLM 응답 생성
    return await this.llmProvider.generateText(prompt);
  }
  
  async addKnowledge(documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>): Promise<void> {
    for (const doc of documents) {
      await this.vectorStore.addDocument(doc.id, doc.content, doc.metadata);
    }
  }
}

interface RAGOptions {
  contextLimit?: number;
  threshold?: number;
}

API 서버 구현

Express.js 기반 REST API

import express from 'express';
import rateLimit from 'express-rate-limit';
import { v4 as uuidv4 } from 'uuid';

const app = express();

// 미들웨어 설정
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Rate Limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 100, // 요청 제한
  message: { error: 'Too many requests from this IP' }
});
app.use('/api/', limiter);

// 서비스 인스턴스
const llmProvider = LLMProviderFactory.create('openai', {
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-4'
});

const conversationManager = new ConversationManager();
const promptLibrary = new PromptLibrary();

// 기본 채팅 API
app.post('/api/chat', async (req, res) => {
  try {
    const { message, sessionId = uuidv4() } = req.body;
    
    if (!message) {
      return res.status(400).json({ error: 'Message is required' });
    }
    
    // 세션 가져오기
    const session = conversationManager.getSession(sessionId);
    
    // 사용자 메시지 추가
    session.addMessage('user', message);
    
    // LLM 응답 생성
    const context = session.getContext();
    const response = await llmProvider.generateText(context);
    
    // 어시스턴트 응답 추가
    session.addMessage('assistant', response);
    
    res.json({
      sessionId,
      response,
      messageCount: session.getMessages().length
    });
    
  } catch (error) {
    console.error('Chat error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 스트리밍 채팅 API
app.post('/api/chat/stream', async (req, res) => {
  try {
    const { message, sessionId = uuidv4() } = req.body;
    
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'Access-Control-Allow-Origin': '*'
    });
    
    const session = conversationManager.getSession(sessionId);
    session.addMessage('user', message);
    
    let fullResponse = '';
    
    for await (const chunk of llmProvider.generateStream(session.getContext())) {
      fullResponse += chunk;
      res.write(`data: ${JSON.stringify({ chunk, sessionId })}\n\n`);
    }
    
    session.addMessage('assistant', fullResponse);
    res.write(`data: ${JSON.stringify({ done: true, sessionId })}\n\n`);
    res.end();
    
  } catch (error) {
    console.error('Stream error:', error);
    res.write(`data: ${JSON.stringify({ error: 'Stream failed' })}\n\n`);
    res.end();
  }
});

// 특정 작업 API (요약, 번역 등)
app.post('/api/tasks/:taskType', async (req, res) => {
  try {
    const { taskType } = req.params;
    const { content, ...params } = req.body;
    
    if (!promptLibrary.hasTemplate(taskType)) {
      return res.status(400).json({ error: 'Unknown task type' });
    }
    
    const prompt = promptLibrary.render(taskType, { content, ...params });
    const result = await llmProvider.generateText(prompt);
    
    res.json({ result });
    
  } catch (error) {
    console.error('Task error:', error);
    res.status(500).json({ error: 'Task failed' });
  }
});

// 대화 이력 관리
app.get('/api/sessions/:sessionId/messages', (req, res) => {
  const { sessionId } = req.params;
  const session = conversationManager.getSession(sessionId);
  res.json({ messages: session.getMessages() });
});

app.delete('/api/sessions/:sessionId', (req, res) => {
  const { sessionId } = req.params;
  conversationManager.deleteSession(sessionId);
  res.json({ success: true });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`LLM service running on port ${PORT}`);
});

성능 최적화 전략

1. 응답 캐싱

import Redis from 'redis';
import crypto from 'crypto';

class ResponseCache {
  private redis: Redis;
  private ttl: number = 3600; // 1시간
  
  constructor(redisUrl: string) {
    this.redis = Redis.createClient({ url: redisUrl });
  }
  
  private getCacheKey(prompt: string, model: string): string {
    const hash = crypto.createHash('sha256').update(prompt + model).digest('hex');
    return `llm:${hash}`;
  }
  
  async get(prompt: string, model: string): Promise<string | null> {
    const key = this.getCacheKey(prompt, model);
    return await this.redis.get(key);
  }
  
  async set(prompt: string, model: string, response: string): Promise<void> {
    const key = this.getCacheKey(prompt, model);
    await this.redis.setex(key, this.ttl, response);
  }
}

// 캐시를 활용한 LLM 래퍼
class CachedLLMProvider implements LLMProvider {
  constructor(
    private provider: LLMProvider,
    private cache: ResponseCache,
    private model: string
  ) {}
  
  async generateText(prompt: string, options?: GenerationOptions): Promise<string> {
    // 캐시 확인
    const cached = await this.cache.get(prompt, this.model);
    if (cached) return cached;
    
    // LLM 호출
    const response = await this.provider.generateText(prompt, options);
    
    // 캐시 저장
    await this.cache.set(prompt, this.model, response);
    
    return response;
  }
}

2. 요청 큐잉 시스템

import Bull from 'bull';

interface LLMJob {
  prompt: string;
  options?: GenerationOptions;
  sessionId: string;
  requestId: string;
}

class LLMJobQueue {
  private queue: Bull.Queue<LLMJob>;
  
  constructor(redisUrl: string) {
    this.queue = new Bull('llm-processing', redisUrl);
    this.setupProcessor();
  }
  
  async addJob(data: LLMJob, priority: number = 0): Promise<Bull.Job<LLMJob>> {
    return await this.queue.add(data, {
      priority,
      attempts: 3,
      backoff: 'exponential',
      removeOnComplete: 100,
      removeOnFail: 50
    });
  }
  
  private setupProcessor(): void {
    this.queue.process('*', async (job) => {
      const { prompt, options, sessionId } = job.data;
      
      try {
        const response = await llmProvider.generateText(prompt, options);
        
        // 결과를 세션에 저장하거나 웹소켓으로 전송
        await this.notifyResult(sessionId, response);
        
        return response;
      } catch (error) {
        throw new Error(`LLM processing failed: ${error}`);
      }
    });
  }
  
  private async notifyResult(sessionId: string, response: string): Promise<void> {
    // WebSocket이나 Server-Sent Events로 결과 전송
    // 구현은 사용하는 실시간 통신 방식에 따라 다름
  }
}

모니터링 및 로깅

성능 메트릭 수집

import prometheus from 'prom-client';

// Prometheus 메트릭 정의
const requestDuration = new prometheus.Histogram({
  name: 'llm_request_duration_seconds',
  help: 'LLM request duration in seconds',
  labelNames: ['provider', 'model', 'task_type']
});

const requestCount = new prometheus.Counter({
  name: 'llm_requests_total',
  help: 'Total number of LLM requests',
  labelNames: ['provider', 'model', 'status']
});

const tokenUsage = new prometheus.Counter({
  name: 'llm_tokens_total',
  help: 'Total number of tokens processed',
  labelNames: ['provider', 'model', 'type']
});

// 메트릭 수집 래퍼
class MonitoredLLMProvider implements LLMProvider {
  constructor(
    private provider: LLMProvider,
    private providerName: string,
    private modelName: string
  ) {}
  
  async generateText(prompt: string, options?: GenerationOptions): Promise<string> {
    const startTime = Date.now();
    
    try {
      const response = await this.provider.generateText(prompt, options);
      
      // 메트릭 기록
      const duration = (Date.now() - startTime) / 1000;
      requestDuration
        .labels(this.providerName, this.modelName, 'text_generation')
        .observe(duration);
      
      requestCount
        .labels(this.providerName, this.modelName, 'success')
        .inc();
      
      const promptTokens = this.provider.getTokenCount(prompt);
      const responseTokens = this.provider.getTokenCount(response);
      
      tokenUsage
        .labels(this.providerName, this.modelName, 'prompt')
        .inc(promptTokens);
      
      tokenUsage
        .labels(this.providerName, this.modelName, 'completion')
        .inc(responseTokens);
      
      return response;
      
    } catch (error) {
      requestCount
        .labels(this.providerName, this.modelName, 'error')
        .inc();
      
      throw error;
    }
  }
}

// 메트릭 엔드포인트
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', prometheus.register.contentType);
  const metrics = await prometheus.register.metrics();
  res.send(metrics);
});

보안 고려사항

API 키 관리 및 사용자 인증

import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

// 사용자 인증 미들웨어
const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET!, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid token' });
    }
    req.user = user;
    next();
  });
};

// 사용량 제한
const usageLimiter = async (req: Request, res: Response, next: NextFunction) => {
  const userId = req.user.id;
  const today = new Date().toISOString().split('T')[0];
  const key = `usage:${userId}:${today}`;
  
  const currentUsage = await redis.get(key) || 0;
  const limit = req.user.plan === 'pro' ? 10000 : 1000;
  
  if (parseInt(currentUsage) >= limit) {
    return res.status(429).json({ error: 'Daily limit exceeded' });
  }
  
  next();
};

// 입력 검증
const validateInput = (req: Request, res: Response, next: NextFunction) => {
  const { message } = req.body;
  
  if (!message || typeof message !== 'string') {
    return res.status(400).json({ error: 'Valid message required' });
  }
  
  if (message.length > 10000) {
    return res.status(400).json({ error: 'Message too long' });
  }
  
  // 악의적 프롬프트 필터링
  if (containsMaliciousContent(message)) {
    return res.status(400).json({ error: 'Invalid content detected' });
  }
  
  next();
};

function containsMaliciousContent(text: string): boolean {
  // 간단한 필터링 로직 - 실제로는 더 정교한 검증 필요
  const maliciousPatterns = [
    /ignore\s+previous\s+instructions/i,
    /system\s*:/i,
    /<script>/i
  ];
  
  return maliciousPatterns.some(pattern => pattern.test(text));
}

배포 및 확장

Docker 컨테이너화

FROM node:18-alpine

WORKDIR /app

# 종속성 설치
COPY package*.json ./
RUN npm ci --only=production

# 애플리케이션 코드 복사
COPY dist ./dist

# 헬스체크
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:${PORT:-3000}/health || exit 1

EXPOSE 3000

USER node

CMD ["node", "dist/index.js"]

Kubernetes 배포 설정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: llm-service
  template:
    metadata:
      labels:
        app: llm-service
    spec:
      containers:
      - name: llm-service
        image: llm-service:latest
        ports:
        - containerPort: 3000
        env:
        - name: OPENAI_API_KEY
          valueFrom:
            secretKeyRef:
              name: llm-secrets
              key: openai-api-key
        - name: REDIS_URL
          value: "redis://redis-service:6379"
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "1Gi"
            cpu: "1000m"
        readinessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: llm-service
spec:
  selector:
    app: llm-service
  ports:
  - port: 80
    targetPort: 3000
  type: LoadBalancer

비용 최적화 전략

스마트 모델 라우팅

class SmartLLMRouter {
  private providers: Map<string, LLMProvider> = new Map();
  private costs: Map<string, number> = new Map(); // 토큰당 비용
  
  constructor() {
    // 프로바이더별 비용 설정 (토큰당 USD)
    this.costs.set('gpt-3.5-turbo', 0.002);
    this.costs.set('gpt-4', 0.03);
    this.costs.set('claude-3-haiku', 0.00025);
    this.costs.set('claude-3-sonnet', 0.003);
  }
  
  async generateText(prompt: string, requirements: TaskRequirements): Promise<string> {
    const bestModel = this.selectBestModel(prompt, requirements);
    const provider = this.providers.get(bestModel);
    
    if (!provider) {
      throw new Error(`Provider not available: ${bestModel}`);
    }
    
    return await provider.generateText(prompt);
  }
  
  private selectBestModel(prompt: string, requirements: TaskRequirements): string {
    const promptLength = prompt.length;
    
    // 간단한 작업은 저비용 모델 사용
    if (requirements.complexity === 'simple' && promptLength < 1000) {
      return 'claude-3-haiku';
    }
    
    // 복잡한 작업은 고성능 모델 사용
    if (requirements.complexity === 'complex' || requirements.accuracy === 'high') {
      return 'gpt-4';
    }
    
    // 기본값
    return 'gpt-3.5-turbo';
  }
}

interface TaskRequirements {
  complexity: 'simple' | 'medium' | 'complex';
  accuracy: 'low' | 'medium' | 'high';
  speed: 'low' | 'medium' | 'high';
}

결론

텍스트 위주 LLM 서비스 개발에는 다음과 같은 핵심 요소들이 필요합니다:

  1. 모듈화된 아키텍처: 다양한 LLM 프로바이더와 확장 가능한 시스템 설계
  2. 효율적인 컨텍스트 관리: 대화 이력과 토큰 최적화
  3. 프롬프트 엔지니어링: 재사용 가능한 템플릿 시스템
  4. 성능 최적화: 캐싱, 큐잉, 스마트 라우팅
  5. 보안 및 모니터링: 사용자 인증, 입력 검증, 메트릭 수집
  6. 비용 관리: 스마트 모델 선택과 사용량 추적

이러한 요소들을 종합적으로 고려하여 구현하면, 안정적이고 확장 가능한 LLM 서비스를 구축할 수 있습니다.