251221 텍스트 위주 LLM 서비스 개발 솔루션 가이드
21 Dec 2025
대화형 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 서비스 개발에는 다음과 같은 핵심 요소들이 필요합니다:
- 모듈화된 아키텍처: 다양한 LLM 프로바이더와 확장 가능한 시스템 설계
- 효율적인 컨텍스트 관리: 대화 이력과 토큰 최적화
- 프롬프트 엔지니어링: 재사용 가능한 템플릿 시스템
- 성능 최적화: 캐싱, 큐잉, 스마트 라우팅
- 보안 및 모니터링: 사용자 인증, 입력 검증, 메트릭 수집
- 비용 관리: 스마트 모델 선택과 사용량 추적
이러한 요소들을 종합적으로 고려하여 구현하면, 안정적이고 확장 가능한 LLM 서비스를 구축할 수 있습니다.