핸드오프 오케스트레이션을 사용하면 에이전트가 컨텍스트 또는 사용자 요청에 따라 서로 제어를 전송할 수 있습니다. 각 에이전트는 적절한 전문 지식을 갖춘 다른 에이전트에게 대화를 "전달"하여 올바른 에이전트가 작업의 각 부분을 처리하도록 할 수 있습니다. 이는 고객 지원, 전문가 시스템 또는 동적 위임이 필요한 시나리오에서 특히 유용합니다.
핸드오프와 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()를 사용 - 전문 지식: 각 에이전트는 핸드오프를 통해 공동 작업하는 동안 도메인에 중점을 둡니다.