Hermes Agent v0.2.0의 진짜 헤드라인 기능은 어떤 모델 연동도 아니었고, 스킬 시스템도 아니었다. 바로 멀티플랫폼 메시징 게이트웨이 — Hermes 프로세스 하나가 동시에 일곱 개 채팅 플랫폼의 말을 듣고 있는 그것이었다. 텔레그램, 디스코드, 슬랙, 왓츠앱, 시그널, 이메일(IMAP/SMTP), 홈 어시스턴트. 4월 8일 릴리스에 오면 이 목록은 열세 개로 늘어 있었다. 사이사이에 Matrix, 페이슈, 위컴(WeCom), Mattermost, 딩톡, 그리고 트윌리오를 통한 SMS가 차례로 올라탔다.
여기서 놓치기 쉬운 점이 있다. 텔레그램 봇 하나를 만드는 건 별로 어렵지 않다. 어려운 건 일곱 개를 동시에 돌리되 이들이 같은 에이전트, 같은 메모리, 같은 툴 레지스트리를 공유하도록 만드는 쪽이다 — 진짜 아키텍처적인 일은 전부 거기 쌓인다. 이 글은 Hermes가 그 일을 실제로 어떻게 해내는지 따라가는 이야기다.
순진한 접근, 그리고 왜 그게 안 되는가
"텔레그램과 디스코드 둘 다에서 답해 주는 AI 봇"을 만들라고 하면, 첫 반응은 대개 독립된 봇 두 개를 띄우는 거다. 각 봇이 자기 프로세스, 자기 데이터베이스, 자기 상태를 가지고, 둘 다 똑같은 LLM API를 호출한다. 개념적으로는 텔레그램 쪽 사용자와 디스코드 쪽 사용자가 같은 에이전트를 마주하지만, 실제로는 서로를 전혀 모르는 에이전트 두 개를 마주하게 된다.
대부분의 기존 봇 프로젝트가 이렇게 하고 있는데, 이 방식이 얼마나 엉망인지는 시간이 조금 지나야 드러난다.
- •메모리가 갈라진다. 텔레그램에서 땅콩 알레르기가 있다고 말한 사용자가 디스코드로 넘어와 요리 질문을 하면, 그 정보는 사라져 있다. 에이전트는 같은 사실을 두 번 배워야 한다.
- •툴 상태가 떠다닌다. 슬랙으로 걸어 둔 cron 작업은 텔레그램에서 cron을 확인할 때 안 보인다. 세션 이력은 파편화된다. 공유 리소스(예컨대 Notion 연동)의 인증 토큰은 봇마다 따로따로 넣어 줘야 한다.
- •운영 비용이 배수로 붙는다. 봇 N개는 곧 프로세스 N개, 서비스 N개, 로그 스트림 N개, 설정 파일 N개, 고장 날 수 있는 지점 N개를 의미한다. 복잡도가 플랫폼 수에 비례해서 늘어난다.
- •안전 규칙이 쪼개진다. 어느 한 봇의 위험 명령 승인 정책을 조이고 나면, 나머지 봇에도 같은 변경을 적용하는 걸 잊지 말아야 한다. 보안 정책 드리프트가 기본값이 된다.
Hermes는 다른 길을 택했다. 에이전트 프로세스는 정확히 하나, 세션 데이터베이스도 하나, 메모리 저장소도 하나, 툴 레지스트리도 하나. 플랫폼들은 어댑터다 — 공유 에이전트로 메시지를 흘려보내고 받아 가는 얇은 현관.
게이트웨이의 생김새
Hermes 게이트웨이의 내부는 세 개 층으로 나뉜다.
맨 아래는 에이전트 본체다 — CLI 모드에서 돌리는 바로 그 Hermes 코어. 이 친구는 채팅 플랫폼의 존재를 전혀 모른다. 메시지를 받고, 자기 에이전트 루프(LLM 호출, 툴 호출, 메모리 조회, 체크포인트)를 돌리고, 응답을 내놓는다. 바깥 세계와의 유일한 인터페이스는 큐 기반 API다.
가운데에는 게이트웨이 코어가 있다 — 세션 관리, 사용자/플랫폼 라우팅, 승인 디스패치, cron 스케줄링, 스트리밍, 미디어 처리가 이 층에 산다. "플랫폼 X의 채널 Z에서 사용자 Y가 보낸 메시지 한 건"을 "세션 S 안에서 이 권한들을 가진 한 번의 에이전트 실행"으로 바꿔 주는 자리가 여기다. 동시에 플랫폼을 가로질러 공통으로 챙겨야 할 것들 — 레이트 리미트, 플러드 컨트롤, 중복 메시지 감지, 무활동 타임아웃, 승인 버튼 라우팅, 플랫폼 간 상태 — 도 여기서 한 번에 처리한다.
맨 위에는 플랫폼 어댑터들이 있다. 플랫폼마다 하나씩 — 텔레그램 어댑터, 디스코드 어댑터, 슬랙 어댑터, 기타 등등. 어댑터의 역할은 좁다. 자기 플랫폼에 접속하고(폴링, 롱 폴링, WebSocket, 웹훅, 해당 플랫폼 SDK가 선호하는 아무 방식이나), 들어오는 플랫폼 고유 이벤트를 게이트웨이 내부 메시지 포맷으로 옮기고, 나가는 게이트웨이 응답을 다시 그 플랫폼이 이해할 수 있는 형태로 되돌려 놓는다(Markdown, MarkdownV2, mrkdwn, 디스코드 리치 임베드, Matrix HTML, 슬랙 blocks, 이메일 MIME 전부 여기).
어댑터들은 일부러 작게 만들어져 있다. 새 플랫폼을 추가하는 일(v0.4.0에서 커뮤니티 기여자가 Mattermost를 300줄도 안 되는 파이썬으로 붙였다)은 대체로 해당 SDK의 이벤트를 게이트웨이 내부 메시지 모양에 매핑하고, 반대 방향으로 한 번 더 매핑하는 문제다.
플랫폼을 가로지르는 세션
Hermes의 세션 하나는 자기 메모리 창, 툴 상태, 실행 이력을 가진 대화 스레드다. 플랫폼이 하나일 때는 세션 대응이 자연스럽다 — 텔레그램 채팅 하나당 세션 하나, 디스코드 채널 하나당 세션 하나, 슬랙 스레드 하나당 세션 하나. 플랫폼이 여러 개가 되면 이야기가 재미있어진다.
Hermes는 기본적으로 "플랫폼 + 채팅" 조합 각각을 별개의 세션으로 다룬다. 텔레그램에서 봇과 나눈 DM, 슬랙 스레드에서 봇과 나눈 대화, 디스코드 채널에서 봇과 나눈 대화는 각자의 컨텍스트를 따로 돌리는 세 개의 독립된 대화다. 하지만 이들 모두는 플러그 가능한 메모리 프로바이더(v0.7.0부터는 기본값이 Honcho)를 통해 동일한 장기 기억을 공유한다. 그래서 당신이 세션을 건너뛰어 이동하길 바라는 정보 — "이 사용자는 땅콩 알레르기가 있다", "이 사용자 이름은 Alice다", "이 사용자의 프로젝트 이름은 Atlas다" — 는 메모리 계층을 타고 따라간다. 반면 각 대화의 단기 컨텍스트는 지금 쓰고 있는 플랫폼에 단단히 묶여 있다.
이 설계가 있어서 같은 어시스턴트가 어느 플랫폼에서든 같은 어시스턴트처럼 느껴진다. 동시에 모든 메시지를 엉킨 전역 방송으로 만들지도 않는다.
스레드 내부에서 사용자별 세션을 나누는 일이 왜 중요한가
v0.4.0에서 Hermes는 스레드 내 사용자별 세션 기본 적용이라는 기능을 넣었다 — 그룹 채팅에서 같은 스레드라도 사용자마다 각자의 세션을 갖게 한 것이다. 작아 보이는 변경이지만, 그룹 안에서 다중 사용자 봇을 돌려 본 사람이라면 누구나 이게 얼마나 큰 일인지 안다.
그룹 채팅에서 사용자별 세션 분리가 없으면, 모든 메시지가 단 하나의 공유 대화에 속한다. Alice가 봇에게 뭔가 물어보고, 30초 뒤에 Bob이 전혀 다른 질문을 던지면, 봇의 컨텍스트는 두 사람이 뒤엉킨 한 덩어리가 된다. 답변이 서로 섞이고, Alice의 메모리에 Bob의 데이터가 오염돼 들어오고, 사용자가 늘어날수록 게이트웨이 모드는 점점 더 나빠진다.
스레드 내 사용자별 세션이 들어오면, Alice와 Bob은 같은 스레드 안에서도 각자의 사적인 세션을 갖는다. 화면에서는 서로의 메시지를 여전히 볼 수 있지만, 에이전트는 두 사람에 대해 별개의 컨텍스트와 별개의 메모리 기록을 유지한다. v0.8.0부터는 이게 모든 게이트웨이 플랫폼의 기본 동작이 됐다. 이런 류의 수정은 대안에 한 번도 데어 보지 않은 사람에게는 보이지 않는다 — 한 번 데어 본 순간부터는 절대 되돌아가고 싶지 않아진다.
이 게이트웨이는 결국 무얼 위한 것인가
이 아키텍처를 한참 들여다보다 보면, 게이트웨이가 "채팅 플랫폼에서 봇을 돌리는 방법"이라기보다는 사실 이런 것에 가깝다는 걸 깨닫게 된다 — 사람(그 사람이 지금 어떤 앱을 쓰고 있든)과, 공유 메모리와 공유 툴을 가진 단일 AI 어시스턴트 사이의 조율 계층.
채팅 플랫폼들은 제품이 아니다. 입구다. 제품은 그 입구 뒤에 살아 있는 어시스턴트이고, "지금 내가 어떤 입구로 들어왔는지"를 아예 생각하지 않아도 된다는 사실 그 자체다.
이게 Hermes가 첫날에 내놓은 기능이다. 이후 4주 동안 벌어진 모든 일은, 이 조율 계층이 새로운 재주들을 하나씩 익혀 나가는 이야기일 뿐이다.