Middleware

Middleware (미들웨어)

AG-UI 에이전트에서 이벤트를 변환하고 가로채는 방법을 설명드릴게요.

출처: 문서

본문

AG-UI의 미들웨어는 에이전트를 통과하는 이벤트 스트림을 변환·필터링·증강할 수 있는 강력한 방법을 제공해요. 로깅, 인증, 속도 제한, 이벤트 필터링 같은 횡단 관심사(cross-cutting concerns)를 핵심 에이전트 로직을 수정하지 않고 추가할 수 있게 해줍니다.

아래 예시는 관련 RxJS 연산자/유틸리티(map, tap, catchError, switchMap, timer 등)가 import되어 있다고 가정해요.

미들웨어란 무엇인가 (What is Middleware?)

미들웨어는 에이전트 실행과 이벤트 소비자 사이에 위치하며 다음과 같은 일을 가능하게 해요:

  1. 이벤트 변환 – 파이프라인을 흐르면서 이벤트를 수정·향상
  2. 이벤트 필터링 – 특정 이벤트를 선택적으로 허용하거나 차단
  3. 메타데이터 추가 – 추가 컨텍스트나 추적 정보 주입
  4. 오류 처리 – 커스텀 오류 복구 전략 구현
  5. 실행 모니터링 – 로깅, 메트릭, 디버깅 기능 추가

미들웨어가 작동하는 방식 (How Middleware Works)

미들웨어는 각 미들웨어가 다음 것을 감싸는 체인을 형성하며 기능 계층을 만들어요. 에이전트가 실행되면 이벤트 스트림이 각 미들웨어를 순서대로 통과합니다.

import { AbstractAgent } from "@ag-ui/client"

const agent = new MyAgent()

// Middleware chain: logging -> auth -> filter -> agent
agent.use(loggingMiddleware, authMiddleware, filterMiddleware)

// When agent runs, events flow through all middleware
await agent.runAgent()

agent.use(...)로 추가된 미들웨어는 runAgent()에서 적용돼요. connectAgent()는 현재 connect()를 직접 호출하며 미들웨어를 실행하지 않아요.

함수 기반 미들웨어 (Function-Based Middleware)

간단한 변환에는 함수 기반 미들웨어를 사용할 수 있어요. 미들웨어를 추가하는 가장 간결한 방법입니다:

import { MiddlewareFunction } from "@ag-ui/client"
import { EventType } from "@ag-ui/core"

const prefixMiddleware: MiddlewareFunction = (input, next) => {
  return next.run(input).pipe(
    map(event => {
      if (
        event.type === EventType.TEXT_MESSAGE_CHUNK ||
        event.type === EventType.TEXT_MESSAGE_CONTENT
      ) {
        return {
          ...event,
          delta: `[AI]: ${event.delta}`
        }
      }
      return event
    })
  )
}

agent.use(prefixMiddleware)

클래스 기반 미들웨어 (Class-Based Middleware)

상태나 구성이 필요한 더 복잡한 시나리오에는 클래스 기반 미들웨어를 사용하세요:

import { Middleware } from "@ag-ui/client"
import { Observable } from "rxjs"
import { tap } from "rxjs/operators"

class MetricsMiddleware extends Middleware {
  private eventCount = 0

  constructor(private metricsService: MetricsService) {
    super()
  }

  run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
    const startTime = Date.now()

    return this.runNext(input, next).pipe(
      tap(event => {
        this.eventCount++
        this.metricsService.recordEvent(event.type)
      }),
      finalize(() => {
        const duration = Date.now() - startTime
        this.metricsService.recordDuration(duration)
        this.metricsService.recordEventCount(this.eventCount)
      })
    )
  }
}

agent.use(new MetricsMiddleware(metricsService))

클래스 미들웨어를 작성한다면 헬퍼 메서드를 선호하세요:

  • runNext(input, next)는 청크 이벤트를 완전한 TEXT_MESSAGE_*/TOOL_CALL_* 시퀀스로 정규화해요.
  • runNextWithState(input, next)는 각 이벤트 후 누적된 messages와 state도 제공해요.

내장 미들웨어 (Built-in Middleware)

AG-UI는 일반적인 사용 사례를 위한 몇 가지 내장 미들웨어 컴포넌트를 제공합니다:

FilterToolCallsMiddleware

허용/비허용 목록을 기반으로 도구 호출을 필터링하세요:

import { FilterToolCallsMiddleware } from "@ag-ui/client"

// Only allow specific tools
const allowedFilter = new FilterToolCallsMiddleware({
  allowedToolCalls: ["search", "calculate"]
})

// Or block specific tools
const blockedFilter = new FilterToolCallsMiddleware({
  disallowedToolCalls: ["delete", "modify"]
})

agent.use(allowedFilter)

FilterToolCallsMiddleware는 발행되는 TOOL_CALL_* 이벤트를 필터링해요. 업스트림 모델/런타임의 도구 실행을 차단하지는 않아요.

미들웨어 패턴 (Middleware Patterns)

흔한 패턴으로 로깅, forwardedProps를 통한 인증, 속도 제한이 있어요. 구체적인 구현은 JS middleware reference를 참고하세요.

미들웨어 결합 (Combining Middleware)

여러 미들웨어를 결합해 정교한 처리 파이프라인을 만들 수 있어요:

const logMiddleware: MiddlewareFunction = (input, next) => next.run(input)
const metricsMiddleware = new MetricsMiddleware(metricsService)
const filterMiddleware = new FilterToolCallsMiddleware({ allowedToolCalls: ["search"] })

agent.use(logMiddleware, metricsMiddleware, filterMiddleware)

실행 순서 (Execution Order)

미들웨어는 추가된 순서대로 실행되며, 각 미들웨어가 다음 것을 감쌉니다:

  1. 첫 번째 미들웨어가 원래 입력을 받아요
  2. 다음 미들웨어로 넘기기 전에 입력을 수정할 수 있어요
  3. 각 미들웨어는 체인의 다음 것에서 나오는 이벤트를 처리해요
  4. 마지막 미들웨어가 실제 에이전트를 호출해요
agent.use(middleware1, middleware2, middleware3)

// Execution flow:
// → middleware1
//   → middleware2
//     → middleware3
//       → agent.run()
//     ← events flow back through middleware3
//   ← events flow back through middleware2
// ← events flow back through middleware1

모범 사례 (Best Practices)

  1. 미들웨어는 집중적으로 유지 – 각 미들웨어는 단일 책임을 가져야 해요
  2. 오류를 우아하게 처리 – RxJS 오류 처리 연산자 사용
  3. 차단 작업 피하기 – I/O 작업에는 비동기 패턴 사용
  4. 부작용 문서화 – 미들웨어가 상태를 수정하면 명확히 표시
  5. 미들웨어를 독립적으로 테스트 – 각 미들웨어에 단위 테스트 작성
  6. 성능 고려 – 이벤트 스트림의 처리 오버헤드를 유의

고급 사용 사례 (Advanced Use Cases)

조건부 미들웨어 (Conditional Middleware)

런타임 조건에 따라 미들웨어를 적용하세요:

const conditionalMiddleware: MiddlewareFunction = (input, next) => {
  if (input.forwardedProps?.debug === true) {
    // Apply debug logging
    return next.run(input).pipe(
      tap(event => console.debug(event))
    )
  }
  return next.run(input)
}

이벤트 변환과 스트림 제어 변형은 JS middleware reference를 참고하세요.

결론 (Conclusion)

미들웨어는 핵심 로직을 수정하지 않고 AG-UI 에이전트를 확장할 수 있는 유연하고 강력한 방법을 제공해요. 간단한 이벤트 변환이 필요하든 복잡한 상태형 처리가 필요하든, 미들웨어 시스템은 견고하고 유지보수 가능한 에이전트 애플리케이션을 구축할 도구를 제공합니다.

더 알아보기 (Learn more)