콘텐츠로 이동

아키텍처 설계

Agent Harness는 단순한 API 래퍼가 아닙니다. 프로덕션 환경에서 에이전트를 안정적으로 운영하려면 관심사의 명확한 분리 와 교체 가능한 컴포넌트 구조 가 필수입니다. 잘못 설계된 harness는 모델을 교체하거나 툴을 추가할 때마다 전체 코드를 수정해야 하는 상황을 만듭니다.

동일한 모델을 사용해도 컨텍스트 구성, 도구, 검증 루프 같은 harness 변수를 바꾸면 벤치마크 결과가 달라질 수 있습니다. 따라서 아키텍처 결정은 일반적인 향상률을 가정하기보다, 모델과 과제 표본을 고정한 대조 실험으로 검증해야 합니다.

┌─────────────────────────────────────────────────────┐
│ Entry & UI Layer │
│ TUI / Web UI │ ConfigManager │ SessionManager │
│ │ ApprovalManager │
├─────────────────────────────────────────────────────┤
│ Agent Core Layer │
│ MainAgent (parameterized) │ SubAgentSpec │
│ AgentDependencies │ SubAgentDeps │
├─────────────────────────────────────────────────────┤
│ Tool & Context Layer │
│ ToolRegistry │ PromptComposer │ MCPClient │
│ ContextCompactor │ EventReminder │
├─────────────────────────────────────────────────────┤
│ Persistence Layer │
│ SessionStore │ ConfigResolver │ OperationLog │
│ ProviderCache │ GitRollback │
└─────────────────────────────────────────────────────┘

각 계층은 아래 계층에만 의존 하며, 위 계층은 인터페이스를 통해 접근합니다. 이 단방향 의존성이 테스트 용이성과 교체 가능성을 보장합니다.

| 계층 | 핵심 책임 | 주요 컴포넌트 | |------|-----------|---------------| | Entry & UI | 부트스트랩, 사용자 입출력, 승인 요청 | ConfigManager, SessionManager, ApprovalManager | | Agent Core | 에이전트 생명주기, 서브에이전트 조율 | MainAgent, SubAgentSpec, Dependency Injection | | Tool & Context | 툴 실행, 프롬프트 조합, 컨텍스트 관리 | ToolRegistry, PromptComposer, MCP Client | | Persistence | 상태 저장, 설정 해석, 롤백 | SessionStore, ConfigResolver, OperationLog |

1. 관심사 분리 (Separation of Concerns)

섹션 제목: “1. 관심사 분리 (Separation of Concerns)”

각 계층은 하나의 책임만 집니다. UI 계층은 렌더링만, Core 계층은 에이전트 로직만 담당합니다. 이 원칙을 어기면 UI가 바뀔 때 에이전트 로직도 함께 수정해야 하는 결합이 발생합니다.

# 나쁜 예: UI 계층에 에이전트 로직 혼재
async function handleUserInput(input: string) {
renderSpinner()
const response = await llm.complete(buildPrompt(input)) // ← 잘못된 위치
renderResponse(response)
}
# 좋은 예: 계층 경계 유지
async function handleUserInput(input: string) {
renderSpinner()
const response = await agentCore.run(input) // ← 위임
renderResponse(response)
}

2. 의존성 주입 (Dependency Injection)

섹션 제목: “2. 의존성 주입 (Dependency Injection)”

MainAgent 는 구체적인 구현 대신 인터페이스 를 받습니다. 이를 통해 테스트에서 mock을, 프로덕션에서 실제 구현을 주입할 수 있습니다.

interface AgentDependencies {
llmClient: LLMClient;
toolRegistry: ToolRegistry;
sessionStore: SessionStore;
promptComposer: PromptComposer;
}
class MainAgent {
constructor(private deps: AgentDependencies) {}
}

3. 단일 구체 클래스 (Single Concrete Class)

섹션 제목: “3. 단일 구체 클래스 (Single Concrete Class)”

상속 계층 대신 생성 파라미터로 행동을 변화 시킵니다. PlannerAgent, ExecutorAgent 같은 별도 클래스를 만드는 대신, MainAgent 에 SubAgentSpec 을 전달해 역할을 정의합니다.

이 패턴의 장점은 모든 에이전트가 동일한 실행 경로를 공유하므로, 버그 수정이 전체에 즉시 반영된다는 점입니다.

부트스트랩 시점에 공유 매니저들을 초기화합니다.

// 부트스트랩 진입점
async function bootstrap(config: AppConfig) {
const configManager = new ConfigManager(config);
const sessionManager = new SessionManager(configManager);
const approvalManager = new ApprovalManager(configManager);
const ui = config.mode === 'tui'
? new TUIAdapter(approvalManager)
: new WebUIAdapter(approvalManager);
return new Application({ configManager, sessionManager, approvalManager, ui });
}

UICallback 인터페이스를 통해 TUI와 Web UI가 동일한 계약을 구현합니다. Agent Core는 구체적인 UI 타입을 알 필요가 없습니다.

설정은 우선순위 계층 을 통해 해석됩니다.

프로젝트 로컬 (.agent/config.json)
↓ 없으면
사용자 글로벌 (~/.config/agent/config.json)
↓ 없으면
환경 변수 (AGENT_MODEL, AGENT_TOKEN 등)
↓ 없으면
내장 기본값

이 계층적 해석은 로컬 오버라이드를 허용하면서도 글로벌 기본값을 유지하게 해줍니다. CI/CD 환경에서는 환경 변수로, 개발자 로컬에서는 프로젝트 설정으로 제어할 수 있습니다.

아키텍처를 설계할 때 각 계층의 구현 기술을 선택하는 기준이 있습니다. 단순히 인기 있는 기술을 고르는 것이 아니라, 계층의 특성과 요구사항에 맞는 선택이 필요합니다.

| 계층 | 고려 기준 | 추천 방향 | |------|-----------|-----------| | Entry & UI | 렌더링 성능, 플랫폼 지원 범위 | TUI는 ink/blessed, Web은 React/Vue | | Agent Core | 타입 안전성, 비동기 처리 | TypeScript + async/await 패턴 | | Tool & Context | 확장성, 플러그인 지원 | 인터페이스 기반 등록 패턴 | | Persistence | 내구성, 조회 성능 | SQLite(로컬), PostgreSQL(팀) |

LLM 클라이언트는 Agent Core의 핵심 의존성입니다. 다음 기준으로 평가합니다.

1. 스트리밍 지원 여부 — 긴 응답에서 UX에 직접 영향
2. 툴 호출(tool_use) 네이티브 지원 — 파싱 오버헤드 감소
3. 재시도 및 백오프 내장 — 안정성 확보
4. 멀티모달 지원 — 이미지/문서 처리 필요 시
5. 비용 추적 API — 토큰 사용량 모니터링

단일 에이전트 harness가 팀 환경으로 성장할 때 가장 먼저 병목이 되는 지점이 있습니다.

단일 프로세스 (초기)
MainAgent ─── ToolRegistry
멀티 워커 (팀 규모)
LoadBalancer
├── Worker 1: MainAgent ─── ToolRegistry (shared)
├── Worker 2: MainAgent ─── ToolRegistry (shared)
└── Worker 3: MainAgent ─── ToolRegistry (shared)
↓
Persistence Layer (공유 DB)

수평 확장 시 상태를 Persistence 계층에 완전히 위임 해야 합니다. Agent Core가 인메모리 상태를 갖고 있으면 워커 간 세션이 공유되지 않습니다.

프로덕션에서는 런타임에 툴을 추가하거나 비활성화해야 하는 경우가 생깁니다.

class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool): void {
this.tools.set(tool.name, tool);
}
unregister(name: string): void {
this.tools.delete(name);
}
getAvailableTools(context: AgentContext): Tool[] {
// 컨텍스트에 따라 사용 가능한 툴 필터링
return Array.from(this.tools.values())
.filter(tool => tool.isAvailable(context));
}
}

이 패턴은 특정 에이전트에게만 특정 툴을 노출하거나, 위험한 툴을 사용자 승인 없이는 실행 불가능하게 만드는 데 사용합니다.

긴 대화가 이어지면 컨텍스트 윈도우가 포화됩니다. Persistence 계층과 Tool & Context 계층이 협력해 이를 해결합니다.

대화 길이가 임계치 초과
↓
ContextCompactor 활성화
↓
최근 N개 메시지 + 중요 요약 유지
↓
나머지는 SessionStore에 아카이브
↓
필요 시 검색(retrieve)으로 복원

컨텍스트 압축 전략은 에이전트의 작업 특성에 따라 다릅니다. 코드 작성 에이전트는 최근 파일 변경 이력을, 연구 에이전트는 중간 결론을 우선 유지해야 합니다.

계층 간 데이터 흐름은 이벤트 기반 또는 직접 호출 두 가지 패턴으로 구현할 수 있습니다.

| 패턴 | 장점 | 단점 | 적합한 경우 | |------|------|------|-------------| | 직접 호출 | 단순하고 디버깅 쉬움 | 강한 결합 | 단일 프로세스, 소규모 | | 이벤트 버스 | 느슨한 결합, 확장 용이 | 흐름 추적 어려움 | 멀티 에이전트, 대규모 | | 메시지 큐 | 비동기 처리, 백프레셔 | 운영 복잡도 증가 | 고부하, 분산 환경 |

초기에는 직접 호출로 시작하고, 에이전트 수가 늘어나면 이벤트 버스로 전환하는 것이 일반적인 진화 경로입니다.