본문 바로가기
개발/개인공부

채팅형 멀티에이전트 라우팅 설계 - 수정 요청은 어느 에이전트에게 갈까

by beomcoder 2026. 7. 24.
728x90
반응형

 

3줄 요약

  • 채팅창 하나 뒤에 전문 에이전트 여러 개(콘텐츠 생성, 사이트 안내, 외부 발행 등)를 두는 구조에서, 오케스트레이터의 진짜 일은 첫 분배가 아니라 이어지는 대화를 올바른 에이전트에게 계속 잇는 것이에요.
  • 핵심은 세션 상태에 활성 에이전트(active_agent)와 진행 중 작업 요약을 저장하고, 매 메시지마다 "이 메시지는 그 작업의 연장인가, 새 주제인가"를 분류하는 것.
  • 여기에 에이전트별 대화 스레드 분리, 제어권 반환(done/handback) 시그널, 발행처럼 위험한 액션 앞 사용자 확인까지 더하면 골격이 완성됩니다.

채팅창은 하나, 뒤에는 에이전트 여러 개

요즘 구상 중인 구조가 있어요. 사이트에 채팅창을 하나 두고, 그 뒤에 전문 에이전트를 여러 개 두는 거예요.

  • 오케스트레이터 — 사용자와 대화하는 유일한 창구. 메시지를 읽고 어느 에이전트 소관인지 정한다
  • 콘텐츠 에이전트 — 사이트에 쓸 글·이미지를 생성하고 수정한다
  • 사이트 안내 에이전트 — 내 사이트가 뭐 하는 곳인지, 기능이 뭔지 답한다
  • 발행 에이전트 — 완성된 콘텐츠를 블로그 같은 외부 채널에 올린다

사용자 입장에선 그냥 챗봇 하나예요. "메인 배너 이미지 만들어줘"라고 치면 뒤에서 콘텐츠 에이전트가 움직이고, "이 사이트 뭐 하는 데예요?"라고 치면 안내 에이전트가 답하는 거죠.

 

그런데 만들려고 보면 첫 분배는 쉬운데, 진짜 어려운 건 두 번째 메시지부터예요. 이미지를 생성해서 보여줬더니 사용자가 "좀 더 밝게 해줘"라고 쳤을 때 — 이 메시지는 단독으로 보면 뭘 밝게 하라는 건지 알 수 없거든요. 이 글은 그 문제를 어떻게 푸는지 정리한 설계 노트예요.

 

채팅 한 줄이 도착하면 벌어지는 일

오케스트레이터는 매 메시지마다 세 가지 일을 해요. 의도 분류 → 대상 선택 → 응답 정리. 이때 라우팅을 구현하는 방식은 크게 세 가지가 있는데, 채팅형에는 조합이 정답이었어요.

  • LLM 분류기 — 라우터 LLM이 메시지를 읽고 구조화 출력(JSON)으로 대상을 뱉는다. 구현이 단순하고 로깅·디버깅이 쉬워요.
  • 도구 호출(agent-as-tool) — 서브에이전트를 라우터의 도구로 등록해서, 라우터가 함수 부르듯 호출하고 결과를 받아 답한다. 사용자에게 답하는 화자가 항상 라우터라 말투가 일관돼요.
  • 핸드오프(handoff) — 대화 주도권 자체를 서브에이전트에게 넘기고, 이후 메시지가 라우터를 안 거치고 직행한다. 왕복 토큰은 아끼지만, 돌아올 타이밍 관리가 어려워져요.

제 결론은 라우터는 항상 거치되(분류기), 실행은 도구 호출로 하는 쪽이에요. 라우터가 뱉는 구조화 출력은 이런 모양입니다.

{
  "decision": "route",
  "target": "content_agent",
  "reason": "새 이미지 생성 요청",
  "instruction": "메인 배너용 이미지 생성. 사이트 톤은 다크 계열."
}

decision에는 route | continue | direct_reply 세 값이 들어가는데, direct_reply가 있는 게 포인트예요. "안녕하세요" 같은 스몰토크나 "지금 뭐 하고 있어?" 같은 상태 질문까지 서브에이전트로 보낼 필요는 없으니, 라우터가 직접 답하는 경로를 열어둡니다.

"방금 그거 수정해줘"는 누가 받아야 할까

이 글의 핵심이에요. 후속 메시지를 올바르게 잇는 방법은 결국 상태 관리입니다. 세션에 이런 상태를 들고 다녀요.

{
  "active_agent": "content_agent",
  "task_summary": "메인 배너 이미지 생성. 1차 시안 전달 완료",
  "task_status": "waiting_feedback",
  "history_digest": "다크 톤 요청, 텍스트 없는 이미지 선호"
}

그리고 라우터는 매 메시지마다 순서대로 판단해요.

  • 1. 활성 에이전트가 있는가? 없으면 그냥 새 라우팅.
  • 2. 있다면, 이 메시지는 그 작업의 연장인가? 라우터에게 최근 대화 몇 턴 + task_summary를 함께 주고 continue/switch를 분류시킵니다. "좀 더 밝게 해줘"는 task_summary에 "이미지 시안 전달"이 있으니 continue로 잡혀요.
  • 3. 새 주제라면 어느 소관인가? "됐고 이제 블로그에 올려줘"는 switch로 잡히고, 발행 에이전트로 active_agent가 교체됩니다.

여기서 실용적인 팁 하나 — 활성 에이전트가 waiting_feedback 상태면 기본값을 continue로 기울여요. 시안을 보여준 직후에 오는 메시지는 십중팔구 그 시안에 대한 피드백이거든요. 분류기를 매번 중립으로 돌리는 것보다 오분류가 확 줄어요.

 

생성 도중에 수정 요청이 끼어드는 경우도 정해둬야 해요. 이미지가 아직 생성 중인데 "아 참, 글씨는 넣지 말아줘"가 도착하면 선택지는 둘입니다 — 진행 중 작업을 취소하고 요구사항을 합쳐 재시작하거나, 완료 후 다음 턴에 반영하거나. 저는 취소-병합-재시작이 낫다고 봐요. 어차피 버려질 결과물에 시간을 쓰는 것보다, "요청 반영해서 다시 만들고 있어요"라고 알려주는 쪽이 사용자 경험이 좋으니까요.

 

컨텍스트는 얼마나 넘겨야 할까

전체 대화 히스토리를 모든 에이전트에게 다 넘기는 건 최악이에요. 토큰도 낭비고, 안내 에이전트가 이미지 수정 이력까지 알 필요가 없죠. 제가 정리한 원칙은 이래요.

  • 에이전트별 스레드 분리 — 콘텐츠 에이전트는 자기 작업 대화(생성 프롬프트, 수정 요청 이력)만 자기 스레드로 유지해요. 나중에 "아까 그 배너 다시 손보자"로 재진입하면 그 스레드를 복원하고요.
  • 라우터가 요약을 만들어 전달 — 다른 에이전트의 맥락이 필요하면 원문 대신 라우터가 만든 한두 줄 요약(history_digest)을 넘깁니다. "사용자는 다크 톤을 선호" 정도면 충분해요.
  • 넘어갈 때는 산출물 중심으로 — 발행 에이전트에게 필요한 건 콘텐츠 에이전트의 대화가 아니라 최종 결과물과 메타데이터(이미지 경로, 제목, 태그)예요. 대화가 아니라 작업물이 인터페이스가 되게 합니다.

제어권 반환과 안전장치

서브에이전트가 일을 마치면 제어권을 라우터에게 돌려줘야 해요. 저는 에이전트 응답에 시그널 필드를 강제하는 방식을 써요 — done(작업 완료), need_user(사용자 입력 필요), not_mine(내 소관 아님, 핸드백). 특히 not_mine이 중요한데, 라우터가 분류를 틀려도 에이전트가 "이건 제 일이 아닌데요"라고 돌려보내면 시스템이 스스로 복구돼요.

 

그리고 발행 에이전트처럼 외부에 영향을 주는 액션은 실행 전에 반드시 사용자 확인을 끼웁니다. "이 글을 블로그에 올릴게요. 진행할까요?" 한 턴이요. 예전에 클로드 콘텐츠 공장을 만들 때도 콘텐츠 생성·수정은 자동이지만 머지 버튼은 항상 사람이 눌렀는데, 이 한 지점 덕분에 자동화를 마음 놓고 키울 수 있었어요. 되돌릴 수 있는 건 에이전트 재량, 되돌리기 어려운 건 사람 확인 — 이 선은 채팅형에서도 똑같이 유효합니다.

 

FAQ

Q. 라우터가 분류를 틀리면 어떻게 되나요?

이중 방어로 잡아요. 1차는 잘못 받은 에이전트의 not_mine 핸드백, 2차는 라우터가 확신이 낮을 때 사용자에게 "이미지 수정 말씀이신가요, 아니면 글 쪽인가요?"라고 한 번 되묻는 거예요. 그리고 오분류 로그를 모아서 라우팅 프롬프트의 예시로 추가하면 점점 정확해집니다.

 

Q. 매 메시지마다 LLM 분류를 돌리면 비용·지연이 아깝지 않나요?

라우팅은 짧은 분류 작업이라 작고 빠른 모델(Haiku급)로 충분해요. 여기에 규칙을 섞으면 더 줄어요 — 활성 에이전트가 waiting_feedback이면 LLM 없이 기본 continue, 명백한 키워드("올려줘", "발행")면 바로 스위치, 애매할 때만 분류기를 돌리는 식이에요.

 

Q. 서브에이전트가 사용자에게 직접 답하게 하면 안 되나요?

되지만, 채팅형에서는 말투·페르소나가 에이전트마다 달라지는 게 티가 나요. 서브는 원자료(이미지, 초안, 답변 재료)를 만들고 최종 발화는 라우터가 정리하는 쪽이 "챗봇 하나"라는 환상이 유지됩니다. 지연이 아까우면 서브 응답을 스트리밍으로 흘리되 시스템 프롬프트로 말투만 통일하는 절충안도 있어요.

 

마무리

채팅형 멀티에이전트의 성패는 화려한 에이전트 개수가 아니라 라우터의 상태 관리에 달려 있어요. active_agent와 작업 요약을 들고 다니며 continue/switch/direct를 판단하는 것, 에이전트별 스레드를 분리하고 산출물로 인터페이스하는 것, done/not_mine으로 제어권을 돌려받는 것. 이 세 가지가 골격이고, 나머지는 살 붙이기입니다. 다음에는 이 구조를 실제 코드로 옮기면서 겪은 것들을 정리해볼게요.

 

참고 자료

728x90
반응형

댓글