초안 "에이전트 설계 패턴 - 예외 처리"
TypeScript, LangChain, LangGraph 및 Vercel의 서버리스 플랫폼을 사용하여 오류를 우아하게 처리하고, 장애에서 복구하며, 프로덕션에서 신뢰성을 유지하는 내결함성 AI 에이전트를 구축하는 기술을 마스터하세요.
멘탈 모델: 적응형 응급 대응 시스템
에이전트의 예외 처리를 병원의 응급 대응 시스템처럼 생각해보세요. 병원이 각 심각도 수준에 따라 다른 프로토콜을 가지고 있는 것처럼(경미한 부상 → 간호사, 중등도 → 의사, 위독 → 전체 외상 팀), 에이전트는 계층화된 오류 응답이 필요합니다. 네트워크 오류는 공급 지연과 같고(대기 후 재시도), API 장애는 장비 오작동과 같으며(백업 장비 사용), 시스템 충돌은 정전과 같습니다(비상 발전기 가동 및 관리자 알림). 시스템은 문제에 반응하는 것뿐만 아니라 그로부터 학습하고, 프로토콜을 적응시키고, 서비스 연속성을 유지합니다. 당신의 에이전트도 마찬가지로 문제를 조기에 감지하고, 적절하게 대응하고, 우아하게 복구하고, 각 사건에서 개선해야 합니다.
기본 예제: 오류 경계를 가진 견고한 에이전트
1. 오류 계층 및 복구 전략 정의
// lib/errors/exception-types.ts
import { z } from 'zod';
import { isError, isString } from 'es-toolkit';
export const ErrorLevelSchema = z.enum(['transient', 'recoverable', 'critical']);
export type ErrorLevel = z.infer<typeof ErrorLevelSchema>;
export const RecoveryStrategySchema = z.enum([
'retry',
'fallback',
'cache',
'degrade',
'escalate',
'abort'
]);
export type RecoveryStrategy = z.infer<typeof RecoveryStrategySchema>;
export interface ErrorContext {
timestamp: Date;
attempt: number;
maxAttempts: number;
strategy: RecoveryStrategy;
metadata?: Record<string, any>;
}
export class AgentException extends Error {
constructor(
message: string,
public level: ErrorLevel,
public strategy: RecoveryStrategy,
public context?: ErrorContext
) {
super(message);
this.name = 'AgentException';
}
static fromError(error: unknown, level: ErrorLevel = 'recoverable'): AgentException {
if (error instanceof AgentException) return error;
const message = isError(error) ? error.message :
isString(error) ? error :
'Unknown error occurred';
return new AgentException(message, level, 'retry');
}
}
export class ToolException extends AgentException {
constructor(
public toolName: string,
message: string,
strategy: RecoveryStrategy = 'fallback'
) {
super(`Tool [${toolName}]: ${message}`, 'recoverable', strategy);
this.name = 'ToolException';
}
}
export class ValidationException extends AgentException {
constructor(
message: string,
public validationErrors?: z.ZodIssue[]
) {
super(message, 'transient', 'retry');
this.name = 'ValidationException';
}
}
지능적인 오류 처리를 위한 명확한 복구 전략과 컨텍스트 정보를 가진 구조화된 오류 계층을 생성합니다.
2. 오류 복구 관리자 구축
// lib/recovery/recovery-manager.ts
import { retry, delay, throttle } from 'es-toolkit';
import {
AgentException,
ErrorLevel,
RecoveryStrategy,
ErrorContext
} from '@/lib/errors/exception-types';
interface RecoveryConfig {
maxRetries: number;
baseDelay: number;
maxDelay: number;
timeout: number;
fallbackHandlers: Map<string, () => Promise<any>>;
}
export class RecoveryManager {
private errorHistory: AgentException[] = [];
private config: RecoveryConfig;
constructor(config: Partial<RecoveryConfig> = {}) {
this.config = {
maxRetries: config.maxRetries ?? 3,
baseDelay: config.baseDelay ?? 1000,
maxDelay: config.maxDelay ?? 30000,
timeout: config.timeout ?? 777000, // Vercel 제한
fallbackHandlers: config.fallbackHandlers ?? new Map()
};
}
async executeWithRecovery<T>(
operation: () => Promise<T>,
operationName: string
): Promise<T> {
const startTime = Date.now();
let lastError: AgentException | null = null;
for (let attempt = 1; attempt <= this.config.maxRetries; attempt++) {
try {
// 타임아웃 확인
if (Date.now() - startTime > this.config.timeout) {
throw new AgentException(
'Operation timeout exceeded',
'critical',
'abort'
);
}
// 작업 실행
const result = await Promise.race([
operation(),
this.createTimeout(this.config.timeout - (Date.now() - startTime))
]);
// 성공 시 오류 기록 지우기
if (attempt > 1) {
console.log(`Recovery successful for ${operationName} on attempt ${attempt}`);
}
return result;
} catch (error) {
lastError = AgentException.fromError(error);
lastError.context = {
timestamp: new Date(),
attempt,
maxAttempts: this.config.maxRetries,
strategy: this.determineStrategy(lastError, attempt)
};
this.errorHistory.push(lastError);
// 복구 전략 적용
const recovered = await this.applyStrategy(
lastError,
operationName,
attempt
);
if (recovered !== null) {
return recovered;
}
// 백오프 지연 계산
if (attempt < this.config.maxRetries) {
const delayMs = Math.min(
this.config.baseDelay * Math.pow(2, attempt - 1),
this.config.maxDelay
);
await delay(delayMs);
}
}
}
throw lastError || new AgentException(
`${operationName} failed after ${this.config.maxRetries} attempts`,
'critical',
'escalate'
);
}
private determineStrategy(
error: AgentException,
attempt: number
): RecoveryStrategy {
// 중요 오류는 즉시 에스컬레이션
if (error.level === 'critical') return 'escalate';
// 일시적 오류는 먼저 재시도
if (error.level === 'transient' && attempt < this.config.maxRetries) {
return 'retry';
}
// 복구 가능한 오류는 폴백 시도
if (error.level === 'recoverable') {
return 'fallback';
}
// 기본값은 서비스 저하
return 'degrade';
}
private async applyStrategy<T>(
error: AgentException,
operationName: string,
attempt: number
): Promise<T | null> {
const strategy = error.context?.strategy || error.strategy;
switch (strategy) {
case 'fallback':
const fallback = this.config.fallbackHandlers.get(operationName);
if (fallback) {
console.log(`Applying fallback for ${operationName}`);
return await fallback();
}
break;
case 'cache':
// 사용 가능한 경우 캐시된 결과 반환
console.log(`Would return cached result for ${operationName}`);
break;
case 'degrade':
console.log(`Degrading service for ${operationName}`);
return null;
case 'escalate':
console.error(`Escalating error for ${operationName}:`, error.message);
throw error;
case 'abort':
console.error(`Aborting ${operationName}`);
throw error;
}
return null;
}
private createTimeout(ms: number): Promise<never> {
return new Promise((_, reject) =>
setTimeout(() => reject(new AgentException(
'Operation timeout',
'transient',
'retry'
)), ms)
);
}
getErrorHistory(): AgentException[] {
return [...this.errorHistory];
}
clearHistory(): void {
this.errorHistory = [];
}
}
오류 유형에 따라 적절한 복구 전략을 결정하고 적용하는 정교한 복구 관리자를 구현합니다.
3. 도구가 있는 오류 인식 에이전트 생성
// lib/agents/error-aware-agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';
import { RecoveryManager } from '@/lib/recovery/recovery-manager';
import { ToolException, ValidationException } from '@/lib/errors/exception-types';
import { pipe, map, filter, reduce } from 'es-toolkit';
// 오류를 처리하는 안전한 도구 래퍼
export function createSafeTool(
name: string,
description: string,
schema: z.ZodSchema,
implementation: (input: any) => Promise<any>,
fallback?: () => Promise<any>
) {
return new DynamicStructuredTool({
name,
description,
schema,
func: async (input) => {
const recoveryManager = new RecoveryManager({
maxRetries: 2,
fallbackHandlers: fallback ?
new Map([[name, fallback]]) :
new Map()
});
try {
// 입력 유효성 검사
const validated = schema.safeParse(input);
if (!validated.success) {
throw new ValidationException(
'Invalid tool input',
validated.error.issues
);
}
// 복구와 함께 실행
return await recoveryManager.executeWithRecovery(
() => implementation(validated.data),
name
);
} catch (error) {
console.error(`Tool ${name} failed:`, error);
throw new ToolException(name,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
});
}
// 내장 오류 처리 기능이 있는 도구 예제
export function createResilientTools() {
const weatherTool = createSafeTool(
'get_weather',
'Get current weather for a location',
z.object({
location: z.string().min(1),
units: z.enum(['celsius', 'fahrenheit']).default('celsius')
}),
async (input) => {
// 가끔 실패 시뮬레이션
if (Math.random() < 0.3) {
throw new Error('Weather API unavailable');
}
return {
location: input.location,
temperature: 22,
units: input.units,
conditions: 'Partly cloudy'
};
},
async () => ({
location: 'Unknown',
temperature: 20,
units: 'celsius',
conditions: 'Data unavailable',
source: 'fallback'
})
);
const calculatorTool = createSafeTool(
'calculator',
'Perform mathematical calculations',
z.object({
expression: z.string(),
precision: z.number().int().min(0).max(10).default(2)
}),
async (input) => {
try {
// Function 생성자를 사용한 안전한 평가
const result = new Function('return ' + input.expression)();
return {
expression: input.expression,
result: Number(result.toFixed(input.precision))
};
} catch {
throw new Error('Invalid mathematical expression');
}
}
);
const searchTool = createSafeTool(
'web_search',
'Search the web for information',
z.object({
query: z.string().min(1).max(200),
maxResults: z.number().int().min(1).max(10).default(5)
}),
async (input) => {
// 실패 가능성이 있는 검색 시뮬레이션
if (Math.random() < 0.2) {
throw new Error('Search service timeout');
}
return {
query: input.query,
results: Array.from({ length: input.maxResults }, (_, i) => ({
title: `Result ${i + 1} for "${input.query}"`,
snippet: `Sample content for result ${i + 1}`,
url: `https://example.com/result-${i + 1}`
}))
};
},
async () => ({
query: 'fallback',
results: [],
error: 'Search unavailable, please try again later'
})
);
return [weatherTool, calculatorTool, searchTool];
}
신뢰할 수 있는 실행을 위한 유효성 검사, 복구 메커니즘 및 폴백 핸들러가 있는 오류 인식 도구를 생성합니다.
4. 오류 경계가 있는 에이전트 API 구현
// app/api/error-aware-agent/route.ts
import { NextResponse } from 'next/server';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { createResilientTools } from '@/lib/agents/error-aware-agent';
import { RecoveryManager } from '@/lib/recovery/recovery-manager';
import { AgentException } from '@/lib/errors/exception-types';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { HumanMessage } from '@langchain/core/messages';
export const runtime = 'nodejs';
export const maxDuration = 300;
export async function POST(req: Request) {
const recoveryManager = new RecoveryManager();
try {
const { message } = await req.json();
if (!message || typeof message !== 'string') {
throw new AgentException(
'Invalid request: message is required',
'transient',
'retry'
);
}
// 복구와 함께 에이전트 실행
const result = await recoveryManager.executeWithRecovery(
async () => {
const model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-flash',
temperature: 0.3,
maxOutputTokens: 2048
});
const tools = createResilientTools();
const agent = createReactAgent({
llm: model,
tools,
messageModifier: `당신은 오류 복구 기능을 갖춘 도움이 되는 어시스턴트입니다.
도구가 실패하면 우아하게 인정하고 가능한 경우 대안을 시도하세요.
도구를 사용할 수 없는 경우에도 항상 유용한 응답을 제공하세요.`
});
const response = await agent.invoke({
messages: [new HumanMessage(message)]
});
return response.messages[response.messages.length - 1].content;
},
'agent-execution'
);
const errorHistory = recoveryManager.getErrorHistory();
return NextResponse.json({
success: true,
result,
recoveryAttempts: errorHistory.length,
errors: errorHistory.map(e => ({
message: e.message,
level: e.level,
strategy: e.strategy,
attempt: e.context?.attempt
}))
});
} catch (error) {
const agentError = AgentException.fromError(error, 'critical');
console.error('Agent execution failed:', agentError);
return NextResponse.json(
{
success: false,
error: agentError.message,
level: agentError.level,
strategy: agentError.strategy,
errorHistory: recoveryManager.getErrorHistory()
},
{ status: agentError.level === 'critical' ? 500 : 503 }
);
}
}
상세한 오류 추적 및 복구 시도를 포함하는 포괄적인 오류 경계를 구현하는 API 라우트.
5. 오류 피드백이 있는 프론트엔드 생성
// components/ErrorAwareChat.tsx
'use client';
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { pipe, groupBy, map as mapUtil } from 'es-toolkit';
interface ChatResponse {
success: boolean;
result?: string;
error?: string;
level?: string;
recoveryAttempts?: number;
errors?: Array<{
message: string;
level: string;
strategy: string;
attempt?: number;
}>;
}
export default function ErrorAwareChat() {
const [message, setMessage] = useState('');
const [chatHistory, setChatHistory] = useState<Array<{
role: 'user' | 'assistant' | 'error';
content: string;
metadata?: any;
}>>([]);
const sendMessage = useMutation<ChatResponse, Error, string>({
mutationFn: async (message: string) => {
const response = await fetch('/api/error-aware-agent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
const data = await response.json();
if (!response.ok && !data.success) {
throw new Error(data.error || 'Request failed');
}
return data;
},
onSuccess: (data) => {
if (data.success) {
setChatHistory(prev => [
...prev,
{ role: 'assistant', content: data.result!, metadata: data }
]);
}
},
onError: (error) => {
setChatHistory(prev => [
...prev,
{
role: 'error',
content: `오류: ${error.message}`,
metadata: { timestamp: new Date() }
}
]);
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim()) return;
setChatHistory(prev => [...prev, { role: 'user', content: message }]);
sendMessage.mutate(message);
setMessage('');
};
const getRecoveryBadge = (attempts?: number) => {
if (!attempts) return null;
const badgeClass = attempts > 2 ? 'badge-warning' : 'badge-info';
return (
<div className={`badge ${badgeClass} badge-sm`}>
{attempts} 복구 시도
</div>
);
};
return (
<div className="card w-full bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">오류 인식 AI 어시스턴트</h2>
{/* 채팅 기록 */}
<div className="h-96 overflow-y-auto space-y-2 p-4 bg-base-200 rounded-lg">
{chatHistory.map((msg, idx) => (
<div
key={idx}
className={`chat ${msg.role === 'user' ? 'chat-end' : 'chat-start'}`}
>
<div className="chat-header">
{msg.role === 'user' ? '당신' :
msg.role === 'assistant' ? '어시스턴트' : '시스템'}
</div>
<div className={`chat-bubble ${
msg.role === 'error' ? 'chat-bubble-error' :
msg.role === 'user' ? 'chat-bubble-primary' :
'chat-bubble-secondary'
}`}>
{msg.content}
{msg.metadata?.recoveryAttempts && (
<div className="mt-2">
{getRecoveryBadge(msg.metadata.recoveryAttempts)}
</div>
)}
</div>
{msg.metadata?.errors && msg.metadata.errors.length > 0 && (
<div className="chat-footer opacity-50 text-xs">
복구됨: {msg.metadata.errors[0].strategy}
</div>
)}
</div>
))}
{sendMessage.isPending && (
<div className="chat chat-start">
<div className="chat-bubble chat-bubble-secondary">
<span className="loading loading-dots loading-sm"></span>
</div>
</div>
)}
</div>
{/* 입력 폼 */}
<form onSubmit={handleSubmit} className="join w-full mt-4">
<input
type="text"
className="input input-bordered join-item flex-1"
placeholder="무엇이든 물어보세요..."
value={message}
onChange={(e) => setMessage(e.target.value)}
disabled={sendMessage.isPending}
/>
<button
type="submit"
className="btn btn-primary join-item"
disabled={sendMessage.isPending || !message.trim()}
>
전송
</button>
</form>
{/* 오류 상태 */}
{sendMessage.isError && (
<div className="alert alert-error mt-2">
<span>메시지 전송에 실패했습니다. 다시 시도해주세요.</span>
</div>
)}
</div>
</div>
);
}
오류 복구 시도에 대한 시각적 피드백과 우아한 오류 표시를 제공하는 React 컴포넌트.
고급 예제: 자가 수정 멀티 에이전트 시스템
1. 에이전트 계층에서 오류 전파 구현
// lib/agents/error-propagation.ts
import { EventEmitter } from 'events';
import { z } from 'zod';
import { throttle, debounce } from 'es-toolkit';
export interface ErrorEvent {
agentId: string;
parentId?: string;
error: Error;
timestamp: Date;
handled: boolean;
propagated: boolean;
}
export class ErrorPropagationManager extends EventEmitter {
private errorChain: Map<string, ErrorEvent[]> = new Map();
private handlers: Map<string, (error: ErrorEvent) => Promise<boolean>> = new Map();
constructor() {
super();
this.setupErrorHandling();
}
private setupErrorHandling() {
// 과도한 오류 방출을 방지하기 위해 스로틀
const throttledEmit = throttle((event: ErrorEvent) => {
this.emit('error', event);
}, 1000);
this.on('error', throttledEmit);
}
registerAgent(
agentId: string,
parentId?: string,
errorHandler?: (error: ErrorEvent) => Promise<boolean>
) {
this.errorChain.set(agentId, []);
if (errorHandler) {
this.handlers.set(agentId, errorHandler);
}
if (parentId) {
// 오류 전파 체인 설정
this.on(`error:${agentId}`, async (event: ErrorEvent) => {
event.propagated = true;
// 먼저 로컬 핸들러 시도
const handled = await this.tryHandleError(agentId, event);
if (!handled) {
// 상위로 전파
this.emit(`error:${parentId}`, {
...event,
parentId: agentId
});
}
});
}
}
async reportError(agentId: string, error: Error): Promise<boolean> {
const event: ErrorEvent = {
agentId,
error,
timestamp: new Date(),
handled: false,
propagated: false
};
// 오류 체인에 저장
const chain = this.errorChain.get(agentId) || [];
chain.push(event);
this.errorChain.set(agentId, chain);
// 처리를 위해 방출
this.emit(`error:${agentId}`, event);
// 처리 대기
return new Promise((resolve) => {
setTimeout(() => {
resolve(event.handled);
}, 100);
});
}
private async tryHandleError(
agentId: string,
event: ErrorEvent
): Promise<boolean> {
const handler = this.handlers.get(agentId);
if (handler) {
try {
event.handled = await handler(event);
return event.handled;
} catch (handlerError) {
console.error(`Handler for ${agentId} failed:`, handlerError);
return false;
}
}
return false;
}
getErrorChain(agentId: string): ErrorEvent[] {
return this.errorChain.get(agentId) || [];
}
clearErrorChain(agentId?: string) {
if (agentId) {
this.errorChain.delete(agentId);
} else {
this.errorChain.clear();
}
}
}
로컬 처리 및 상위 에스컬레이션을 통해 에이전트 계층 구조를 통한 오류 전파를 관리합니다.
2. 검증을 사용한 자가 수정 워크플로우 구축
// lib/workflows/self-correcting-workflow.ts
import { StateGraph, END } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage, HumanMessage, SystemMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { ErrorPropagationManager, ErrorEvent } from '@/lib/agents/error-propagation';
import { pipe, chunk, map, filter } from 'es-toolkit';
// 출력 검증 스키마
const DataExtractionSchema = z.object({
entities: z.array(z.string()),
relationships: z.array(z.object({
source: z.string(),
target: z.string(),
type: z.string()
})),
metadata: z.record(z.any())
});
const AnalysisResultSchema = z.object({
insights: z.array(z.string()),
confidence: z.number().min(0).max(1),
recommendations: z.array(z.string())
});
interface WorkflowState {
messages: BaseMessage[];
stage: string;
extractedData?: z.infer<typeof DataExtractionSchema>;
analysisResult?: z.infer<typeof AnalysisResultSchema>;
validationErrors: string[];
correctionAttempts: number;
finalOutput?: string;
}
export class SelfCorrectingWorkflow {
private model: ChatGoogleGenerativeAI;
private errorManager: ErrorPropagationManager;
private maxCorrectionAttempts = 3;
constructor() {
this.model = new ChatGoogleGenerativeAI({
modelName: 'gemini-2.5-pro',
temperature: 0.2,
maxOutputTokens: 4096
});
this.errorManager = new ErrorPropagationManager();
this.setupErrorHandlers();
}
private setupErrorHandlers() {
// 오류 핸들러로 에이전트 등록
this.errorManager.registerAgent('extraction', undefined, async (event) => {
console.log('Extraction error:', event.error.message);
return false; // 전파하도록 함
});
this.errorManager.registerAgent('validation', 'extraction', async (event) => {
console.log('Validation error, attempting correction');
return true; // 로컬에서 처리
});
this.errorManager.registerAgent('analysis', 'validation');
this.errorManager.registerAgent('synthesis', 'analysis');
}
createWorkflow() {
const workflow = new StateGraph<WorkflowState>({
channels: {
messages: {
value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
default: () => []
},
stage: {
value: (x: string, y: string) => y || x,
default: () => 'extraction'
},
extractedData: {
value: (x: any, y: any) => y || x,
default: () => undefined
},
analysisResult: {
value: (x: any, y: any) => y || x,
default: () => undefined
},
validationErrors: {
value: (x: string[], y: string[]) => [...x, ...y],
default: () => []
},
correctionAttempts: {
value: (x: number, y: number) => y ?? x,
default: () => 0
},
finalOutput: {
value: (x: string, y: string) => y || x,
default: () => undefined
}
}
});
// 구조화된 출력이 있는 추출 노드
workflow.addNode('extraction', async (state) => {
try {
const prompt = `다음 텍스트에서 엔티티와 관계를 추출하세요.
다음 구조의 JSON을 반환하세요:
{
"entities": ["entity1", "entity2"],
"relationships": [
{"source": "entity1", "target": "entity2", "type": "relation_type"}
],
"metadata": {}
}
텍스트: ${state.messages[0].content}`;
const response = await this.model.invoke([
new SystemMessage('당신은 데이터 추출 전문가입니다. 항상 유효한 JSON을 반환하세요.'),
new HumanMessage(prompt)
]);
// JSON 파싱 및 검증
const jsonStr = response.content.toString()
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '');
const parsed = JSON.parse(jsonStr);
const validated = DataExtractionSchema.parse(parsed);
return {
extractedData: validated,
stage: 'validation'
};
} catch (error) {
await this.errorManager.reportError('extraction', error as Error);
return {
stage: 'correction',
validationErrors: [`추출 실패: ${error}`]
};
}
});
// 검증 노드
workflow.addNode('validation', async (state) => {
if (!state.extractedData) {
return {
stage: 'correction',
validationErrors: ['검증할 데이터가 없습니다']
};
}
const errors: string[] = [];
// 추출된 데이터 품질 검증
if (state.extractedData.entities.length === 0) {
errors.push('추출된 엔티티가 없습니다');
}
if (state.extractedData.relationships.length === 0 &&
state.extractedData.entities.length > 1) {
errors.push('여러 엔티티가 있지만 정의된 관계가 없습니다');
}
// 고아 관계 확인
const entities = new Set(state.extractedData.entities);
for (const rel of state.extractedData.relationships) {
if (!entities.has(rel.source) || !entities.has(rel.target)) {
errors.push(`관계가 알 수 없는 엔티티를 참조합니다: ${rel.source} -> ${rel.target}`);
}
}
if (errors.length > 0) {
return {
stage: 'correction',
validationErrors: errors
};
}
return { stage: 'analysis' };
});
// 자가 수정 노드
workflow.addNode('correction', async (state) => {
if (state.correctionAttempts >= this.maxCorrectionAttempts) {
return {
stage: 'failure',
finalOutput: `${this.maxCorrectionAttempts}번의 수정 시도 후 실패. 오류: ${state.validationErrors.join('; ')}`
};
}
const correctionPrompt = `이전 추출에 다음 오류가 있었습니다:
${state.validationErrors.join('\n')}
이 텍스트에 대한 추출을 수정하세요:
${state.messages[0].content}
모든 검증 오류를 해결하세요.`;
try {
const response = await this.model.invoke([
new SystemMessage('당신은 데이터 추출 전문가입니다. 오류를 수정하고 유효한 JSON을 반환하세요.'),
new HumanMessage(correctionPrompt)
]);
const jsonStr = response.content.toString()
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '');
const parsed = JSON.parse(jsonStr);
const validated = DataExtractionSchema.parse(parsed);
return {
extractedData: validated,
stage: 'validation',
correctionAttempts: state.correctionAttempts + 1,
validationErrors: [] // 오류 지우기
};
} catch (error) {
return {
stage: 'correction',
correctionAttempts: state.correctionAttempts + 1,
validationErrors: [...state.validationErrors, `수정 실패: ${error}`]
};
}
});
// 분석 노드
workflow.addNode('analysis', async (state) => {
if (!state.extractedData) {
return {
stage: 'failure',
finalOutput: '분석할 데이터가 없습니다'
};
}
try {
const analysisPrompt = `다음 추출된 데이터를 분석하고 인사이트를 제공하세요:
엔티티: ${state.extractedData.entities.join(', ')}
관계: ${JSON.stringify(state.extractedData.relationships)}
JSON 형식으로 분석을 제공하세요:
{
"insights": ["insight1", "insight2"],
"confidence": 0.0-1.0,
"recommendations": ["rec1", "rec2"]
}`;
const response = await this.model.invoke([
new SystemMessage('당신은 데이터 분석가입니다. 사려 깊은 인사이트를 제공하세요.'),
new HumanMessage(analysisPrompt)
]);
const jsonStr = response.content.toString()
.replace(/```json\n?/g, '')
.replace(/```\n?/g, '');
const parsed = JSON.parse(jsonStr);
const validated = AnalysisResultSchema.parse(parsed);
return {
analysisResult: validated,
stage: 'synthesis'
};
} catch (error) {
await this.errorManager.reportError('analysis', error as Error);
// 우아하게 저하
return {
analysisResult: {
insights: ['분석이 부분적으로 완료되었습니다'],
confidence: 0.3,
recommendations: ['수동 검토를 권장합니다']
},
stage: 'synthesis'
};
}
});
// 종합 노드
workflow.addNode('synthesis', async (state) => {
const report = `## 분석 보고서
### 추출된 데이터
- **발견된 엔티티**: ${state.extractedData?.entities.length || 0}
- **식별된 관계**: ${state.extractedData?.relationships.length || 0}
### 주요 인사이트
${state.analysisResult?.insights.map(i => `- ${i}`).join('\n') || '인사이트 없음'}
### 신뢰도 수준
${(state.analysisResult?.confidence || 0) * 100}%
### 권장 사항
${state.analysisResult?.recommendations.map(r => `- ${r}`).join('\n') || '권장 사항 없음'}
### 데이터 품질
- 발생한 검증 오류: ${state.validationErrors.length}
- 수정 시도: ${state.correctionAttempts}
- 최종 상태: ${state.validationErrors.length === 0 ? '성공' : '부분 성공'}`;
return {
finalOutput: report,
stage: 'complete'
};
});
// 엣지 정의
workflow.addConditionalEdges('extraction', [
{ condition: (s) => s.stage === 'validation', node: 'validation' },
{ condition: (s) => s.stage === 'correction', node: 'correction' }
]);
workflow.addConditionalEdges('validation', [
{ condition: (s) => s.stage === 'analysis', node: 'analysis' },
{ condition: (s) => s.stage === 'correction', node: 'correction' }
]);
workflow.addConditionalEdges('correction', [
{ condition: (s) => s.stage === 'validation', node: 'validation' },
{ condition: (s) => s.stage === 'failure', node: 'synthesis' }
]);
workflow.addEdge('analysis', 'synthesis');
workflow.addEdge('synthesis', END);
workflow.setEntryPoint('extraction');
return workflow.compile();
}
async execute(input: string): Promise<{
success: boolean;
output: string;
metrics: {
correctionAttempts: number;
validationErrors: string[];
errorChains: Map<string, ErrorEvent[]>;
};
}> {
const workflow = this.createWorkflow();
const result = await workflow.invoke({
messages: [new HumanMessage(input)],
stage: 'extraction',
validationErrors: [],
correctionAttempts: 0
});
return {
success: result.validationErrors.length === 0,
output: result.finalOutput || '처리 실패',
metrics: {
correctionAttempts: result.correctionAttempts,
validationErrors: result.validationErrors,
errorChains: this.errorManager['errorChain']
}
};
}
}
출력을 검증하고 오류가 감지되면 자동으로 수정을 시도하는 자가 수정 워크플로우를 구현합니다.
3. 모니터링 대시보드 API 생성
// app/api/self-correcting/route.ts
import { NextResponse } from 'next/server';
import { SelfCorrectingWorkflow } from '@/lib/workflows/self-correcting-workflow';
import { z } from 'zod';
export const runtime = 'nodejs';
export const maxDuration = 300;
const RequestSchema = z.object({
text: z.string().min(10).max(5000),
enableMonitoring: z.boolean().default(true)
});
export async function POST(req: Request) {
const encoder = new TextEncoder();
const stream = new TransformStream();
const writer = stream.writable.getWriter();
(async () => {
try {
const body = await req.json();
const validation = RequestSchema.safeParse(body);
if (!validation.success) {
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'error',
message: '잘못된 요청',
errors: validation.error.issues
})}\n\n`)
);
await writer.close();
return;
}
const { text, enableMonitoring } = validation.data;
// 초기 확인
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'start',
message: '자가 수정 워크플로우 시작'
})}\n\n`)
);
const workflow = new SelfCorrectingWorkflow();
// 워크플로우 실행
const startTime = Date.now();
const result = await workflow.execute(text);
const executionTime = Date.now() - startTime;
// 진행 상황 업데이트 스트리밍
if (result.metrics.correctionAttempts > 0) {
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'correction',
attempts: result.metrics.correctionAttempts,
errors: result.metrics.validationErrors
})}\n\n`)
);
}
// 모니터링이 활성화된 경우 오류 체인 스트리밍
if (enableMonitoring && result.metrics.errorChains.size > 0) {
const errorSummary = Array.from(result.metrics.errorChains.entries())
.map(([agent, events]) => ({
agent,
errorCount: events.length,
handled: events.filter(e => e.handled).length
}));
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'monitoring',
errorSummary
})}\n\n`)
);
}
// 최종 결과
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'complete',
success: result.success,
output: result.output,
executionTime,
metrics: {
correctionAttempts: result.metrics.correctionAttempts,
errorCount: result.metrics.validationErrors.length
}
})}\n\n`)
);
} catch (error) {
console.error('Workflow execution error:', error);
await writer.write(
encoder.encode(`data: ${JSON.stringify({
type: 'critical_error',
message: error instanceof Error ? error.message : '알 수 없는 오류'
})}\n\n`)
);
} finally {
await writer.close();
}
})();
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
워크플로우 실행 및 오류 수정에 대한 실시간 업데이트를 제공하는 스트리밍 API 엔드포인트.
4. 대화형 모니터링 대시보드 구축
// components/SelfCorrectingDashboard.tsx
'use client';
import { useState, useEffect } from 'react';
import { useMutation } from '@tanstack/react-query';
import { pipe, groupBy, map as mapUtil, reduce } from 'es-toolkit';
interface WorkflowEvent {
type: 'start' | 'correction' | 'monitoring' | 'complete' | 'error' | 'critical_error';
message?: string;
attempts?: number;
errors?: string[];
errorSummary?: Array<{
agent: string;
errorCount: number;
handled: number;
}>;
output?: string;
success?: boolean;
executionTime?: number;
metrics?: {
correctionAttempts: number;
errorCount: number;
};
}
export default function SelfCorrectingDashboard() {
const [inputText, setInputText] = useState('');
const [events, setEvents] = useState<WorkflowEvent[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [enableMonitoring, setEnableMonitoring] = useState(true);
const [result, setResult] = useState<string | null>(null);
const processWorkflow = useMutation({
mutationFn: async (params: { text: string; enableMonitoring: boolean }) => {
setEvents([]);
setResult(null);
setIsProcessing(true);
const response = await fetch('/api/self-correcting', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),
});
if (!response.ok) {
throw new Error('워크플로우 실패');
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const event: WorkflowEvent = JSON.parse(line.slice(6));
setEvents(prev => [...prev, event]);
if (event.type === 'complete' && event.output) {
setResult(event.output);
}
} catch (e) {
console.error('Failed to parse event:', e);
}
}
}
}
},
onSettled: () => {
setIsProcessing(false);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (inputText.trim().length >= 10) {
processWorkflow.mutate({ text: inputText, enableMonitoring });
}
};
const getCorrectionStats = () => {
const correctionEvents = events.filter(e => e.type === 'correction');
if (correctionEvents.length === 0) return null;
const lastCorrection = correctionEvents[correctionEvents.length - 1];
return {
attempts: lastCorrection.attempts || 0,
errors: lastCorrection.errors || []
};
};
const getExecutionMetrics = () => {
const completeEvent = events.find(e => e.type === 'complete');
if (!completeEvent) return null;
return {
time: completeEvent.executionTime,
success: completeEvent.success,
corrections: completeEvent.metrics?.correctionAttempts || 0,
errors: completeEvent.metrics?.errorCount || 0
};
};
return (
<div className="container mx-auto p-4 space-y-4">
{/* 헤더 */}
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h1 className="card-title text-2xl">자가 수정 워크플로우 대시보드</h1>
<p className="text-base-content/70">
AI가 실시간으로 오류를 자동 감지하고 수정하는 과정을 확인하세요
</p>
</div>
</div>
{/* 입력 폼 */}
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<form onSubmit={handleSubmit} className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">입력 텍스트 (최소 10자)</span>
</label>
<textarea
className="textarea textarea-bordered h-32"
placeholder="추출 및 분석을 위한 텍스트를 입력하세요..."
value={inputText}
onChange={(e) => setInputText(e.target.value)}
disabled={isProcessing}
/>
</div>
<div className="form-control">
<label className="label cursor-pointer">
<span className="label-text">오류 모니터링 활성화</span>
<input
type="checkbox"
className="toggle toggle-primary"
checked={enableMonitoring}
onChange={(e) => setEnableMonitoring(e.target.checked)}
/>
</label>
</div>
<button
type="submit"
className="btn btn-primary w-full"
disabled={isProcessing || inputText.trim().length < 10}
>
{isProcessing ? (
<>
<span className="loading loading-spinner"></span>
워크플로우 처리 중...
</>
) : '자가 수정 워크플로우 실행'}
</button>
</form>
</div>
</div>
{/* 실행 타임라인 */}
{events.length > 0 && (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">실행 타임라인</h2>
<ul className="timeline timeline-vertical">
{events.map((event, idx) => (
<li key={idx}>
{idx > 0 && <hr />}
<div className="timeline-start">{idx + 1}</div>
<div className="timeline-middle">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={`w-5 h-5 ${
event.type === 'error' || event.type === 'critical_error' ? 'text-error' :
event.type === 'correction' ? 'text-warning' :
event.type === 'complete' ? 'text-success' :
'text-primary'
}`}>
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clipRule="evenodd" />
</svg>
</div>
<div className="timeline-end timeline-box">
<div className="font-semibold capitalize">{event.type}</div>
{event.message && (
<p className="text-sm opacity-80">{event.message}</p>
)}
{event.attempts && (
<div className="badge badge-warning badge-sm mt-1">
{event.attempts} 수정 시도
</div>
)}
</div>
{idx < events.length - 1 && <hr />}
</li>
))}
</ul>
</div>
</div>
)}
{/* 지표 대시보드 */}
{getExecutionMetrics() && (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">실행 지표</h2>
<div className="stats stats-vertical lg:stats-horizontal shadow">
<div className="stat">
<div className="stat-title">상태</div>
<div className="stat-value text-lg">
{getExecutionMetrics()?.success ? (
<span className="text-success">성공</span>
) : (
<span className="text-warning">부분적</span>
)}
</div>
</div>
<div className="stat">
<div className="stat-title">실행 시간</div>
<div className="stat-value text-lg">
{(getExecutionMetrics()?.time || 0) / 1000}초
</div>
</div>
<div className="stat">
<div className="stat-title">수정</div>
<div className="stat-value text-lg">
{getExecutionMetrics()?.corrections || 0}
</div>
</div>
<div className="stat">
<div className="stat-title">처리된 오류</div>
<div className="stat-value text-lg">
{getExecutionMetrics()?.errors || 0}
</div>
</div>
</div>
</div>
</div>
)}
{/* 결과 표시 */}
{result && (
<div className="card bg-base-100 shadow-xl">
<div className="card-body">
<h2 className="card-title">워크플로우 결과</h2>
<div className="mockup-code">
<pre className="text-sm"><code>{result}</code></pre>
</div>
</div>
</div>
)}
{/* 오류 표시 */}
{processWorkflow.isError && (
<div className="alert alert-error shadow-lg">
<span>워크플로우 실행이 실패했습니다. 다시 시도해주세요.</span>
</div>
)}
</div>
);
}
워크플로우 실행, 오류 수정 및 성능 지표의 실시간 시각화를 제공하는 대화형 대시보드.
결론
에이전트 설계 패턴의 예외 처리는 단순한 try-catch 블록을 넘어섭니다. 이는 장애를 예상하고, 우아하게 복구하며, 오류로부터 학습하는 지능적이고 자가 치유 시스템을 구축하는 것입니다. 여기서 시연된 패턴(오류 경계, 복구 관리자, 자가 수정, 오류 전파)은 프로덕션 준비 에이전트의 기초를 형성합니다. 서버리스 환경에서는 적절한 타임아웃 관리, 상태 지속성 및 우아한 성능 저하가 필수적이라는 것을 기억하세요. 핵심은 오류를 처리할 뿐만 아니라 이를 신뢰성과 사용자 경험을 향상시키는 기회로 활용하는 시스템을 구축하는 것입니다.