콘텐츠로 이동

아키텍처 설계

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)으로 복원

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

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

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

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