다음을 통해 공유


Microsoft 에이전트 프레임워크 워크플로 오케스트레이션 - 전달

핸드오프 오케스트레이션을 사용하면 에이전트가 컨텍스트 또는 사용자 요청에 따라 서로 제어를 전송할 수 있습니다. 각 에이전트는 적절한 전문 지식을 갖춘 다른 에이전트에게 대화를 "전달"하여 올바른 에이전트가 작업의 각 부분을 처리하도록 할 수 있습니다. 이는 고객 지원, 전문가 시스템 또는 동적 위임이 필요한 시나리오에서 특히 유용합니다.

핸드오프 오케스트레이션

핸드오프와 Agent-as-Tools의 차이점

에이전트-도구는 일반적으로 여러 에이전트 패턴으로 간주되며 처음에는 핸드오프와 비슷하게 보일 수 있지만, 두 가지 간에는 근본적인 차이가 있습니다.

  • 제어 흐름: 핸드오프 오케스트레이션에서 정의된 규칙에 따라 에이전트 간에 제어가 명시적으로 전달됩니다. 각 에이전트는 전체 작업을 다른 에이전트에 전달하도록 결정할 수 있습니다. 워크플로를 관리하는 중앙 기관은 없습니다. 반면, 에이전트-as-tools에는 하위 작업을 다른 에이전트에 위임하는 기본 에이전트가 포함되며 에이전트가 하위 작업을 완료하면 컨트롤이 주 에이전트로 돌아갑니다.
  • 작업 소유권: 핸드오프에서 핸드오프를 받는 에이전트는 작업의 전체 소유권을 맡습니다. 에이전트-as-tools에서 주 에이전트는 작업에 대한 전반적인 책임을 유지하지만 다른 에이전트는 특정 하위 작업을 지원하는 도구로 처리됩니다.
  • 컨텍스트 관리: 핸드오프 오케스트레이션에서 대화는 완전히 다른 에이전트에 전달됩니다. 수신 에이전트에는 지금까지 수행된 작업의 전체 컨텍스트가 있습니다. 에이전트-as-tools에서 기본 에이전트는 전체 컨텍스트를 관리하며 필요에 따라 도구 에이전트에 관련 정보만 제공할 수 있습니다.

학습 내용

  • 다른 도메인에 대한 특수 에이전트를 만드는 방법
  • 에이전트 간에 핸드오프 규칙을 구성하는 방법
  • 동적 에이전트 라우팅을 사용하여 대화형 워크플로를 빌드하는 방법
  • 에이전트 전환으로 다중 턴 대화를 처리하는 방법
  • 중요한 작업에 대한 도구 승인을 구현하는 방법(HITL)
  • 내구적인 핸드오프 워크플로를 위한 체크포인트 사용 방법

핸드오프 오케스트레이션에서 에이전트는 컨텍스트에 따라 서로 제어를 전송하여 동적 라우팅 및 전문 지식 처리를 수행할 수 있습니다.

Azure OpenAI 클라이언트 설정

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
using Microsoft.Agents.AI;

// 1) Set up the Azure OpenAI client
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ??
    throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
var client = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential())
    .GetChatClient(deploymentName)
    .AsIChatClient();

특수 에이전트 정의

라우팅을 위한 도메인별 에이전트 및 심사 에이전트를 만듭니다.

// 2) Create specialized agents
ChatClientAgent historyTutor = new(client,
    "You provide assistance with historical queries. Explain important events and context clearly. Only respond about history.",
    "history_tutor",
    "Specialist agent for historical questions");

ChatClientAgent mathTutor = new(client,
    "You provide help with math problems. Explain your reasoning at each step and include examples. Only respond about math.",
    "math_tutor",
    "Specialist agent for math questions");

ChatClientAgent triageAgent = new(client,
    "You determine which agent to use based on the user's homework question. ALWAYS handoff to another agent.",
    "triage_agent",
    "Routes messages to the appropriate specialist agent");

핸드오프 규칙 구성

어떤 에이전트가 다른 에이전트에게 인계를 할 수 있는지 정의합니다.

// 3) Build handoff workflow with routing rules
var workflow = AgentWorkflowBuilder.StartHandoffWith(triageAgent)
    .WithHandoffs(triageAgent, [mathTutor, historyTutor]) // Triage can route to either specialist
    .WithHandoff(mathTutor, triageAgent)                  // Math tutor can return to triage
    .WithHandoff(historyTutor, triageAgent)               // History tutor can return to triage
    .Build();

대화형 핸드오프 워크플로 실행

동적 에이전트 전환을 사용하여 다중 턴 대화를 처리합니다.

// 4) Process multi-turn conversations
List<ChatMessage> messages = new();

while (true)
{
    Console.Write("Q: ");
    string userInput = Console.ReadLine()!;
    messages.Add(new(ChatRole.User, userInput));

    // Execute workflow and process events
    StreamingRun run = await InProcessExecution.StreamAsync(workflow, messages);
    await run.TrySendMessageAsync(new TurnToken(emitEvents: true));

    List<ChatMessage> newMessages = new();
    await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
    {
        if (evt is AgentRunUpdateEvent e)
        {
            Console.WriteLine($"{e.ExecutorId}: {e.Data}");
        }
        else if (evt is WorkflowOutputEvent outputEvt)
        {
            newMessages = (List<ChatMessage>)outputEvt.Data!;
            break;
        }
    }

    // Add new messages to conversation history
    messages.AddRange(newMessages.Skip(messages.Count));
}

샘플 상호 작용

Q: What is the derivative of x^2?
triage_agent: This is a math question. I'll hand this off to the math tutor.
math_tutor: The derivative of x^2 is 2x. Using the power rule, we bring down the exponent (2) and multiply it by the coefficient (1), then reduce the exponent by 1: d/dx(x^2) = 2x^(2-1) = 2x.

Q: Tell me about World War 2
triage_agent: This is a history question. I'll hand this off to the history tutor.
history_tutor: World War 2 was a global conflict from 1939 to 1945. It began when Germany invaded Poland and involved most of the world's nations. Key events included the Holocaust, Pearl Harbor attack, D-Day invasion, and ended with atomic bombs on Japan.

Q: Can you help me with calculus integration?
triage_agent: This is another math question. I'll route this to the math tutor.
math_tutor: I'd be happy to help with calculus integration! Integration is the reverse of differentiation. The basic power rule for integration is: ∫x^n dx = x^(n+1)/(n+1) + C, where C is the constant of integration.

채팅 클라이언트 설정

from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential

# Initialize the Azure OpenAI chat client
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())

특수 에이전트 정의

라우팅을 위한 코디네이터를 사용하여 도메인별 에이전트를 만듭니다.

# Create triage/coordinator agent
triage_agent = chat_client.create_agent(
    instructions=(
        "You are frontline support triage. Read the latest user message and decide whether "
        "to hand off to refund_agent, order_agent, or support_agent. Provide a brief natural-language "
        "response for the user. When delegation is required, call the matching handoff tool "
        "(`handoff_to_refund_agent`, `handoff_to_order_agent`, or `handoff_to_support_agent`)."
    ),
    name="triage_agent",
)

# Create specialist agents
refund_agent = chat_client.create_agent(
    instructions=(
        "You handle refund workflows. Ask for any order identifiers you require and outline the refund steps."
    ),
    name="refund_agent",
)

order_agent = chat_client.create_agent(
    instructions=(
        "You resolve shipping and fulfillment issues. Clarify the delivery problem and describe the actions "
        "you will take to remedy it."
    ),
    name="order_agent",
)

support_agent = chat_client.create_agent(
    instructions=(
        "You are a general support agent. Offer empathetic troubleshooting and gather missing details if the "
        "issue does not match other specialists."
    ),
    name="support_agent",
)

핸드오프 규칙 구성

HandoffBuilder를 사용하여 핸드오프 워크플로를 빌드합니다.

from agent_framework import HandoffBuilder

# Build the handoff workflow
workflow = (
    HandoffBuilder(
        name="customer_support_handoff",
        participants=[triage_agent, refund_agent, order_agent, support_agent],
    )
    .set_coordinator("triage_agent")
    .with_termination_condition(
        # Terminate after a certain number of user messages
        lambda conv: sum(1 for msg in conv if msg.role.value == "user") >= 10
    )
    .build()
)

고급 라우팅을 위해 전문가-전문가 핸드오프를 구성할 수 있습니다.

# Enable return-to-previous and add specialist-to-specialist handoffs
workflow = (
    HandoffBuilder(
        name="advanced_handoff",
        participants=[coordinator, technical, account, billing],
    )
    .set_coordinator(coordinator)
    .add_handoff(coordinator, [technical, account, billing])  # Coordinator routes to all specialists
    .add_handoff(technical, [billing, account])  # Technical can route to billing or account
    .add_handoff(account, [technical, billing])  # Account can route to technical or billing
    .add_handoff(billing, [technical, account])  # Billing can route to technical or account
    .enable_return_to_previous(True)  # User inputs route directly to current specialist
    .build()
)

대화형 핸드오프 워크플로 실행

사용자 입력 요청을 사용하여 다중 턴 대화를 처리합니다.

from agent_framework import RequestInfoEvent, HandoffUserInputRequest, WorkflowOutputEvent

# Start workflow with initial user message
events = [event async for event in workflow.run_stream("I need help with my order")]

# Process events and collect pending input requests
pending_requests = []
for event in events:
    if isinstance(event, RequestInfoEvent):
        pending_requests.append(event)
        request_data = event.data
        print(f"Agent {request_data.awaiting_agent_id} is awaiting your input")
        for msg in request_data.conversation[-3:]:
            print(f"{msg.author_name}: {msg.text}")

# Interactive loop: respond to requests
while pending_requests:
    user_input = input("You: ")

    # Send responses to all pending requests
    responses = {req.request_id: user_input for req in pending_requests}
    events = [event async for event in workflow.send_responses_streaming(responses)]

    # Process new events
    pending_requests = []
    for event in events:
        if isinstance(event, RequestInfoEvent):
            pending_requests.append(event)
        elif isinstance(event, WorkflowOutputEvent):
            print("Workflow completed!")
            conversation = event.data
            for msg in conversation:
                print(f"{msg.author_name}: {msg.text}")

고급: 핸드오프 워크플로의 도구 승인

핸드오프 워크플로에는 실행 전에 사람의 승인이 필요한 도구가 있는 에이전트가 포함될 수 있습니다. 이는 환불 처리, 구매 또는 돌이킬 수 없는 작업 실행과 같은 중요한 작업에 유용합니다.

승인이 필요한 도구 정의

from typing import Annotated
from agent_framework import ai_function

@ai_function(approval_mode="always_require")
def submit_refund(
    refund_description: Annotated[str, "Description of the refund reason"],
    amount: Annotated[str, "Refund amount"],
    order_id: Annotated[str, "Order ID for the refund"],
) -> str:
    """Submit a refund request for manual review before processing."""
    return f"Refund recorded for order {order_id} (amount: {amount}): {refund_description}"

승인 필요 도구를 사용하여 에이전트 만들기

from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential

client = AzureOpenAIChatClient(credential=AzureCliCredential())

triage_agent = client.create_agent(
    name="triage_agent",
    instructions=(
        "You are a customer service triage agent. Listen to customer issues and determine "
        "if they need refund help or order tracking. Use handoff_to_refund_agent or "
        "handoff_to_order_agent to transfer them."
    ),
)

refund_agent = client.create_agent(
    name="refund_agent",
    instructions=(
        "You are a refund specialist. Help customers with refund requests. "
        "When the user confirms they want a refund and supplies order details, "
        "call submit_refund to record the request."
    ),
    tools=[submit_refund],
)

order_agent = client.create_agent(
    name="order_agent",
    instructions="You are an order tracking specialist. Help customers track their orders.",
)

사용자 입력 및 도구 승인 요청 모두 처리

from agent_framework import (
    FunctionApprovalRequestContent,
    HandoffBuilder,
    HandoffUserInputRequest,
    RequestInfoEvent,
    WorkflowOutputEvent,
)

workflow = (
    HandoffBuilder(
        name="support_with_approvals",
        participants=[triage_agent, refund_agent, order_agent],
    )
    .set_coordinator("triage_agent")
    .build()
)

pending_requests: list[RequestInfoEvent] = []

# Start workflow
async for event in workflow.run_stream("My order 12345 arrived damaged. I need a refund."):
    if isinstance(event, RequestInfoEvent):
        pending_requests.append(event)

# Process pending requests - could be user input OR tool approval
while pending_requests:
    responses: dict[str, object] = {}

    for request in pending_requests:
        if isinstance(request.data, HandoffUserInputRequest):
            # Agent needs user input
            print(f"Agent {request.data.awaiting_agent_id} asks:")
            for msg in request.data.conversation[-2:]:
                print(f"  {msg.author_name}: {msg.text}")

            user_input = input("You: ")
            responses[request.request_id] = user_input

        elif isinstance(request.data, FunctionApprovalRequestContent):
            # Agent wants to call a tool that requires approval
            func_call = request.data.function_call
            args = func_call.parse_arguments() or {}

            print(f"\nTool approval requested: {func_call.name}")
            print(f"Arguments: {args}")

            approval = input("Approve? (y/n): ").strip().lower() == "y"
            responses[request.request_id] = request.data.create_response(approved=approval)

    # Send all responses and collect new requests
    pending_requests = []
    async for event in workflow.send_responses_streaming(responses):
        if isinstance(event, RequestInfoEvent):
            pending_requests.append(event)
        elif isinstance(event, WorkflowOutputEvent):
            print("\nWorkflow completed!")

지속성 워크플로를 위한 체크포인트 처리

장시간 실행되는 워크플로에서 도구 승인이 몇 시간 또는 며칠 후에 발생할 수 있는 경우, 체크포인트를 사용하세요.

from agent_framework import FileCheckpointStorage

storage = FileCheckpointStorage(storage_path="./checkpoints")

workflow = (
    HandoffBuilder(
        name="durable_support",
        participants=[triage_agent, refund_agent, order_agent],
    )
    .set_coordinator("triage_agent")
    .with_checkpointing(storage)
    .build()
)

# Initial run - workflow pauses when approval is needed
pending_requests = []
async for event in workflow.run_stream("I need a refund for order 12345"):
    if isinstance(event, RequestInfoEvent):
        pending_requests.append(event)

# Process can exit here - checkpoint is saved automatically

# Later: Resume from checkpoint and provide approval
checkpoints = await storage.list_checkpoints()
latest = sorted(checkpoints, key=lambda c: c.timestamp, reverse=True)[0]

# Step 1: Restore checkpoint to reload pending requests
restored_requests = []
async for event in workflow.run_stream(checkpoint_id=latest.checkpoint_id):
    if isinstance(event, RequestInfoEvent):
        restored_requests.append(event)

# Step 2: Send responses
responses = {}
for req in restored_requests:
    if isinstance(req.data, FunctionApprovalRequestContent):
        responses[req.request_id] = req.data.create_response(approved=True)
    elif isinstance(req.data, HandoffUserInputRequest):
        responses[req.request_id] = "Yes, please process the refund."

async for event in workflow.send_responses_streaming(responses):
    if isinstance(event, WorkflowOutputEvent):
        print("Refund workflow completed!")

## Sample Interaction

```plaintext
User: I need help with my order

triage_agent: I'd be happy to help you with your order. Could you please provide more details about the issue?

User: My order 1234 arrived damaged

triage_agent: I'm sorry to hear that your order arrived damaged. I will connect you with a specialist.

support_agent: I'm sorry about the damaged order. To assist you better, could you please:
- Describe the damage
- Would you prefer a replacement or refund?

User: I'd like a refund

triage_agent: I'll connect you with the refund specialist.

refund_agent: I'll process your refund for order 1234. Here's what will happen next:
1. Verification of the damaged items
2. Refund request submission
3. Return instructions if needed
4. Refund processing within 5-10 business days

Could you provide photos of the damage to expedite the process?

주요 개념

  • 동적 라우팅: 에이전트는 컨텍스트에 따라 다음 상호 작용을 처리해야 하는 에이전트를 결정할 수 있습니다.
  • AgentWorkflowBuilder.StartHandoffWith(): 워크플로를 시작하는 초기 에이전트를 정의합니다.
  • WithHandoff()WithHandoffs(): 특정 에이전트 간에 핸드오프 규칙을 구성합니다.
  • 컨텍스트 보존: 전체 대화 기록은 모든 핸드오프에서 유지 관리됩니다.
  • 다중 턴 지원: 원활한 에이전트 전환으로 진행 중인 대화 지원
  • 전문 지식: 각 에이전트는 핸드오프를 통해 공동 작업하는 동안 도메인에 중점을 둡니다.
  • 동적 라우팅: 에이전트는 컨텍스트에 따라 다음 상호 작용을 처리해야 하는 에이전트를 결정할 수 있습니다.
  • HandoffBuilder: 자동 핸드오프 도구 등록을 사용하여 워크플로 만들기
  • set_coordinator(): 먼저 사용자 입력을 수신하는 에이전트를 정의합니다.
  • add_handoff(): 에이전트 간의 특정 핸드오프 관계 구성
  • enable_return_to_previous(): 코디네이터 재평가를 건너뛰고 사용자 입력을 현재 전문가에게 직접 라우팅합니다.
  • 컨텍스트 보존: 전체 대화 기록은 모든 핸드오프에서 유지 관리됩니다.
  • 요청/응답 주기: 워크플로가 사용자 입력을 요청하고, 응답을 처리하며, 종료 조건이 충족될 때까지 계속됩니다.
  • 도구 승인: 사용자 승인이 필요한 중요한 작업에 사용 @ai_function(approval_mode="always_require")
  • FunctionApprovalRequestContent: 에이전트가 승인이 필요한 도구를 호출할 때 내보냅니다. 응답하는 데 사용 create_response(approved=...)
  • 체크포인팅: 프로세스가 재시작되는 동안 일시 중지하고 다시 시작할 수 있는 내구성 있는 워크플로에 with_checkpointing()를 사용
  • 전문 지식: 각 에이전트는 핸드오프를 통해 공동 작업하는 동안 도메인에 중점을 둡니다.

다음 단계