멀티 에이전트 오케스트레이터
기능 설명
다중 에이전트 협업 시스템을 설계하고 관리합니다.
아키텍처 패턴
┌─────────────┐
│ Orchestrator │ ← 작업 분해, 조정
└──────┬──────┘
│
┌───┼───┐
▼ ▼ ▼
┌───┐┌───┐┌───┐
│ A ││ B ││ C │ ← 전문 에이전트
└───┘└───┘└───┘
핵심 구현
1. 에이전트 기본 클래스
interface AgentConfig {
name: string;
role: string;
capabilities: string[];
llm: LLMConfig;
tools: Tool[];
instructions: string;
}
class BaseAgent {
protected config: AgentConfig;
protected memory: AgentMemory;
constructor(config: AgentConfig) {
this.config = config;
this.memory = new AgentMemory(config.name);
}
async think(task: Task): Promise<Response> {
const context = await this.memory.buildContext(task.description);
const prompt = this.buildPrompt(task, context);
const response = await this.callLLM(prompt);
await this.memory.add({ type: 'semantic', content: task.description + ' -> ' + response.content, importance: 8 });
return response;
}
protected buildPrompt(task: Task, context: string): Message[] {
return [
{ role: 'system', content: this.config.instructions },
{ role: 'system', content: context },
{ role: 'user', content: task.description }
];
}
protected async callLLM(messages: Message[]): Promise<Response> {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
body: JSON.stringify({ model: this.config.llm.model, messages, tools: this.config.tools.map(t => t.definition) })
});
return res.json();
}
}
2. 오케스트레이터
interface TaskResult {
agentId: string;
status: 'pending' | 'running' | 'done' | 'failed';
output?: string;
dependencies: string[];
startTime?: number;
endTime?: number;
}
class Orchestrator {
private agents: Map<string, BaseAgent> = new Map();
private taskGraph: DAG<Task>;
constructor(private llmRouter: LLMRouter) {}
registerAgent(agent: BaseAgent) {
this.agents.set(agent.config.name, agent);
}
async execute(goal: string): Promise<string> {
// 1. 작업 분해
const plan = await this.decompose(goal);
// 2. DAG 구축
this.taskGraph = this.buildDAG(plan);
// 3. 실행 스케줄링
const results = await this.schedule();
// 4. 결과 취합
return this.summarize(goal, results);
}
private async decompose(goal: string): Promise<Task[]> {
const response = await this.llmRouter.route({
prompt: `다음 목표를 실행 가능한 하위 작업으로 분해하고 JSON 배열로 반환하세요:
목표: ${goal}
요구사항:
- 각 하위 작업은 하나의 에이전트만 담당합니다.
- 작업 간 의존 관계를 명시합니다.
- 반환 형식: [{"id":"t1","description":"...","agent":"researcher","depends":[]},...]`,
system: '당신은 작업 분해 전문가입니다.'
});
return JSON.parse(response.content);
}
private async schedule(): Promise<Map<string, TaskResult>> {
const results = new Map<string, TaskResult>();
const pending = new Set(this.taskGraph.nodes);
const running: Promise<void>[] = [];
const maxConcurrent = 3;
while (pending.size > 0 || running.length > 0) {
// 병렬 실행 가능한 작업 시작
while (running.length < maxConcurrent) {
const next = this.findNextRunnable(pending, results);
if (!next) break;
pending.delete(next.id);
const p = this.runTask(next, results).catch(console.error);
running.push(p);
}
// 하나가 완료될 때까지 대기
await Promise.race(running);
running.splice(running.findIndex(p => false), 1);
}
return results;
}
private async runTask(task: Task, results: Map<string, TaskResult>) {
results.set(task.id, { agentId: task.agent, status: 'running', dependencies: task.depends || [], startTime: Date.now() });
try {
// 의존 작업 완료 대기
for (const depId of task.depends || []) {
const dep = results.get(depId);
if (dep?.status !== 'done') {
await this.waitFor(depId, results);
}
}
const agent = this.agents.get(task.agent);
const context = this.buildContext(task, results);
const response = await agent.think({ id: task.id, description: task.description, context });
results.set(task.id, { ...results.get(task.id)!, status: 'done', output: response.content, endTime: Date.now() });
} catch (error) {
results.set(task.id, { ...results.get(task.id)!, status: 'failed', output: String(error), endTime: Date.now() });
}
}
private buildContext(task: Task, results: Map<string, TaskResult>): string {
return (task.depends || []).map(depId => {
const dep = results.get(depId);
return dep?.output || '';
}).join('\n\n');
}
}
3. 메시지 버스
class MessageBus {
private subscriptions = new Map<string, Subscriber[]>();
publish(channel: string, message: Message) {
const subs = this.subscriptions.get(channel) || [];
for (const sub of subs) {
sub.handler(message);
}
}
subscribe(channel: string, handler: (msg: Message) => void): () => void {
if (!this.subscriptions.has(channel)) {
this.subscriptions.set(channel, []);
}
const sub = { id: crypto.randomUUID(), handler };
this.subscriptions.get(channel)!.push(sub);
return () => this.unsubscribe(channel, sub.id);
}
unsubscribe(channel: string, subId: string) {
const subs = this.subscriptions.get(channel) || [];
const idx = subs.findIndex(s => s.id === subId);
if (idx >= 0) subs.splice(idx, 1);
}
}
// 메시지 유형
interface Message {
id: string;
type: 'request' | 'response' | 'broadcast' | 'event';
from: string;
to?: string;
content: any;
timestamp: number;
}
4. 상태 머신
type AgentState = 'idle' | 'thinking' | 'waiting' | 'acting' | 'error';
interface AgentSession {
id: string;
agentId: string;
state: AgentState;
currentTask?: string;
history: Turn[];
sharedContext: Record<string, any>;
}
class StateManager {
private sessions = new Map<string, AgentSession>();
transition(sessionId: string, newState: AgentState) {
const session = this.sessions.get(sessionId);
if (!session) return;
const oldState = session.state;
session.state = newState;
// 상태 전환 훅
this.onTransition(sessionId, oldState, newState);
}
// 상태 전환 규칙
private canTransition(from: AgentState, to: AgentState): boolean {
const rules: Record<AgentState, AgentState[]> = {
idle: ['thinking'],
thinking: ['waiting', 'acting', 'error', 'idle'],
waiting: ['thinking', 'error', 'idle'],
acting: ['thinking', 'error', 'idle'],
error: ['idle', 'thinking']
};
return rules[from]?.includes(to) || false;
}
}
통신 패턴
| 패턴 | 설명 | 적용 사례 |
|---|---|---|
| 브로드캐스트 | 모든 에이전트가 수신 | 전역 알림 |
| 점대점 | 지정된 에이전트가 수신 | 작업 할당 |
| 발행/구독 | 주제별 배포 | 이벤트 기반 |
| 블랙보드 | 공유 지식 공간 | 협업 추론 |
일반적인 패턴
역할극 (Role Play)
class RolePlayOrchestrator extends Orchestrator {
async execute(goal: string) {
// 역할 할당
const planner = this.getAgent('planner');
const executor = this.getAgent('executor');
const critic = this.getAgent('critic');
const plan = await planner.think({ description: goal });
const result = await executor.think({ description: plan.output, context: '' });
const review = await critic.think({ description: result.output, context: '' });
return review.output;
}
}
토론 (Debate)
async debate(topic: string, rounds = 3) {
const pro = this.getAgent('pro');
const con = this.getAgent('con');
const judge = this.getAgent('judge');
let context = '';
for (let i = 0; i < rounds; i++) {
const proArg = await pro.think({ description: `찬성 측 주장 (${i+1}라운드): ${topic}`, context });
context += `\n찬성: ${proArg.output}`;
const conArg = await con.think({ description: `반대 측 주장 (${i+1}라운드): ${topic}`, context });
context += `\n반대: ${conArg.output}`;
}
return judge.think({ description: `판결: ${topic}`, context });
}
모범 사례
- 단일 책임: 각 에이전트는 명확한 전문 영역을 가져야 합니다.
- 느슨한 결합: 메시지 버스를 통해 통신하여 직접적인 의존성을 피하세요.
- 타임아웃 제어: 에이전트가 멈추는 것을 방지합니다.
- 차단 메커니즘: 실패 횟수가 너무 많으면 자동으로 성능을 저하시킵니다.
- 관측 가능성: 완전한 로그와 추적을 제공합니다.
사용법
- 스킬을 설치하세요.
- 필요에 따라 설정하세요.
- OpenClaw로 실행하세요.