본문으로 바로가기
리옵트 핸드북
리옵트 핸드북
AI 에이전트 오케스트레이션 패턴

개념·설계

오케스트레이션 개념과 지형도핵심 아키텍처 패턴에이전트 설계 원칙

도구·프로토콜

도구(Tool) 설계 패턴MCP와 에이전트 간 프로토콜

조율·상태

통신과 핸드오프상태 관리와 메모리 시스템라우팅과 디스패치

안정성·운영

에러 처리와 복구 전략평가와 테스트프로덕션 운영 패턴

실전

실전 아키텍처 사례프레임워크 비교와 선택

부록

검증 리포트업데이트 내역
핸드북›AI 에이전트 오케스트레이션 패턴›라우팅과 디스패치

라우팅과 디스패치

Classifier 기반 라우팅, semantic routing, 폴백 체인, 동적 에이전트 선택

핵심 요약

  • 오케스트레이션의 첫 번째 실패 지점은 대부분 라우팅이며, 규칙 → 분류기 → semantic fallback 순으로 낮은 비용 필터를 앞단에 배치합니다.
  • 동적 에이전트 선택은 자유 텍스트보다 capability registry(agent_id·capabilities·allowed_tools·cost_profile·status)로 후보를 줄입니다.
  • 모델이 말한 confidence는 그대로 믿을 수 없으므로 precision-recall 기반 calibration으로 threshold를 잡고, 결제 취소 같은 고위험은 0.90+로, FAQ는 낮춰 클래스별로 차등 적용합니다.
  • 배포 후 drift는 accuracy·confidence 분포·fallback 비율·신규 클러스터로 모니터링하고, accuracy 5%p 하락이나 fallback 20% 초과 시 재학습을 트리거합니다.
  • 라우팅 품질은 카테고리별 10~20개의 golden test set(명확 50%·경계 30%·모호 20%)으로 CI에서 regression을 잡고, 사용자 의도보다 시스템 action 기준으로 분류합니다.

오케스트레이션이 가장 먼저 무너지는 곳은 대부분 라우팅입니다. 무엇을 누구에게 보낼지 한 번 잘못 정하면, 뒤이은 agent 설계를 아무리 정교하게 짜도 거의 소용이 없습니다.

라우팅 방식 비교

방식적합한 상황장점주의점
Deterministic rules명시적 조건이 있는 경우설명 가능성 높음규칙 폭증 가능
Classifier routing입력 유형이 다양할 때유연하고 빠름분류 drift 가능
Semantic routing의미 유사도로 적합한 작업을 찾을 때새로운 표현에 강함근거 설명이 약할 수 있음
Policy engine권한, 비용, SLA 조건이 함께 중요할 때운영 통제에 강함설계와 유지 비용 증가

실무에서는 보통 규칙 -> 분류기 -> semantic fallback 순서를 권합니다. 처음부터 전부 LLM에 맡기면 비용도 늘고 설명 가능성도 떨어집니다.

권장 디스패치 파이프라인

이 구조의 핵심은 "한 번의 마법 같은 분류"가 아니라 싼 필터를 앞단에 두는 것입니다.

동적 에이전트 선택

agent를 동적으로 고를 때는 자유 텍스트보다 capability registry가 낫습니다.

필드설명
agent_id고유 식별자
capabilities수행 가능한 작업
allowed_tools접근 가능한 tool 묶음
cost_profile대략적인 비용/지연 특성
locale / domain지원 언어, 도메인
status사용 가능, 점검 중, deprecated

라우터는 이 registry를 보고 후보를 추린 뒤 최종 agent를 고릅니다.

confidence와 fallback

모델이 confidence를 임의 숫자로 내뱉게 두는 것만으로는 부족합니다. 다음 기준과 함께 써야 합니다.

  • 최소 confidence threshold
  • low confidence 시 fallback 경로
  • ambiguity category 기록
  • human triage 조건

예를 들어 billing과 refund를 자주 헷갈린다면, 두 클래스를 같은 worker로 보내고 내부에서 다시 세분화하는 편이 낫습니다.

비용과 SLA를 함께 고려한 디스패치

조건디스패치 전략
대량, 저위험 요청저비용 라우터 + 단순 worker
고위험 write 요청deterministic policy + human approval
긴 컨텍스트가 필요한 요청retrieval 후 전문 worker agent
지연에 민감한 요청semantic search보다 rules 우선

최소 구현 스켈레톤

type RouteResult = {
  target: string
  confidence: number
  reason: string
}

async function dispatchRequest(input: string) {
  if (matchesPolicyRule(input)) return sendTo('policy-gate')

  const route: RouteResult = await classify(input)

  if (route.confidence >= 0.85) return sendTo(route.target)
  if (route.confidence >= 0.6) return sendTo('generalist-review')

  return sendTo('human-triage')
}

중요한 건 classifier를 넣는 것 자체가 아니라 low confidence -> safer path를 코드 경로로 강제하는 것입니다.

잘못된 라우팅을 줄이는 방법

  1. 클래스 정의를 업무 언어로 다시 쓴다.
  2. negative example을 eval 세트에 포함한다.
  3. 애매한 입력을 억지로 분류하지 말고 needs_triage 클래스를 둔다.
  4. 최종 사용자 intent보다 시스템이 취해야 할 action 기준으로 분류한다.

안티패턴

안티패턴문제개선
agent 이름만 보고 자유롭게 선택capability 충돌이 생김registry 기반 선택
항상 가장 강한 모델로 라우팅비용 급증2단 라우팅 도입
confidence가 낮아도 강제 분류오분류 누적needs_triage와 human fallback
분류 기준이 출력 주제 중심실제 action과 맞지 않음action-oriented taxonomy

Confidence Calibration

분류기가 돌려주는 confidence 수치는 그 자체로 믿을 게 못 됩니다. 모델이 "0.92"라고 말해도 실제 정확도가 92%라는 보장은 없습니다. threshold를 제대로 잡으려면 calibration을 거쳐야 합니다.

임계값 설정의 딜레마

threshold 수준현상결과
너무 높음 (0.95+)대부분의 입력이 fallback으로 빠짐human triage 과부하, 응답 지연
너무 낮음 (0.5 이하)확신 없는 분류도 통과오분류 누적, 사용자 불만
적정 (0.7~0.85)precision과 recall의 균형도메인별 튜닝 필요

핵심은 하나의 고정값이 아니라 도메인과 위험도에 따라 다른 threshold를 쓰는 것입니다. 예를 들어 결제 취소 같은 고위험 작업은 threshold를 높이고, 일반 FAQ는 낮춰도 됩니다.

Precision-Recall 기반 Threshold 선택

from sklearn.metrics import precision_recall_curve
import numpy as np

def find_optimal_threshold(
    y_true: list[int],
    y_scores: list[float],
    min_precision: float = 0.90,
) -> float:
    """
    최소 precision을 보장하면서 가장 높은 recall을 달성하는
    threshold를 찾는다.
    """
    precisions, recalls, thresholds = precision_recall_curve(
        y_true, y_scores
    )

    # min_precision 이상인 구간에서 recall이 최대인 threshold
    valid = precisions[:-1] >= min_precision
    if not valid.any():
        return float(thresholds[-1])  # 가장 보수적인 값

    best_idx = np.where(valid)[0][np.argmax(recalls[:-1][valid])]
    return float(thresholds[best_idx])


# 사용 예시
# y_true: 정답 라벨 (1=해당 클래스, 0=아님)
# y_scores: 분류기가 반환한 confidence 값
threshold = find_optimal_threshold(y_true, y_scores, min_precision=0.90)
print(f"최적 threshold: {threshold:.3f}")

클래스별 차등 Threshold

const THRESHOLDS: Record<string, { high: number; low: number }> = {
  'payment-cancel':  { high: 0.90, low: 0.75 },  // 고위험: 높은 기준
  'general-inquiry': { high: 0.70, low: 0.50 },  // 저위험: 낮은 기준
  'account-delete':  { high: 0.92, low: 0.80 },  // 고위험
  'faq':             { high: 0.65, low: 0.40 },  // 저위험
}

function routeWithCalibration(category: string, confidence: number) {
  const t = THRESHOLDS[category] ?? { high: 0.85, low: 0.60 }

  if (confidence >= t.high) return 'direct-dispatch'
  if (confidence >= t.low)  return 'generalist-review'
  return 'human-triage'
}

주의

Calibration은 일회성이 아닙니다. 입력 분포가 바뀌면 threshold도 재조정해야 합니다. 최소 월 1회 eval 세트로 precision-recall 곡선을 다시 확인하세요.

Drift Detection

배포 직후엔 잘 돌던 라우팅이 시간이 지나며 성능이 떨어지는 현상을 drift라고 합니다. 새 유형의 요청이 들어오거나 사용자 표현 패턴이 바뀌거나 제품 기능이 추가되면, 기존 분류 체계가 현실과 어긋나기 시작합니다.

모니터링 지표

지표측정 방법경고 기준 (예시)
라우팅 정확도샘플링 기반 사후 평가주간 accuracy가 5%p 이상 하락
Confidence 분포평균·중앙값·p10 추적평균 confidence가 0.1 이상 하락
Fallback 비율human-triage 또는 needs_triage 비율전체 요청의 20% 초과
신규 클러스터기존 카테고리에 속하지 않는 입력 군집미분류 클러스터 크기가 일일 요청의 5% 초과
오분류 피드백사용자 또는 운영자 보고주간 오분류 보고 건수 증가 추세

Drift 탐지 구현

interface DriftMetrics {
  windowStart: Date
  windowEnd: Date
  accuracy: number
  avgConfidence: number
  fallbackRate: number
  totalRequests: number
}

function detectDrift(
  baseline: DriftMetrics,
  current: DriftMetrics,
): { drifted: boolean; reasons: string[] } {
  const reasons: string[] = []

  const accuracyDrop = baseline.accuracy - current.accuracy
  if (accuracyDrop > 0.05) {
    reasons.push(
      `accuracy ${accuracyDrop.toFixed(2)} 하락 ` +
      `(${baseline.accuracy.toFixed(2)} → ${current.accuracy.toFixed(2)})`
    )
  }

  const confidenceDrop = baseline.avgConfidence - current.avgConfidence
  if (confidenceDrop > 0.1) {
    reasons.push(
      `평균 confidence ${confidenceDrop.toFixed(2)} 하락`
    )
  }

  const fallbackIncrease = current.fallbackRate - baseline.fallbackRate
  if (fallbackIncrease > 0.1) {
    reasons.push(
      `fallback 비율 ${(fallbackIncrease * 100).toFixed(1)}%p 증가`
    )
  }

  return { drifted: reasons.length > 0, reasons }
}

자동 재학습 트리거 조건

drift를 감지하는 데서 그치면 안 됩니다. 재학습을 언제 돌릴지 기준이 분명해야 합니다.

트리거 조건자동화 수준비고
accuracy 5%p+ 하락자동 재학습 파이프라인 실행eval 세트 기반 검증 후 배포
fallback 비율 20%+자동 알림 + 수동 검토분류 체계 자체를 재설계해야 할 수도 있음
신규 클러스터 탐지라벨링 요청 후 재학습새 카테고리 추가 여부 판단 필요
오분류 피드백 누적주간 리뷰에서 판단특정 클래스 쌍 혼동 패턴 분석

라우팅 테스트 세트 작성법

라우팅 품질을 유지하려면 golden test set이 꼭 있어야 합니다. 이 세트는 라우팅을 바꿀 때 regression을 잡아내는 안전망입니다.

Golden Test Set 구성 원칙

  1. 카테고리별 10~20개: 각 라우팅 대상(agent/worker)마다 최소 10개, 이상적으로 20개의 테스트 케이스를 확보합니다.
  2. 실제 사용자 입력 기반: 합성 데이터보다 실제 로그에서 뽑은 입력이 훨씬 효과적입니다.
  3. 난이도 분포: 명확한 입력 50%, 경계 사례 30%, 의도적 모호 입력 20%로 구성합니다.
  4. 정기 갱신: 월 1회 새로운 실제 입력을 추가하고 오래된 케이스를 교체합니다.

Edge Case 포함 전략

유형예시왜 필요한가
경계 사례"환불인데 부분 환불이요" (환불 vs 부분환불)인접 카테고리 간 혼동 탐지
다중 의도"배송 조회하고 환불도 해주세요"단일 분류로 처리 불가한 입력
모호한 입력"이거 어떻게 해요"정보 부족 시 fallback 동작 확인
도메인 외 입력"오늘 날씨 어때?"needs_triage 분류 확인
적대적 입력"환불해줘 아니 환불 말고 배송 조회"모순된 의도 처리 확인
긴 입력500자 이상의 복합 요청토큰 제한·요약 오류 탐지

테스트 세트 관리 코드

interface RoutingTestCase {
  id: string
  input: string
  expectedTarget: string
  category: 'clear' | 'boundary' | 'ambiguous' | 'adversarial'
  addedAt: string        // ISO date
  source: 'production-log' | 'synthetic' | 'feedback'
  notes?: string
}

const goldenTestSet: RoutingTestCase[] = [
  {
    id: 'refund-001',
    input: '지난주 결제한 거 환불 가능한가요?',
    expectedTarget: 'refund-agent',
    category: 'clear',
    addedAt: '2026-03-01',
    source: 'production-log',
  },
  {
    id: 'boundary-001',
    input: '부분 환불 되나요? 배송비만 빼고요',
    expectedTarget: 'refund-agent',
    category: 'boundary',
    addedAt: '2026-03-01',
    source: 'production-log',
    notes: 'partial-refund와 refund 경계',
  },
  {
    id: 'multi-001',
    input: '배송 어디쯤 왔는지 확인하고, 안 오면 환불할게요',
    expectedTarget: 'needs_triage',
    category: 'ambiguous',
    addedAt: '2026-03-05',
    source: 'feedback',
    notes: '다중 의도: 배송조회 + 조건부 환불',
  },
]

async function runRoutingEval(
  router: (input: string) => Promise<{ target: string; confidence: number }>,
  testSet: RoutingTestCase[],
) {
  const results = await Promise.all(
    testSet.map(async (tc) => {
      const result = await router(tc.input)
      return {
        id: tc.id,
        category: tc.category,
        expected: tc.expectedTarget,
        actual: result.target,
        confidence: result.confidence,
        pass: result.target === tc.expectedTarget,
      }
    })
  )

  const total = results.length
  const passed = results.filter((r) => r.pass).length
  const byCategory = Object.groupBy(results, (r) => r.category)

  console.log(`전체: ${passed}/${total} (${((passed / total) * 100).toFixed(1)}%)`)
  for (const [cat, items] of Object.entries(byCategory)) {
    const catPassed = items!.filter((r) => r.pass).length
    console.log(`  ${cat}: ${catPassed}/${items!.length}`)
  }

  // 실패 케이스 상세
  results
    .filter((r) => !r.pass)
    .forEach((r) => {
      console.log(
        `  FAIL [${r.id}] expected=${r.expected} actual=${r.actual} ` +
        `confidence=${r.confidence.toFixed(3)}`
      )
    })

  return results
}

팁

테스트 세트를 JSON 파일로 분리해서 버전 관리하세요. 라우팅 로직 변경 PR마다 이 eval을 CI에서 자동 실행하면 regression을 조기에 발견할 수 있습니다.

Semantic Routing 임베딩 선택 가이드

Semantic routing은 입력 텍스트를 임베딩 벡터로 바꾼 뒤, 미리 등록해 둔 route 벡터와 cosine similarity를 비교해 가장 잘 맞는 경로를 고릅니다. 이때 어떤 임베딩 모델을 쓰느냐가 라우팅 정확도와 비용을 크게 좌우합니다.

임베딩 모델 비교

모델차원한국어 지원비용 (1M 토큰)속도비고
OpenAI text-embedding-3-large3072양호~$0.13빠름차원 축소 옵션 지원 (256~3072)
OpenAI text-embedding-3-small1536양호~$0.02매우 빠름비용 대비 성능 우수
Cohere embed-v4.01024양호~$0.10빠름검색 특화, matryoshka 지원
Voyage voyage-3-large1024보통~$0.18보통코드 임베딩에 강점
multilingual-e5-large1024우수무료 (셀프호스팅)GPU 필요다국어 성능 상위권
BGE-m31024우수무료 (셀프호스팅)GPU 필요dense + sparse + colbert 지원
KR-SBERT-V40K-klueNLI-augSTS768최우수무료 (셀프호스팅)GPU 필요한국어 전용, STS 벤치마크 상위

선택 기준

  • 한국어 비중이 높은 서비스: multilingual-e5-large, BGE-m3, 또는 한국어 전용 모델 우선 검토
  • 빠른 프로토타이핑: text-embedding-3-small (저비용, API 즉시 사용 가능)
  • 정확도 최우선: text-embedding-3-large + 도메인별 fine-tuning 또는 BGE-m3
  • 셀프호스팅 가능: 오픈소스 모델 + GPU 서버 (장기적으로 비용 절감)

Cosine Similarity Threshold 결정 방법

임베딩 모델을 고른 뒤에는 어느 수준의 유사도를 "매칭"으로 볼지 정해야 합니다. 이 threshold는 모델마다 도메인마다 다르므로 반드시 실험으로 정합니다.

import numpy as np
from sklearn.metrics import f1_score

def find_similarity_threshold(
    similarities: list[float],
    labels: list[int],  # 1=정답 매칭, 0=오매칭
    candidates: np.ndarray | None = None,
) -> dict:
    """
    다양한 threshold에서 F1 score를 계산하고
    최적 threshold를 반환한다.
    """
    if candidates is None:
        candidates = np.arange(0.50, 0.96, 0.01)

    sims = np.array(similarities)
    labs = np.array(labels)
    best = {'threshold': 0.0, 'f1': 0.0}

    for t in candidates:
        preds = (sims >= t).astype(int)
        f1 = f1_score(labs, preds)
        if f1 > best['f1']:
            best = {'threshold': float(t), 'f1': float(f1)}

    return best


# 사용 예시
# 검증 세트에서 각 (입력, route) 쌍의 cosine similarity와 정답 여부를 수집
result = find_similarity_threshold(
    similarities=[0.89, 0.72, 0.91, 0.55, 0.83, 0.61],
    labels=      [1,    0,    1,    0,    1,    0],
)
print(f"최적 threshold: {result['threshold']:.2f}, F1: {result['f1']:.3f}")

실전 Semantic Router 구현

interface SemanticRoute {
  name: string
  description: string          // route를 설명하는 텍스트
  examples: string[]           // 해당 route에 해당하는 예시 입력
  embedding?: number[]         // 사전 계산된 평균 임베딩
  threshold?: number           // route별 개별 threshold
}

async function semanticRoute(
  input: string,
  routes: SemanticRoute[],
  embed: (text: string) => Promise<number[]>,
  defaultThreshold = 0.78,
): Promise<{ route: string; similarity: number } | null> {
  const inputVec = await embed(input)

  let best: { route: string; similarity: number } | null = null

  for (const r of routes) {
    if (!r.embedding) continue
    const sim = cosineSimilarity(inputVec, r.embedding)
    const threshold = r.threshold ?? defaultThreshold

    if (sim >= threshold && (!best || sim > best.similarity)) {
      best = { route: r.name, similarity: sim }
    }
  }

  return best
}

function cosineSimilarity(a: number[], b: number[]): number {
  let dot = 0, normA = 0, normB = 0
  for (let i = 0; i < a.length; i++) {
    dot += a[i] * b[i]
    normA += a[i] * a[i]
    normB += b[i] * b[i]
  }
  return dot / (Math.sqrt(normA) * Math.sqrt(normB))
}

주의

Cosine similarity threshold를 한 번 정하고 고정하지 마세요. 임베딩 모델을 교체하면 유사도 분포가 완전히 달라집니다. 모델 변경 시 반드시 threshold를 재측정하세요.

ADR 스타일 결론

Decision

라우팅은 LLM 한 번 호출로 끝내지 않고, 규칙 기반 필터와 capability registry를 앞단에 둔 다단계 디스패치로 설계합니다. 애매한 입력은 억지로 분류하지 않고, fallback 또는 human triage 경로를 명시적으로 남깁니다.

실무 체크리스트

  • 라우팅 taxonomy가 시스템 action 기준으로 정의되어 있는가
  • deterministic rule과 classifier의 책임이 분리되어 있는가
  • low confidence fallback이 있는가
  • agent registry에 권한과 비용 정보가 포함되는가
  • 라우팅 오분류를 추적하는 eval 세트가 있는가

다음에 읽을 장

  • 핵심 아키텍처 패턴
  • 통신과 핸드오프
  • 평가와 테스트

관련 문서

에러 처리와 복구 전략

재시도/폴백/서킷브레이커, 에이전트 실패 격리, Human-in-the-Loop, 부분 실패 복구

실전 아키텍처 사례

고객 지원, 코드 생성, 리서치, 데이터 파이프라인 — 4가지 사례를 패턴 조합으로 분석

고객지원 에이전트 아키텍처

Vercel 엔터프라이즈 AI 플랫폼 · 고객-facing 챗/지원 에이전트를 AI SDK, AI Gateway, MCP, escalation 구조로 설계하는 방법을 정리합니다.

AI SaaS 비용 구조

SaaS 유료 플랜 설계 · 전통 SaaS와 AI SaaS의 비용 차이, LLM 제공업체별 비용 비교

상태 관리와 메모리 시스템

컨텍스트 윈도우 전략, 단기/장기 메모리, 체크포인트/복원, state reducer 패턴

에러 처리와 복구 전략

재시도/폴백/서킷브레이커, 에이전트 실패 격리, Human-in-the-Loop, 부분 실패 복구

On this page

라우팅 방식 비교권장 디스패치 파이프라인동적 에이전트 선택confidence와 fallback비용과 SLA를 함께 고려한 디스패치최소 구현 스켈레톤잘못된 라우팅을 줄이는 방법안티패턴Confidence Calibration임계값 설정의 딜레마Precision-Recall 기반 Threshold 선택클래스별 차등 ThresholdDrift Detection모니터링 지표Drift 탐지 구현자동 재학습 트리거 조건라우팅 테스트 세트 작성법Golden Test Set 구성 원칙Edge Case 포함 전략테스트 세트 관리 코드Semantic Routing 임베딩 선택 가이드임베딩 모델 비교선택 기준Cosine Similarity Threshold 결정 방법실전 Semantic Router 구현ADR 스타일 결론실무 체크리스트다음에 읽을 장