아래 세 워크플로에 알맞은 상태 관리 패턴(스크립트 변수, 체크포인트, 외부 저장소)을 고르고 이유를 설명하세요.
레벨 1: 상태 관리 패턴 고르기워크플로 A: 이미지 20장 일괄 압축, 장당 5초, 총 100초
워크플로 B: 머신러닝 모델 학습, 에폭당 10분씩 50에폭, 총 500분(8시간)
워크플로 C: PR 100건 리뷰, 각각 병합 전에 사람의 승인이 필요하고, 전체 과정이 며칠씩 이어질 수 있음
학습 목표:
- 워크플로 상태와 에이전트 컨텍스트 구분하기
- 세 가지 상태 관리 패턴 익히기
- 체크포인트와 복구 이해하기
전제: 레슨 3: 복잡한 작업을 워크플로로 분해하기 | 다음: 레슨 5 >>
완벽한 워크플로를 설계합니다. 10단계, 깔끔한 의존 관계. 그런데 8단계에서 서버가 재시작됩니다. 워크플로가 죽습니다.
다시 돌릴까요? 그러면 앞선 7단계의 작업이 — 어쩌면 30분어치가 — 그대로 버려집니다.
그것이 상태 관리가 없을 때 치르는 대가입니다.
상태 관리는 세 가지 문제를 풉니다.1
상태 관리가 없으면 에이전트는 대화 이력을 통해서만 정보를 전달할 수 있습니다. 대화 이력은 넘치고, 유실되고, 에이전트가 잊어버립니다.
상태 관리가 있으면 워크플로는 분명한 "기억"을 갖습니다. 영속적이고, 조회할 수 있고, 복구할 수 있는 기억입니다.2
이 세 단어는 뒤섞이기 쉬우니 먼저 못을 박아 둡시다.1
상태(state)
컨텍스트(context)
메모리(memory)
예시:
핵심 원칙: 상태는 전역이고, 컨텍스트는 지역입니다.3
언제 쓰나: 프로세스나 머신을 넘어갈 필요가 없는 짧은 워크플로(10분 미만).
장점: 단순하고, 빠르고, 외부 의존이 없습니다.
단점: 프로세스가 죽으면 상태가 사라지고, 복구할 방법이 없습니다.
상태는 어디에 있나요? 함수의 지역 변수(processed, results, errors) 안에 있습니다.
프로세스가 죽으면요? 상태가 전부 사라지고, 맨 처음부터 다시 시작합니다.
무엇이 좋아지나: 상태에 분명한 구조가 생기고, 다른 함수로 넘기기 쉬워지며, 직렬화하기도 쉬워집니다(영속화가 필요할 때).
언제 쓰나: 비용이 큰 작업 뒤에 진행 상황을 저장해야 하는 중간 길이의 워크플로(10~60분).
장점: 죽은 뒤에 가장 최근 체크포인트부터 이어서 실행할 수 있어, 작업을 다시 하지 않아도 됩니다.
단점: 체크포인트 위치와 복구 로직을 직접 설계해야 합니다.1
체크포인트 전략:
언제 쓰나: 오래 도는 워크플로(1시간 초과), 여러 머신에 걸쳐 조율해야 하는 작업, 또는 사람의 승인이 필요한 작업.
장점: 상태가 영속적입니다. 프로세스가 죽든 머신이 재시작하든 상관없고, 일시 중지와 재개를 지원합니다.
단점: 외부 의존(데이터베이스, Redis)이 필요하고 복잡도가 올라갑니다.4
핵심 패턴: 상태 기계2
워크플로의 단계는 곧 상태 기계의 상태입니다.
모든 단계 전이가 외부 저장소에 저장되고, 그 덕분에 워크플로는 어느 단계에서든 이어서 실행할 수 있습니다.
왜 그럴까요? 컨텍스트가 클수록 에이전트가 주의를 빼앗기기 쉬워집니다. 추론 품질은 떨어지고 비용은 올라갑니다.3
왜 그럴까요? 구조화된 컨텍스트는 에이전트가 이해하기도 쉽고, 여러분이 디버깅하기도 쉽습니다.
누적 컨텍스트: 각 단계의 결과가 컨텍스트에 더해져 계속 커집니다.
초기화 컨텍스트: 각 단계에서 컨텍스트를 비우고 필요한 것만 남깁니다.
어느 쪽을 고를까: 대부분의 경우 초기화 컨텍스트를 써서 컨텍스트 폭발을 피합니다. 뒤 단계가 앞의 모든 결과를 정말로 필요로 할 때만(마지막 요약 단계 같은 경우) 누적 컨텍스트를 씁니다.5
좋은 워크플로는 이런 질문에 답할 수 있어야 합니다.
다음 레슨: 레슨 5: 에러 처리와 재시도 전략 — 워크플로가 실패했을 때 곧바로 죽지 않고 우아하게 복구하도록 만드는 법을 배웁니다
MachineLearningMastery: 5 Architectural Patterns for Persistent Memory and State in AI Agents — https://machinelearningmastery.com/5-architectural-patterns-for-persistent-memory-and-state-in-ai-agents/ ↩ ↩2 ↩3
MindStudio: Workflow State vs. Session State — https://www.mindstudio.ai/blog/workflow-state-vs-session-state-ai-agents ↩ ↩2
Chrono Innovation: Architecture for Scalable Agentic AI Workflows — https://www.chronoinnovation.com/resources/agentic-ai-workflows-architecture/ ↩ ↩2
Appamass: State Management Patterns for Reliable AI Agent Workflows — https://appamass.com/en/blog/state-management-patterns-for-reliable-ai-agent-workflows-5yemlru6ui6cacast3l5 ↩
Ranjan Kumar: Building Agents That Remember — https://ranjankumar.in/building-agents-that-remember-state-management-in-multi-agent-ai-systems ↩
워크플로 A: 이미지 20장 일괄 압축, 장당 5초, 총 100초
워크플로 B: 머신러닝 모델 학습, 에폭당 10분씩 50에폭, 총 500분(8시간)
워크플로 C: PR 100건 리뷰, 각각 병합 전에 사람의 승인이 필요하고, 전체 과정이 며칠씩 이어질 수 있음
요구 사항:
// 상태: 워크플로가 알고 있는 모든 것
const workflowState = {
phase: 'testing',
filesProcessed: 47,
totalFiles: 100,
issues: [/* 앞선 단계에서 찾은 모든 이슈 */],
currentBatch: [/* 지금 처리 중인 파일들 */]
};
// 컨텍스트: 이 에이전트를 위한 정보(상태에서 뽑아냄)
const agentContext = {
file: workflowState.currentBatch[0],
previousIssues: workflowState.issues.filter(i => i.severity === 'high')
};
// 에이전트 호출
const result = await agent({
task: '파일 테스트',
context: agentContext // 상태 전체가 아니라 관련 정보만
});
// 상태 갱신
workflowState.filesProcessed++;
workflowState.issues.push(...result.newIssues);
async function simpleWorkflow(files) {
// 상태는 그냥 평범한 JavaScript 변수
let processed = 0;
let results = [];
let errors = [];
for (const file of files) {
try {
const result = await processFile(file);
results.push(result);
processed++;
console.log(`진행: ${processed}/${files.length}`);
} catch (error) {
errors.push({ file, error });
}
}
return { results, errors, total: files.length };
}
async function betterWorkflow(files) {
// 상태를 객체로 정리 — 더 명확하다
const state = {
input: { files, total: files.length },
progress: { current: 0, phase: 'processing' },
output: { results: [], errors: [] },
metadata: { startTime: Date.now() }
};
for (const file of state.input.files) {
try {
const result = await processFile(file);
state.output.results.push(result);
state.progress.current++;
} catch (error) {
state.output.errors.push({ file, error });
}
}
state.progress.phase = 'completed';
state.metadata.endTime = Date.now();
state.metadata.duration = state.metadata.endTime - state.metadata.startTime;
return state;
}
async function workflowWithCheckpoints(tasks) {
const checkpointFile = '.workflow-state.json';
// 이전 상태 복원 시도
let state = await loadCheckpoint(checkpointFile) || {
completed: [],
pending: tasks,
phase: 'processing'
};
console.log(`이어서 실행: ${state.completed.length}/${tasks.length} 완료`);
while (state.pending.length > 0) {
const task = state.pending.shift();
// 작업 실행
const result = await executeTask(task);
state.completed.push({ task, result });
// 체크포인트: 10개 작업마다 저장
if (state.completed.length % 10 === 0) {
await saveCheckpoint(checkpointFile, state);
console.log(`체크포인트: ${state.completed.length}개 작업 완료`);
}
}
state.phase = 'completed';
await saveCheckpoint(checkpointFile, state);
return state;
}
async function saveCheckpoint(file, state) {
await fs.writeFile(file, JSON.stringify(state, null, 2));
}
async function loadCheckpoint(file) {
try {
const data = await fs.readFile(file, 'utf-8');
return JSON.parse(data);
} catch {
return null; // 파일이 없으면 처음부터 시작
}
}
// 상태 저장소 인터페이스
class WorkflowStateStore {
constructor(db) {
this.db = db;
}
async save(workflowId, state) {
await this.db.set(`workflow:${workflowId}`, JSON.stringify(state));
}
async load(workflowId) {
const data = await this.db.get(`workflow:${workflowId}`);
return data ? JSON.parse(data) : null;
}
async delete(workflowId) {
await this.db.del(`workflow:${workflowId}`);
}
}
// 외부 저장소를 쓰는 워크플로
async function persistentWorkflow(workflowId, tasks) {
const store = new WorkflowStateStore(redis);
// 상태 로드(있다면)
let state = await store.load(workflowId) || {
id: workflowId,
phase: 'init',
completed: [],
pending: tasks,
createdAt: Date.now(),
updatedAt: Date.now()
};
console.log(`워크플로 ${workflowId}: 단계 ${state.phase},
진행 ${state.completed.length}/${tasks.length}`);
// 1단계: 작업 처리
if (state.phase === 'init' || state.phase === 'processing') {
state.phase = 'processing';
while (state.pending.length > 0) {
const task = state.pending.shift();
const result = await executeTask(task);
state.completed.push({ task, result });
state.updatedAt = Date.now();
// 작업마다 상태 저장
await store.save(workflowId, state);
}
state.phase = 'awaiting_approval';
await store.save(workflowId, state);
}
// 2단계: 사람의 승인 대기(다른 프로세스나 머신에서 재개될 수 있음)
if (state.phase === 'awaiting_approval') {
console.log('승인 대기 중...');
// 여기서 반환하고, 다른 프로세스가(또는 몇 시간 뒤에) 이어받게 할 수 있다
return { workflowId, status: 'awaiting_approval' };
}
// 3단계: 최종 작업 실행(승인 후)
if (state.phase === 'approved') {
state.phase = 'finalizing';
await store.save(workflowId, state);
await executeFinalAction(state.completed);
state.phase = 'completed';
state.completedAt = Date.now();
await store.save(workflowId, state);
}
return state;
}
// 승인 워크플로
async function approveWorkflow(workflowId) {
const store = new WorkflowStateStore(redis);
const state = await store.load(workflowId);
if (!state) throw new Error('워크플로가 존재하지 않습니다');
if (state.phase !== 'awaiting_approval') {
throw new Error(`승인할 수 없습니다: 현재 단계는 ${state.phase}입니다`);
}
state.phase = 'approved';
state.approvedAt = Date.now();
await store.save(workflowId, state);
// 워크플로 계속 실행
return await persistentWorkflow(workflowId, []);
}
init → processing → awaiting_approval → approved → finalizing → completed ↓ rejected → cancelled// ❌ 나쁨: 에이전트에게 상태 전부를 넘긴다
const result = await agent({
task: '이 파일 분석',
context: workflowState // 파일 100개의 분석 결과, 설정, 로그...
});
// ✓ 좋음: 관련 정보만 준다
const result = await agent({
task: '이 파일 분석',
context: {
file: currentFile,
guidelines: workflowState.config.analysisGuidelines,
similarIssues: workflowState.results
.filter(r => r.file.type === currentFile.type)
.slice(0, 3) // 유사 사례는 최대 3건
}
});
// ❌ 나쁨: 구조 없는 텍스트
const context = `
앞서 47개 파일을 분석해 23개 이슈를 찾았다.
현재 파일은 src/utils.js, 350줄이다.
설정상 SQL 인젝션과 XSS를 검사해야 한다.
`;
// ✓ 좋음: 구조화된 객체
const context = {
progress: { filesAnalyzed: 47, issuesFound: 23 },
currentFile: { path: 'src/utils.js', lines: 350 },
checkTypes: ['sql_injection', 'xss']
};
let context = { task: '코드베이스 리팩터링' };
for (const file of files) {
const result = await agent({ task: '분석', context });
context.results = context.results || [];
context.results.push(result); // 누적
}
// 결국 context에 모든 파일의 결과가 담겨 아주 커질 수 있다
const allResults = [];
for (const file of files) {
const context = {
file,
guidelines: config.guidelines,
exampleIssues: allResults.slice(-3) // 최근 3건만
};
const result = await agent({ task: '분석', context });
allResults.push(result); // 컨텍스트가 아니라 워크플로 상태에 담긴다
}
class ObservableWorkflow {
constructor(name, totalSteps) {
this.state = {
name,
totalSteps,
currentStep: 0,
phase: 'init',
startTime: Date.now(),
errors: [],
results: []
};
}
async executeStep(stepName, fn) {
this.state.currentStep++;
this.state.phase = stepName;
console.log(`[${this.state.name}]
단계 ${this.state.currentStep}/${this.state.totalSteps}:
${stepName}`);
const stepStart = Date.now();
try {
const result = await fn();
this.state.results.push({ stepName, result, duration: Date.now() - stepStart });
return result;
} catch (error) {
this.state.errors.push({ stepName, error: error.message });
throw error;
}
}
getStatus() {
const progress = (this.state.currentStep / this.state.totalSteps) * 100;
const elapsed = Date.now() - this.state.startTime;
const avgStepTime = elapsed / this.state.currentStep;
const remainingSteps = this.state.totalSteps - this.state.currentStep;
const estimatedRemaining = avgStepTime * remainingSteps;
return {
progress: `${progress.toFixed(1)}%`,
currentPhase: this.state.phase,
elapsed: `${(elapsed / 1000).toFixed(1)}s`,
estimatedRemaining: `${(estimatedRemaining / 1000).toFixed(1)}s`,
errors: this.state.errors.length
};
}
}
// 사용법
async function myWorkflow() {
const wf = new ObservableWorkflow('데이터 마이그레이션', 4);
const data = await wf.executeStep('원본 데이터 읽기', async () => {
return await readSourceData();
});
const transformed = await wf.executeStep('형식 변환', async () => {
return await transformData(data);
});
await wf.executeStep('대상 데이터베이스에 쓰기', async () => {
return await writeToTarget(transformed);
});
await wf.executeStep('검증', async () => {
return await validateMigration();
});
console.log('최종 상태:', wf.getStatus());
}