마이크로소프트 AI Agent 프레임워크: 상세 설명과 예제

이 글에는 짧은 영상이 있습니다:TikTok

1. 개요

마이크로소프트가 방금 Semantic Kernel과 AutoGen이라는 두 가지 기존 제품을 통합하여 AI Agent를 구축, 오케스트레이션 및 운영하기 위한 통합 프레임워크를 공식 릴리스(GA - General Availability)했습니다. 이것은 단순한 제품 뉴스가 아니라, LLM이 ’스스로 행동을 결정’하는 시스템을 설계하는 방식을 재정의합니다. 단순히 질문에 답하는 것이 아니라 말이죠.

이 글에서는 AI Agent와 일반 채팅봇의 차이점, 마이크로소프트가 구체적으로 출시한 것, 그리고 Java 21 + Spring Boot 3 + Spring AI(Azure OpenAI 통합)를 사용하여 최소한의 agent를 직접 구축하는 방법을 설명합니다. 마이크로소프트의 원본 프레임워크는 .NET과 Python에서 실행되지만, 이를 통해 내부 메커니즘을 명확히 이해할 수 있습니다.

2. AI Agent란 무엇인가?

일반적인 채팅봇은 질문을 받고 LLM을 한 번 호출한 다음 답변을 반환합니다. Agent는 다릅니다 - 반복적으로 생각-행동 루프를 여러 번 실행하면서, 도구(API, 데이터베이스, 파일…)를 호출할 필요가 있는지 스스로 결정한 후 최종 답변을 제공합니다.

특징 채팅봇 AI Agent
LLM 호출 횟수 1회/질문 여러 번, 자동 반복
외부 도구 사용 보통 아니오 예, 모델이 선택
상태 관리 Stateless 또는 채팅 기록 저장 Memory 있음, checkpoint 가능
지연시간 낮음 높음 (여러 round-trip)
신뢰성 제어하기 쉬움 Guardrail 필요, human-in-the-loop 필요

2.1. Reason-Act(ReAct) 루프

현재 대부분의 agent 프레임워크가 사용하는 핵심 메커니즘은 ReAct입니다: 모델이 ’생각(Thought)’한 후, 도구를 호출하는 행동(Action)을 선택하고, 결과(Observation)를 받은 다음, 충분한 정보를 얻을 때까지(Final Answer) 반복합니다.

Thought: 사용자가 주문 ORD-001의 상태를 묻고 있으니, 조회가 필요하다.
Action: lookupOrder("ORD-001")
Observation: 주문 ORD-001: 상태 SHIPPED, 총액 450000 VND
Thought: 충분한 정보를 얻었다.
Final Answer: 귀하의 주문 ORD-001은 배송 중입니다, 총액은 450,000동입니다.

이것이 Microsoft Agent Framework와 아래에서 사용할 Spring AI가 자동화하는 부분입니다. 이 루프를 직접 작성할 필요가 없으며, 도구와 시스템 프롬프트만 선언하면 됩니다.

3. 마이크로소프트가 AI Agent를 위해 ’출시’한 것들

통합하기 전에, 마이크로소프트는 두 가지 개발 방향을 병행했습니다:

  • Semantic Kernel: 프로덕션 지향 SDK, 타입 안전 플러그인을 강조하며, 엔터프라이즈 통합(Azure, .NET)에 초점을 맞추어 안정적인 배포에 적합합니다.
  • AutoGen: 연구 지향, 다중 agent 대화에 강점 - 여러 agent가 서로 ’대화’하여 복잡한 작업을 해결합니다.

새로운 프레임워크는 두 가지 강점을 결합하며, 다음과 같은 주요 기능을 포함합니다:

  • 내장 Orchestration 패턴: sequential (agent A가 완료되면 B로), concurrent (여러 agent가 병렬로 실행 후 통합), hand-off (agent가 task를 전문가 agent에게 전달), group chat (여러 agent가 답변 전에 토론).
  • 내구성 있는/상태 유지 실행: agent는 중간에 checkpoint를 설정하고 프로세스가 충돌하면 재개할 수 있습니다 - 오래 걸리는 작업(몇 분에서 몇 시간)에 중요합니다.
  • 내장 Observability: OpenTelemetry 표준에 따라 추적하며, 프로덕션에서 각 단계의 Thought/Action/Observation을 볼 수 있습니다.
  • Human-in-the-loop: 민감한 작업(환불, 데이터 삭제…)을 수행하기 전에 검토자의 승인을 기다리도록 agent를 일시 중지합니다.
  • Azure AI Foundry Agent Service: agent 배포를 위한 호스팅 런타임, 기본 도구 카탈로그(Bing 검색, 코드 인터프리터, 파일 검색) 포함 및 외부 도구를 벤더에 종속되지 않는 개방형 표준으로 연결할 수 있는 Model Context Protocol(MCP) 지원.

원본 프레임워크는 .NET/Python에서 실행되지만, 패턴은 보편적입니다. 다음 섹션에서는 Java에서 정확한 패턴을 재구현하여 코드 레벨에서 어떻게 작동하는지 보여드리겠습니다.

4. AI Agent의 핵심 아키텍처

최소한의 agent는 4가지 구성 요소가 필요합니다: Planner (LLM 자체), Tool (agent가 호출할 수 있는 함수), Memory (대화 기록/도구 결과), Executor (오케스트레이션 루프). Spring AI는 ChatClient를 통해 Executor와 Planner를 이미 캡슐화하고 있으므로, Tool만 선언하면 됩니다.

4.1. Maven 의존성

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

Azure OpenAI 연결 구성 (마이크로소프트의 GPT 서비스, Azure를 통해 배포):

spring.ai.azure.openai.api-key=${AZURE_OPENAI_KEY}
spring.ai.azure.openai.endpoint=${AZURE_OPENAI_ENDPOINT}
spring.ai.azure.openai.chat.options.deployment-name=gpt-4o-mini

4.2. Agent용 Tool 정의

Tool은 일반 Java 메서드이며, @Tool로 표시되어 모델이 호출할 수 있음을 알고 어떤 매개변수로 호출하는지 알 수 있습니다.

public record Order(String id, String status, BigDecimal total) {}

public interface OrderService {
    Order findById(String orderId);
}

@Service
public class OrderTools {

    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @Tool(description = "주문 ID로 주문의 상태와 총액을 조회합니다")
    public String lookupOrder(String orderId) {
        Order order = orderService.findById(orderId);
        return "주문 %s: 상태 %s, 총액 %s VND"
            .formatted(order.id(), order.status(), order.total());
    }
}

@Tool의 description은 모델이 lookupOrder를 호출해야 하는지 결정하기 위해 읽는 것입니다 - 명확하고 구체적으로 작성하면 agent가 올바른 도구를 더 정확하게 선택합니다.

4.3. ChatClient를 통해 자동으로 Tool 호출하는 Agent

@Service
public class SupportAgentService {

    private final ChatClient chatClient;

    public SupportAgentService(ChatClient.Builder builder, OrderTools orderTools) {
        this.chatClient = builder
            .defaultSystem("""
                당신은 온라인 상점의 고객 지원 어시스턴트입니다.
                질문이 주문과 관련되면 lookupOrder 도구를 사용하세요.
                간결하고 친절하게 한국어로 답변하세요.
                """)
            .defaultTools(orderTools)
            .build();
    }

    public String handle(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }
}

.defaultTools(orderTools) 행은 도구 집합을 등록하고, ChatClient는 내부적으로 ReAct 루프를 자동 실행합니다: 도구 목록과 함께 프롬프트를 모델에 전송하고, 모델이 lookupOrder 호출을 요청하면 Spring AI는 해당하는 Java 메서드를 자동으로 실행하고 결과(Observation)를 모델에 반환한 후 최종 답변을 받습니다. 루프를 직접 작성할 필요가 없습니다 - 이것이 정확히 Microsoft Agent Framework와 Spring AI가 본질적으로 동일하게 수행하는 부분입니다.

> handle("내 주문 ORD-001은 어디에 있나요?")
주문 ORD-001은 배송 중입니다(SHIPPED), 총액은 450,000동입니다!

참고: 모델은 “필요하다”고 판단할 때만 도구를 호출합니다 - 모든 질문이 도구를 활성화하는 것은 아닙니다. 사용자가 “가게에서 국제 배송을 하나요?“라고 묻는다면, 모델은 lookupOrder를 호출하지 않고 직접 답변합니다.

5. Multi-agent: 하나의 agent가 충분하지 않을 때

마이크로소프트가 출시한 가장 눈에 띄는 기능 중 하나는 hand-off - 전문가 agent 간에 대화를 전달하는 것입니다. 작은 문제의 경우, 복잡한 multi-agent 프레임워크 없이 간단한 라우터로 시뮬레이션할 수 있습니다:

@Service
public class AgentRouter {

    private final SupportAgentService orderAgent;
    private final RefundAgentService refundAgent;

    public AgentRouter(SupportAgentService orderAgent, RefundAgentService refundAgent) {
        this.orderAgent = orderAgent;
        this.refundAgent = refundAgent;
    }

    public String route(String userMessage) {
        String lower = userMessage.toLowerCase();
        if (lower.contains("환불") || lower.contains("refund")) {
            return refundAgent.handle(userMessage);
        }
        return orderAgent.handle(userMessage);
    }
}

이 라우터는 최소한의 “orchestrator” 역할을 합니다. 더 큰 규모에서는 어떤 agent가 처리할지 선택하는 것도 LLM 자체에서 결정하도록 해야 합니다(모델을 사용한 intent classification). contains()를 hard-code하는 대신 말이죠 - 수작업 방식은 데모 또는 매우 작고 안정적인 intent 세트에만 적합합니다.

6. Tool과 Agent를 위한 테스트 작성

agent 시스템에서 가장 쉽게 테스트할 수 있는 부분은 tool입니다 - LLM에 의존하지 않는 순수 Java 메서드이기 때문입니다. 이 부분을 철저히 테스트해야 합니다. agent는 호출하는 tool의 품질만큼만 ’지능적’이기 때문입니다.

class OrderToolsTest {

    @Test
    void shouldReturnOrderSummaryWithStatusAndTotal() {
        OrderService fakeOrderService = orderId ->
            new Order(orderId, "SHIPPED", new BigDecimal("450000"));

        OrderTools tools = new OrderTools(fakeOrderService);

        String result = tools.lookupOrder("ORD-001");

        assertThat(result)
            .contains("ORD-001")
            .contains("SHIPPED")
            .contains("450000");
    }
}

OrderService는 findById 메서드만 가지고 있으므로, 추가 mocking 프레임워크 의존성을 추가하는 대신 lambda를 사용하여 가짜 구현을 만들었습니다 - 이 단위 테스트에는 간결하고 충분합니다.

class AgentRouterTest {

    @Test
    void shouldRouteRefundKeywordToRefundAgent() {
        SupportAgentService orderAgent = mock(SupportAgentService.class);
        RefundAgentService refundAgent = mock(RefundAgentService.class);
        when(refundAgent.handle(anyString())).thenReturn("환불 요청이 등록되었습니다");

        AgentRouter router = new AgentRouter(orderAgent, refundAgent);

        String result = router.route("주문 ORD-002를 환불받고 싶습니다");

        assertThat(result).isEqualTo("환불 요청이 등록되었습니다");
        verify(refundAgent).handle(anyString());
        verify(orderAgent, never()).handle(anyString());
    }
}

참고: 단위 테스트에서 “LLM을 실제로 호출하는 end-to-end 테스트”를 작성하면 안 됩니다 - 결과가 결정론적이지 않습니다(모델은 매번 다르게 답할 수 있음), API 호출로 비용이 들고, CI를 느리게/flaky하게 만듭니다. ChatClient를 모킹하거나 위의 tool/router 부분을 별도로 테스트하세요. 실제 상황에서 agent가 올바른 도구를 선택했는지 확인하기 위해 별도의 통합 테스트 세트(수동 실행 또는 nightly)를 유지하세요.

7. 흔한 실수와 agent를 사용하지 말아야 할 때

  • if-else로 해결할 수 있는 작업에 agent 사용: 행동 선택 로직이 미리 알려져 있고 고정적이면(예: ORD-\d+ 형식의 주문 코드가 있을 때 항상 주문 조회), 코드를 작성하는 것이 일반적으로 LLM이 매번 ’생각’하도록 하는 것보다 훨씬 싸고, 빠르고, 테스트하기 쉽습니다.
  • 민감한 작업에 대한 guardrail 무시: agent가 데이터를 삭제하고, 환불하고, 고객에게 이메일을 보내는 도구를 호출할 권한이 있다면, 확인 단계(human-in-the-loop)나 도구 권한 제한이 없으면 prompt injection이나 모델의 추론 오류가 실제 결과를 초래할 수 있습니다.
  • 루프 횟수 제한 없음: 모델이 계속 잘못된 도구를 선택하거나 결론을 내릴 충분한 데이터가 없으면, ReAct 루프가 무한정 반복되어 토큰과 비용을 낭비할 수 있습니다. 항상 최대 단계 수와 시간 초과를 설정하세요.
  • 부족한 Observability: agent가 잘못 답변했을 때, 디버깅을 위해 전체 Thought/Action/Observation 체인을 다시 볼 수 있어야 합니다 - 자세한 로그/trace가 없으면 모델이 어떻게 ’생각’했는지 알아내기 거의 불가능합니다.
  • 지연시간과 비용: 루프의 각 단계는 LLM 호출 한 번입니다. 간단한 질문도 2-3번의 모델 호출이 소요될 수 있으며(각 호출은 수백ms에서 몇 초가 소요됨), 즉각적인 응답이 필요한 API(예: 200ms 이하)에는 이러한 유형의 agent가 적합하지 않습니다.

8. 결론

마이크로소프트가 Semantic Kernel과 AutoGen을 하나의 프레임워크로 통합한 것은 명확한 추세를 보여줍니다: AI Agent는 “흥미로운 데모”에서 orchestration, state, observability 및 엄격한 권한 제어가 필요한 프로덕션 인프라로 전환되고 있습니다 - 다른 모든 엔터프라이즈 시스템과 마찬가지로. .NET, Python 또는 이 글의 예제처럼 Java/Spring AI를 사용하든, 핵심 아키텍처는 항상 네 가지 블록으로 회전합니다: Planner, Tool, Memory, Executor - 이 네 가지 블록을 확실히 이해하면, 앞으로 나타날 어떤 agent 프레임워크도 이해하고 적용할 수 있습니다.

AI AgentMicrosoftSpring AIAzure OpenAILLM