
.NET에서의 수동 멀티 에이전트 라우팅 (Manual Multi-Agent Routing)
요약
단일 거대 에이전트가 겪는 토큰 비용 증가와 성능 저하 문제를 해결하기 위해, 전문화된 에이전트들로 시스템을 나누는 수동 라우팅 방식을 제안합니다. 의도 에이전트(Intent agent)를 활용하여 요청을 적절한 전문 에이전트로 배분하는 아키텍처를 다룹니다.
핵심 포인트
- 단일 에이전트의 비대화로 인한 토큰 비용 및 성능 저하 문제 지적
- 의도 에이전트를 활용한 수동 라우팅 아키텍처 제안
- 에이전트 전문화를 통한 프롬프트 관리 및 유지보수 효율화
이 글은 Microsoft Agent Framework에 관한 제 시리즈의 10번째 파트입니다. 원문 게시물은 lukaswalter.dev에서 읽으실 수 있습니다.
지금까지 우리의 에이전트(Agent)는 질문에 답하고, 응답을 스트리밍(Stream)하며, 히스토리(History)를 기억하고, 컨텍스트(Context)를 줄이며, 도구(Tools)를 사용하고, 구조화된 출력(Structured output)을 반환하며, MCP에 연결하고, 에이전트 스킬(Agent Skills)을 로드할 수 있습니다.
이미 많은 기능을 갖추고 있습니다.
그리고 바로 그 지점에서 다음 문제가 시작됩니다.
하나의 거대한 에이전트에 계속해서 지침(Instructions)과 도구(Tools)를 추가하고 싶은 유혹이 생깁니다.
코딩 어시스턴트(Coding assistant)로 만듭니다.
또한 음악 전문가로 만듭니다.
또한 커피에 관한 질문에도 답하게 만듭니다.
또한 지원 도구(Support tools), 문서화 도구(Documentation tools), 그리고 내부 프로세스 규칙(Internal process rules)도 부여합니다.
어느 시점에 이르면, 에이전트는 팔방미인(Jack of all trades)이 됩니다.
프롬프트(Prompt)는 커집니다.
도구 목록(Tool list)도 늘어납니다.
모델은 무시해야 할 무관한 지침(Irrelevant instructions)을 더 많이 갖게 됩니다.
그리고 사용자가 간단한 질문만 던질 때조차, 매 요청마다 불필요한 입력 토큰(Input tokens) 비용을 지불하게 됩니다.
한 가지 실질적인 해결책은 수동 라우팅(Manual routing)입니다.
하나의 에이전트에게 모든 책임을 부여하는 대신, 시스템을 더 작고 전문화된 에이전트들로 나누고 그 앞에 저렴한 의도 에이전트(Intent agent)를 배치하는 것입니다.
의도 에이전트(Intent agent)는 사용자에게 직접 답하지 않습니다.
그저 요청이 어디로 가야 할지를 결정할 뿐입니다.
하나의 거대한 에이전트가 가진 문제점
단일 거대 에이전트는 처음에는 단순해 보입니다. 모든 것에 대해 단 하나의 진입점(Entry point)만 있으면 되니까요. 하지만 그 단순함은 오해의 소지가 있습니다.
만약 동일한 에이전트가 커피, 음악, 지원 티켓(Support tickets), 코드 리뷰(Code review), 그리고 내부 문서(Internal documentation)에 대해 모두 알고 있다면, 각 요청은 짐을 지게 됩니다.
기타 앰프에 대한 질문을 할 때도 여전히 커피 관련 지침에 대한 비용을 지불해야 합니다.
커피 질문을 할 때도 여전히 음악 관련 지침을 포함하게 됩니다.
가벼운 대화(Small talk) 메시지조차 결코 필요하지 않을 도구 설명(Tool descriptions)을 보게 됩니다.
이는 세 가지 문제를 야기합니다:
- 요청당 더 많은 입력 토큰 (Input tokens)
- 모델이 잘못된 동작을 선택할 가능성 증가
- 유지 관리 및 테스트가 더 어려운 프롬프트 (Prompts)
목표는 화려한 멀티 에이전트 아키텍처 (Multi-agent architecture)를 만드는 것이 아닙니다.
목표는 각 모델 호출 (Model call)이 집중된 상태를 유지하는 것입니다.
의도 에이전트 (The Intent Agent)
의도 에이전트는 디스패처 (Dispatcher)입니다.
시스템의 최전방에 위치하여 사용자 요청을 분류합니다.
이 예제에서는 의도적으로 도메인 (Domain)을 작게 유지합니다:
- 커피 관련 질문은 커피 전문가 에이전트 (Coffee expert agent)로 전달됩니다.
- 음악 관련 질문은 음악 전문가 에이전트 (Music expert agent)로 전달됩니다.
- 그 외의 모든 것은 제어된 폴백 (Fallback) 처리가 됩니다.
의도 에이전트에는 한 가지 엄격한 규칙이 있습니다:
요청을 분류만 합니다.
요청에 직접 답변하지 않습니다.
이 규칙은 매우 중요합니다.
만약 라우터 (Router)가 직접 답변하기 시작하면, 그것은 또 다른 범용 어시스턴트 (General-purpose assistant)가 되어버립니다.
그렇게 되면 시스템은 한 가지 문제가 아닌 두 가지 문제를 안게 됩니다.
의도 에이전트는 분류만 수행하기 때문에, 일반적으로 전문가 에이전트들보다 더 작고 저렴한 모델을 사용할 수 있습니다.
단순히 경로를 선택하기 위해 깊은 추론 (Deep reasoning)이 필요한 것은 아닙니다.
필요한 것은 신뢰할 수 있는 카테고리 (Category)입니다.
라우팅 결정을 위한 구조화된 출력 (Structured Output) 사용
의도 에이전트가 다음과 같이 일반 텍스트를 반환하게 두지 마세요:
This is probably a music question.
이렇게 하면 애플리케이션이 생성된 텍스트를 파싱 (Parse)해야만 합니다.
문자열 파싱 (String parsing)은 취약한 경계 (Boundary)입니다.
우리는 이미 구조화된 출력 (Structured output) 관련 문서에서 이 내용을 살펴보았습니다.
라우팅을 위해 대신 작은 C# 계약 (Contract)을 정의하세요:
public enum UserIntent
{
Coffee,
...
이제 라우터는 애플리케이션이 직접 사용할 수 있는 데이터를 반환합니다.
using Microsoft.Agents.AI;
AIAgent intentAgent = smallChatClient.AsAIAgent(
...
그런 다음 RunAsync<T>를 사용하여 호출합니다:
string userMessage = "How do I get a dirty Hendrix tone on my Strat?";
AgentResponse<IntentResult> intentResponse =
...
유용한 부분은 바로 경계 (Boundary)입니다.
애플리케이션은 여전히 해석해야 하는 문장을 받는 것이 아니라,
IntentResult를 받게 됩니다.
구조화된 출력 (Structured output) 지원은 여전히 에이전트 유형, 제공자 (Provider), 모델 및 채팅 클라이언트 (Chat client)에 따라 달라집니다. ChatClientAgent 및 호환 가능한 채팅 클라이언트를 사용하는 경우, 출력 유형이 컴파일 타임에 알려져 있다면 RunAsync<T>가 가장 깔끔한 옵션입니다. 만약 사용 중인 제공자가 이를 안정적으로 지원하지 않는다면, 응답 형식 (Response format)을 통해 명시적인 JSON 스키마 (JSON schema)를 사용하거나 재시도 (Retry) 및 검증 (Validation) 레이어를 추가하십시오.
C#이 주도권을 잡다
IntentResult를 얻었다면, 모델에게 오케스트레이션 (Orchestration)을 요청하는 것을 중단하십시오. 일반적인 C#을 사용하십시오.
AIAgent coffeeAgent = coffeeChatClient.AsAIAgent(
name: "coffee-expert",
instructions: """
...
이것이 핵심 패턴입니다:
의도 에이전트 (Intent agent)가 분류합니다.
C#이 라우팅 (Routing)합니다.
전문가 에이전트 (Specialist agent)가 답변합니다.
Other의 경우, 두 번째 모델 호출이 필요하지 않습니다. 고정된 메시지를 반환하거나, 명확한 질문을 던지거나, 지원되는 주제를 보여주거나, 애플리케이션의 맥락에 맞다면 일반적인 폴백 에이전트 (Fallback agent)로 라우팅할 수 있습니다.
중요한 점은 제어권 (Control)입니다. 어떤 비용이 많이 드는 에이전트를 호출할지 모델이 결정하지 않습니다. 여러분의 애플리케이션이 결정합니다.
비용이 많이 드는 작업을 라우팅하기 전에 신뢰도 추가하기
라우터 (Router)를 맹목적으로 신뢰하지 마십시오. 라우터는 여전히 LLM 호출입니다. 틀릴 수도 있고, 불확실할 수도 있으며, 모호한 요청을 과잉 분류할 수도 있습니다.
그렇기 때문에 IntentResult에는 신뢰도 점수 (Confidence score)가 포함되어 있습니다. 하지만 이 신뢰도를 진실이 아닌 라우팅 신호 (Routing signal)로 취급하십시오.
모델이 생성한 신뢰도는 자동으로 보정 (Calibrated)되지 않습니다. 0.9라는 결과가 반드시 해당 경로가 90%의 확률로 정확하다는 것을 의미하지는 않습니다. 이는 단지 라우터가 높은 신뢰도를 표현했다는 것만을 의미합니다.
여전히 이를 실질적인 게이트 (Gate)로 사용할 수 있습니다:
if (route.Confidence < 0.75)
{
return "커피에 관한 내용인가요, 아니면 음악에 관한 내용인가요?";
...
정확한 임계값(Threshold)은 애플리케이션에 따라 달라집니다.
일상적인 비서(Casual assistant)의 경우, 잘못된 라우팅이 큰 문제가 되지 않을 수 있습니다.
하지만 고객 지원 자동화(Support automation)의 경우, 라우팅 오류는 시간을 낭비하거나 잘못된 다운스트림 프로세스(Downstream process)를 트리거할 수 있습니다.
실제 사례를 통해 이를 측정하십시오.
직관만으로 임계값을 조정하지 마십시오.
대표적인 사용자 요청에 대한 작은 라벨링된 데이터셋(Labeled dataset)을 만드십시오.
의도 에이전트(Intent agent)를 사용하여 이를 실행하십시오.
각 의도가 얼마나 정확하게 분류되는지 추적하십시오.
그 후에 신뢰도 임계값(Confidence threshold)을 어디에 설정할지 결정하십시오.
신뢰도(Confidence)는 유용합니다.
하지만 실제 도메인에서 어떻게 작동하는지 확인한 후에만 유용합니다.
라우팅을 관찰 가능하게 만들기 (Make Routing Observable)
수동 라우팅은 깔끔한 평가 지점(Evaluation point)을 제공하기도 합니다.
라우터가 전문 에이전트(Specialist agent)가 실행되기 전에 타입이 지정된 결과(Typed result)를 반환하기 때문에, 최종 답변과는 별도로 라우팅 결정을 로그로 남길 수 있습니다.
예를 들어:
logger.LogInformation(
"Intent routed. Intent={Intent}, Confidence={Confidence}, SelectedAgent={SelectedAgent}, FallbackUsed={FallbackUsed}",
route.Intent,
...
유용한 필드에는 다음이 포함됩니다:
userMessage(사용자 메시지)predictedIntent(예측된 의도)confidence(신뢰도)selectedAgent(선택된 에이전트)fallbackUsed(폴백 사용 여부)
실제 시스템에서는 사용자 메시지 전체를 직접 로그로 남기고 싶지 않을 수도 있습니다.
개인정보 보호 및 컴플라이언스(Compliance) 요구 사항에 따라 요청 ID(Request id), 마스킹 처리된 메시지(Redacted message) 또는 해시된 참조(Hashed reference)를 대신 로그로 남길 수 있습니다.
중요한 점은 라우팅이 측정 가능해진다는 것입니다.
이제 다음과 같은 질문에 답할 수 있습니다:
- 어떤 의도들이 가장 자주 혼동되는가?
- 폴백(Fallback)이 얼마나 자주 트리거되는가?
- 어떤 신뢰도 범위에서 가장 많은 잘못된 라우팅이 발생하는가?
- 특정 유형의 사용자 요청이 지속적으로 잘못 분류되는가?
- 모델 업그레이드가 라우팅 품질을 개선했는가, 아니면 손상시켰는가?
이것이 수동 라우팅 (Manual Routing)의 과소평가된 이점 중 하나입니다.
당신은 토큰 (Tokens)을 절약할 뿐만 아니라,
시스템의 나머지 부분 앞에 작고 테스트 가능한 제어 지점 (Control Point)을 생성하게 됩니다.
이것이 토큰을 절약하는 이유
수동 라우팅은 각 에이전트 (Agent)가 필요한 컨텍스트 (Context)만을 받기 때문에 도움이 됩니다.
하지만 라우팅이 공짜는 아니라는 점을 명심하십시오.
라우팅은 전문가 호출 (Specialist Call) 전에 모델 호출 (Model Call)을 한 번 더 추가합니다.
라우팅 호출 비용이 피할 수 있는 무관한 컨텍스트 비용보다 저렴할 때만 이 방식은 이득이 됩니다.
만약 전문가 프롬프트 (Specialist Prompts)가 매우 작다면, 추가적인 라우터 호출은 그만한 가치가 없을 수도 있습니다.
하지만 프롬프트, 도구 (Tools), 그리고 모델 크기 (Model Sizes)가 서로 차이를 보이기 시작하면, 라우팅은 빠르게 유용해집니다.
의도 에이전트 (Intent Agent)는 작게 유지될 수 있습니다:
- 짧은 지침 (Instructions)
- 도메인 도구 (Domain Tools) 없음
- 긴 전문가 프롬프트 없음
- 저렴한 모델
- 구조화된 출력 (Structured Output)만 수행
전문가 에이전트 (Specialist Agents)는 집중력을 유지할 수 있습니다:
- 커피 관련 질문에는 커피 지침만 제공
- 음악 관련 질문에는 음악 지침만 제공
- 유용한 곳에만 도메인 도구 사용
- 요청이 요구하는 수준에 따라 더 강력한 모델 사용
이것이 토큰 제한 (Token Limits)을 완전히 없애주는 것은 아닙니다.
다만 어떤 지침, 도구, 컨텍스트가 어떤 모델 호출로 전송될지를 변경하는 것입니다.
다음과 같은 방식 대신:
모든 요청
-> 커피 프롬프트 + 음악 프롬프트 + 모든 도구 + 모든 규칙
다음과 같은 방식을 얻게 됩니다:
모든 요청
-> 작은 라우팅 프롬프트
...
이것이 진정한 승리입니다.
모든 요청마다 무관한 컨텍스트에 대해 비용을 지불하는 것을 방지할 수 있습니다.
수동 라우팅 vs 워크플로 엔진 (Workflow Engine)
이 글에서는 의도적으로 일반 C# 라우팅을 사용합니다.
나중에 라우팅을 워크플로 (Workflow)로 모델링할 수도 있습니다.
Agent Framework에는 명시적인 오케스트레이션 (Orchestration), 체크포인트 (Checkpoints), 핸드오프 (Handoffs), 그리고 인간 참여 (Human-in-the-loop) 시나리오를 위한 워크플로 엔진이 있습니다.
하지만 이 예제에는 아직 그것이 필요하지 않습니다.
흐름은 단순합니다:
- 의도 분류 (Classify intent)
- 결과에 따른 분기 (Switch on the result)
- 한 명의 전문가 호출 (Call one specialist)
- 답변 반환 (Return the answer)
이러한 사례에서는 C#의 switch 문이 워크플로 그래프 (workflow graph)보다 읽기 쉽고, 테스트하기 쉬우며, 디버깅하기에도 용이합니다.
이는 유용한 설계 규칙입니다:
충분한 제어권을 제공하는 가장 작은 오케스트레이션 메커니즘 (orchestration mechanism)을 사용하십시오.
단순한 라우팅 (routing)의 경우, 이는 종종 일반적인 C#입니다.
수동 의도 라우팅 (Manual Intent Routing)을 사용해야 하는 경우
다음과 같은 경우에 수동 의도 라우팅을 사용하십시오:
- 도메인 (domains)이 명확하게 분리되어 있는 경우
- 하나의 거대한 프롬프트 (prompt)가 너무 비용이 많이 들거나 초점이 흐려지는 경우
- 서로 다른 요청에 서로 다른 도구 (tools)가 필요한 경우
- 서로 다른 요청에 서로 다른 모델 크기 (model sizes)가 적합한 경우
- 애플리케이션 코드 내에서 예측 가능한 라우팅 로직을 원하는 경우
- 실제 사례를 통해 라우팅 품질을 평가할 수 있는 경우
다음과 같은 경우에는 수동 의도 라우팅을 사용하지 마십시오:
- 단일 소형 에이전트 (agent)만으로도 이미 충분한 경우
- 카테고리가 모호하고 끊임없이 중복되는 경우
- 잘못된 경로로 라우팅되었을 때 비용이 많이 발생하며 검증 수단이 없는 경우
- 라우터가 대체하려는 시스템만큼 복잡해지는 경우
- 실제로 체크포인트 (checkpoints), 장기 실행 (long-running execution) 또는 사람의 승인 (human approval)이 필요한 경우
이 패턴은 보편적인 아키텍처 (architecture)가 아닙니다.
이는 멀티 에이전트 시스템 (multi-agent systems)으로 나아가기 위한 저렴하고 실용적인 첫 단계입니다.
결론
수동 멀티 에이전트 라우팅은 단순합니다.
작은 의도 에이전트 (intent agent)를 사용하여 요청을 분류하십시오.
타입이 지정된 IntentResult를 반환하십시오.
C#이 적절한 전문가 에이전트 (specialist agent)로 라우팅하게 하십시오.
이렇게 하면 비용이 많이 드는 에이전트들이 초점을 유지할 수 있으며, 모든 지시 사항과 모든 도구를 모든 모델 호출에 보내는 것을 방지할 수 있습니다.
또한 애플리케이션에 폴백 (fallbacks), 신뢰도 임계값 (confidence thresholds), 로깅 (logging) 및 테스트를 위한 깔끔한 제어 지점 (control point)을 제공합니다.
주요 제한 사항은 라우팅 품질이 시스템 품질의 일부가 된다는 점입니다.
예시, 임계값 및 폴백 동작이 필요합니다.
하지만 이는 모든 것을 한꺼번에 처리하려고 시도하는 과부하된 단일 에이전트보다 여전히 추론하기 쉽습니다.
다음으로, 우리는 이 아이디어를 더 발전시킬 수 있습니다.
C#에서 에이전트로 라우팅하는 대신, 에이전트를 도구 (Tools)로 노출하여 하나의 코디네이터 (Coordinator)가 의도적으로 작업을 위임하도록 할 수 있습니다.
이는 더 많은 유연성을 제공하지만, 토큰 (Token), 비용 (Cost) 및 제어 (Control) 사이의 트레이드오프 (Tradeoffs) 문제도 다시 불러옵니다.
추가 읽을거리 (Further Reading)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기

